DreamShaderLang
生成与产物

重新生成

一次重建会毁掉什么、什么能留下来,归属守卫,以及决定要不要重建的 source hash 缓存。

生成出来的资产是源码的产物,不是文档。 当源文件被再次编译时,生成的图会被拆掉,然后从 .dsm.dsf 重建。 资产内部的每一处手改都会被毁掉,只有一个例外。

把这句话记住,本页剩下的内容基本都能顺推出来。

项目说明
适用于所有生成出来的 UMaterialUDreamShaderMaterialInstanceUMaterialFunctionUMaterialFunctionMaterialLayerUMaterialFunctionMaterialLayerBlend
触发于任何没有被 source hash 跳过的编译
效果生成的图被拆掉并从源码重建

什么能留下来

手改内容能否在重新生成后留存
文本 不以 DreamShader: 开头的注释框
文本以 DreamShader: 开头的注释框不能 —— 被删除
你手动加的节点不能 —— 被删除
对生成节点做的属性微调不能 —— 节点被删掉并重建
节点位置不能,除非被 Layout section 钉住
在编辑器里改过的材质设置不能 —— 下面"重置属性"里的每一项都会恢复默认值,然后重新应用 Settings
生成的 ThinCustom 实例上的参数覆盖不能 —— 见下面的警告
材质函数上的 FunctionInput / FunctionOutput pin 身份 since 1.3.2
命名 reroute 的变量 GUID能 —— 只有在无效时才重新生成

执行顺序

  1. 对目标对象调用 Modify()
  2. 清除生成的注释 —— 每一个文本以字面量 DreamShader: 开头的 UMaterialExpressionComment
  3. 清空每一个材质属性输入,从第一个材质属性槽到最后一个。
  4. 删除图中的每一个表达式
  5. 把材质重置为默认值 —— 见下面的"重置属性"。材质函数跳过这一步。
  6. 应用 Settings
  7. 重建:Properties 节点、Graph 主体或整表面 Custom 节点、Outputs 绑定。
  8. 布局 —— 纯内存模式下跳过。
  9. 重新编译。

第 4 步有两套策略。表达式少于 1200 个时,通过材质编辑库逐个删除节点,最多 64 轮外层遍历,并报告 Deleting old Material node '{Name}'...。达到 1200 及以上时,整个表达式集合会被一次性解除根引用并标记为垃圾。 材质路径还会重置材质的编辑器参数缓存;材质函数路径不会。

整文件解析、Settings 校验和 Outputs 校验都在目标资产被创建或清空 之前 执行,所以任何一项失败的源文件都不会动到 之前生成好的资产。但 Graph内部 的语法错误不属于这些闸门:语句解析器在第 4 步之后才跑,因此这类失败会留下一个被清空的资产。

唯一能留下来的手改

文本不带 DreamShader 前缀的注释框。

项目
前缀DreamShader: —— 单词、冒号,加一个尾随空格
比较区分大小写
效果文本以该前缀开头的注释在重建前被删除;其它注释一概不动

dreamshader: NotesDREAMSHADER: NotesDreamShader:Notes(没有空格)都通不过前缀检测,因此都会 留下来。 这是官方支持的、手工标注生成材质的方式。

推论:把生成的注释框从 DreamShader: Sampling 改名成 Sampling,它就变成永久的了,而下一次重新生成会在它上面再创建 第二个DreamShader: Sampling 的框。想让 DreamShader 自己的框保持同步,就别动它们的文本,改源码 Layout section 里的 Comment(Name=…) 条目。

被刻意保留的身份

有两种身份被刻意保留下来,好让已有的调用点不被打断。

  • 材质函数 pin。 在图被清空之前,每个 UMaterialExpressionFunctionInputUMaterialExpressionFunctionOutputId GUID 会按名字缓存起来,然后恢复到同名的新建 pin 上。项目里别处的 MaterialFunctionCall 节点因此能在该函数重新生成后保住连线。since 1.3.2
  • 命名 reroute。 声明的变量 GUID 只有在现有的那个无效时才重新生成。

因此在源码里重命名一个输入或输出,对它的调用点来说是 破坏性变更:旧名字的 GUID 没有可以恢复的目标了。

生成实例上的参数覆盖

ThinCustom backend 下,重新生成会对发出的 UDreamShaderMaterialInstance 调用 ClearParameterValuesEditorOnly()在生成实例上手工设置的每一个参数覆盖,都会在每次重新生成时被清空 —— 标量、向量、纹理、static switch、static component mask 一视同仁。没有任何诊断;下次编译源文件时,这些值就是没了。

替代做法: 永远不要直接调生成出来的实例。要么

  • 把值挪进源码,作为 Properties 的默认值,这样生成的实例自带它;要么
  • 创建一个以生成实例为父级的 UMaterialInstanceConstant,在子实例上覆盖。子实例是普通资产,重新生成永远不会碰它; 而且由于生成实例持有静态排列,子实例共享它的 shader map,不额外增加编译开销。

Material Content Browser 的实例创建操作生成的正是这样一个子实例,位置在 <parent directory>/<Instance Subfolder> —— 见 内存材质

归属守卫

DreamShader 拒绝覆盖不是自己生成的资产。

项目说明
触发条件目标 package 存在于 磁盘上,且已有对象 没有 DreamShader.SourceFile 元数据
适用于Graph backend 下的 Shader,以及 ShaderFunction / ShaderLayer / ShaderLayerBlend
结果生成失败;已有资产原封不动
消息针对
Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your shader or move/delete the existing asset before regenerating.材质
Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your function or move/delete the existing asset before regenerating.材质函数

对不在磁盘上的 package,这个守卫是不生效的:纯内存资产没有已保存的 package 需要保护,所以检查不会执行。

ThinCustom 实例路径上没有归属守卫。 在默认 backend 下把一个 Shader 生成到已经放着手写 UDreamShaderMaterialInstance 的路径上,只会检查类,不会检查来源元数据 —— 然后直接重建它,并清空它的参数覆盖。 该路径上 其它 任何类的手写资产仍然会被拒绝,报 Asset '{ObjectPath}' already exists and is not a DreamShader instance material. Delete it (or remove Backend="Instance") before switching backends.

替代做法: 不要手写 UDreamShaderMaterialInstance 资产。子实例一律用普通的 UMaterialInstanceConstant, 守卫的类检查会直接把它挡掉。

重置属性

在图被重建之前,材质的渲染状态会按下表顺序恢复成这些值。Settings 在之后才应用,所以你声明的键会赢; 你 没有 声明的项则一律回到下表的值,不管材质编辑器里之前是什么。

属性重置为
BlendModeBLEND_Opaque
MaterialDomainMD_Surface
shading modelMSM_DefaultLit
TwoSidedfalse
OpacityMaskClipValue0.3333
Wireframefalse
DitheredLODTransitionfalse
DitherOpacityMaskfalse
bAllowNegativeEmissiveColorfalse
bCastDynamicShadowAsMaskedfalse
bCastRayTracedShadowstrue
bEnableResponsiveAAfalse
bScreenSpaceReflectionsfalse
bContactShadowsfalse
bDisableDepthTestfalse
bOutputTranslucentVelocityfalse
bWriteOnlyAlphafalse
BlendableOutputAlphafalse
TranslucencyLightingModeTLM_VolumetricNonDirectional
bTangentSpaceNormaltrue
bAlwaysEvaluateWorldPositionOffsetfalse
bFullyRoughfalse
bIsSkyfalse
bIsThinSurfacefalse
MaterialDecalResponseMDR_ColorNormalRoughness
bHasPixelAnimation since UE 5.4false
NumCustomizedUVs0

材质函数没有渲染状态;重新应用的是它们的资产级字段:

源码设置字段未声明时
DescriptionDescription清空
UserExposedCaptionUserExposedCaption清空
ExposeToLibrarybExposeToLibrary设为 false
LibraryCategoriesLibraryCategoriesText —— 逗号分隔,各项去空白,空项丢弃清空

每次重新生成还会按块类型重新打上 material function usage。

source hash 缓存

重新生成的开销不小,所以未改动的源文件会被完全跳过。

项目
被哈希的文本预处理后 的源码 —— 递归内联了每一个 import 的那份文件
算法FCrc::StrCrc32,按 %08x 格式化 —— 八位小写十六进制
存储位置生成资产的 package 元数据,以资产对象为键
绕过方式生成入口上的 bForce 标记
prepared text  ->  CRC32  ->  "%08x"  ->  DreamShader.SourceHash   e.g. "9f2c41ab"
source path    ->  project-relative, forward slashes  ->  DreamShader.SourceFile

哈希覆盖什么

哈希覆盖的是解析器真正看到的那份文本,也就是 import 内联 之后 的文本,而不是磁盘上文件的字节。这是这套缓存里最容易让人意外的一点。

改动会改变哪些文件的哈希
编辑 M_Foo.dsmM_Foo.dsm
编辑被 M_Foo.dsmM_Bar.dsm 导入的 Common.dshM_Foo.dsmM_Bar.dsm 两个都变
把项目移到别的目录什么都不变 —— 存的是项目相对路径
重命名源文件存下来的路径对不上了,于是什么都不会被跳过
重排空白或改注释哈希会变 —— 文本是逐字节比较的,不是语义比较

编辑一个 .dsh 会让每一个依赖它的 .dsm.dsf 失效,但头文件自己永远不生成任何东西 —— 保存它会失败并报 DreamShader header '{File}' does not generate assets directly. Recompile dependent .dsm or .dsf files instead. 这些依赖方只有在它们自己被编译时才会重建:保存它们自己、执行 Generate all in-memory materials、 点 Material Content Browser 的 Compile 按钮、跑 commandlet,或者 cook。

元数据存在哪

有两个键写进生成资产的 package 元数据,以资产对象为键。这也是 DreamShader 唯二会写的键 —— 没有任何地方写生成时间戳。

DreamShader.SourceFile相对于项目目录的源码路径,使用正斜杠。项目之外的源码保留绝对路径。
DreamShader.SourceHash八位十六进制的 CRC32。只在非空时写入。

项目相对 路径是刻意的:换台机器检出、或者把项目目录挪个地方,它仍然认得出自己生成的资产,而不会把一切重新生成一遍。 DreamShader.SourceFile 同时充当 归属标记 —— 上面那个守卫检查的就是它,Clean Persisted Generated Assets 过滤的也是它。

哪些资产会被打标记,以及什么时候打:

资产打标记时机
UDreamShaderMaterialInstance(ThinCustom)总是 —— 纯内存和已持久化都打
隐藏的 MB_DreamThinBase_* base仅持久化模式
UMaterial(Graph backend)仅持久化模式
UMaterialFunction / layer / layer blend仅持久化模式

ThinCustom 实例还额外把源码路径和哈希作为只读 UPROPERTY 携带 —— SourceFilePathSourceHash,分类 DreamShader —— 所以不用翻 package 元数据就能在详情面板里看到。SourceFilePath 存的是 完整规范化 的源码路径, 而不是元数据里那种项目相对形式。

什么时候会跳过重新生成

短路只在 全部 条件成立时才触发:

#条件
1这次生成调用没有设置 bForce
2资产存在,且新算出的哈希非空
3存下来的 DreamShader.SourceFile 存在且非空
4存下来的源码路径等于正在编译的源码的项目相对路径,忽略大小写
5存下来的 DreamShader.SourceHash 等于新哈希,区分大小写
资产跳过点附加条件消息
ThinCustom 材质实例创建或复用之后,隐藏 base 创建 之前Skipped {AssetPath} from {File}; source hash is unchanged.
Graph backend 材质材质创建或复用之后Skipped {AssetPath} from {File}; source hash is unchanged.
材质函数函数资产创建或复用之后资产的 material function usage 必须已经和块要求的一致无消息 —— 直接返回资产路径

把 ThinCustom 的检查放在 base 创建之前,正是跳过之所以便宜的原因:不建 base、不做归属检查、不拆图。 而 usage 不匹配的材质函数 —— 比如一个 ShaderLayer 块对应的资产还标着 Default —— 即使哈希相同也会重新生成,并顺手纠正 usage。

强制重建

路径是否强制
保存时自动编译 —— 哈希短路生效
Generate all in-memory materials(启动、backend 设置变更)
Material Content Browser 的 Compile / 缩略图刷新
实时预览渲染
Materialize 与子实例创建
Cook
Commandlet -run=DreamShader仅在带 -Force 时;否则报 Skipped {AssetPath} from {SourceFile}; source hash is unchanged.

源语言里没有清除已存哈希的办法。要在没有强制入口的情况下强制重建,要么改动源码文本(任何改动,包括空白),要么删掉生成的资产。

说明

  • 重新生成不可撤销。 生成的材质实例刻意不是 RF_Transactional,因为撤销/重做会让 shader map 失步。
  • 最稳妥的心智模型:把 .dsm / .dsf 当成资产本身。任何你想保留的东西都应该写在源码里。
  • 被 source hash 跳过的那次重新生成,上面这些一件都不做 —— 资产完全没有被碰。
  • 删掉生成的资产再重新编译,效果永远等价于一次强制重新生成,唯一的差别是材质函数的 pin GUID 会丢失,它的调用点会断。
  • 这个哈希是 CRC32,不是密码学摘要。它用来检测改动,不是完整性机制。
  • 生成的 .ush helper include 受这个短路保护。只要该单元声明了 Function 块,每次编译都会重写它。
  • 反编译导出是把手改捞回来的正道:把改过的材质导出回 .dsm / .dsf,然后以那份源码为准。

诊断

消息触发原因处理
Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your shader or move/delete the existing asset before regenerating.材质上的归属守卫。给 Shader 换个名字,或把手写资产移走 / 删掉。
Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your function or move/delete the existing asset before regenerating.材质函数上的归属守卫。
Asset '{ObjectPath}' already exists and is not a Material.Graph backend,路径上不是 UMaterial。详解
Asset '{ObjectPath}' already exists and is not a DreamShader instance material. Delete it (or remove Backend="Instance") before switching backends.ThinCustom backend,路径上的类不对。详解
Asset '{ObjectPath}' already exists and is not a MaterialFunction asset.函数类块,路径上的类不对。
Asset '{ObjectPath}' already exists as '{ActualClass}', but {Kind} generation requires '{ExpectedClass}'. Delete or move the existing asset and regenerate it.函数类块,material function 子类不对。
Generated DreamShader asset '{Path}' could not be saved.重建成功后 package 保存失败。
Generated DreamShader asset packages could not be saved.实例与 base 这一对保存失败。
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.日志警告:已保存的资产遮蔽了纯内存重建。详解
Skipped {AssetPath} from {File}; source hash is unchanged.不是错误 —— 短路生效了。加 -Force,或改动源码文本,即可强制重建。
DreamShader header '{File}' does not generate assets directly. Recompile dependent .dsm or .dsf files instead.直接编译了一个 .dsh。

完整示例

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); }
}

手改生成出来的资产,然后再保存一次 .dsm

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 Layout

生成实例上的元数据:

DreamShader.SourceFile   DShader/Docs/M_Regen.dsm
DreamShader.SourceHash   9f2c41ab

继续阅读

本页目录