DreamShaderLang
生成与产物

重新生成

一次重建会毁掉什么、什么能留下来,手改检测与三个出口,归属守卫,以及决定要不要重建的构建键。

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

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

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

一次会毁掉手改的重建,不会发生。 since 1.8.0 每次成功生成都会给资产打一个指纹, 而发现指纹对不上的重建会停下来、让你在三个动作里选一个 —— 见下面的「手改检测」。

所以下面这张表描述的,是一次重建对仍然没被动过的资产做了什么,以及 Revert to Source 对一个被动过的资产做了什么。

什么能留下来

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

执行顺序

  1. 资产开着编辑器就拒绝,然后指纹对不上就拒绝 —— 两者都在下面任何一步之前,也在任何一步变得 不可逆之前。since 1.8.0
  2. 对目标对象调用 Modify()
  3. 清除生成的注释 —— 每一个文本以字面量 DreamShader: 开头的 UMaterialExpressionComment
  4. 清空每一个材质属性输入,从第一个材质属性槽到最后一个。
  5. 删除图中的每一个表达式
  6. 把材质重置为默认值 —— 见下面的"重置属性"。材质函数跳过这一步。
  7. 应用 Settings
  8. 重建:Properties 节点、Graph 主体或整表面 Custom 节点、Outputs 绑定。
  9. 布局 —— 纯内存模式下也会运行 since 1.6.0,见项目设置
  10. 重新编译。

第 4 步现在只有一套策略:整张图作为一个整体被摘下来。逐节点删除原本是为了断开入边,而当图整体离开时这件事 没有意义,还是 O(n^2) 的,所以原来 1200 个表达式的阈值在 1.8.0 里消失了,每次重建都走快路径。 材质路径还会重置材质的编辑器参数缓存;材质函数路径不会。

重建是原子的

since 1.8.0

第 1–9 步要么全部生效、要么一条都不生效。旧图是被摘下来而不是被销毁的,只有在重建完整成功之后才真正销毁; 中途任何一点失败,都会在编译返回之前把它放回去 —— 节点、连线、渲染状态、材质函数 usage 和 pin GUID 一并回来。 一次失败的编译留下的资产,就是它开始时的那个资产。

这件事非做不可,是因为不是所有失败都能提前拦住:整文件解析、Settings 校验和 Outputs 校验确实都是在 资产被碰之前跑的闸门,但 Graph 块是由图构建器一句一句编的,跑在第 7 步 —— 也就是拆除之后。 在 1.8.0 之前,这类失败会留下一个被清空的资产:材质如此已经很糟,材质函数更糟 —— 它的调用点是从活着的 资产上读 pin 的,一个坏掉的 .dsf 会把每一个调用它的材质一起带走,而且没有 undo(生成资产刻意不是 RF_Transactional 的)。

回滚有两个细节让它真的成立:快照是关掉 delta 序列化取的(否则等于类默认值的属性会被跳过,恢复时就会留下失败的 那次构建设进去的值),而且它覆盖资产的编辑器专用数据对象 —— UE 5.1 起,表达式集合和材质属性输入住在一个独立的 UMaterialEditorOnlyData / UMaterialFunctionEditorOnlyData 对象上,只给材质本身做快照,抓到的不过是一个指向它的指针。

回滚唯一恢复不了的是第 2 步删掉的 DreamShader: 注释框 —— 它们必然删在快照之前,因为先取快照就会捕获到 一批已经消失的注释的引用。它们是生成出来的装饰,下一次成功编译会重新造出来。你自己写的注释从来不带这个前缀, 一开始就不会被删。

唯一能留下来的手改

文本不带 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 一视同仁。

since 1.8.0 起,这件事不再是静默的:参数覆盖被算进输出指纹,所以下一次重建会被手改检测拦下来, 让你在 Revert / Adopt / Detach 里选。在此之前没有任何诊断 —— 下次编译源文件时,这些值就是没了。

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

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

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

手改检测

since 1.8.0

source hash 给编译读的源码打指纹,它对那次编译产出的资产一无所知。所以一个被你手动改过的资产 看上去仍然是干净的:下一次它的 .dsm 一动,重建就把图拆了、重建了,手改没了,没有诊断,也没有 undo。

输出指纹是缺的另一半:每次成功生成的末尾,把资产实际持有的内容写成 DreamShader.OutputDigest; 下一次重建在清空资产之前拿它比一次,回答 source hash 回答不了的那个问题 —— 有人在这里面动过手吗?

DreamShader.SourceFile 决定归属,指纹决定内容是否还是我们的:

状态含义对重建的影响
外来(Foreign)没有 DreamShader.SourceFile —— 不是 DreamShader 生成的归属守卫拒绝,什么都不碰
未打标(Unstamped)是我们的,但没有可比对的指纹正常重建,并补上指纹
已生成(Generated)是我们的,内容与指纹相符正常重建
已分歧(Diverged)是我们的,内容与指纹不符拒绝 —— 见下

「未打标」覆盖两种无害情况:指纹机制存在之前生成的资产,以及指纹带着不同 schema 标签的资产。标签是指纹格式版本 加引擎版本(DSD1-5.8),因为指纹遍历的属性集合本来就是引擎的 —— 没有它,升一次引擎会把整个工程的资产一次性 重新指纹化,然后把整个材质库报成「被手改过」。

什么算手改

规则是指纹恰好覆盖一次重建会摧毁的东西。重建本来就不碰的属性,能原样活过重建,因此拿它挡住一次重建, 等于为了保护一个从来没有危险的东西而白白拒掉一次编译。

改动算分歧吗
加、删或重连一个节点
改生成节点上的属性(包括参数的默认值)
重连一个材质属性输入(Outputs 绑定)
「重置属性」里的任何一项
材质函数的 DescriptionUserExposedCaptionExposeToLibraryLibraryCategories、usage
生成的 ThinCustom 实例上的参数覆盖
实例的父级,或隐藏 base 材质的图
把节点拖到别的位置不算
手加一个注释框(任何文本)不算
节点的注释气泡、折叠状态、预览可见性、Desc不算
命名 reroute 的显示颜色不算
重置列表之外的材质属性 —— 预览网格、缩略图、物理材质不算
pin Id GUID 与命名 reroute 的变量 GUID不算 —— 这是身份,刻意跨重建保留

命名 reroute 的显示颜色是从它自己的对象路径名派生的,而这个名字每次节点被重建都会变。把它排除在外, 既因为它是装饰,也因为留着它会让同一份未改源码的两次重建互相不一致

拒绝长什么样

生成失败,资产保持你离开时的样子 —— 没有清空,也没有半成品:

Asset '/Game/Materials/M_Emissive.M_Emissive' has been edited by hand since DreamShader generated it
from 'DShader/Materials/M_Emissive.dsm', so it was NOT rebuilt (rebuilding would destroy those
edits). Right-click the asset > DreamShader and choose one: 'Revert to Source' discards the edits and
rebuilds, 'Adopt Into Source' rewrites 'DShader/Materials/M_Emissive.dsm' from the edited asset,
'Detach From DreamShader' hands the asset over to you and stops managing it.

检查跑在任何东西改动资产之前。对材质函数来说这个顺序就是全部意义所在:闸门在 usage 被重新打标之前就开火, 而不只是在拆图之前。

这道闸门和上面的原子重建回答的是两个不同的问题,而且两个你都想要:闸门拦住一次本来会成功的重建覆盖掉你手写的 东西;原子性拦住一次失败的重建把资产留成碎片。谁也盖不住谁的场景。

-Force 过不了这道闸门bForce 回答的是「source hash 过期了吗」,而编辑器在自己的启动扫描里对每个文件 都置了它 —— 认它就等于在编辑器唯一常驻的模式里让闸门直接失效。只有 Revert 能覆盖一次分歧判定, 因为只有人能做这个决定。

只有源码动过时才会尝试重建。没动过的源码早在闸门之前就被 source hash 跳过了,所以「手改了资产但没动它的源码」 这件事根本不会报任何东西 —— 因为没有任何东西处于危险中。

三个出口

三个都在资产右键菜单的 DreamShader 下,材质、材质函数、layer、layer blend 和生成的 ThinCustom 实例一视同仁。

动作做什么
Revert to Source丢掉手改、按源码重建。源文件不动。这是「源码才是对的」那个答案。已保存的资产重建后保存,纯内存的资产在内存里重建 —— 反过来做会把手改留在磁盘上、还报告成功,下个会话再把同一份分歧读回来。
Adopt Into Source用资产当前的内容反写 .dsm / .dsf,让手改成为真相来源,然后重新编译让两边一致。这是「资产才是对的」那个答案。
Detach From DreamShader丢掉全部 DreamShader.* 标记。资产保留内容,变成一个 DreamShader 永远不会再重建的普通手写资产。之后它就是「外来」的,所以编译原本拥有它的源码会撞上归属守卫,直到你给两者之一改名或挪位置。要保存资产才能留住这次改动 —— detach 只改内存。

Adopt Into Source 的三个细节:写任何东西之前,旧源码先被复制成 <source>.bak;源文件声明了不止一个资产时 拒绝执行;文件自身的形态 —— 手写注释、import 指令、排版 —— 会被替换成反编译器的输出。

拒绝多资产文件是必要的:反编译器发射的是一个块而不是一个翻译单元, 所以从一个声明了好几个资产的文件里 adopt 其中一个,会静默删掉其余的。这种情况请用 Export DSM 然后手工合并。

纯内存资产也会被打指纹和源码路径(但不打 source hash —— 那会把跳过重建也一起打开)。没有这条路径, 内存资产会被判成「外来」,闸门在编辑器的默认模式里根本不会触发。

资产开着编辑器时拒绝重建

since 1.8.0

资产开在某个资产编辑器里时,重建被直接拒绝:

Asset '{ObjectPath}' is open in an asset editor, so it was NOT rebuilt. An open editor works on its
own copy of the asset and writes that copy back when you press Apply or Save, which would silently
undo this rebuild. Close the editor and compile again.

原因是资产编辑器并不编辑资产本身FMaterialEditor 把资产复制成一个临时的 UPreviewMaterial, 在 Apply 或 Save 时用 UpdateOriginalMaterial 把副本盖回原件;材质实例编辑器则通过 UMaterialEditorInstanceConstant 包装写回。一个跨越了重建仍然开着的编辑器,手里握的是重建之前的副本, 下一次 Apply 会把重建做的一切还原 —— 然后以一次手改检测的形式出现在再下一次编译里,离病因已经很远。

拒绝是这里唯一安全的答案。那个副本里有没有未 Apply 的改动,是 FMaterialEditor::bMaterialDirty, 它对 MaterialEditor 模块是私有的 —— 所以「干净就关掉、脏就拒绝」不是这个插件问得出来的问题。 盲目关掉更糟:引擎的保存提示会在一次「保存即编译」的中途弹出来,还会把无头构建挂死。

两个来历动作是例外。 Revert to SourceAdopt Into Source 自己关掉编辑器、动手、再打开 —— 因为你刚刚点了它们,很可能就是从那个编辑器自己的工具栏上点的,而一个在最有用的地方永远是灰的菜单项不是保护, 是 bug。关闭过程中可能出现引擎的保存提示;对 Adopt 来说它是承重的而不是噪音,因为未 Apply 的编辑器改动 在这个提示被回答之前,不算「这个资产当前的内容」。取消它就取消整个动作。

归属守卫

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

项目说明
触发条件目标 package 存在于 磁盘上,且已有对象 没有 DreamShader.SourceFile 元数据
适用于每一种 backend、每一种块(ThinCustom 实例路径自 1.8.0 起)
结果生成失败;已有资产原封不动
消息针对
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 需要保护,所以检查不会执行。 这个缺口由另一侧补上 —— 手改检测在内存里同样生效,因为源码路径在那边也会被打上。

目标路径上放着类不对的资产,是在问归属之前就被一次类检查挡掉的:

Asset '{ObjectPath}' already exists and is not a DreamShader instance material. Delete it (or remove
Backend="Instance") before switching backends.

1.8.0 之前,ThinCustom 实例路径只检查类、不检查来历 —— 于是往一个放着手写 UDreamShaderMaterialInstance 的路径上生成,会直接接管并重建它,顺手清空它的参数覆盖。ThinCustom 是默认 backend,这是三条创建路径里覆盖面最广、却唯一没有这个检查的一条。现在它跑的守卫和另外两条一样。

重置属性

在图被重建之前,材质的渲染状态会按下表顺序恢复成这些值。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
bUsedWithVolumetricCloud since 1.8.0Domain="Volume" 时为 true,否则 false

bUsedWithVolumetricCloud 是上表里唯一不是常量的一项:由 domain 决定。Volume domain 的材质就是体积 材质,而体积云渲染器会拒绝一个没打这个标志的材质 —— 所以你永远不需要手写它。显式写出来可以覆盖 domain 的选择: 一个只喂体积雾的 Volume 材质可以用 bUsedWithVolumetricCloud = "false"; 把标志关掉,省下云的 shader 排列。

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

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

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

构建键(source hash)

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

项目
被哈希的内容预处理后的源码,加上编译它的上下文 since 1.8.0
算法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 两个都变
把项目移到别的目录什么都不变 —— 存的是项目相对路径
重命名源文件存下来的路径对不上了,于是什么都不会被跳过
重排空白或改注释哈希会变 —— 文本是逐字节比较的,不是语义比较
Default Compiler Backend每一个源文件 since 1.8.0
改 shading model / blend mode / domain 映射表每一个源文件 since 1.8.0
升级插件,或升级引擎每一个源文件 since 1.8.0

它是一把构建键,不只是源码哈希

since 1.8.0

这个 stamp 给源码在编译它的上下文里打指纹。任何会改变某份源码产出的东西都被折进来, 因为把它漏掉,就意味着跳过检查会对一个其实已经不是最新的资产回答「还是最新的」:

输入为什么在键里
预处理后的源码文本编译真正的输入,import 已经内联 —— 这也是改过的 .dsh 或被调用的 .dsf 不需要额外条目的原因
Default Compiler Backend决定一个 Shader 块变成 UMaterial 还是一个薄实例
三张映射表决定 Settings 的键解析成什么
插件版本,加一个手动 bump 的格式标签升级生成器会让旧生成器写出的东西作废
引擎版本能生成什么会随它变化(Substrate 就是一例)

在此之前键只哈希源码文本,于是改 backend 会让每个已生成的资产都看起来仍是最新的 —— 这正是那一个设置当年 外挂了一次「强制全量重扫」的原因。现在那次强制没有了:受影响的资产各自过不了跳过检查,而受影响的 资产仍然被跳过,不做无谓重建。

改变键的组成会让所有既有 stamp 作废 —— 每个资产重建一次,一次而已,这正是预期效果。

一批文件按依赖顺序编译

since 1.8.0

一批里同时有函数和调用它的东西时,批次会被排成依赖先编。这不是锦上添花:一个调用 ShaderFunction.dsm 会把调用节点绑到活着的 UMaterialFunction 资产上(SetMaterialFunction 是从对象而不是从源码上读 pin 的), 所以先编调用方,就绑到了那个函数上一版的接口。改一个函数输入的名字、两个文件一起保存就能撞上, 而谁赢取决于待编译文件表的遍历顺序。

两个入口 —— watcher 的待编译批次和全工程扫描 —— 都按 import 图做拓扑排序;只有批次内部的边被考虑, 成环则留给 import 加载器用 DreamShader import cycle detected at '{File}'. 拒绝。

编辑一个 .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 设置变更)
编辑器启动扫描 since 1.8.0 —— 见内存材质
Material Content Browser 的 Compile / 缩略图刷新
实时预览渲染
Materialize 与子实例创建
Cook
Commandlet -run=DreamShader仅在带 -Force 时;否则报 Skipped {AssetPath} from {SourceFile}; source hash is unchanged.

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

说明

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

诊断

消息触发原因处理
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.日志警告:已保存的资产遮蔽了纯内存重建。详解
Asset '{ObjectPath}' has been edited by hand since DreamShader generated it from '{SourceFile}', so it was NOT rebuilt (rebuilding would destroy those edits). …输出指纹对不上 —— 资产被手改过。-Force 不能绕过。右键资产 ▸ DreamShader,在 Revert to Source / Adopt Into Source / Detach From DreamShader 里选一个。
Asset '{ObjectPath}' is open in an asset editor, so it was NOT rebuilt. An open editor works on its own copy of the asset and writes that copy back when you press Apply or Save, which would silently undo this rebuild. Close the editor and compile again.目标资产开在某个资产编辑器里。关掉那个编辑器再编译。Revert / Adopt 会自己关它。
Skipped {AssetPath}; another editor owns this project's DreamShader bridge, and only that one writes generated assets to disk.同一工程开了第二个编辑器,它不是 bridge 的持有者。详解
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 —— 自 1.8.0 起这会先被手改检测拦下来, 下面这张表是你选了 Revert to Source 之后发生的事:

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

继续阅读

本页目录