重新生成
一次重建会毁掉什么、什么能留下来,归属守卫,以及决定要不要重建的 source hash 缓存。
生成出来的资产是源码的产物,不是文档。 当源文件被再次编译时,生成的图会被拆掉,然后从 .dsm 或 .dsf 重建。
资产内部的每一处手改都会被毁掉,只有一个例外。
把这句话记住,本页剩下的内容基本都能顺推出来。
| 项目 | 说明 |
|---|---|
| 适用于 | 所有生成出来的 UMaterial、UDreamShaderMaterialInstance、UMaterialFunction、UMaterialFunctionMaterialLayer、UMaterialFunctionMaterialLayerBlend |
| 触发于 | 任何没有被 source hash 跳过的编译 |
| 效果 | 生成的图被拆掉并从源码重建 |
什么能留下来
| 手改内容 | 能否在重新生成后留存 |
|---|---|
文本 不以 DreamShader: 开头的注释框 | 能 |
文本以 DreamShader: 开头的注释框 | 不能 —— 被删除 |
| 你手动加的节点 | 不能 —— 被删除 |
| 对生成节点做的属性微调 | 不能 —— 节点被删掉并重建 |
| 节点位置 | 不能,除非被 Layout section 钉住 |
| 在编辑器里改过的材质设置 | 不能 —— 下面"重置属性"里的每一项都会恢复默认值,然后重新应用 Settings |
| 生成的 ThinCustom 实例上的参数覆盖 | 不能 —— 见下面的警告 |
材质函数上的 FunctionInput / FunctionOutput pin 身份 | 能 since 1.3.2 |
| 命名 reroute 的变量 GUID | 能 —— 只有在无效时才重新生成 |
执行顺序
- 对目标对象调用
Modify()。 - 清除生成的注释 —— 每一个文本以字面量
DreamShader:开头的UMaterialExpressionComment。 - 清空每一个材质属性输入,从第一个材质属性槽到最后一个。
- 删除图中的每一个表达式。
- 把材质重置为默认值 —— 见下面的"重置属性"。材质函数跳过这一步。
- 应用
Settings。 - 重建:
Properties节点、Graph主体或整表面Custom节点、Outputs绑定。 - 布局 —— 纯内存模式下跳过。
- 重新编译。
第 4 步有两套策略。表达式少于 1200 个时,通过材质编辑库逐个删除节点,最多 64 轮外层遍历,并报告
Deleting old Material node '{Name}'...。达到 1200 及以上时,整个表达式集合会被一次性解除根引用并标记为垃圾。
材质路径还会重置材质的编辑器参数缓存;材质函数路径不会。
整文件解析、Settings 校验和 Outputs 校验都在目标资产被创建或清空 之前 执行,所以任何一项失败的源文件都不会动到
之前生成好的资产。但 Graph 块 内部 的语法错误不属于这些闸门:语句解析器在第 4 步之后才跑,因此这类失败会留下一个被清空的资产。
唯一能留下来的手改
文本不带 DreamShader 前缀的注释框。
| 项目 | 值 |
|---|---|
| 前缀 | DreamShader: —— 单词、冒号,加一个尾随空格 |
| 比较 | 区分大小写 |
| 效果 | 文本以该前缀开头的注释在重建前被删除;其它注释一概不动 |
dreamshader: Notes、DREAMSHADER: Notes 和 DreamShader:Notes(没有空格)都通不过前缀检测,因此都会 留下来。
这是官方支持的、手工标注生成材质的方式。
推论:把生成的注释框从 DreamShader: Sampling 改名成 Sampling,它就变成永久的了,而下一次重新生成会在它上面再创建
第二个 叫 DreamShader: Sampling 的框。想让 DreamShader 自己的框保持同步,就别动它们的文本,改源码
Layout section 里的 Comment(Name=…) 条目。
被刻意保留的身份
有两种身份被刻意保留下来,好让已有的调用点不被打断。
- 材质函数 pin。 在图被清空之前,每个
UMaterialExpressionFunctionInput和UMaterialExpressionFunctionOutput的IdGUID 会按名字缓存起来,然后恢复到同名的新建 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 在之后才应用,所以你声明的键会赢;
你 没有 声明的项则一律回到下表的值,不管材质编辑器里之前是什么。
| 属性 | 重置为 |
|---|---|
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 |
材质函数没有渲染状态;重新应用的是它们的资产级字段:
| 源码设置 | 字段 | 未声明时 |
|---|---|---|
Description | Description | 清空 |
UserExposedCaption | UserExposedCaption | 清空 |
ExposeToLibrary | bExposeToLibrary | 设为 false |
LibraryCategories | LibraryCategoriesText —— 逗号分隔,各项去空白,空项丢弃 | 清空 |
每次重新生成还会按块类型重新打上 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.dsm | M_Foo.dsm |
编辑被 M_Foo.dsm 和 M_Bar.dsm 导入的 Common.dsh | M_Foo.dsm 和 M_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 携带 —— SourceFilePath 和 SourceHash,分类
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,不是密码学摘要。它用来检测改动,不是完整性机制。
- 生成的
.ushhelper 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