DreamShaderLang
生成与产物

内存材质

为什么编译好的 DreamShader 材质不出现在 Content Browser,隐藏 base 材质是什么,以及怎么把它写到磁盘。

你保存了一个 .dsm,日志说材质已生成,然后 Content Browser 里什么都没有。这不是坏了。本页解释真正发生了什么, 建议在提 bug 之前先读一遍。

项目说明
适用于ThinCustom backend(项目默认)和 Graph backend 下的 Shader
发出的类UDreamShaderMaterialInstance(ThinCustom)或 UMaterial(Graph)
标记所在 package 仍带着 PKG_NewlyCreated
起始版本since 1.5.0 —— ThinCustom backend 与 Content Browser 可见性开关

DreamShader 材质到底是什么

在默认 backend 下,一个 Shader不会 产出 UMaterial 资产。它产出两个对象:

UDreamShaderMaterialInstance          "M_Emissive"            <- the asset you reference
  └─ UMaterial (subobject, hidden)    "MB_DreamThinBase_M_Emissive"
       └─ the generated node graph

节点图 —— 每个 Properties 节点、每条 Graph 语句、每条 Outputs 绑定 —— 都构建在这个 隐藏 base 上。实例只是个薄壳, 携带参数值、来源元数据和编译好的 shader map。Settings 里的东西全部落在 base 上,这也是为什么从生成出来的实例上读 BlendMode 看到的是 继承来的 值。

成员可见性含义
SourceFilePath详情面板中只读,分类 DreamShader生成这个实例的 .dsm
SourceHash详情面板中只读,分类 DreamShadersource hash —— 见 重新生成
Parent标准隐藏的 base 材质

两个 override 决定了这个类的行为,而且两个都很关键。

Override结果后果
HasOverridenBaseProperties()当且仅当父级是 UMaterial 时为 true 实例拥有自己的静态排列和 shader map;挂在它下面的子 UMaterialInstanceConstant 走回引擎默认行为,共享 这份 shader map,于是几十个颜色或参数变体只花一次编译
IsAsset()当 package 仍是 PKG_NewlyCreated Show In-Memory Materials In Content Browser 关闭时为 false纯内存材质对 Content Browser、资产选择器和保存选择器隐身

第二个 override 就是"我的材质去哪了"的全部答案。

隐藏 base

模式base 对象名Outer对象标记
纯内存MB_DreamThinBase_<sanitized Name>transient packageRF_PublicRF_StandaloneRF_Transient
已持久化MB_DreamThinBase_<instance leaf name>实例对象自身RF_PublicRF_Standalone

<sanitized Name> 是块的完整逻辑 Name,其中 [A-Za-z0-9_] 之外的每个字符都换成 _,连续下划线折叠,所以 Shader(Name="Mat/Test") 得到 MB_DreamThinBase_Mat_Test。这个净化不是为了好看:FName 里的 / 会被读成子对象分隔符, 那样 base 就无法复用,每次重新生成都会泄漏一个新 base。

持久化模式下 base 是实例的子对象,因此它作为普通 export 序列化 进实例自己的 package。一个资产、一个 .uasset、 Content Browser 里没有 MB_DreamThinBase_* 兄弟资产,cook 时也不会丢跨 package 的父级 import。由于非 package 的 outer 本身就让 IsAsset() 为 false,base 在两种模式下都不可见。

由 1.5.0 之前的版本保存、父级位于独立 MB_* package 中的实例 不会 被复用。重新生成会创建一个新的子对象 base, 把旧的兄弟 package 留成孤儿。这个孤儿无害,可以删掉。

为什么 Content Browser 里什么都没有

交互式编译是刻意做成纯内存的。.dsm 文件才是创作面,而磁盘上躺着的生成 .uasset 会遮蔽它 —— 你会同时拥有两个事实来源, 且分不清谁赢。除 cook、commandlet 和显式 Materialize 之外的所有触发方式都在内存里生成;见 什么会触发编译

当材质处于纯内存状态时:

  • IsAsset() 返回 false,所以它不出现在任何 Content Browser 视图和资产选择器里。
  • 生成结束时它的 package 被标记为非脏,因此 Save All 和退出提示都无法悄悄把它落盘。
  • 但它是完全可用的 —— 通过 Material Content Browser、实时预览,以及任何持有它指针的东西。

可见性开关

项目
设置项Show In-Memory Materials In Content Browser,分类 Compiler
默认
菜单Tools ▸ DreamShader ▸ Show In-Memory Materials
另一处DreamShader Project 页面上的复选框

切换会写入项目配置,并立刻为每个 package 仍是 PKG_NewlyCreated 的活 UDreamShaderMaterialInstance 广播 asset-created / asset-deleted 事件,所以图块会即时出现和消失,不需要重新扫描。确认消息是:

Showing {Count} in-memory material(s) in the Content Browser and asset pickers.
Hidden {Count} in-memory material(s) from the Content Browser and asset pickers.

开关打开后,纯内存材质看上去就是一个普通图块 —— 对它执行显式 Save 会真的往磁盘写出一个 .uasset。这个已保存资产 随后就会遮蔽该路径上的内存重新生成。真的想要文件时,请用 Materialize,不要用 Save

落盘(Materialize)

Materialize 会以"开启持久化 + 强制"重新执行该材质自己源文件的生成,然后在解析出的路径上重新加载对象。

入口操作
Material Content Browser 的 Gen 页Materialize 按钮 —— "Write this memory-only material (and its base) to disk."
Content Browser 右键菜单DreamShader materialize 操作
隐式为纯内存父材质创建子材质实例时,会先把父级落盘

创建子实例 必须 先落盘,因为 transient 的 base 不能作为父级 import。子实例的默认目标是 <parent directory>/<Instance Subfolder> —— 即 Material Instance Subfolder 项目设置,默认 Instances; 留空则创建在父级旁边。子实例命名为 MI_<parent leaf>,并做唯一化处理。

已经持久化的材质会原样返回。

消息原因
This material is memory-only and has no DreamShader source file to materialize from.对象不是 UDreamShaderMaterialInstance,或它的 SourceFilePath 为空
Failed to materialize the material to disk: {Message}用于落盘的那次重新生成失败了
Materialized the material but could not reload it at {ObjectPath}.生成成功,但对象无法被重新加载回来

磁盘上已经存在的资产

存储决定一次重建怎么落地,而不是发起编译的那一方决定。 since 1.8.0 一次纯内存编译落在磁盘上 已经存在的 package 上时,资产会被重建并保存,而不是在自己的文件之上于内存里重建。生成成功,并记一条:

'{ObjectPath}' exists as a saved asset, so it is rebuilt and saved on disk rather than in memory. Run
Tools > DreamShader > Clean Persisted Generated Assets to make it memory-only.
项目说明
适用于每一种触发方式 —— 因为每一次交互式编译要的都是纯内存
1.8.0 —— 在此之前,这样一次编译在内存里重建资产,然后清掉脏标记

旧行为产出的是一个既不等于磁盘上的文件、也不等于将来会被写出的任何东西、还自称干净的对象 —— 于是一次 Save All 就能落下一个谁也没选过的状态,而你在编辑器里看到的版本会在重启后消失。 现在「这个资产到底是什么」重新只有一个答案:上一次编译产出的东西,无论这个资产住在哪。

一个值得知道的推论:启动扫描不再强制重建。在每个内存资产无论如何都会重建时,强制是免费的; 一旦磁盘资产开始被保存,它就不免费了 —— 每次启动都会重写每一个持久化的生成资产,带来磁盘写入、 版本控制噪音和更慢的启动,而这些重建早已被 source hash 排除掉了。现在启动遵守 source hash 跳过,所以一个已经最新的已保存资产不会被动。 改 Default Compiler Backend 仍然强制,因为哈希看不见那个设置。

有两个命令处理磁盘上的这些资产:

命令效果
Tools ▸ DreamShader ▸ Clean Persisted Generated Assets删除带 DreamShader 来源元数据的已保存资产,并弹出确认框逐个列出。手写资产绝不会被碰。空的情况:No persisted DreamShader-generated assets found.
Tools ▸ DreamShader ▸ Clean Generated Shaders删除生成着色器目录下的每个 *.ush,并排入一次全量重编

清理器会递归扫描 /Game,寻找 package 存在于磁盘、且元数据中 DreamShader.SourceFile 非空的 UMaterialUMaterialFunctionUDreamShaderMaterialInstance 资产。

修改 Default Compiler Backend 设置会在内存中重新生成全部内容,随后如果还有已保存的生成资产,会警告:

{Count} previously generated asset(s) are still saved on disk and shadow the in-memory materials.
Run Tools > DreamShader > Clean Persisted Generated Assets to remove them.

(自 1.8.0 起,这些资产其实不再遮蔽任何东西 —— 它们只是仍然在磁盘上,并且就在磁盘上被重建。 这条通知是指给想要「纯内存」终态的人看的。)

Cook 行为

cook 才是这些资产变成实体的时候。

方面行为
判定-run= 的值里含 Cook 就认为该进程是 cook
谁来生成只有 cook director —— 以 -cookworker 启动的进程跳过生成,直接加载 director 保存的结果
何时post-engine-init,即引擎子系统就绪之后、commandlet 的 Main 之前
生成什么.dsh 外的所有项目 DreamShader 源文件,强制生成并持久化
资产注册表生成实际写出的每个 package 都会在 commandlet 的 Main 之前交给 IAssetRegistry::ScanModifiedAssetFiles since 1.8.0
失败时中止整个 cook

cook 日志:

DreamShader cook: generating {Count} source file(s) as persistent assets...
  [Cook] {Message}
  [Cook] Failed: {Message}
DreamShader cook asset generation complete.

只要有一个文件生成失败,cook 就会以一条 fatal 日志结束: DreamShader cook generation failed for {Count} source file(s); aborting the cook. See the [Cook] Failed entries above. 在 cook 之前,先在编辑器里把每个源文件干净地编译一遍,或者跑一次 commandlet

生成放在 post-engine-init 而不是模块启动时,是因为它依赖的材质编辑库需要编辑器子系统,而那些在模块加载期还不存在。 限定只在 director 上跑,则避免了所有 worker 争抢保存同一批 package。

注册进资产注册表这一步是必要的,不是保险。 since 1.8.0 一个 cook 请求是经 IAssetRegistry::DoesPackageExistOnDisk 解析的,而这个函数只查注册表的内存状态、没有文件系统兜底 —— 而生成恰好跑在注册表枚举完内容目录之后。所以在此之前,刚写出的 package 对那次查询是不可见的: DirectoriesToAlwaysCook 把文件从磁盘上扫了进来,然后请求又被丢掉了,全程没有任何报错 —— 材质根本没进 pak,LoadObject 在 Shipping 构建里才失败。现在会记一条 DreamShader cook: registered {Count} generated package(s) with the AssetRegistry.

说明

  • IsMemoryOnly 只由一件事决定:package 是否仍带着 PKG_NewlyCreated。除此之外没有任何东西区分内存材质和已持久化材质。
  • 1.6.0 起,纯内存材质也会跑自动布局,所以保存之后你看到的图和生成资产是一样的。 Project Settings ▸ … ▸ Compiler ▸ Lay Out In-Memory Graphs 可以关掉它,关掉之后这些图保留构建阶段的 坐标 —— 一根高柱子。见项目设置
  • 来源元数据在 两种 模式下都会打在 ThinCustom 实例上,持久化模式下还会额外打在 base 上。Graph backend 材质和材质函数 只在持久化时才打。
  • 生成出来的实例刻意 不是 RF_Transactional:材质实例不支持撤销/重做,否则 shader map 会失步。
  • 这些都不影响 source hash 短路;纯内存编译在哈希未变时同样会跳过工作。

完整示例

Shader(Name="Materials/M_Emissive")
{
    Properties { ScalarParameter Intensity = 2.0 [Slider(0, 10)]; }
    Settings   { Backend = "ThinCustom"; ShadingModel = "Unlit"; }
    Outputs    { vec3 Color; Base.EmissiveColor = Color; }
    Graph      { Color = vec3(1.0, 0.4, 0.1) * Intensity; }
}

在编辑器里保存这个文件,只在内存中产生:

package        /Game/Materials/M_Emissive          (PKG_NewlyCreated, not dirty, not on disk)
  object       M_Emissive                          UDreamShaderMaterialInstance
    subobject  MB_DreamThinBase_Materials_M_Emissive   UMaterial, holds the node graph

执行 Materialize 之后:

on disk        <Project>/Content/Materials/M_Emissive.uasset
  export       M_Emissive                          UDreamShaderMaterialInstance
  export       MB_DreamThinBase_M_Emissive         UMaterial (hidden, same package)

注意 base 的名字在两种模式下不同:内存里是净化后的完整 Name,磁盘上是实例的 leaf 名。

继续阅读

本页目录