DreamShaderLang

HLSL 通道

HLSL 通道

HLSL 通道

DreamShader » 自定义通道运行时 » HLSL 通道

compute 通道,或没有 Material 的 fullscreen 通道,会运行 HLSL。代码写在 .dsp 本身中——写在通道的 hlsl { } 块中,或作为文件 hlsl { } 块的一个条目——或写在通过 Shader = 为通道命名、 位于项目中任意位置的 .usf 中。本页说明该 HLSL 可以使用什么,以及它如何 进入引擎。

头文件/Plugin/DreamShader/Pass/DreamPass.ush — 已为你包含;再次包含也无害
着色器类型FDreamPassCS(计算)和 FDreamPassPS(像素),每个 槽位 一个排列
槽位32 个计算,16 个像素(DREAMSHADER_PASS_COMPUTE_SLOTS、DREAMSHADER_PASS_PIXEL_SLOTS,可从 Target.cs 用 GlobalDefinitions 覆盖)
快照<DShader>/.dreampass/ — 与你的源文件一起 提交它

代码在哪里

每个 HLSL 通道的代码都只存在于一个地方:

位置通道如何声明Entry
它自己的块通道中的 hlsl { ... }完整函数:运行的那个函数,未写入时为 Main;入口的语句:无
文件的块仅 Entry = <function>;必需:文件 hlsl { } 块中的一个函数
着色器文件Shader = "<file>.usf";必需:该文件中的一个函数

一个块要么存放 完整函数——即 .usf 所存放的内容:函数、groupshared、struct、#define、 #include——要么仅存放 入口的语句,其函数由编译器写出。究竟属于哪一种, 根据块的顶层来判定:函数定义、groupshared、struct 或 cbuffer 声明,或 [numthreads(...)] 会使其成为完整函数;其他任何内容——语句、if、for、[unroll]——会使其成为 入口的语句。将 .usf 移入 .dsp 就是把它的文本粘贴到一个块中。

完整函数

pass TileLuma : compute
{
    Injection = BeforePostProcess;
    Dispatch  = TileAverage * 16;
    read  InColor = SceneColor;
    write Result  = TileAverage;

    hlsl
    {
        groupshared float Partial[256];

        [numthreads(16, 16, 1)]
        void Main(uint3 Id : SV_DispatchThreadID, uint3 Group : SV_GroupID, uint Index : SV_GroupIndex)
        {
            const float2 UV = (Id.xy + 0.5) / (ResultSize.xy * 16.0);
            Partial[Index] = dot(DreamPassSample(InColor, UV).rgb, float3(0.2126, 0.7152, 0.0722));
            GroupMemoryBarrierWithGroupSync();
            for (uint Stride = 128; Stride > 0; Stride >>= 1)
            {
                if (Index < Stride)
                {
                    Partial[Index] += Partial[Index + Stride];
                }
                GroupMemoryBarrierWithGroupSync();
            }
            if (Index == 0)
            {
                Result[Group.xy] = Partial[0] / 256.0;
            }
        }
    }
}

计算通道的入口前面有 [numthreads(x, y, z)],这就是派发所依据的组大小; 仅当用宏书写时才需要 Threads = uint3(...)。

入口的语句

pass Pulse : fullscreen
{
    Injection = BeforePostProcess;
    read  InColor = SceneColor;
    write Out     = SceneColor;
    param PulseTint = PulseColor;

    hlsl
    {
        const float3 Scene = DreamPassSample(InColor, UV).rgb;
        const float  Depth = CalcSceneDepth(DreamPassSceneUV(UV));
        Out = float4(Scene + PulseTint.rgb * saturate(1.0 - Depth / 4000.0) * DP_Weight, 1.0);
    }
}

编译器会在语句周围写出函数,其中包含以下名称:

通道函数语句可见的名称
compute来自 Threads 的 [numthreads(...)](未写入时为 uint3(8, 8, 1));超过 DP_DispatchSize 的线程会在你的语句之前返回Id(SV_DispatchThreadID)、GroupId(SV_GroupID)、LocalId(SV_GroupThreadID)、LocalIndex(SV_GroupIndex)
fullscreen每个 write 一个 out float4,以其命名并清零为 0,在 write 顺序中为 SV_Target0..3SvPosition(SV_POSITION)、Pixel(从视图左上角计起的像素中心)、UV(它在视图中的位置,0..1),以及写入

return; 会提前结束语句(像素通道的输出已经清零)。绑定不能采用这些 名称之一,语句也不能包含函数、groupshared 或 #include——这些要放在 文件的块中,或者让该块存放完整函数。

文件的 hlsl 块

hlsl
{
    // Shared code: compiled into every pass whose code is in the .dsp.
    float GaussianWeight(int Offset, float Spread)
    {
        return exp(-float(Offset * Offset) / (2.0 * Spread * Spread));
    }

    // An entry: compiled only for the passes that name it.
    [numthreads(8, 8, 1)]
    void BlurCS(uint3 Id : SV_DispatchThreadID)
    {
        if (any(Id.xy >= DP_DispatchSize.xy))
        {
            return;
        }
        float4 Sum = 0;
        float Total = 0;
        for (int i = -12; i <= 12; ++i)
        {
            const float W = GaussianWeight(i, SigmaTexels);
            Sum   += W * Source.Load(int3(clamp(int2(Id.xy) + int2(Axis) * i, int2(0, 0), int2(SourceSize.xy) - 1), 0));
            Total += W;
        }
        Result[Id.xy] = Sum / Total;
    }
}

pass BlurH : compute
{
    Entry = BlurCS;
    read  Source = Bright;
    write Result = BlurX;
    param SigmaTexels = Sigma;
    param Axis        = float2(1, 0);
}

一个 .dsp 拥有一个。通道用 Entry 命名的内容是 入口;块中的其他所有内容都是 共享 代码,会在代码位于 .dsp 中的每个通道的代码之前编译。根据通道名称的生成方式(见下文), 有两条规则:

  • 共享代码不能使用任何通道的名称——不能使用 read、write 或 param 名称,也不能使用它们的 Size 和 UVRect 名称,甚至不能作为参数或局部变量:这些名称是它所编译到的槽位的 #define。 入口可以使用它们:它只为命名它的那些通道编译,而其中每个通道 都会绑定它所使用的内容。
  • 没有任何东西调用入口。 在任何其他通道的槽位中它都不存在。将两个入口共享的内容移到 它自己的函数中。

两者都会在管线编译时检查。通道自己的块也能看到共享代码,并可自由调用其 函数。

通道生成的名称

pass Blur : compute
{
    Shader = "BoxBlur.usf";
    Entry  = BlurCS;
    read   Source = Mask;
    write  Result = Blurred;
    param  Radius = BlurRadius;
}

名称 Source、Result 和 Radius 之所以存在,是因为通道绑定了它们:在其代码之前,每个绑定 都会按绑定的书写顺序变成一个指向槽位固定参数的 #define。

绑定定义含义
read X = …(第 i 个)X、XSize、XUVRectDP_Input<i>(一个 Texture2D<float4>)、其尺寸(xy,以及 zw 中的其倒数)、其图片在纹理内的 UV 矩形
write Y = …(第 j 个)Y、YSize计算:DP_Output<j>(一个 RWTexture2D<float4>);像素:仅尺寸——写入 SV_Target<j>
param P = …PDP_Params[] 的一个分量:float 是一个,float2 是一个对齐的一半,float3 是一个新向量的 xyz,float4 是一个完整的;int 和 bool 用 asint / asuint 读回

因为它们是宏,请选择不会与引擎标识符冲突的绑定名称(用 InMask,而不是 Texture)。Entry 同样会被重命名为槽位的固定入口点,因此在其前面的 [numthreads] 就是派发所使用的内容。

固定参数

名称
DP_Input0…DP_Input7read 纹理;未绑定的为黑色
DP_InputSize[8]、DP_InputUVRect[8]尺寸(xy)和逆尺寸(zw);图片的 UV 矩形(最小 xy,最大 zw)——管线缓冲区为 (0, 0, 1, 1),分屏中场景颜色为子矩形
DreamPassInputUV(i, uv)输入 i 的图片上的 uv,作为其纹理中的 UV
DreamPassSample(Name, UV)在视图上的 UV 处、其图片内部,对 read 名称采样(线性)
DreamPassLoad(Name, Pixel)read 名称的一个纹素,位于从视图左上角计起的像素处
DreamPassSceneUV(UV)视图上的 UV,作为场景纹理的 UV,用于 CalcSceneDepth 及其余部分;不用于 BeginView
DP_PointClamp、DP_LinearClamp采样器
DP_Output0…DP_Output3仅计算:write UAV
DP_OutputSize[4]输出的尺寸
DP_Params[16]打包的 param 值
DP_Weight管线在视图中的权重
DP_ViewRect此视图的输出 0 的矩形(最小 xy,最大 zw)。管线的缓冲区从 0 开始;分屏中一个视图的场景颜色则不是——写入 Out[DP_ViewRect.xy + Id.xy]
DP_DispatchSize计算:请求的线程数;一次派发会向上取整到整组,因此超过它要提前返回
DP_Time世界时间、世界增量时间、真实时间(秒)、帧号
View、SceneTexturesStruct引擎的视图 uniform 缓冲区和场景纹理——CalcSceneDepth、CalcSceneCustomStencil 以及 SceneTexturesCommon.ush 的其余部分可用

DreamPassSample、DreamPassLoad 和 DreamPassSceneUV 是宏——DreamPassSample(InColor, UV) 会读取 InColorUVRect,即该绑定所使用的名称——它们既可在 .usf 中工作,也可在块中工作。

在 BeginView 处,视图 uniform 缓冲区尚不存在;在那里 pass 中使用 View 会产生编译错误, 而 DP_Time 会代替计时值。那里的场景纹理是占位符。文件块的共享代码可以使用 View——其他 pass 可能会在帧的更晚时间运行——但一个 BeginView pass 若调用 这样的函数,会在其槽位被预检查时被拒绝(DSH8318)。

HLSL 如何进入引擎

pass 不能拥有自己的全局 shader 类型:shader 类型会在插件加载时注册。每个 HLSL pass 在其管线编译时都会被分配一个槽位——即 FDreamPassCS 或 FDreamPassPS 的一种排列:

  1. 预检查会针对项目的 shader 格式,用你的代码编译该槽位。若出现错误,便是 一个带有文件和行号的 DreamShader 诊断——对于 .dsp 中的代码,则是 .dsp 的行号—— 并且该管线不会编译。文件块共享代码中的错误只会报告一次,并附带其失败的 pass。 没有任何入口可达的代码完全不会被编译——引擎会在 shader 编译器读取该槽位之前将其丢弃—— 因此尚无 pass 调用的辅助函数不会被检查,这与 .usf 中一样。
  2. 代码的快照——即 .usf 及其通过相对路径包含的每个文件,或者,对于 .dsp 中的代码,则是将该文件中其他 pass 的入口置空后的文件块、其后是该 pass 自身的代码,以及它们通过相对于 .dsp 的路径包含的每个文件——会被写入 <DShader>/.dreampass/Slots/<C|P><NN>/, 并且注册表(RegistryCompute.ush、RegistryPixel.ush)会把该槽位映射到它。Registry.json 会记录 哪个 pass 拥有哪个槽位。
  3. 这两种 shader 类型会在编辑器中重新编译,该 pass 会在下一帧运行。

全局 shader 是基于快照构建的,永远不会基于你正在编辑的文件。这一点很重要,因为全局 shader 中的 编译错误是致命的:编辑器或 cook 如果直接编译你半成品的代码,就会停止。 有了快照,拉取你的改动的队友会得到通过你预检查的内容,cook 也是如此。 在块上方添加到 .dsp 的注释不会移动快照的任何一行:快照不保存 .dsp 的行号,因此不会重新编译任何全局 shader。

  • 槽位是稳定的:一个 pass 在多次编译之间会保持其槽位;新 pass 会取最低的可用槽位; 被移除的 pass 会在其管线再次编译时释放其槽位;dsc pass-registry -Gc 会释放 不再存在的管线的槽位。槽位耗尽会是一个编译错误,并指出需要提高的数量。
  • 未使用的槽位会编译为空入口,因此排列列表在每个构建中都是相同的——这是 烘焙构建所要求的。
  • 全局 shader 会被烘焙进基础游戏的全局 shader 缓存:HLSL pass 无法随 DLC 发布。
  • 块结束于与其 { 匹配的 };注释、字符串和 # 行中的大括号不计入。 在块中不支持包含大括号的多行宏(行末为 \)。

另请参阅

本页目录