项目结构
DShader/ 怎么组织、插件如何贡献自己的源码根、哪些目录归插件所有,以及决定什么会被编译的 Package 规则。
DreamShaderLang 的源文件放在项目根目录下的一个目录里 —— 默认是 DShader,由
Project Settings ▸ DreamPlugin ▸ Dream Shader ▸ Paths ▸ Source Directory 配置。相对路径相对项目
目录解析。
源目录、它的 Packages 子目录和生成 shader 目录都由插件在模块启动时创建,也就是编辑器第一次启动时,
无论你有没有写过任何源文件。
自 since 1.6.0 起,这不再是唯一的根:每个带 DShader 目录的已启用插件都贡献一个自己的源码根 ——
见下面的插件源码根。
目录树
插件源码根
since 1.6.0一个插件可以自带那些构建它材质的 .dsm / .dsf / .dsh,而不必把它们停在工程的树里。
Root="Plugin.<Name>" 从 1.2.0 起就能把资产写进插件;缺的一直是源码这一半。
| 行为 | 规则 |
|---|---|
| 发现 | 无需配置。开关是 Project Settings ▸ … ▸ Paths ▸ Scan Plugin Source Directories(默认开) |
| 默认资产目标 | 插件根下没写 Root= 的文件生成到它自己插件的挂载点 —— /MoonToon,不是 /Game。只有缺失或全空白的 Root 才被默认化,所以 Root="/" 可以显式选回 /Game |
| import | 永不跨根。一个文件只针对自己所在的根及该根的 Packages 解析 import;刻意跨根要写 import "Plugin.MoonToon:Shared/Toon.dsh"; —— 见 import |
| 可写性 | 只有工程根是可写的。VirtualFunction 同步跳过插件根下的文件:插件按作者写的样子交付定义,编辑器不再改写它们 |
| 监视器 | 每个根注册一个监视,所以插件的源文件同样享受 Auto Compile On Save |
| workspace | DreamShader.code-workspace 每个根一个 folders 条目 —— 工程是 ".",插件是 Plugin: <Name> |
| Gen 页 | 插件根下的文件在行副标题里标出根名,搜索框也匹配根名 —— 输入插件名就能筛出它带的全部文件 |
和某个既有根重叠的插件 DShader 目录会被忽略并给出警告,而不是把同一个文件交给两个所有者。
把 Source Directory 指向一个插件目录、或指向一个包含插件目录的位置时,就会出现这种情况。
根列表是缓存的,只在 Source Directory 或那个扫描开关变化、或按下 DreamShader Gen ▸ Refresh 时重建 ——
后者正是用来拾取「你刚建的 DShader 文件夹」或「会话中途挂载的插件」的。但 watcher 只在启动时注册一次,
所以会话中途才出现的根,它的 Auto Compile On Save 要到下次编辑器启动才开始工作。
各目录职责
| 目录 | 归属 | 内容 |
|---|---|---|
DShader/ | 你 | 源文件根目录。下面怎么分都行,这里给的是约定而不是规则。 |
DShader/Materials/ | 你 | 材质 .dsm,通常一个文件一个 Shader。 |
DShader/Functions/ | 你 | 可复用的 .dsf 函数文件。 |
DShader/Shared/ | 你 | 项目内部的 .dsh header。 |
DShader/VirtualFunctions/ | 编辑器 | Create Virtual Function 为已有 UMaterialFunction 写声明的位置。 |
DShader/Decompiled/ | 编辑器 | Export DSM / Export DSF 的输出目录,分为 Materials、Functions、Layers、LayerBlends。 |
DShader/Packages/ | 扩展 | 已安装的共享库。永远是源根下那个字面量子目录 Packages,不能单独配置。 |
DShader/DreamShader.code-workspace | 插件 | 每次执行 Open Dream Shader Workspace 都会整体重写。 |
DShader/dreamshader.lock.json | 扩展 | 记录已安装 Package 的版本。插件既不读也不写。 |
Intermediate/DreamShader/GeneratedShaders/ | 插件 | 生成的 .ush helper include,挂载在 /DreamShaderGenerated。 |
DreamShader.code-workspace 每次调用都从一个固定对象序列化而来 —— 从不读取、合并或保留原文件。手动
加的 launch、tasks、extensions 或额外 settings 都会被销毁。把个人配置放在
DShader/.vscode/settings.json,这个命令不会碰它。
命名约定
| 类型 | 约定 |
|---|---|
| 材质文件 | M_*.dsm |
| 材质函数文件 | F_*.dsf |
| 共享 header | 按领域命名 —— Texture.dsh、Color.dsh |
| Package 入口 | Library/<Name>.dsh |
扩展名比较是大小写不敏感的,所以 M_Water.DSM 也是材质文件。标识符则是另一回事:插件把命名空间和
函数名净化成 HLSL 与资产标识符时会替换掉非 ASCII 字符,所以这些名字请用 ASCII。
import
每个材质尽量只 import 少量稳定入口:
import "Shared/Common.dsh";
import "@typedreammoon/dream-noise/Library/Noise.dsh";一个 specifier 会依次尝试三个候选:导入方文件所在目录、所属根的源目录、该根的 Packages 目录。
第一个存在且没有跑出自己包含根的候选胜出 —— Package 内部的文件不能用 ../ 爬出去。没有扩展名的
specifier 会被补上 .dsh,所以 import "…/Noise" 永远不可能解析到 .dsf。
裸 specifier since 1.6.0 不会离开自己所在的根:装一个插件因此不可能改变某条既有 import 的含义。
要刻意跨根,写 import "Plugin.MoonToon:Shared/Toon.dsh";。完整规则见
import 与命名空间。
import 保持浅层还有一个好处:源 hash 覆盖的是整段内联后的 import 闭包,改一个 header 会让所有引入它的
文件都失效。
什么会被编译,什么不会
插件有两个枚举器,它们排除的东西不一样。
| 枚举器 | 扩展名 | 排除 | 使用者 |
|---|---|---|---|
| 全量源枚举 | .dsm、.dsh、.dsf | 任意根的 Packages 下的全部文件 | 启动时的内存生成、commandlet 的 compile -All、cook、Gen 页列表、VirtualFunction 同步 |
| 材质源枚举 | .dsm、.dsf | 仅 DShader/Packages 下的 .dsm | Recompile DSM、自动编译队列、依赖图 |
Packages 排除对 .dsm 是完整的,对 .dsf 只是部分。Package 里的 .dsf 在交互式编辑器会话中
会被编译 —— 文件监视器和 Recompile DSM 都会拾取它 —— 但 compile -All、cook 和 Gen 页
不会。也就是说,一个附带 .dsf 函数资产的 Package 在本地能用,在 headless 构建里会静默地什么都
不生成。库代码请以 .dsh header 形式分发,或者把 .dsf 从 Packages 复制到自己的源码树里。
Package 内的 Examples/**/*.dsm 在任何路径下都不会被编译,也不会出现在 Gen 页。要用示例,把它从
DShader/Packages 复制到 DShader/ 下。
Package 的 .dsh header 完全可以被 import,但从不参与 VirtualFunction 声明扫描 —— Package 里带的
VirtualFunction 永远不会与它的资产做校验。
版本管理
| 内容 | 是否提交 |
|---|---|
.dsm / .dsf / .dsh | 提交 —— 这就是材质逻辑本身 |
dreamshader.lock.json | 如果团队会安装 Package,提交 |
Config/DefaultEngine.ini | 提交 —— 项目设置存在这里,是共享的 |
DShader/Packages/ | 团队自定:直接入库,或按 lock 文件重新安装 |
生成的 .uasset | 一般不提交 —— 默认 backend 下编辑器本来也不会写出它们 |
Intermediate/DreamShader/ | 不提交 |
团队内统一 Source Directory,这样 import 在每台机器上的解析结果才一致。插件带的 DShader 目录随插件
一起提交 —— 它和插件的 Content/ 属于同一份交付物。
下一步
- 文件模型 —— 每种扩展名可以写什么
- import 与命名空间 —— specifier 语法和循环处理
- Package —— 安装根目录,以及插件没有实现的那一半
- 日常工作流 —— 一次保存到底触发了什么