自由学习记录(217)

"普通 Unlit 材质保持 0"

这个结论还需要再看一层,不能从 GBufferD 存在直接推出。

你要查:

复制代码
SHADINGMODELID_UNLIT

以及 GBuffer 初始化/编码路径,确认究竟是哪一种:

复制代码
Unlit shader└─ 显式写 GBufferD.x = 0

还是:

复制代码
Unlit└─ 根本不写这个 attachment   └─ 依赖 GBuffer clear value = 0

还是:

复制代码
某些 permutation└─ MRT mask 根本不包含 D

这三个在最后 Texture Viewer 里可能都"看起来是黑的",但机制完全不同。

所以"Unlit 保持 0"属于需要 shader permutation + render target load/clear 行为共同证明的结论。

至于:

"这样头发只画一次"

这个反而很好验证。

看:

复制代码
BasePassPixelShader└─ 原来的 Hair Mesh Draw   └─ Pixel Shader 同一次 invocation      ├─ 写原来的 GBuffer      └─ 顺便写你的数据

如果实现最终确实长这样,那:

复制代码
Draw Hair└─ Base Pass   ├─ BaseColor   ├─ Normal   ├─ Roughness   └─ YourMask

就是一次 rasterization / 一份 mesh draw。

而不是:

复制代码
Base Pass└─ Draw HairCustom Mask Pass└─ Draw Hair 第二次

最直接根本不用猜源码,用 RenderDoc 看:

复制代码
BasePass└─ 找到这个 Hair draw call   └─ Output Merger      ├─ RT0      ├─ RT1      ├─ RT2      ├─ RT3      └─ RT4 / GBufferD

然后确认:

复制代码
Packed R 改变└─ 发生在原 Hair BasePass draw

而 Frame Event List 里没有第二次 Hair mask draw。

这就把"只画一次"实锤了。

官方 UE 5.8 API 本身也把 GBuffer A--F 作为 Scene Texture 集合暴露,因此"复用现有 GBuffer"与"新建另一张 RDG Texture"在资源层面本来就是两件不同的事。dev.epicgames.com/documentation/unreal-engine/API/Runtime/Renderer/ESceneTextureSetupMode


Material IR,这里的 IR 就是 Intermediate Representation,中间表示

它的位置不是"材质"和"最终画面"之间某个 GPU buffer,而是在材质编译器内部

复制代码
Material Graph
├ Texture Sample
├ Multiply
├ Substrate BSDF
├ Mix / Layer
└ Material Output

        ↓ 解析

Material IR
└ 把上面的节点图
   转成编译器更容易分析的一套内部表示

        ↓ translation

HLSL / Preshader opcodes
        ↓
Shader compiler
        ↓
DXIL / SPIR-V / machine code

UE 5.8 官方的 FMaterialIRModule 定义得很明确:它代表一次 material build 的 intermediate representation,内部包含一个由 expression analysis 得到的 IR value graph ,以及 resource usage、reflection 等 metadata;之后还必须翻译到 HLSL 或 Preshader opcode 这样的 target backend 才能执行。dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/FMaterialIRModule

为什么不直接:

复制代码
Material Node
→ HLSL

因为中间插一个 IR 后,可以集中做很多事情:

复制代码
Material Graph
↓
Material IR
├ 类型/值分析
├ expression dependency
├ resource usage
├ reflection
├ 常量传播/简化的空间
├ 判断哪些东西进入 shader
├ 判断哪些东西能成为 preshader
└ 针对不同 backend 再翻译

这就是编译器普遍使用 IR 的原因:前端只负责理解原始语言,后端只负责生成目标代码,中间统一成一种 representation。

在 Substrate 体系里尤其容易碰到它。Substrate 本身首先是一棵 material/BSDF 结构:

复制代码
Substrate Material
└ Operator
   ├ Slab / BSDF
   └ Operator
      ├ Slab / BSDF
      └ Slab / BSDF

Epic 5.8 的 API 也直接描述 Substrate material 是一棵 tree:FrontMaterial 是 root,BSDF 是 leaves,operators 在中间;这些 expression 同样参与 Material IR 的构建。dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/UMaterialExpressionSubstrateEyeB-

它为什么叫 Relative Strength,可以从公式理解。经典 RSI 默认通常看最近 14 个周期

复制代码
RS = Average Gain / Average Loss

RSI = 100 - 100 / (1 + RS)

比如最近上涨力量明显大于下跌力量:

复制代码
平均上涨 = 3
平均下跌 = 1

RS = 3 / 1 = 3

RSI = 100 - 100/(1+3)
    = 75

所以 RSI 真正压缩的是:

最近这段时间里,上涨幅度相对于下跌幅度有多强。

这也是为什么只记"相对强弱指数"很容易忘------中文名没告诉具体在 relative 什么。可以直接记成:

RSI = Recent Up Strength ÷ Recent Down Strength,再压成 0--100。

至于 Binance Futures:有 RSI。 Binance Futures 的 K 线图支持技术指标,RSI 是其中常见指标;Binance 自己也有 Futures 场景下的 RSI 内容。www.binance.com/en/square/post/31445029732921

加进去以后,它一般出现在 K 线下方的独立 panel,而不是画在 candle 上。

RSI 是由所选 K 线周期计算的。

最基础的关系是:

复制代码
Expression
└ 描述"计算出什么值 / 发生什么计算"的结构

Function
└ 一段被定义好的可调用计算
   └ 调用它时,会形成一个 function-call expression

C/C++ 对 expression 的定义很直接:由 operands 和 operators 构成,指定一个 computation;求值可能产生一个值,也可能产生副作用。en.cppreference.com/cpp/language/expressions

例如:

复制代码
C++



a + b

这是 expression。

复制代码
C++



a * b + c

也是 expression。

复制代码
C++



sin(x)

还是 expression,而且更具体地说,这是一个 function-call expression 。C++ 标准语境本身就使用这个术语。en.cppreference.com/cpp/language/operator_other

plumbing 这个词为什么会被这么用,也很好理解。它原本就是:

复制代码
water source
↓
pipe
↓
valve
↓
destination

软件里借来以后变成:

复制代码
data producer
↓
API / structs / bindings
↓
renderer
↓
shader

重点不是水本身,而是:

这些东西有没有被正确接通。

Foo() 明确告诉读者:

名字没有语义,不要浪费时间理解这个名字。

这就是为什么程序员很爱用它。它其实解决了一个很实际的问题:如果示例写成

复制代码
C++



Car car;
Engine engine;

读者很可能开始思考:

复制代码
为什么是 Car?
为什么有 Engine?
这里是不是在讲 composition?

但:

复制代码
C++



Foo foo;
Bar bar;

几乎就是在说:

这些名字都是空气,只看结构。

历史倒很奇怪,foo 并不是计算机行业凭空发明的。

目前最完整的经典整理是 RFC 3092《Etymology of "Foo"》。它追到 1930 年代美国漫画 Smokey Stover。漫画作者 Bill Holman 会到处塞毫无意义的 FOO,例如车牌、背景笑话、文字游戏;1930 年代末甚至形成过一阵 "Foo" 流行文化。1938 年 Warner Bros. 的《The Daffy Doc》里也出现过 SILENCE IS FOO!www.rfc-editor.org/info/rfc3092

大致传播链可以看成:

复制代码
1930s 美国漫画
└ FOO = 故意荒唐、没有明确意义的词
       ↓
美国流行文化
       ↓
二战时期美国军队
├ foo fighters
└ FUBAR 等相近用法
       ↓
战后工程师 / hacker culture
       ↓
早期计算机文档
       ↓
foo / bar
成为程序示例占位符

RFC 是一套文档系列的名字,历史全称是 Request for Comments 。现在互联网里的很多协议规范、技术说明、历史记录都会以 RFC 的形式正式归档。IETF 官方就把 RFC 称为自己的主要技术文档形式。www.ietf.org/process/rfcs

组织关系大概是:

复制代码
Internet technical community
├ IETF
│ └ 制定大量互联网协议和标准
│
├ IRTF
│ └ 偏长期研究
│
├ IAB
│ └ Internet 架构与监督
│
└ Independent submissions
       ↓
     RFC Series
       ↓
   RFC Editor 负责正式出版和归档

RFC Editor 是这套文档系列的正式出版/编辑机构,而不是"制定所有 RFC 的组织"。RFC Editor 官方说明,RFC 可以来自 IETF、IRTF、IAB 和 Independent Submission Editor 等不同 stream。www.rfc-editor.org

最容易混淆的是:

复制代码
IETF ≈ 标准制定社区/组织

RFC ≈ 最终发布出来的技术文档

RFC Editor ≈ 管理、编辑、出版 RFC Series

例如:

复制代码
HTTP/1.1
TLS 1.3
QUIC
IPv6

这些互联网技术都有对应 RFC。IETF 明确列出 TLS 1.3、QUIC、WebRTC 等协议都是通过 RFC 规范的。www.ietf.org/process/rfcs

但还有一个重要点:

RFC ≠ 一定是互联网标准。

最早 1969 年 ARPANET 刚发展时,这些文档真的比较像:

复制代码
"这里有个网络设计想法,
大家看看,
有什么意见?"

所以叫:

Request for Comments

请求评论 / 征求意见

后来整个体系越来越正式,但名字没换。

还有一个非常重要的制度特点:RFC 发布后是不可修改的 archival document。 IETF 明确说 RFC 一旦发布,连一个字符都不会直接修改;如果规范后来改变,通常发布新的 RFC 来更新或取代旧 RFC。www.ietf.org/blog/how-read-rfc

所以经常看到:

复制代码
RFC 1234
↓
Updated by RFC 5678
↓
Obsoleted by RFC 9012

而不是打开 RFC 1234 直接编辑掉。

词汇借用(lexical borrowing)

社会语言学会区分这种稳定借词和真正的 code-switching。

也就是说 Codex 的实现确实是在:

UE 正常完成 GBuffer encoding 以后,强行覆盖 D.x / D.y,然后用同一个 Base Pass MRT 写出去。

因此它没有创建第二张 Hair RT,也没有第二次画 Hair。就这个"只画一次、复用已有 GBufferD"的机制,现在源码已经能直接证明。

UE 5.8 真正正在工作的路径是:

复制代码
BasePassPixelShader.usf
└─ EncodeGBufferToMRT(...)
   └─ 这个函数不是普通 .ush 里手写的
      └─ ShaderGenerationUtil.cpp 动态生成

我也把这点查到了。

Source/Runtime/Engine/Private/ShaderCompiler/ShaderGenerationUtil.cpp 约 770:

复制代码
C++



static FString CreateGBufferEncodeFunction(const FGBufferInfo& BufferInfo)

然后约 774:

复制代码
C++



FullStr += TEXT(
    "void EncodeGBufferToMRT("
    "inout FPixelShaderOut Out, "
    "FGBufferData GBuffer, "
    "float QuantizationBias)\n");

并且同文件约 1939:

复制代码
C++



static bool bUseRefactor = true;

所以不是猜测:

UE 5.8 当前已经切到新的 GBuffer shader-generation/refactor 路径了。

对于你要用的这个条件:

复制代码
Desktop
└─ Deferred
   └─ 非 Adaptive Substrate
      └─ Unlit 材质

GBufferD.xy 确实非常适合拿来放这两个数据。

为什么?

首先,UE 明确定义:

Source/Runtime/RenderCore/Public/GBufferInfo.h

复制代码
C++



GBS_CustomData, // RGBA8, no compression

然后 GBufferInfo.cpp 约 536:

复制代码
C++



// GBufferD
Info.Slots[GBS_CustomData] =
    FGBufferItem(
        GBS_CustomData,
        GBC_Raw_Unorm_8_8_8_8,
        GBCH_Both);

Info.Slots[GBS_CustomData].Packing[0] =
    FGBufferPacking(TargetGBufferD, 0, 0);

Info.Slots[GBS_CustomData].Packing[1] =
    FGBufferPacking(TargetGBufferD, 1, 1);

Info.Slots[GBS_CustomData].Packing[2] =
    FGBufferPacking(TargetGBufferD, 2, 2);

Info.Slots[GBS_CustomData].Packing[3] =
    FGBufferPacking(TargetGBufferD, 3, 3);

这已经把物理关系说死:

复制代码
GBufferD
├─ R = CustomData.x
├─ G = CustomData.y
├─ B = CustomData.z
└─ A = CustomData.w

格式
└─ RGBA8 UNORM

不是"类似 CustomData"。

它就是 CustomData 的那个 RT。

第二个关键问题:"普通 Unlit 是否真的保持 0?"

是。

而且 UE 5.8 当前真正运行的新路径就明确这么干。

BasePassPixelShader.usf 约 2386:

复制代码
hlsl



if (
    GBuffer.ShadingModelID == SHADINGMODELID_UNLIT &&
    (!SUBSTRATE_ENABLED || SUBSTRATE_GBUFFER_FORMAT==0)
)
{
    Out.MRT[1] = 0;
    SetGBufferForUnlit(Out.MRT[2]);
    Out.MRT[3] = 0;

    Out.MRT[GBUFFER_HAS_VELOCITY ? 5 : 4] = 0;

    Out.MRT[GBUFFER_HAS_VELOCITY ? 6 : 5] = 0;
}

这里:

复制代码
hlsl



Out.MRT[GBUFFER_HAS_VELOCITY ? 5 : 4]

就是对应的 GBufferD target。

因此 UE 原生行为就是:

复制代码
Unlit Base Pass
└─ GBufferD = float4(0,0,0,0)

这一次不是"推测普通 Unlit 应该不用"。

是源码直接明确清零。

旧的 EncodeGBuffer() 其实也是完全一样:

DeferredShadingCommon.ush 约 984:

复制代码
hlsl



if (GBuffer.ShadingModelID == SHADINGMODELID_UNLIT)
{
    OutGBufferA = 0;
    SetGBufferForUnlit(OutGBufferB);
    OutGBufferC = 0;
    OutGBufferD = 0;
    OutGBufferE = 0;
}

所以新旧两条路径行为一致。

即使你把 raw GBufferD.x/y 改成自己的数据,UE 后面的正常 deferred lighting 也不会把它误认为 Unlit 的材质 CustomData。

因为解码时有:

DeferredShadingCommon.ush 约 1124:

复制代码
hlsl



GBuffer.CustomData =
    HasCustomGBufferData(GBuffer.ShadingModelID)
        ? InGBufferD
        : 0;

然后 HasCustomGBufferData()

复制代码
hlsl



return ShadingModelID == SHADINGMODELID_SUBSURFACE
    || ShadingModelID == SHADINGMODELID_PREINTEGRATED_SKIN
    || ShadingModelID == SHADINGMODELID_CLEAR_COAT
    || ShadingModelID == SHADINGMODELID_SUBSURFACE_PROFILE
    || ShadingModelID == SHADINGMODELID_TWOSIDED_FOLIAGE
    || ShadingModelID == SHADINGMODELID_HAIR
    || ShadingModelID == SHADINGMODELID_CLOTH
    || ShadingModelID == SHADINGMODELID_EYE
    || ShadingModelID == SHADINGMODELID_SUBSTRATE_TOON;

注意:

复制代码
UNLIT

不在里面。

所以对于你的 Unlit 刘海:

复制代码
物理 GBufferD
└─ R = 你的 Hair Mask
└─ G = 你的 Subject Mask

正常 UE GBuffer Decode
└─ 发现 ShadingModel == Unlit
   └─ GBuffer.CustomData = 0

这形成了一个相当漂亮的隔离:

复制代码
Raw GBufferD.xy
└─ 你的自定义 Pass 可以读取

UE 正常 Deferred Lighting
└─ 看不到它
   └─ 因为 Unlit → CustomData 强制解码为 0

所以"会不会污染 UE 原来的 Unlit lighting"这件事,从源码来看,答案基本可以定为:

不会,只要你严格限定在 Unlit 像素上使用。

UE 默认用了 Unity Build,所以你明明只改一个 .cpp,有时日志会显示一个比较大的:

复制代码
Module.Renderer.XX.cpp

被重新编译。

因为几个 .cpp 被合并成一个 unity translation unit:

复制代码
A.cpp ─┐
B.cpp ─┼→ Module.Renderer.7.cpp → 一个 obj
C.cpp ─┘

你改 A:

复制代码
Module.Renderer.7.cpp

整个 unity blob 要重编。

但这仍然不等于整个 Renderer,更不等于整个 Engine。Epic 甚至提供 Adaptive Unity,就是专门减少这种迭代成本。dev.epicgames.com/documentation/unreal-engine/unreal-engine-build-tool-target-reference

最后一个实际区别:

复制代码
Compile
.cpp → .obj

和:

复制代码
Link
.obj → UnrealEditor-Renderer.dll

是两件事。

所以如果你在:

复制代码
[800/1200] Compile Renderer...

的时候停掉,已经完成的 .obj 往往还在。

如果你 Renderer 已经全部 compile 完:

复制代码
[1200/1200] Compile ...
↓
Link UnrealEditor-Renderer.dll

那再停就更可惜一点,因为下次主要就是重新确认 action graph + link。

所以你完全不需要抱着:

"我改两行 Renderer 源码,就必须重新等几个小时编整个 UE。"

这个模型。

更准确的是:

第一次把源码版 UE 建起来很重;之后修改 Renderer 的 Private 实现,正常工作方式就是增量编译 Renderer。

而且 Epic 早就把 Renderer 独立成模块,历史文档甚至直接写过这样做的目的之一就是:"修改 rendering code 时,不必重新链接整个 application"。dev.epicgames.com/documentation/en-us/unreal-engine/graphics-programming-overview

键不是"第一次改 Renderer 这个动作特殊",而是:

第一次本机编译时,UE 没有可复用的中间产物;第一次成功 Build 之后,这些产物已经存在,UBT 就能做增量编译。

具体是这条链:

复制代码
第一次源码 Build

Renderer/A.cpp
Renderer/B.cpp
Renderer/C.cpp
...
        ↓
大量 C++ 编译
        ↓
A.obj
B.obj
C.obj
...
        ↓
UnrealEditor-Renderer.dll

第一次很重,是因为本地可能还没有:

复制代码
Intermediate/
├─ .obj
├─ PCH
├─ Unity translation units
├─ generated headers
└─ dependency records

Binaries/
└─ UnrealEditor-Renderer.dll

成功一次以后这些都留下来了。

例如第二天你只改:

复制代码
Renderer/Private/BasePassRendering.cpp

UBT 检查整个 Build Action Graph:

复制代码
A.cpp 没变
└─ A.obj 继续用

B.cpp 没变
└─ B.obj 继续用

BasePassRendering.cpp 变了
└─ 重新生成 BasePassRendering.obj

其他 300 个 obj
└─ 继续用

最后:
旧 obj + 新 obj
↓
重新 Link Renderer.dll

所以真正发生的是:

复制代码
第一次:
.cpp × 很多
→ .obj × 很多
→ Renderer.dll

以后小改:
1~几个 .cpp
→ 1~几个新 .obj
→ Renderer.dll

这就是标准的 C/C++ incremental build。

至于"能不能直接下别人的 Intermediate":

理论上不是物理上绝对不行,但工程上通常不这么干,因为 .obj/PCH 对构建环境非常敏感:

复制代码
必须高度一致:
├─ 完全相同 UE commit
├─ 完全相同源码修改
├─ Visual Studio / MSVC toolchain
├─ Windows SDK
├─ build configuration
├─ compiler flags / defines
├─ Target
└─ 某些路径/PCH状态

只要依赖签名对不上,UBT 就会判定这些 action 失效,然后重新编。

所以团队真正采用的不是:

复制代码
把同事的 Engine/Intermediate 整个压缩给你

而是:

复制代码
某台 Build Machine
↓
把源码 Engine 完整编好
↓
BuildGraph
↓
生成 Installed Build
↓
分发给整个团队

Epic 官方就是专门支持这个流程的,并明确说这种 Installed Build 可以重新分发给团队,让其他人不必从源码编 Engine。dev.epicgames.com/documentation/en-us/unreal-engine/installed-build-reference-guide-for-unreal-engine

应该直接塞进当前活动的 Unlit 清零块:

复制代码
hlsl



if (
    GBuffer.ShadingModelID == SHADINGMODELID_UNLIT &&
    (!SUBSTRATE_ENABLED || SUBSTRATE_GBUFFER_FORMAT==0)
)
{
    Out.MRT[1] = 0;
    SetGBufferForUnlit(Out.MRT[2]);
    Out.MRT[3] = 0;

    Out.MRT[GBUFFER_HAS_VELOCITY ? 5 : 4] = 0;

    // 就应该接在这里
    #if NUM_MATERIAL_OUTPUTS_GETHAIREYEREVEALMASK > 0
        Out.MRT[GBUFFER_HAS_VELOCITY ? 5 : 4].x =
            saturate(GetHairEyeRevealMask0(MaterialParameters));
    #endif

    #if NUM_MATERIAL_OUTPUTS_GETHAIREYEREVEALSUBJECT > 0
        Out.MRT[GBUFFER_HAS_VELOCITY ? 5 : 4].y =
            saturate(GetHairEyeRevealSubject0(MaterialParameters));
    #endif

    Out.MRT[GBUFFER_HAS_VELOCITY ? 6 : 5] = 0;
}

练的不是「看到中文以后想英文翻译」,而是让英文 lexical item / phrase 在真正产生意义的时候参与竞争。双语词汇研究本来就发现,两套语言并不是完全独立开关;词汇访问存在 cross-language interaction,而语言切换本身也有可测量的 switching cost。pmc.ncbi.nlm.nih.gov/articles/PMC9544540

word → collocation → phrase → clause → sentence → several connected sentences

如果你只是:

复制代码
Launcher UE
↓
打开 Engine/Source/Runtime/Renderer/...
↓
改两行代码

但没有成功重新构建 Renderer DLL,那么运行时根本不会使用你的修改

如果你现在确实已经做到了:

复制代码
改 Renderer 源码
↓
UBT 编译 Renderer
↓
生成新的 UnrealEditor-Renderer.dll
↓
Editor 使用这个 DLL

那么情况就特殊了:你实际上已经开始维护一个自己编译的 Engine binary。工程意义上已经在做"源码引擎修改",但这不等于这个安装目录从官方定义上自动变成标准的 GitHub Source Build。

现在 Codex 写进去的 HairEyeReveal 那两行,放在 UE 5.8 已经不执行的旧编码分支里。也就是说,按目前源码,它大概率根本没有真正写进 Base Pass 的 GBufferD。

Shaders/Private/BasePassPixelShader.usf 约 2371:

// this is the new encode, the older encode is the #else,

// keeping it around briefly until the new version is confirmed stable.

而 UE 5.8 真正正在工作的路径是:

BasePassPixelShader.usf

└─ EncodeGBufferToMRT(...)

└─ 这个函数不是普通 .ush 里手写的

└─ ShaderGenerationUtil.cpp 动态生成

我也把这点查到了。

Source/Runtime/Engine/Private/ShaderCompiler/ShaderGenerationUtil.cpp 约 770:

static FString CreateGBufferEncodeFunction(const FGBufferInfo& BufferInfo)

然后约 774:

FullStr += TEXT(

"void EncodeGBufferToMRT("

"inout FPixelShaderOut Out, "

"FGBufferData GBuffer, "

"float QuantizationBias)\n");

并且同文件约 1939:

static bool bUseRefactor = true;

这也意味着你的 Custom Render Pass 读取时有一个非常重要的限制。

不要这样读:

GetScreenSpaceData(UV).GBuffer.CustomData.x

对于 Unlit:

DecodeGBuffer

└─ CustomData = 0

你永远读到 0。

SceneTextures.GBufferD

└─ .r / .g

也就是 raw texture。

还有两个不能跨过去的边界。

一是 Substrate Adaptive。

源码 ShaderGenerationUtil.cpp 约 1815:

复制代码
C++



bool bIsSubstrateMaterial = Mat.SUBSTRATE_ENABLED;
bool bIsSubstrateNewGBuffer =
    Mat.SUBSTRATE_GBUFFER_FORMAT == 1;

// Substrate doesn't use gbuffer,
// and thus doesn't need CustomData
const bool bUseCustomData =
    !bIsSubstrateMaterial || !bIsSubstrateNewGBuffer;

所以:

复制代码
SUBSTRATE_GBUFFER_FORMAT == 0
└─ 这套 GBufferD 思路成立

SUBSTRATE_GBUFFER_FORMAT == 1
└─ Adaptive / new GBuffer
   └─ 不成立

而 BasePass 自己的条件也正好写的是:

复制代码
hlsl



(!SUBSTRATE_ENABLED || SUBSTRATE_GBUFFER_FORMAT==0)

这不是巧合。

UE 自己也明确把 Adaptive 排除在这条传统 GBuffer 路径之外。

第二是 Mobile。

不能把桌面这个结论复制到 Mobile。

DeferredShadingCommon.ush 里面:

复制代码
hlsl



#if SHADING_PATH_MOBILE
    MobileEncodeGBuffer(...)
#endif

而 Mobile 对 GBufferD 还有:

复制代码
hlsl



#if MOBILE_EXTENDED_GBUFFER
    GBufferD = ...
#else
    GBufferD = 0;
#endif

甚至 GLES framebuffer-fetch 路径直接写着:

复制代码
hlsl



GBufferD = 0; // PLS is limited to 128bits

所以:

复制代码
Desktop Deferred
└─ 可以按当前方案做

Mobile Deferred
├─ Extended GBuffer:另查 packing
├─ framebuffer fetch:不同
├─ PLS:可能根本没有 D
└─ 不能复用这个 patch

这是一个明确的桌面/移动分歧。

方案本身在 Desktop Deferred + Unlit + 非 Adaptive GBuffer 下是成立的;Codex 现在把真正的写入补丁接错了 UE 5.8 的分支。

不要要求「英文比例逐渐增加」。比例是很差的 proxy。真正想提升的是 英文承担的结构复杂度和自主生成程度

比如:

20 个孤立英文名词 / 100 个词

未必比

1 个自然完整的 English clause / 100 个词

更有训练价值。

HLSL 里声明的 shader parameters,可以在 C++ 侧用一个对应的 parameter struct 来描述,然后 UE 根据这两边的名字和类型建立 binding。

你截图里上下两段是"一一对应关系":

复制代码
hlsl



// .usf / .ush
float2 ViewportSize;
float4 Hello;
float World;

Texture2D BlueNoiseTexture;
SamplerState BlueNoiseSampler;

RWTexture2D<float4> SceneColorOutput;

C++ 侧概念上对应:

复制代码
C++



struct FMyShaderParameters
{
    FVector2D ViewportSize;
    FVector4 Hello;
    float World;

    FRHITexture* BlueNoiseTexture;
    FRHISamplerState* BlueNoiseSampler;

    FRHIUnorderedAccessView* SceneColorOutput;
};

Epic 紧接着就说明:实际 UE 不是让你普通地手写这个 struct,而是使用:

复制代码
C++



BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
    SHADER_PARAMETER(FVector2D, ViewportSize)
    SHADER_PARAMETER(FVector4, Hello)
    SHADER_PARAMETER(float, World)

    SHADER_PARAMETER_TEXTURE(Texture2D, BlueNoiseTexture)
    SHADER_PARAMETER_SAMPLER(SamplerState, BlueNoiseSampler)

    SHADER_PARAMETER_UAV(RWTexture2D, SceneColorOutput)
END_SHADER_PARAMETER_STRUCT()

这些宏会生成等价的 C++ 数据结构,同时生成 UE 需要的 reflection metadata,包括参数名、C++ type、HLSL type、byte offset 等,之后才能让 shader binding / RHI / RDG 使用。dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine

不是"只有 Unlit 不用 GBufferD"。

Default Lit 也不是 CustomData/GBufferD 的使用者。真正占用 GBufferD = CustomData 的,是 Subsurface、ClearCoat、Hair、Cloth、Eye、TwoSidedFoliage 等那批 shading model。

只是 Unlit 对我们特别好用,因为 UE 5.8 当前 Base Pass 里直接有一个非常明确的:

复制代码
hlsl



if (GBuffer.ShadingModelID == SHADINGMODELID_UNLIT ...)
{
    ...
    Out.MRT[GBufferD] = 0;
}

所以它给了我们一个几乎写着"这里原本没有 payload"的位置。

而且你最后那个理解需要反过来:

Unlit 下 GBufferD != 0,并不会让 UE 自动把它拿去做某个效果。

恰恰相反,UE 后面会因为这是 Unlit,主动把它当成"无语义数据"丢掉。

真正决定它能不能离开 Pixel Shader、进入 GPU Render Target 的,是 PixelShaderOutputCommon.ush

复制代码
hlsl



#if PIXELSHADEROUTPUT_MRT4
    , out float4 OutTarget4 : SV_Target4
#endif

也就是说:

复制代码
Out.MRT[4]
只是 shader 内部变量

                ↓ 必须存在这个桥

PIXELSHADEROUTPUT_MRT4 = 1

                ↓

SV_Target4

                ↓

D3D12 PS output

                ↓

绑定的 MRT4 / GBufferD

现在你的 Unlit 缺的就是中间这座桥。

ShaderGenerationUtil.cpp 现在会先分析这个 material permutation 实际用了哪些 GBuffer slot:

复制代码
C++



DetermineUsedMaterialSlots(...)

然后得到:

复制代码
C++



TargetUsage[]

最后只有:

复制代码
C++



if (TargetUsage[Iter] >= EGBufferSlotUsage::Written)
{
    OutEnvironment.SetDefine(
        "PIXELSHADEROUTPUT_MRT%d",
        1);
}

才会产生对应的 SV_TargetN

而 Unlit 在 DetermineUsedMaterialSlots() 中调用:

复制代码
C++



SetStandardGBufferSlots(...)

但没有:

复制代码
C++



Slots[GBS_CustomData] = ...

所以逻辑就是:

复制代码
Unlit
↓
不需要 GBS_CustomData
↓
GBufferD 对这个 material permutation 不属于 Written
↓
PIXELSHADEROUTPUT_MRT4 / MRT5 不开启
↓
PixelShaderOutputCommon 不生成对应 SV_Target
↓
最终编译出来的 PS 只有 SV_Target0...3

这就是截图里看到的现象。

UE shader permutation/output-mask 先决定"不导出 MRT4",然后 shader compiler 把无效的 Out.MRT[4] 写入消掉。

这也是为什么 Codex 现在说还必须改 RenderCore,而不仅仅改:

复制代码
hlsl



BasePassPixelShader.usf

因为我们真正要告诉 UE 的不是:

"请算一个值。"

而是:

"这个特殊 Unlit material permutation 现在真的需要 CustomData/GBufferD 这个物理 MRT,因此不要把这个输出目标从 Pixel Shader signature 里删掉。"

因為 Windows DLL 模型允許:

复制代码
新的 RenderCore.obj
       +
舊的 Core.lib
舊的 RHI.lib
舊的其他 Engine import libs
       ↓
重新 link
       ↓
新的 UnrealEditor-RenderCore.dll

所以你其實是在做:

module-level binary replacement

而不是:

rebuild entire Unreal Engine

這兩件事差很多。
RenderCore
└─ 足夠完整
└─ 可以局部重建
Core
└─ 所需某些低層 source/dependency 沒有隨 Installed Build 分發
└─ 完整重建失敗

immutable 是 UBT 的構建策略,不是 Windows 或 CPU 的物理限制。

所以你修改:

复制代码
C++



ShaderGenerationUtil.cpp

它屬於:

复制代码
RenderCore

最終只需要得到:

复制代码
新的 UnrealEditor-RenderCore.dll

只要它的 ABI 和其他現有 DLL 仍然兼容:

复制代码
舊 Core.dll
舊 Engine.dll
舊 Renderer.dll
       ↕
新 RenderCore.dll

Editor 就可以正常啟動。

這跟你在 Windows 上替換某個普通程序 DLL,本質上沒有區別。

GBS 幾乎可以直接讀成:

GBuffer Slot

所以:

GBS_CustomData

= GBuffer Slot --- CustomData

我查了 UE 5.8 源碼,它確實屬於 enum EGBufferSlot,同一組還有:

复制代码
C++



GBS_BaseColor
GBS_Roughness
GBS_WorldTangent
GBS_Anisotropy
GBS_CustomData
GBS_SubsurfaceColor
GBS_Opacity

Epic 5.8 官方 API 也把它列在 EGBufferSlot 裡。dev.epicgames.com/documentation/unreal-engine/API/Runtime/RenderCore/EGBufferSlot

GBS_CustomData

= GBuffer Slot --- CustomData

我查了 UE 5.8 源碼,它確實屬於 enum EGBufferSlot,同一組還有:

复制代码
C++



GBS_BaseColor
GBS_Roughness
GBS_WorldTangent
GBS_Anisotropy
GBS_CustomData
GBS_SubsurfaceColor
GBS_Opacity

Epic 5.8 官方 API 也把它列在 EGBufferSlot 裡。dev.epicgames.com/documentation/unreal-engine/API/Runtime/RenderCore/EGBufferSlot

例如 UE 5.8 源碼中:

复制代码
C++



GBS_CustomData // RGBA8, no compression

而後面的 GBuffer layout / packing 系統再決定這個 logical slot 最終被 pack 到哪個 render target/channel。也就是:

GBS_CustomData

→ 「需要 CustomData 這項資料」

不是:

GBS_CustomData

→ 「直接代表 GBufferD」

後者只是某些 legacy GBuffer layout 下最終的 physical packing 結果。

另外我查到 UE 5.8 的 ShaderGenerationUtil.cpp 中,像 Subsurface、ClearCoat、Hair、Eye、Toon 等 shading model 都會按需做:

复制代码
C++



Slots[GBS_CustomData] = GetGBufferSlotUsage(bUseCustomData);

這正好能看出 Slot 的含義:它是 shader/GBuffer codegen 用來描述「這個材質需要哪些 semantic fields」的一層抽象,而不是 MRT 本身。這個區分後面讀 GBufferInfo.cpp 時非常有用。

前面的問題其實跟 Launcher 無關:

复制代码
Hair Custom Output
↓
BasePass 裡 Out.MRT[GBufferD].x = PackedR
↓
最終 shader 沒有 SV_Target4/5

這是 UE 5.8 本身的 shader output-mask 機制。GitHub 源碼版也一樣會遇到。因為 UE 5.8 已經把原來 BasePass .usf 裡這種判斷:

复制代码
hlsl



#define PIXELSHADEROUTPUT_MRT4 ...
#define PIXELSHADEROUTPUT_MRT5 ...

基本移到了 C++ 的 GBuffer usage 分析。

真正的 Launcher 限制出現在這裡:

复制代码
想重編 ShaderCompileWorker
↓
發現 ShaderCompileWorker.Target.cs 不存在
↓
Build.bat ShaderCompileWorker ...
無法建立完整 target

我剛又直接查了你這份 E:\UE_5.8\Engine

复制代码
class ShaderCompileWorkerTarget

在你安裝版 Source 裡確實搜不到。

但是這裡又出現了一個新的工程問題:

它現在這個條件其實不是:

「只有 RevealHair Unlit 才保留 GBufferD」。

而是:

复制代码
C++



OutEnvironment.SetDefine(
    "HAIR_EYE_REVEAL_FORCE_UNLIT_GBUFFERD_OUTPUT",
    1);

對所有 Base Pass shader 都設成 1。

然後 HLSL 只過濾:

复制代码
hlsl



MATERIAL_SHADINGMODEL_UNLIT

所以實際效果是:

复制代码
所有 BasePass
↓
宏 = 1

其中所有 Unlit material
↓
全部強制導出 GBufferD

不是只有你的 Hair Reveal。

這和我們上一輪討論的最理想版本有差別。

現在是:

复制代码
普通 Unlit A
→ SV_Target4/5

普通 Unlit B
→ SV_Target4/5

Hair Reveal Unlit
→ SV_Target4/5

只是普通 Unlit 最後仍然寫:

复制代码
GBufferD = 0

所以功能上大概率沒錯,但失去了 UE 原本對普通 Unlit 的 MRT output pruning。

真正漂亮的版本仍然是:

复制代码
Material 有 HairEyeReveal Custom Output
↓
產生一個 compile-time material property
↓
只有這個 material:
HAIR_EYE_REVEAL_OUTPUT = 1
↓
才強制 GBufferD output

這部分才需要更深入地把 Custom Output 的存在傳到 compilation environment。

Renderer 在前面准备"这次 shader 要怎么编";ShaderCompileWorker 在后面真正执行 HLSL 编译。

时间顺序其实是:

复制代码
Renderer
    ↓
Derived shader rules
    ↓
ShaderCompileWorker

不是:

复制代码
ShaderCompileWorker
    ↓
Renderer

你之所以感觉 Worker 更"上游",主要是因为它名字叫 Compiler Worker。

重新编译 ShaderCompileWorker

这句话实际意思是:

复制代码
UE C++ 源码
↓
MSVC
↓
重新生成 ShaderCompileWorker.exe

这是"编译一个 Windows C++ 程序"。

而 ShaderCompileWorker 平时做的是:

复制代码
BasePassPixelShader.usf
↓
ShaderCompileWorker.exe
↓
DXC
↓
GPU shader bytecode

这是"编译 HLSL Shader"。

完全不是同一个 compile。

BasePassRendering.cpp 中:

复制代码
C++



ModifyBasePassCSPSCompilationEnvironment(...)

属于 Renderer。

它做的事情类似:

复制代码
C++



OutEnvironment.SetDefine("IS_BASE_PASS", 1);
OutEnvironment.SetDefine("GBUFFER_LAYOUT", ...);

现在 Codex 又加:

复制代码
C++



OutEnvironment.SetDefine(
    "HAIR_EYE_REVEAL_FORCE_UNLIT_GBUFFERD_OUTPUT",
    1);

它是在说:

我是 Base Pass Renderer,我给即将发生的 shader compilation 提供一组事实和开关。

所以这是:

复制代码
Renderer
= compile-job 的需求提出者

UE 5.8 這段 source 的實際資料流是:

复制代码
FShaderCompilerEnvironment 裡已存在的 #define
        ↓ 讀出
MaterialDefines / LightmapDefines / GlobalDefines / CompilerDefines
        ↓
CalculateDerivedMaterialParameters(...)
        ↓
FShaderMaterialDerivedDefines

所以這個函數不是「計算材質參數」,而是在做:

把一組已有的 shader compile defines,經過 UE 的規則,推導成另一組 compile-time decisions。

例如已有:

复制代码
MATERIALBLENDING_SOLID = 1
FORWARD_SHADING = 0
FeatureLevel 足夠

函數才推導:

复制代码
USES_GBUFFER = 1

因此 FShaderMaterialDerivedDefines 最精確的身份是:

由現有 shader compile defines 推導出的第二級 defines 集合。

derived 的對立面就在這裡非常清楚:

复制代码
已有 define
MATERIALBLENDING_SOLID
FORWARD_SHADING
...
        ↓ derive
派生 define
USES_GBUFFER
WRITES_CUSTOMDATA_TO_GBUFFER
PIXELSHADEROUTPUT_MRT...
...

Parameters 這個函數名用詞其實相當鬆。它不是 ShaderParameter,也不是材質裡的 Scalar/Vector Parameter。若只看實際資料,CalculateDerivedMaterialDefines() 會比現在的名字更接近它做的事情。

复制代码
static void DetermineUsedMaterialSlots(...)

最重要的部分。對 function 而言這裡不是「函數一直活著」------function 本來就不是用 object lifetime 那套描述。static 在這裡真正關鍵的是:

internal linkage:其他 translation unit 不能透過這個名字引用它。

scope 最核心就是「名字有效的範圍」。

在 C++ 裡更精確地說:

scope = 一個 name 可以被直接找到、引用的程式文字範圍。

例如:

复制代码
C++



void F()
{
    int X = 0;
}

X 的 scope 就在 F() 這個 block 裡。

出了這個 {}

复制代码
C++



X = 1; // 找不到 X

所以可以先把它記成:

scope 管的是「這個名字在哪裡看得見」。

不要先把它和 lifetime 混在一起。

例如:

C++

void F(){ static int X = 0; } X 的 scope 仍然只在 F() 內,但它的 lifetime 很長。

真正決定 C++ 這個 member「是什麼東西」的,主要是前面的宏:

复制代码
SHADER_PARAMETER
└─ RDG
   └─ TEXTURE
      └─ UAV

UE 5.8 源碼裡這個宏最後實際展開的 C++ member type 是:

复制代码
C++



FRDGTextureUAV* Output;

也就是說,Output 在 C++ 世界裡並不是:

复制代码
C++



RWTexture2D<float4> Output;   // ❌

而是:

复制代码
C++



FRDGTextureUAV* Output;       // ✅

所以你的感覺是對的:這裡首先是在描述一個「RDG Texture 的 UAV view parameter」。

它描述的是 shader-side type,也就是 HLSL 那一側應當把這個 binding 看成什麼:

复制代码
hlsl



RWTexture2D<float4> Output;

而 UE C++ 側則是:

复制代码
C++



FRDGTextureUAV* Output;

兩邊形成:

复制代码
C++ / RDG                          HLSL
────────────────────────────────────────────────
FRDGTextureUAV* Output    ↔    RWTexture2D<float4> Output
        ↑                              ↑
   RDG resource view              shader type

例如宏:

复制代码
C++



#define TEST(ShaderType) TEXT(#ShaderType)

你调用:

复制代码
C++



TEST(RWTexture2D<float4>)

宏参数实际收到的是:

复制代码
ShaderType =
RWTexture2D<float4>

其中 <float4> 本来就是你传进去的内容。

然后:

复制代码
C++



#ShaderType

得到:

复制代码
C++



"RWTexture2D<float4>"

UE 5.8 的实际源码,这个文件自己就写了相当实质的逻辑。它开头甚至直接说明用途:

To allow PS input/output passed into functions through a single struct

也就是把各种 Pixel Shader 的输入输出统一包装起来,减少到处写 #ifdef

它的关系更接近:

复制代码
BasePassPixelShader.usf
DeferredDecal.usf
MeshDecals.usf
VirtualTextureMaterial.usf
        │
        │ #include
        ↓
PixelShaderOutputCommon.ush
        │
        ├─ 根据前面定义好的 PIXELSHADEROUTPUT_* 宏
        │
        ├─ 生成 MainPS(...) 的参数签名
        │
        ├─ 决定有没有 SV_Target0
        ├─ 决定有没有 SV_Target1
        ├─ ...
        ├─ 决定各种 interpolants
        │
        └─ 包装并调用各个 shader 自己的
           FPixelShaderInOut_MainPS(...)

比如 Base Pass 在 include 它之前会先准备:

复制代码
hlsl



#define PIXELSHADEROUTPUT_MRT...
#define PIXELSHADEROUTPUT_BASEPASS ...
...
#include "PixelShaderOutputCommon.ush"

然后 PixelShaderOutputCommon.ush 里面真的会生成:

复制代码
hlsl



void MainPS(
    ...
#if PIXELSHADEROUTPUT_MRT0
    , out float4 OutTarget0 : SV_Target0
#endif

#if PIXELSHADEROUTPUT_MRT1
    , out float4 OutTarget1 : SV_Target1
#endif
    ...
)

所以它其实很像一个 macro-parameterized shader wrapper/template

复制代码
调用者 shader
    └─ 先定义「我要什么」
          MRT 数量
          BasePass / Decal
          Interpolants
          Coverage
          ...
              ↓
PixelShaderOutputCommon.ush
    └─ 根据这些配置
       生成真正的 Pixel Shader entry-point/output interface

它确实也有:

复制代码
hlsl



#include "ShaderOutputCommon.ush"

但这不是它名字叫 Common 的主要原因。

你可以暂时把 UE shader 文件命名里的 Common 理解为:

这一层代码不是某一个具体 Pass 独占,而是被多个 Shader/Pass 共享。

JPEG、PNG,它们有很好的压缩率和压缩品质,但必須CPU端解压整张贴图,才能读取贴图上的像素值(也就是不支持随机读取)---所以uav就是強調了讀的東西解壓了的可以unorder access了

JPEG和PNG都是熵编码压缩(霍夫曼编码、算术编码),压缩数据是连续流式存储的,像素之间互相依赖,如果你只需要贴图上某一个局部像素,必须从头解压整个文件才能拿到对应的位置,根本做不到"拿到哪个位置就直接读哪个位置"。

解决方案是GPU专用块纹理压缩​,比如BC系列、ASTC、ETC这些格式,它们把整张贴图切成一个个独立的4x4/8x8的小块,每个小块单独压缩,GPU可以只解压需要用到的那几个小块,完美支持随机访问,不需要处理整张图,非常适配GPU渲染的需求。

  • 传统着色器用的着色器资源视图SRV,只允许GPU只读,而且访问是对齐到纹理采样规则的,不能随意改写内存中任意位置的数据。
  • UAV放开了这个限制,允许GPU的多个线程同时对同一个缓冲区/纹理的任意位置做读写,只要你处理好同步就行。

"CPU端解压、不适合GPU解压",现在其实已经有成熟的解决方案了:如果要动态加载压缩贴图,可以用CPU按需解压小块,或者直接让GPU做并行解压​,

TU / A / F 有一個很重要的差別:

复制代码
T ─ template class
    └─ 描述 C++ declaration 的形式

U ─ UObject-derived class
    └─ 描述 inheritance

A ─ AActor-derived class
    └─ 描述 inheritance

S ─ SWidget-derived class
    └─ 描述 inheritance

I ─ abstract interface
    └─ 描述 class role / interface convention

F ─ 其他大多數普通 UE class / struct
    └─ 特別是非 UObject 類型

所以看到 TWhatever 時,最應該形成的第一反應不是「這是一個 UE 特殊對象」,而是:

先找它的 template <...> declaration。

例如本質上會是這類結構:

复制代码
C++



template<typename InElementType, typename InAllocatorType>
class TArray
{
    ...
};

而不是:

复制代码
C++



class TArray : public Something

T 不代表 inheritance、ownership、lifetime,也不代表 containerTArray/TMap/TSet 恰好都是 container,容易造成「T = container」的錯覺;但 TSharedPtrTOptionalTFunction 立即可以排除這個理解。

還有一個很好用的反例是:

复制代码
C++



FString

FString 內部建立在 TArray<TCHAR> 之上,但 FString 自己不是以 template class 的形式暴露,因此叫 FString,不是 TStringdev.epicgames.com/documentation/unreal-engine/fstring-in-unreal-engine

當重力可以忽略、時空近似平坦時,廣義相對論局部退化成狹義相對論。

  • 狹義相對論 Special Relativity, SR:1905。處理沒有重力,主要研究慣性參考系之間的時空關係。核心是洛倫茲變換、光速不變、時間膨脹、長度收縮、。

  • 廣義相對論 General Relativity, GR:1915。把重力納入,核心變成「重力不是普通力,而是時空幾何的曲率」。

GPS 裡面,狹義和廣義相對論是在修正兩種完全不同的效應:

  • 狹義相對論:因為衛星在高速運動,所以衛星上的鐘相對地面鐘會變慢。

  • 廣義相對論:因為衛星所在位置重力比較弱,所以衛星上的鐘相對地面鐘會變快。

GPS 剛好同時具有:

  1. 衛星相對地面高速運動 → SR。

  2. 衛星和地面處在不同重力勢 → GR。

所以兩個效應必須一起算。
General Relativity
└─ spacetime 可以彎曲、可以有重力
└─ 當 spacetime 是平坦的
└─ 就退化成 Special Relativity

時空沒有曲率,那麼廣義相對論使用的局部物理就回到 Minkowski spacetime,也就是狹義相對論。

還有一個更重要的說法:即使整個宇宙的時空是彎的,在一個足夠小的自由落體區域內,仍然可以近似成狹義相對論。

普通人不是一個數學上的質點。身體有:

复制代码
皮膚
└─ 骨骼
   └─ 肌肉
      └─ 血液
         └─ 大腦

力只能透過材料內部逐步傳播。

假設某個裝置先推你的背:

复制代码
背部先開始加速 →
   └─ 胸腔被壓縮
      └─ 內臟受到剪切
         └─ 血液產生巨大壓力梯度
            └─ 頭部稍後才跟上

如果加速度足夠大,人體會先被機械應力破壞,而不是「完整地瞬間變成 」。

這個人中間發生了加速、減速,因而更換了 inertial frame。這時就不能只拿一句「彼此都看對方變慢」處理,而要沿各自的 worldline 積分 proper time。


C++ / permutation / shader environment
└─ 決定這次 shader 需要哪些能力

BasePassPixelShader.usf
└─ 定義 PIXELSHADEROUTPUT_MRT0/1/2/... 之類的宏

#include "PixelShaderOutputCommon.ush"

PixelShaderOutputCommon.ush
└─ 根據宏真正生成
SV_Target0
SV_Target1
SV_Target2
...

也就是你現在看到的:

复制代码
hlsl



#if PIXELSHADEROUTPUT_MRT4
    , out float4 OutTarget4 : SV_Target4
#endif

這裡才是「HLSL 語法層面,這個 Pixel Shader 到底有沒有 SV_Target4」的最直接位置。

但「拼接」這個詞不要泛化。真正叫 token concatenation / token pasting 的只有 ##

例如 UE 那裡:

复制代码
C++



zzMemberId##MemberName

如果:

复制代码
MemberName = Output

才是真的拼成:

复制代码
C++



zzMemberIdOutput

而:

复制代码
C++



#MemberName

不是拼接,是 stringify:

复制代码
C++



TEXT(#MemberName)

變成:

复制代码
C++



TEXT("Output")

##

复制代码
C++



zzMemberId##MemberName

代入後:

复制代码
C++



zzMemberId##Output

## 把左右兩個 token 黏成一個新的 token:

复制代码
C++



zzMemberIdOutput

所以 compiler 看到的是一個真正的 C++ identifier:

#

复制代码
C++



#MemberName

代入:

复制代码
C++



#Output

結果不是 identifier,而是字串:

复制代码
C++



"Output"

## = token pasting,造新名字。

# = stringification,把 token 變成字串。

### 都是標準的巨集預處理機制;## 官方名稱就是 token-pasting operator,把兩個 preprocessing tokens 合成一個新的 token。gcc.gnu.org/onlinedocs/cpp/Concatenation.html

用一個宏自動生成 setter:

复制代码
C++



#define DECLARE_PROPERTY(Name) \
    int Name;                  \
    void Set##Name(int Value)  \
    {                          \
        Name = Value;          \
    }

寫:

复制代码
C++



DECLARE_PROPERTY(Health)

preprocessor 展開:

复制代码
C++



int Health;

void SetHealth(int Value)
{
    Health = Value;
}

這個 SetHealth 是真正的 C++ function identifier。Compiler 後面真的可以解析:

复制代码
C++



SetHealth(100);

如果你用 #

复制代码
C++



#Name

只會得到:

复制代码
C++



"Health"

你不能:

复制代码
C++



"SetHealth"(100);   // 完全不是函數

所以兩者最大的用途差別就是:

复制代码
#
Health
↓
"Health"
↓
產生資料
可以拿去 log / metadata / reflection name


##
Set + Health
↓
SetHealth
↓
產生程式碼中的 symbol / identifier
可以成為 variable / function / type 名字

Microsoft 和 GCC 都給了一個很典型的實際例子:假設一個 command 同時需要文字名稱和函數名稱,可以只寫一次 quit

复制代码
C++



#define COMMAND(NAME) \
    { #NAME, NAME##_command }

然後:

复制代码
C++



COMMAND(quit)

展開:

复制代码
C++



{ "quit", quit_command }

左邊的 "quit" 是資料;右邊的 quit_command 是實際函數 identifier。這就是 ### 經常一起出現的原因。gcc.gnu.org/onlinedocs/cpp/Concatenation.html

Renderer / BasePass C++
└─ 决定 Base Pass 的 rendering policy
├─ 哪些 Mesh 进入这个 Pass
├─ 每个 Mesh/Material 选哪个 shader permutation
├─ compile-time defines
├─ GBuffer layout / velocity / platform 等条件
└─ 实际绑定哪些 Render Targets
Material / Shading Model
└─ 是 Renderer + Shader 编译系统判断输出需求的重要输入
├─ Unlit / DefaultLit / Hair ...
└─ 影响是否需要 CustomData、GBuffer 等

代价是:目前会让符合该编译条件的 Unlit Base Pass 都保留 GBufferD 输出,而不是只让含 HairEyeRevealMask 的单个材质保留。

  • Renderer 在编译 Base Pass 时传入专用宏
  • PixelShaderOutputCommon.ush 根据这个宏,让 Unlit Base Pass 声明并写出 GBufferD 对应的 MRT
  • BasePassPixelShader.usf 在 UE 5.8 当前执行的编码路径中,把 HairEyeRevealMask 写进这个输出

你寫一個 CustomPass.cpp,並不是說:

GPU/CPU 執行時把 CustomPass.cpp 這個文件整份拿來調用。

更準確是:

复制代码
CustomPass.cpp
└─ C++ compiler 把它當成一個 translation unit
   ├─ 展開它 #include 的 .h
   ├─ 展開那些 .h 再 include 的其他 .h
   ├─ 處理 template / macro / inline code
   └─ 編譯成 .obj
      └─ linker 再和其他 .obj / library 組成 UE module / DLL

所以 .cpp 是「源碼組織邊界」,不是 runtime 的調用單位。

PixelShaderOutputCommon.ush,而且它確實存在,並且是被 BasePassPixelShader.usf 明確 #include 進去的。

UE 5.8 源碼就是:

复制代码
hlsl



// BasePassPixelShader.usf

// all PIXELSHADEROUTPUT_ and "void FPixelShaderInOut_MainPS()"
// need to be setup before this include

// this include generates the wrapper code to call
// MainPS(inout FPixelShaderOutput PixelShaderOutput)

#if COMPUTE_SHADED
    #include "ComputeShaderOutputCommon.ush"
#else
    #include "PixelShaderOutputCommon.ush"
#endif

所以你之前接觸的那條鏈實際是:

复制代码
BasePass 的 C++ Renderer code
└─ 選擇 / 編譯 BasePass Pixel Shader
   └─ BasePassPixelShader.usf
      ├─ 前面定義大量 PIXELSHADEROUTPUT_xxx
      ├─ 寫 Base Pass 自己的 shader 邏輯
      │
      └─ #include PixelShaderOutputCommon.ush
         └─ 根據前面那些 PIXELSHADEROUTPUT_xxx
            生成最終 MainPS 輸入/輸出 wrapper

PixelShaderOutputCommon.ush 自己開頭又有:

复制代码
hlsl



#include "ShaderOutputCommon.ush"

所以還會繼續形成:

复制代码
BasePassPixelShader.usf
└─ PixelShaderOutputCommon.ush
   └─ ShaderOutputCommon.ush
相关推荐
LYS_06181 小时前
Python学习(3)(数据类型小尾巴,条件控制,循环语句)
学习
迪丽热爱2 小时前
每日英语-12
学习
PC2005-cloud2 小时前
Elasticsearch 学习笔记:集群实战(3 控制节点 + 3 数据节点部署与故障转移)
笔记·学习·elasticsearch
世人万千丶2 小时前
物品借还闭环:鸿蒙物品清单种子数据与清单效果
学习·华为·harmonyos·鸿蒙
我命由我123452 小时前
商圈六大配套
学习·职场和发展·求职招聘·职场发展·产品经理·学习方法·零售
云水初2 小时前
【agent篇】RAG 知识库构建避坑指南
开发语言·python·学习·agent·rag
墨雨晨曦882 小时前
2026/08/16 AI学习笔记
笔记·学习
Orange_sparkle3 小时前
从五个 TypeScript 文件看懂 Coding Agent:一次 nano-pi 学习复盘
javascript·学习·typescript
m4Rk_3 小时前
【论文阅读】Agent 记忆机制(40):HiAgent——通过子目标级记忆提升长程任务执行能力
论文阅读·人工智能·学习·开源·github