DreamShaderLang
工具链

Package

DShader/Packages —— 插件实现的 import 根、扩展实现的 manifest 与锁文件,以及两者之间那道枚举缺口。

Package 就是一个可复用的 DreamShaderLang 库:.dsh 头文件、.dsf 函数文件、示例、README。 Package 安装在 DShader/Packages 下,通过 import 触达。

项目
类型目录约定
路径<SourceDirectory>/Packages —— 默认 <Project>/DShader/Packages
创建者DreamShader 运行时模块,启动时无条件创建
可配置性只能间接配置,通过 Source Directory 项目设置

关于 package,插件只实现了两件事:import 根,以及把 package 文件排除出源文件枚举。 manifest、锁文件、商店和安装命令,都是编辑器扩展实现的约定 —— 插件里没有任何 C++ 读这两个 JSON 文件。 排查 package 问题之前先读职责划分,因为它决定了你该去看哪个仓库。

目录结构

M_Sample.dsm
dreamshader.lock.json
dreamshader.package.json
README.md
LICENSE
Noise.dsh
路径插件会编译吗?
DShader/Materials/M_Sample.dsm会 —— 普通源文件
DShader/Packages/**/Library/*.dsh从不直接编译;可以被 import,并被内联进 import 它的文件
DShader/Packages/**/*.dsf.dsf 缺口 —— 交互式会,无头不会
DShader/Packages/**/Examples/*.dsm任何路径下都不会
DShader/dreamshader.lock.json插件根本不读

目录名是硬编码的:package 根永远是配置的源目录下那个字面量子目录 Packages。改 Source Directory 会把它一起搬走; 没有单独的 package 目录设置。

职责划分

标注 扩展 的每一项都由编辑器扩展在它们各自的仓库里定义和执行, 插件侧完全观察不到。

方面实现方
启动时创建 <SourceDirectory>/Packages插件
<SourceDirectory>/Packages 作为第三个 import 解析根插件
把 package 文件排除出源文件枚举插件
dreamshader.package.json —— 存在性、字段、校验扩展
dreamshader.lock.json —— 存在性、内容、更新扩展
安装、更新、移除 package扩展
Package 商店、index 源、GitHub topic 搜索扩展
Package 脚手架 / "创建 package" 流程扩展
带 scope 的 @scope/name 命名扩展

一个 package 能被解析,纯粹是因为它的文件存在于 <SourceDirectory>/Packages 下。删掉 manifest 不会让 import 失效,加上 manifest 也不会改变插件的行为。manifest 和锁文件是给扩展的安装器和商店用的。

Import 解析

import 说明符先被归一化,然后按顺序试三个候选根。第一个既待在自己根内部、又在磁盘上存在的候选胜出。 这个解析器由编译器的 import 内联器和编辑器的依赖扫描器共用,所以两边结论一致。

说明符归一化

步骤规则
1剥掉首尾空白
2每个 \ 换成 /
3反复剥掉所有前导 ./ 序列 —— ././X 变成 X
4结果没有扩展名时,追加 .dsh

第 4 步意味着 import "@scope/pkg/Library/Noise"import "@scope/pkg/Library/Noise.dsh" 是同一个 import。 因此不带扩展名的说明符永远解析不到 .dsf

候选根

顺序候选路径必须待在的根
1<发起 import 的文件所在目录>/<specifier>该文件自己的 import 根 —— <SourceDirectory><SourceDirectory>/Packages 中包含它的那个(取最长匹配),两者都不包含时取文件自己所在目录
2<SourceDirectory>/<specifier><SourceDirectory>
3<SourceDirectory>/Packages/<specifier><SourceDirectory>/Packages

对住在 package 里的文件来说,候选 1 的根是 Packages,所以 package 内的 .dsh 可以用相对说明符触达兄弟文件, 但爬不出 package 树 —— 用 ../ 时逃逸守卫会否掉这个候选,解析继续落到根 2 和根 3。

任何根下都解析不到的说明符会产生 DreamShader import '{Specifier}' referenced from '{File}' could not be resolved. 完整的语法、环处理和行号映射见 import 与命名空间

源文件枚举

插件有两个枚举器。它们排除的东西不一样,而这个不对称决定了哪些 package 文件会被编译。

枚举器扫描的扩展名排除排序
全量源枚举.dsm.dsh.dsf<SourceDirectory>/Packages 下的一切
材质源枚举.dsm.dsf只排除 <SourceDirectory>/Packages 下的 .dsm

排除判定是与 package 目录做大小写不敏感的路径前缀比较,既命中 package 目录本身,也命中它下面任意深度的一切。

特性枚举器看得到 package .dsm看得到 package .dsf / .dsh
启动时的内存生成全量
命令行 compile -All全量
Cook 期落盘全量
Material Content Browser 的 Gen 页列表全量
VirtualFunction 声明同步全量
Recompile DSM / 全量重扫队列材质.dsf
依赖图重建材质.dsf

Packages 排除对 .dsm 是完整的,对 .dsf 只是部分的。 自动编译路径判定的是"Packages 下的一个 .dsm", 而任何 .dsf 都不满足这个条件 —— 于是 package 里的 .dsf 会被 Recompile DSM 排入队列、 在文件 watcher 看到它被保存时重新编译、并参与依赖图,同时对 compile -All、cook、Gen 页和 VirtualFunction 同步保持不可见。

因此,一个附带 .dsf 函数资产的 package 会在交互式编辑器会话里生成那些资产, 而在无头构建里不会。请把库代码以 .dsh 头文件形式发布,或者把 .dsfPackages 复制到项目自己的源树里。

package 里的 Examples/**/*.dsm 在任何路径下都不会被编译,也不会出现在 Material Content Browser 的 Gen 页。 要用某个示例,把它从 DShader/Packages 复制到 DShader/ 或它下面任何不叫 Packages 的子目录。

package 的 .dsh 头文件完全可以被 import,但从不被扫描 VirtualFunction 声明, 所以 package 里附带的 VirtualFunction 块永远不会被校验,也不会与它的 UMaterialFunction 资产同步。 见编辑器工具

dreamshader.package.json

放在 package 根目录。插件不读它。 下面这些字段是扩展的安装器和商店使用的约定,以扩展仓库为准。

{
  "name": "@typedreammoon/dream-noise",
  "version": "1.0.0",
  "displayName": "Dream Noise",
  "description": "Reusable noise functions for DreamShaderLang.",
  "author": "TypeDreamMoon",
  "repository": "https://github.com/TypeDreamMoon/dream-noise",
  "license": "MIT",
  "dreamshader": {
    "language": "DreamShaderLang",
    "version": ">=1.0.0",
    "entry": "Library/Noise.dsh"
  },
  "keywords": ["noise", "fbm", "voronoi"]
}
字段类型用途
namestring包身份;普通 name 或带 scope 的 @scope/name
versionstring包版本;推荐 SemVer
displayNamestring商店里展示的可读名
descriptionstring商店里展示的简短说明
authorstring作者署名
repositorystringGit 地址;用于更新已安装的包
licensestringSPDX 标识符
dreamshader.languagestring语言标识,DreamShaderLang
dreamshader.versionstring包面向的 DreamShaderLang 版本范围
dreamshader.entrystring推荐入口头文件,用于文档和商店展示
keywordsstring[]商店搜索标签

给仓库打上 GitHub topic dreamshader-package,它就能在商店里被发现。

dreamshader.lock.json

项目
路径<SourceDirectory>/dreamshader.lock.json
写入方扩展,在安装和更新时
读取方扩展
记录包名、版本、仓库、commit、安装路径

插件既不读也不写。 删掉它对编译毫无影响;它的存在是为了让团队看清签入的是哪些 package 版本。 如果团队会安装 package,请把它提交进版本库。

扩展命令

VSCode 扩展提供的命令面板条目。安装和更新需要 PATH 上有可用的 git —— 插件从不下载任何东西。

DreamShaderLang: Install Package from GitHub
DreamShaderLang: Browse Package Store
DreamShaderLang: Update Installed Packages
DreamShaderLang: Remove Installed Package
DreamShaderLang: Open Packages Folder
DreamShaderLang: Add Package Store Index Source
DreamShaderLang: Remove Package Store Index Source
DreamShaderLang: Create Package Step by Step

安装既接受 owner/repo 简写,也接受完整的 https://github.com/owner/repo 地址。商店会读取一个或多个 index JSON 文件(通过 dreamshader.packageStoreIndexUrls 配置),外加可选的 GitHub topic 搜索 —— 见 VSCode 与 Rider

编写一个 package

建目录

用带 scope 的名字,这样绝不会和别人撞:DShader/Packages/@scope/package-name/

加 manifest

package 根目录下的 dreamshader.package.json,至少要有 nameversion。插件不需要它,商店和安装器需要。

公共 API 放 Library/

一个主题一个 .dsh,用 Namespace 包起来,这样 helper 名字不会和使用方项目里的撞。 把 dreamshader.entry 指向用户应该 import 的那个头文件。

示例放 Examples/

它们在 package 内部永远不会被编译 —— 这正是你想要的:既能说明用法,又不会在每个安装了它的项目里生成资产。

发布

推到 GitHub 并打上 topic dreamshader-package;希望在商店里被发现的话,再往某个 package store index 里加一条。

一个库头文件:

Namespace(Name = "DreamNoise")
{
    Function float Remap01(float x)
    {
        return saturate(x * 0.5 + 0.5);
    }
}

以及使用它的项目材质:

import "@typedreammoon/dream-noise/Library/Noise.dsh";

Shader(Name="Materials/M_Noise")
{
    Properties = {
        float Scale = 4.0 [Slider(0.1, 32)];
    }

    Outputs = {
        vec3 Color;
        Base.EmissiveColor = Color;
    }

    Graph = {
        vec2  UV = UE.TexCoord(Index = 0);
        float N  = DreamNoise::Remap01(UV.x * Scale);
        Color = vec3(N, N, N);
    }
}

版本号

变更建议的版本位
新增 helper,不破坏已有调用minor
修实现,保持 API 不变patch
重命名或移除公共 helpermajor
显著改变生成产物的行为major,并在 README 里写迁移说明

设计建议

建议原因
公共 helper 放进 namespace包名和项目名不会冲突
导出稳定的入口头文件材质不该依赖你内部的文件布局
库代码用 .dsh 发布,不要用 .dsfpackage 里的 .dsf 对无头构建不可见 —— 见上文
示例放 Examples/既说明用法,又不会被编译
不要写项目专属资产路径package 应该能原样在项目之间搬
README 里写清 import 那一行那是每个用户唯一必须知道的东西

说明

  • package 目录在模块启动时就会创建,即使它是空的,即使在 commandlet 进程里也一样, 与源目录和生成着色器目录一起创建。
  • package 文件就是普通文件。没有任何东西阻止你原地编辑 package 里的 .dsh —— 但扩展的更新命令会把它覆盖掉。
  • 因为排除是路径判定,把非 package 路径指向 package 内容的符号链接或 junction, 会按它解析出来的真实路径处理。

示例

一个安装在 DShader/Packages/@typedreammoon/dream-noise/ 的 package,它的说明符是这样解析的:

specifier   @typedreammoon/dream-noise/Library/Noise.dsh
candidate 1 <Project>/DShader/Materials/@typedreammoon/dream-noise/Library/Noise.dsh   missing
candidate 2 <Project>/DShader/@typedreammoon/dream-noise/Library/Noise.dsh             missing
candidate 3 <Project>/DShader/Packages/@typedreammoon/dream-noise/Library/Noise.dsh    resolved

省掉扩展名,解析结果完全相同:

import "@typedreammoon/dream-noise/Library/Noise";

继续阅读

本页目录