差异
差异
差异
DreamShader » 生成 » 差异
当一个生成的资产被手动编辑后会发生什么:DreamShader 如何察觉、为何停止,以及三种解决方式。
| 适用于 | 每一个生成的 UMaterial、UDreamShaderMaterialInstance、UMaterialFunction、UMaterialFunctionMaterialLayer、UMaterialFunctionMaterialLayerBlend;UDreamPassPipeline (自 2.1.0 起) —— 参见自定义 Pass 管线 |
| 存储于 | 生成资产的包元数据中,键为 DreamShader.OutputDigest |
| 检查时机 | 在重建清除资产之前立即检查,之后绝不再检查 |
| 起始版本 | 1.8.0 |
它解决的问题
源哈希(缓存)为一次编译所读取的源文件建立指纹。它对那次编译产生的资产只字未提。因此,你手动编辑过的资产看起来仍然未被改动:下一次它的 .dsm 发生变化时,重新生成会拆掉图表并重建,你的编辑就此消失,既没有诊断也没有撤销。
输出摘要是另一半:它是资产实际内容的指纹,写入每一次成功生成的末尾。在重建之前将其与资产进行比对,可以回答源哈希无法回答的问题——有人在这里动过手脚吗?
状态
DreamShader.SourceFile 决定归属;摘要决定内容是否仍属于我们。
| 状态 | 含义 | 对重建的影响 |
|---|---|---|
| 外部 | 没有 DreamShader.SourceFile —— DreamShader 从未生成过它 | 归属守卫会拒绝;不触碰任何内容 |
| 未盖章 | 属于我们,但没有可比较的摘要 | 正常重建,并重新盖章 |
| 已生成 | 属于我们,且内容与盖章匹配 | 正常重建 |
| 已调整 (自 1.9.0 起) | 属于我们,内容匹配,且 ThinCustom 实例带有参数覆盖 | 正常重建,并且覆盖会被恢复 |
| 已分歧 | 属于我们,但内容不再匹配 | 拒绝 —— 见下文 |
Unstamped 涵盖两种情况,都是无害的:一种是在摘要机制存在之前生成的资产,另一种是其盖章带有不同 schema 标签的资产。该标签由摘要格式版本、引擎版本以及资产所使用的每个表达式类的反射布局指纹组成(DSD5-5.8-1a2b3c4d),因为摘要遍历的属性集合是引擎的。没有版本号,升级引擎会一次性为项目中的每个资产重新生成指纹,并把整个库报告为被手动编辑;没有布局指纹,一个源码构建的引擎在两次会话之间为某个表达式类添加一个引脚,也会对使用该类的每个资产造成同样后果 (自 1.9.0 起)。
指纹涵盖的类就是生成器放置的那些类,它们记录在盖章旁边的第二个元数据键中,即 DreamShader.OutputDigestClasses。检查会针对那个列表而不是当前图表重新计算标签,因此你手动添加的节点——通常是生成图表从未有过的类——会被比较并读取为 Diverged,而不会移动标签并作为 schema 变更而蒙混过关。没有该列表而写入的盖章会回退到当前图表的类,并在下一次重建时随着列表一起重写。
什么算作手动编辑
规则是:摘要恰好涵盖重建会销毁的内容。重建从不触碰的属性会在重建后原样保留,因此因为该属性而阻止重建,等于为了保护从未处于危险之中的东西而拒绝重建。
| 更改 | 是否分歧 |
|---|---|
| 添加、删除或重新连接节点 | 是 |
| 更改生成节点上的属性(包括参数的默认值) | 是 |
GetMaterialAttributes 发布哪些特性,或 SetMaterialAttributes 设置哪些特性 | 是 (Get:自 2.0.0 起) |
重新连接材质属性输入(Outputs 绑定) | 是 |
| 重置属性中的任何属性 | 是 |
材质函数的 Description、UserExposedCaption、ExposeToLibrary、LibraryCategories、用法 | 是 |
| 实例的父级,或隐藏基础材质的图表 | 是 |
| 生成的 ThinCustom 实例上的参数覆盖 | 否 —— 反而会 Tweaked,且重建会恢复它 (自 1.9.0 起) |
生成实例上参数覆盖的 ExpressionGUID —— UE 在包加载时会填充它 | 否 (直到 2.1.1,首次重新加载带有覆盖的 .dsi 实例会被读取为手动编辑,其重建和烘焙会被拒绝) |
| 将节点拖动到新位置 | 否 |
| 手动添加的注释框(任何文本 —— 参见重新生成) | 否 |
节点的注释气泡、折叠状态、预览可见性、Desc | 否 |
| 命名重路由的显示颜色 | 否 |
| 重置列表之外的材质属性 —— 预览网格、缩略图、物理材质 | 否 |
引脚 Id GUID 和命名重路由变量 GUID | 否 —— 这些是标识,有意在重建之间保留 |
| 仅名称的大小写 —— 引脚、参数、输出、引用的资产 | 否 (2.1.2 之后) —— 参见下方说明 |
[!NOTE] 命名重路由从自身的对象路径名称中获取显示颜色的种子,而该名称在节点每次被重建时都会改变。它被排除在外,既因为它是装饰性的,也因为这个原因:把它留在摘要中,会使同一个未更改源的两次重建彼此不一致。
[!NOTE] 摘要不依赖于编辑器运行的语言 (自 2.0.0 起)。它为节点的输入命名,而三个引擎节点用翻译后的文本为其输入命名 ——
BreakMaterialAttributes,以及GetMaterialAttributes/SetMaterialAttributes,它们的引脚显示特性的显示名称 —— 因此直到 1.9,由英文编辑器盖印的层在中文编辑器中会被读取为Diverged。现在这些输入由特性本身命名。格式标签随之从DSD3变为DSD4,因此每个资产在升级后都会读取为一次Unstamped。
[!NOTE] 名称在比较时不区分大小写 (2.1.2 之后):一个
FName值,无论是单独出现还是位于数组或结构体中,一个输入的名称,一个对象路径。Unreal 就是这样比较名称的,而一个包中每个名称只保留一种拼写:在一个同时包含Result的包中,生成时写作result的函数输出会以Result加载回来。直到 2.1.2,这样的资产在首次由新会话再次编译时会读取为Diverged,重建被拒绝。格式标签从DSD4变为DSD5,因此每个资产在升级后都会读取为一次Unstamped。
生成的 ThinCustom 实例上的参数覆盖
(自 1.9.0 起)
在 ThinCustom 后端下,图表位于隐藏的基础 UMaterial 上,而生成资产是它的一个瘦 UDreamShaderMaterialInstance —— 即你调优的表面。直到 1.9.0,该实例上的覆盖会像其他内容一样被摘要处理,因此拖动一个滑块就会拒绝该 .dsm 未来的每一次重建:该插件最普通的用法被判为违规,唯一的出路是还原,而这会丢弃你刚刚设置的值。
现在它有自己的状态。Tweaked 表示“已生成,且已调优”;关卡只回应 Diverged,因此它不会阻止任何操作。
| 步骤 | 发生什么 |
|---|---|
| 重建之前 | 实例自身携带的每个覆盖都会被读取——标量、向量、纹理、字体、静态开关、静态组件遮罩,以及此引擎版本拥有的其他每一种类型 |
| 期间 | 基础图被拆除并重建,ClearParameterValuesEditorOnly 清空该实例,与之前完全相同 |
| 之后 | 每个捕获的值都在相同的名称和类型下写回,然后更新静态排列,使着色器映射与恢复的开关匹配 |
| 重建不再声明的某个名称 | 丢弃,并在一行 DSH8155 日志中列出所有被丢弃的参数 |
恢复按名称进行,因为名称是唯一能在生成器拆除并从头重建的图中幸存下来的东西——引脚 ID 和节点标识则不能。因此在源中重命名一个参数会丢失其覆盖,这是源的决定,而不是插件能够事后质疑的。
[!NOTE] 只有成功路径才会恢复。失败的重建会回滚基础图,永远不会到达清除步骤,因此实例在编译失败后其覆盖保持原样。
[!WARNING] 采用到源 仍然拒绝携带覆盖的实例,这一点没有改变:反编译器 写入基础图,而基础图无处安放实例级覆盖,因此采用会写入一个除你实际所做的编辑之外描述一切的源。将值作为
Properties默认值移入源中——或在你自己的子材质实例上覆盖它们——然后再采用。
拒绝是什么样子
生成失败时资产完全保持你离开时的样子——没有被清除,也没有被构建一半。编译报告 DSH8207,其消息携带资产层的 DSH8115 文本:
Asset '/Game/Materials/M_Emissive.M_Emissive' was edited by hand since DreamShader generated it from
'DShader/Materials/M_Emissive.dsm', so it was NOT rebuilt (rebuilding would destroy those edits).
Use the notification, or right-click the asset > DreamShader, and choose one: Revert to Source,
Adopt Into Source, or Detach From DreamShader.[!NOTE] 该消息在
1.9.0之前是四句话,并在日志中解释了三种答案中的每一种。现在它是两句话,解释已移至此页面以及下面描述的通知上的工具提示中——旧文本太长,以至于说明该做什么的部分出现在人们已经停止阅读的部分之后。
检查在资产被改变之前运行。对于材质函数来说,这个顺序就是全部意义所在:门禁在使用被重新标记之前触发,而不仅仅是在图拆除之前。
此门禁和 原子重建 回答的是两个不同的问题,而你两个都需要。门禁阻止一个本会成功的重建覆盖你手动完成的工作;原子性阻止一个失败的重建使资产支离破碎。两者都不能覆盖对方的情况。
[!IMPORTANT]
-Force不会绕过这一点。bForce仅仅意味着“忽略源哈希”,而 Recompile DSM、材质内容浏览器的编译、Materialize 以及 cook 都会设置它——在这里遵守它会恰好在整个项目被重建时使门禁失效。只有 Revert 操作会覆盖分歧,因为只有人才能做出那个决定。
对未更改的源进行非强制编译会在询问门禁之前被源哈希跳过,因此手动编辑资产而让其源保持不变不会报告任何内容,直到源更改或强制编译到达该资产——在那之前没有任何东西处于危险之中。
你会看到什么 (自 1.9.0 起)
拒绝还会在编辑器右下角引发一个通知,携带答案本身。它会指出资产和源,并且不会超时——这是一个问题,而一个淡出消失的问题是你从未回答过的问题。
| 按钮 | 作用 | 等同于 |
|---|---|---|
| Revert to Source | 丢弃编辑并重建 | Revert to Source |
| Adopt Into Source | 从资产重写源 | Adopt Into Source |
| Detach | 停止管理该资产 | Detach From DreamShader |
| Show Ephemeral Materials | 在内容浏览器中显示该资产——仅当它是一个隐藏的 Ephemeral 实例时 | 工具菜单开关 |
| Dismiss | 关闭通知,不改变任何内容 | — |
前三个与右键菜单运行的是相同的代码,因此每个仍然会用对话框确认,仍然会关闭并重新打开任何打开的资产编辑器,并且之后仍然会重新编译。除 Show Ephemeral Materials 之外的所有按钮都会关闭通知:那一个的存在是为了让你能够去查看一个你看不到的资产,而在那一刻把三个答案拿走会让你面对一个可见的图块却无事可做。
第四个按钮正是通知存在的原因。在编辑器的默认 Ephemeral 状态 下,生成的 ThinCustom 实例在内容浏览器中没有图块,除非 Show Ephemeral Materials 处于开启状态——因此“右键单击该资产”指代的是屏幕上不存在的东西,而三种答案恰恰对最有可能发生分歧的资产不可达。
它出现的频率
| 规则 | 效果 |
|---|---|
| 每资产每轮一个通知 | 一个源两次报告同一个资产,或两个源到达它,仍然只产生一个 |
| 第一个仍在屏幕上时不出现第二个通知 | 反复保存损坏的源不会堆叠提示 |
| 一轮中超过 5 个资产 | 其余的折叠为一条携带 Open Material Browser 的摘要 |
| 新一轮 | 仍然存在分歧的资产会再次被报告 |
一轮是你所要求的一批工作:一次保存(连同其防抖窗口扫入的一切),或 Recompile DSM / Clean Generated Shaders 背后的一次全项目扫描,或从 材质内容浏览器 或桥接单独请求的一次编译。一批内部的编译属于该批次的轮,而不是各自开启一轮。
因此一个拥有五十个手动编辑资产的项目在完全重新编译时最多产生六条通知,而不是五十条。摘要在该轮结束时发出,因为只有那时总数才为人所知。
[!NOTE] 通知从不会以无头方式发出。命令行工具、cook 以及任何带有
-unattended的运行都会与之前完全相同地获得日志行和 诊断;没有可点击的东西,也没有人点击它。关闭编辑器会撤下任何仍在等待的通知。
日志行、VSCode 问题面板以及材质内容浏览器的每源状态保持不变,仍然是持久的记录——通知是在事情发生的时刻摆在你面前的部分,而不是你事后去寻找的部分。
三条出路
这三者都位于资产的右键菜单中 DreamShader 之下,适用于材质、材质函数、层、层混合以及生成的 ThinCustom 实例——并且自 1.9.0 起,也位于拒绝所引发的通知上。
Revert to Source
丢弃手动编辑并从源重建。源文件不会被修改。这是“源终究是对的”这一答案。
恢复一个已保存的资产会重建并保存它;恢复一个仅存在于内存中的资产会在内存中重建它。反过来做会将编辑留在磁盘上并报告成功,而下一次会话会直接从包中读回相同的分歧。
Adopt Into Source
从资源的当前内容重写 .dsm / .dsf,使手工编辑成为事实来源,然后重新编译,让两者再次一致。这就是“资源才是对的”这个答案。
| 备份 | 在写入任何内容之前,先把之前的源文件复制到 <source>.bak |
| 拒绝条件 | 源文件声明了多个资源 |
| 拒绝条件 | 源文件中包含预处理器指令 —— DSH8149 (自 1.9.0 起) |
| 替换 | 文件自身的形态 —— 手写注释、import 指令和格式都会变成反编译器的输出 |
这两种拒绝出于同一个原因:采纳会用单个资源重写整个文件,而文件中说过、但资源并不持有的任何内容都会丢失。
多资源拒绝之所以重要,是因为反编译器只输出一个代码块,而不是一个翻译单元,所以从一个声明了多个资源的文件中采纳其中一个,会悄悄地删除其他资源。
指令拒绝之所以重要,是因为生成的资源只持有裁剪之后的结果 —— 即实际所走的那条分支,没有任何记录表明曾经存在过分支 —— 所以用它重写源文件会抹掉文件中的每一个 #if,并把最后恰好参与构建的那个配置冻结下来。该门禁对任何指令都会触发,无论是否被采用,一个只含有 #define 的文件也会被拒绝;#Region 不是预处理器指令,不会触发它。
无论哪种情况,请使用 Export DSM 并手工合并到正确的分支。
[!NOTE] VirtualFunction 启动同步也会拒绝条件源文件,并给出
DSH9001。它不是本页上的三个操作之一 —— 它是在编辑器启动时无人值守运行的,而不是从菜单中运行的 —— 但它是源文件的另一个写入者,并且它按字节偏移进行拼接,而行数守恒并不能保持这一点。
从 DreamShader 分离
清除所有 DreamShader.* 标记。资源保留其内容,并变为一个普通的手工编写资源,DreamShader 将永远不会再重建它。这就是“停止管理这个”这个答案。
之后该资源就是 Foreign,因此编译过去拥有它的源文件会因所有权保护而失败,直到你重命名或移动两者之一。保存资源以保留该更改 —— 分离只在内存中编辑它。
自定义 Pass 管线
(自 2.1.0 起) 一个 UDreamPassPipeline 没有图表,是要手工调节的:它的细节面板会编辑它,下一帧就会运行该编辑。它像其他所有生成资源一样受到保护,而采纳就是调节结果到达其 .dsp 的方式。
| 摘要 | 资源除其两个源标记(SourceFilePath、SourceHash)之外的每一个属性,各自完整,以导出文本形式呈现:顺序、默认注入点、视图、需求、启用的参数、参数、缓冲区、带各自槽位的 pass,以及导出渲染目标。模式标签会为管线类及其内容所触达的每一个结构体生成指纹,因此添加了字段的插件更新会被读取为一次 Unstamped,而不是一次手工编辑 |
| 手工编辑 | 细节面板中的任何更改 —— 一个值、一个 pass 被添加、移动或移除 —— 都会被读取为 Diverged。面板的 Source 行会随变化显示该状态 |
| 门禁 | 对已更改的 .dsp 的下一次编译会被拒绝,并给出通知及其选项。不会写入任何内容:不写资源,不写槽位注册表,不写快照,不写渲染目标 |
| 选项 | 通知上的 Revert to Source、Adopt Into Source 和 Detach,以及材质内容浏览器中的 —— 其 Provenance 部分及其上下文菜单;前两个也是细节面板上的按钮。管线没有内容浏览器上下文菜单项 |
采纳到 .dsp
一个 .dsp 永远不会被重新打印:采纳会把资源的值拼接进文件,就像它对一个 .dsi 所做的那样。
- 读写文件并像编译那样检查它,在构建任何内容之前停止。
- 资源被反编译为
.dsp所绑定的载荷。 - 资源仍然持有且未变的每个值都会取回文件自身的拼写形式:解析到同一资源或文件的材质或着色器保留其书写时的引用,精度与资源所保持的一致(一个
float)的数值保留其字面量或折叠出它的表达式 —— 包括一个static const—— 以相同位设置的标志保留其书写形式,即使某个键陈述的是默认值,只要写出来了就保持写出。参数、缓冲区和 pass 按名称匹配,绑定按位置匹配。 - 只重写值不同的声明和键,因此
//注释和文件的顺序得以保留。当有任何更改时,文件会先被复制到<file>.bak。 - 管线从该文件重新构建,越过分歧门禁,并保存。
当文件已经陈述了每一个值时,不会写入任何内容,管线只会被重新构建。
| 拒绝条件 | 代码 |
|---|---|
| 文件使用了预处理器指令 —— 拼接针对的是文件的字节,而解析器读取的是预处理后的文本 | DSH8149 |
资源所标记的源不是 .dsp | DSH9227 |
从 .dsp 构建的资源不是 pass 管线 | DSH9226 |
| 文件检查不通过 —— 它所命名的材质不再能解析、着色器文件已消失 —— 因此没有可供检查重写的东西 | 该文件自身的诊断 |
| 文件不再构建任何管线 | DSH9228 |
资源持有的内容是一个 .dsp 无法陈述的:一个既没有材质也没有着色器、或两者都有的全屏 pass,层级表没有名称的层级位,完全没有视图,一个不检查任何使用标志的网格 pass,一个 param 中的纹理常量,一个没有对应缓冲区的自身深度 | DSH9211、DSH9212、DSH9215、DSH9216、DSH9217、DSH9219、DSH9220 |
| 源文件随插件一起发布 | —— 该操作被禁用 |
与每一次采纳一样,会先弹出询问,然后关闭并重新打开打开在该管线上的资源编辑器。
备注
- 对于仅存在于内存中的资源,摘要也会被标记,连同源路径和哈希一起。没有路径,资源会被读取为
Foreign,门禁永远不会触发 —— 这会使它在编辑器默认的 Ephemeral 状态下成为死局。 - 摘要只会与携带相同模式标签的摘要进行比较。更改标签是弃用某种摘要格式而不惊动全世界的受支持方式。
1.9.0更改了它 —— 把实例的覆盖项从摘要中移除改变了摘要的构成 —— 所以每一个由更早版本标记的资源都会读取为一次Unstamped:正常重建,并重新标记。它不会拒绝任何东西,而在那第一次重建之前所做的手工编辑不受门禁保护。 - 门禁保护的是资源,而不是源文件。除了采纳操作之外,这里的任何内容都不会写入
.dsm,而采纳操作会先询问并备份。