Regeneration
What a rebuild destroys and what survives, the ownership guard, and the source-hash cache that decides whether a rebuild happens at all.
A generated asset is source-derived output, not a document. When a source file is compiled again,
the generated graph is torn down and rebuilt from the .dsm or .dsf. Every hand edit inside the
asset is destroyed, with exactly one exception.
Internalize that sentence and most of this page follows from it.
| Aspect | Value |
|---|---|
| Applies to | every generated UMaterial, UDreamShaderMaterialInstance, UMaterialFunction, UMaterialFunctionMaterialLayer, UMaterialFunctionMaterialLayerBlend |
| Triggered by | any compile that is not skipped by the source hash |
| Effect | the generated graph is torn down and rebuilt from source |
What survives
| Edit | Survives regeneration |
|---|---|
a comment box whose text does not begin with DreamShader: | yes |
a comment box whose text begins with DreamShader: | no — deleted |
| nodes you added by hand | no — deleted |
| node property tweaks on generated nodes | no — the node is deleted and recreated |
| node positions | no, unless pinned by a Layout section |
| material settings changed in the editor | no — every property in Reset properties is restored to its default, then Settings is reapplied |
| parameter overrides on a generated ThinCustom instance | no — see the warning below |
FunctionInput / FunctionOutput pin identities on a material function | yes since 1.3.2 |
| named-reroute variable GUIDs | yes — regenerated only when invalid |
Sequence
Modify()the target object.- Clear the generated comments — every
UMaterialExpressionCommentwhose text starts with the literalDreamShader:. - Null every material property input, from the first to the last material-property slot.
- Delete every expression in the graph.
- Reset the material to defaults — see Reset properties. Material functions skip this step.
- Apply
Settings. - Rebuild:
Propertiesnodes, theGraphbody or the whole-surfaceCustomnode, theOutputsbindings. - Lay out — skipped in memory-only mode.
- Recompile.
Step 4 has two strategies. Below 1200 expressions each node is deleted individually through the
material editing library, in up to 64 outer passes, reporting Deleting old Material node '{Name}'....
At 1200 or more the whole expression collection is un-rooted and marked as garbage in one pass. The
material path also resets the material's editor parameter cache; the material-function path does not.
The whole-file parse, the Settings validation and the Outputs validation all run before the
target asset is created or cleared, so a source file that fails any of them leaves the previously
generated asset intact. A syntax error inside a Graph block is not one of those gates: the
statement parser runs after step 4, so such a failure leaves the asset emptied.
The one hand edit that survives
A comment box whose text does not carry the DreamShader prefix.
| Aspect | Value |
|---|---|
| Prefix | DreamShader: — the word, a colon, and a single trailing space |
| Comparison | case-sensitive |
| Effect | a comment whose text starts with the prefix is deleted before the rebuild; every other comment is left untouched |
dreamshader: Notes, DREAMSHADER: Notes and DreamShader:Notes (no space) all fail the prefix
test and therefore survive. This is the supported way to annotate a generated material by hand.
The corollary: renaming a generated box from DreamShader: Sampling to Sampling makes it
permanent, and the next regeneration creates a second box named DreamShader: Sampling on top of
it. To keep DreamShader's own boxes in sync, leave their text alone and change the Comment(Name=…)
entry in the source Layout section instead.
Preserved identities
Two identities are deliberately preserved so that existing call sites do not break.
- Material function pins. Before the graph is cleared, the
IdGUID of everyUMaterialExpressionFunctionInputandUMaterialExpressionFunctionOutputis cached by name and restored onto the newly created pin with the same name. AMaterialFunctionCallnode elsewhere in the project keeps its wiring across a regeneration of the function. since 1.3.2 - Named reroutes. A declaration's variable GUID is regenerated only when the existing one is invalid.
Renaming an input or output in the source is therefore a breaking change for its call sites: the old name's GUID has nothing to restore onto.
Parameter overrides on a generated instance
Under the ThinCustom backend, regeneration calls ClearParameterValuesEditorOnly() on the emitted
UDreamShaderMaterialInstance. Every parameter override set by hand on a generated instance is
wiped on every regeneration — scalar, vector, texture, static switch and static component-mask
alike. No diagnostic is emitted; the values are simply gone the next time the source is compiled.
Workaround: never tune a generated instance directly. Either
- move the value into the source as a
Propertiesdefault, so the generated instance carries it, or - create a child
UMaterialInstanceConstantparented to the generated instance and override there. The child is a normal asset that regeneration never touches, and because the generated instance owns the static permutation, the child shares its shader map at no extra compile cost.
The Material Content Browser's instance-creation action produces exactly such a child, in
<parent directory>/<Instance Subfolder> — see
In-memory Materials.
The ownership guard
DreamShader refuses to overwrite an asset it did not generate.
| Aspect | Value |
|---|---|
| Fires when | the target package exists on disk and the existing object carries no DreamShader.SourceFile metadata |
| Applies to | Shader under the Graph backend, and ShaderFunction / ShaderLayer / ShaderLayerBlend |
| Result | generation fails; the existing asset is untouched |
| Message | Raised for |
|---|---|
Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your shader or move/delete the existing asset before regenerating. | a material |
Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your function or move/delete the existing asset before regenerating. | a material function |
The guard is inert for packages that are not on disk: a memory-only asset has no saved package to protect, so the check does not run.
There is no ownership guard on the ThinCustom instance path. Generating a Shader under the
default backend onto a path that holds a hand-authored UDreamShaderMaterialInstance checks only the
class, not the provenance metadata — and then rebuilds it, clearing its parameter overrides. A
hand-authored asset of any other class at that path is still rejected, with
Asset '{ObjectPath}' already exists and is not a DreamShader instance material. Delete it (or remove Backend="Instance") before switching backends.
Workaround: do not hand-author UDreamShaderMaterialInstance assets. Create child instances as
plain UMaterialInstanceConstant, which the guard's class check rejects outright.
Reset properties
Before the graph is rebuilt, a material's render state is restored to these values, in this order.
Settings is applied afterwards, so any key you declare wins; anything you do not declare returns
to the value below, regardless of what the material editor last held.
| Property | Reset to |
|---|---|
BlendMode | BLEND_Opaque |
MaterialDomain | MD_Surface |
| shading model | MSM_DefaultLit |
TwoSided | false |
OpacityMaskClipValue | 0.3333 |
Wireframe | false |
DitheredLODTransition | false |
DitherOpacityMask | false |
bAllowNegativeEmissiveColor | false |
bCastDynamicShadowAsMasked | false |
bCastRayTracedShadows | true |
bEnableResponsiveAA | false |
bScreenSpaceReflections | false |
bContactShadows | false |
bDisableDepthTest | false |
bOutputTranslucentVelocity | false |
bWriteOnlyAlpha | false |
BlendableOutputAlpha | false |
TranslucencyLightingMode | TLM_VolumetricNonDirectional |
bTangentSpaceNormal | true |
bAlwaysEvaluateWorldPositionOffset | false |
bFullyRough | false |
bIsSky | false |
bIsThinSurface | false |
MaterialDecalResponse | MDR_ColorNormalRoughness |
bHasPixelAnimation since UE 5.4 | false |
NumCustomizedUVs | 0 |
Material functions have no render state; their asset-level fields are reapplied instead:
| Source setting | Field | When absent |
|---|---|---|
Description | Description | cleared |
UserExposedCaption | UserExposedCaption | cleared |
ExposeToLibrary | bExposeToLibrary | set to false |
LibraryCategories | LibraryCategoriesText — comma-separated, entries trimmed, empties dropped | cleared |
The material-function usage is also re-stamped from the block kind on every regeneration.
The source-hash cache
Regeneration is expensive, so an unchanged source skips it entirely.
| Aspect | Value |
|---|---|
| Hashed text | the prepared source — the file with every import recursively inlined |
| Algorithm | FCrc::StrCrc32, formatted %08x — eight lowercase hex digits |
| Stored in | the generated asset's package metadata, keyed by the asset object |
| Bypassed by | the bForce flag on the generation entry points |
prepared text -> CRC32 -> "%08x" -> DreamShader.SourceHash e.g. "9f2c41ab"
source path -> project-relative, forward slashes -> DreamShader.SourceFileWhat is hashed
The hash covers the text the parser actually sees, after import inlining — not the bytes of the file on disk. That is the single most surprising property of the cache.
| Change | Changes the hash of |
|---|---|
edit M_Foo.dsm | M_Foo.dsm |
edit Common.dsh, imported by M_Foo.dsm and M_Bar.dsm | both M_Foo.dsm and M_Bar.dsm |
| move the project to another directory | nothing — the stored path is project-relative |
| rename the source file | the stored path no longer matches, so nothing is skipped |
| reformat whitespace or edit a comment | the hash — the text is compared byte for byte, not semantically |
Editing a .dsh invalidates every dependent .dsm and .dsf, but a header never generates anything
by itself — saving it fails with DreamShader header '{File}' does not generate assets directly. Recompile dependent .dsm or .dsf files instead. The dependents are rebuilt only when they are
themselves compiled: on their own save, through Generate all in-memory materials, through the
Material Content Browser's Compile button, through the
commandlet, or at cook.
Where the metadata lives
Two keys are written into the generated asset's package metadata, keyed by the asset object. These are the only two keys DreamShader ever sets — nothing writes a generation timestamp.
| Key | Value |
|---|---|
DreamShader.SourceFile | the source path made relative to the project directory, with forward slashes. A source outside the project keeps its absolute path. |
DreamShader.SourceHash | the eight-hex-digit CRC32. Written only when non-empty. |
Storing the project-relative path is deliberate: a checkout on another machine, or a moved project
directory, still recognizes its own generated assets instead of regenerating everything.
DreamShader.SourceFile doubles as the ownership marker — its presence is what the guard above
tests, and what Clean Persisted Generated Assets filters on.
Which assets get stamped, and when:
| Asset | Stamped |
|---|---|
UDreamShaderMaterialInstance (ThinCustom) | always — memory-only and persisted alike |
the hidden MB_DreamThinBase_* base | persist mode only |
UMaterial (Graph backend) | persist mode only |
UMaterialFunction / layer / layer blend | persist mode only |
A ThinCustom instance additionally carries the source path and the hash as read-only UPROPERTYs —
SourceFilePath and SourceHash, category DreamShader — so they are visible in the details panel
without inspecting package metadata. SourceFilePath holds the full normalized source path, not
the project-relative form the metadata stores.
When regeneration is skipped
The short circuit fires only when all of the following hold:
| # | Condition |
|---|---|
| 1 | the generation call did not set bForce |
| 2 | the asset exists and the newly computed hash is non-empty |
| 3 | the stored DreamShader.SourceFile is present and non-empty |
| 4 | the stored source path equals the project-relative path of the source being compiled, ignoring case |
| 5 | the stored DreamShader.SourceHash equals the new hash, case-sensitively |
| Asset | Skip point | Extra condition | Message |
|---|---|---|---|
| ThinCustom material | after the instance is created or reused, before the hidden base is created | — | Skipped {AssetPath} from {File}; source hash is unchanged. |
Graph-backend material | after the material is created or reused | — | Skipped {AssetPath} from {File}; source hash is unchanged. |
| Material function | after the function asset is created or reused | the asset's material-function usage must already match the one the block requires | silent — the asset path is returned with no message |
Placing the ThinCustom check before the base is created is what makes a skip cheap: no base material,
no ownership check, no graph teardown. A material function whose usage does not match — a
ShaderLayer block whose asset is still marked Default, for instance — is regenerated even when
the hash matches, and the usage is corrected.
Forcing a rebuild
| Path | Force |
|---|---|
| Auto-compile on save | no — the hash short circuit is active |
| Generate all in-memory materials (startup, backend-setting change) | yes |
| Material Content Browser Compile / thumbnail refresh | yes |
| Live preview render | yes |
| Materialize, and child-instance creation | yes |
| Cook | yes |
Commandlet -run=DreamShader | only with -Force; otherwise it reports Skipped {AssetPath} from {SourceFile}; source hash is unchanged. |
There is no way to clear the stored hash from the source language. To force a rebuild without a force-capable entry point, either change the source text — any change, including whitespace — or delete the generated asset.
Notes
- Regeneration is not undoable. Generated material instances are deliberately not
RF_Transactional, because undo/redo desynchronizes the shader map. - The safest mental model: treat the
.dsm/.dsfas the asset. Anything you want to persist belongs in the source. - A regeneration that is skipped by the source hash does none of the above — the asset is not touched at all.
- Deleting the generated asset and recompiling is always equivalent to a forced regeneration, except that a material function's pin GUIDs are lost and its call sites break.
- The hash is a CRC32, not a cryptographic digest. It detects edits; it is not an integrity mechanism.
- The generated
.ushhelper include is not covered by this short circuit. It is rewritten on every compile of a unit that declaresFunctionblocks. - The decompiler is the way to capture hand edits: export the edited
material back to
.dsm/.dsf, then make that the source of truth.
Diagnostics
| Message | Cause | Fix |
|---|---|---|
| Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your shader or move/delete the existing asset before regenerating. | The ownership guard, on a material. | Rename the Shader, or move or delete the hand-authored asset. |
| Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your function or move/delete the existing asset before regenerating. | The ownership guard, on a material function. | |
| Asset '{ObjectPath}' already exists and is not a Material. | Graph backend, a non-UMaterial at the path. | Details |
| Asset '{ObjectPath}' already exists and is not a DreamShader instance material. Delete it (or remove Backend="Instance") before switching backends. | ThinCustom backend, wrong class at the path. | Details |
| Asset '{ObjectPath}' already exists and is not a MaterialFunction asset. | A function kind, wrong class at the path. | |
| Asset '{ObjectPath}' already exists as '{ActualClass}', but {Kind} generation requires '{ExpectedClass}'. Delete or move the existing asset and regenerate it. | A function kind, wrong material-function subclass. | |
| Generated DreamShader asset '{Path}' could not be saved. | The package save failed after a successful rebuild. | |
| Generated DreamShader asset packages could not be saved. | The paired instance and base save failed. | |
| In-memory material mode: '{PackageName}' already exists as a saved asset, which shadows in-memory regeneration. Delete the saved asset to make it fully in-memory. | A log warning: a saved asset shadows a memory-only rebuild. | Details |
| Skipped {AssetPath} from {File}; source hash is unchanged. | Not an error — the short circuit fired. | Pass -Force, or change the source text, to rebuild anyway. |
| DreamShader header '{File}' does not generate assets directly. Recompile dependent .dsm or .dsf files instead. | A .dsh was compiled directly. |
Worked example
Shader(Name="Docs/M_Regen")
{
Properties {
ScalarParameter Intensity = 2.0 [Group="Look"; SortPriority=10];
VectorParameter Tint = float4(1.0, 0.4, 0.1, 1.0) [Group="Look"];
}
Settings { Domain = "UI"; ShadingModel = "Unlit"; }
Outputs { vec3 Color; Base.EmissiveColor = Color; }
Graph { Color = Tint.rgb * Intensity; }
Layout { Node(Var="Color", X=-400, Y=0); }
}Hand-edit the generated asset, then save the .dsm again:
before regeneration after regeneration
----------------------------------------------- --------------------------------------------
comment "DreamShader: Output: EmissiveColor" recreated
comment "Reviewed 2026-07-30" KEPT — no DreamShader: prefix
extra Multiply node wired in by hand deleted
Two Sided ticked in the material editor reset to false (not declared in Settings)
Intensity override = 5.0 on the instance cleared, back to the source default 2.0
Color node dragged to (900, 400) back to (-400, 0), pinned by LayoutAnd the metadata on the generated instance:
DreamShader.SourceFile DShader/Docs/M_Regen.dsm
DreamShader.SourceHash 9f2c41ab