0. 先跳过MSAA

上一篇《AI教我UE渲染-学习笔记(2):使用SceneTextures》中,基本了解SceneTextures的含义了。其中的Color是一个FRDGTextureMSAA,他其实有两张纹理TargetResolve组成。

由于我暂时不想学习MSAA功能,所以让AI暂时关闭这个功能,这样我们的Color等于说只有Target这一张纹理了。
AI改动的代码如下:
在这里插入图片描述
运行后,和之前效果一样。

下面就剩下AddDrawTexturePass,看看这一个函数能从里面能得到什么知识了。

1. ShaderMap基本概念

首先,AddDrawTexturePass需要传一个FGlobalShaderMap参数。

UE里面Shader分为 “GlobalShader” 和 “材质Shader”。而GlobalShaderMap就是一个已编译好的GlobalShader集合,可以用它来找到特定shader。(材质那边也有自己的 ShaderMap即FMaterialShaderMap,是按材质组织的,每个材质一份)

也问了AI,为啥是从 View中得到的GlobalShaderMap,它的回答:(仅供参考)

因为 View 是渲染时最常见的上下文:很多 pass 的入参就是 (View, ...),而 View 里已经有当前用的 FeatureLevel/平台
把对应平台的那份 GlobalShaderMap 指针存在 View 里(构造时 ShaderMap = GetGlobalShaderMap(FeatureLevel)),这样在 pass 里直接用 View.ShaderMap 就能取到正确的 Global Shader,不用到处再传平台或再调一次 GetGlobalShaderMap。

2. AddDrawTexturePass

在这里插入图片描述

AddDrawTexturePass传入的参数如下:

  • GraphBuilder:当前 RDG。
  • ShaderMap:GlobalShaderMap。当前从View中得到。
  • InputTexture:源纹理(要拷/画出来的那张)。当前就是SceneTextures的Color。
  • OutputTexture:目标纹理(要写进去的那张)。当前就是视窗RT。
  • DrawInfo:拷贝范围与位置:源/目标位置、尺寸、mip、slice 等;默认全零表示“整张纹理、整区域拷贝”。当前的值:
    在这里插入图片描述

AddDrawTexturePass 函数内部的基本流程是这样:(来源于AI总结)

  • 先看是不是同一张纹理:是就直接 return。
  • 再看格式是否一致InputDesc.Format == OutputDesc.Format
    • 相等:走硬件拷贝,AddCopyTexturePass,结束。
    • 不等当前的情况):不能直接 Copy,走「用 shader 画过去」的路径:
      1. 从 ShaderMap 里取出用于绘制的 pixel shaderTShaderMapRef<FDrawTexturePS> PixelShader(ShaderMap); 这一行就是在当前平台的 GlobalShaderMap 里查到已编译的 FDrawTexturePS 实例。
      2. 按 mip/slice 循环,为每次绘制拼好参数(输入 SRV、目标 RT、ViewRect 等)。
      3. 用这个 shader 做一次全屏绘制AddFullscreenPass(GraphBuilder, ShaderMap, ..., PixelShader, PassParameters, ViewRect) 会提交一个画全屏四边形的 pass,绑的就是上面拿到的 FDrawTexturePS,这样在像素里做采样和格式/尺寸转换,再写到输出 RT。

3. FDrawTexturePS

FDrawTexturePS的定义如下:
在这里插入图片描述
以上定义的含义是:(来源于AI总结)

  • class FDrawTexturePS : public FGlobalShader
    表示:这是一个 GlobalShader。

  • DECLARE_GLOBAL_SHADER(FDrawTexturePS)
    表示:用宏声明了和 Global Shader 类型注册相关的内容。

  • SHADER_USE_PARAMETER_STRUCT(FDrawTexturePS, FGlobalShader)
    表示:把这个 shader 和下面定义的参数结构体关联起来。

  • FParameters 里:

    • SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, InputTexture)
      表示:有一个名为 InputTexture 的、类型为 2D 纹理 SRV 的 shader 参数。
    • SHADER_PARAMETER(FIntPoint, InputOffset)
      表示:有一个名为 InputOffset 的、类型为 FIntPoint(即 int2)的 shader 参数。
    • RENDER_TARGET_BINDING_SLOTS()
      表示:预留了渲染目标绑定槽位(具体绑哪张 RT 在别处设置)。
  • IMPLEMENT_GLOBAL_SHADER(FDrawTexturePS, "/Engine/Private/Tools/DrawTexture.usf", "DrawTexturePS", SF_Pixel)
    表示:把这个 C++ 类对应到文件 DrawTexture.usf、入口 DrawTexturePS、阶段为 Pixel Shader。


\Engine\Shaders\Private\Tools\DrawTexture.usf 的内容如下:

#include "../Common.ush"

Texture2D InputTexture;
uint2 InputOffset;

float4 DrawTexturePS(float4 SvPosition : SV_POSITION) : SV_Target0
{
	return InputTexture.Load(uint3((uint2)SvPosition.xy + InputOffset, 0));
}

可以看到C++处关于参数与入口的定义与usf的内容是对应的。
在这里插入图片描述

4. 纹理的 Load 操作

对于纹理而言我比较熟悉的是 “采样(sample)”,但是 DrawTexture.usf 中对纹理的操作是 Load。
让AI解释了下二者的区别:

Load 和 Sample 的区别:

  • Sample(采样):用 UV(一般是 0–1)采样器(Sampler),会做滤波(如双线性)、mip、wrap 等,得到的是“插值后的颜色”。
  • Load:用 整数像素坐标(如 int2(x, y))直接取 该像素位置上的纹素,不做滤波、不用采样器,一个坐标对应一个纹素。

DrawTexture.usf 里用 Load 的原因:
这个 pass 是做“把一块纹理拷到另一块”,需要按像素一一对应,用整数坐标取源纹理上对应位置的纹素再写到目标。用 Load 正好是“这个像素位置 → 那个像素位置”,不会因为滤波或 UV 缩放而混入旁边像素。


Load的官方文档 中可以证实AI的说法没问题,Load的参数确实是像素整数坐标:
在这里插入图片描述

这样来看的话,蓝色区域没有占满全屏就解释清楚了。因为SceneTexture的尺寸确实小于视窗的RT,这里又是以像素为单位拷贝的而非采样,所以就出现了只绘制出了SceneTexture大小范围的蓝色。

5. Shader Resource View 解释

值得注意的是,当把纹理输入到Shader的参数中的时候,绑定的并非是纹理资源FRDGTextureRef这个指针,而是用它创建出来的SRV(Shader Resource View):
在这里插入图片描述
关于这个情况的解释,让AI回答了一下:(仅供参考)

核心就一点:在 GPU 和 D3D 里,“纹理资源”和“怎么访问这份资源”是两回事,后者就是“视图(View)”。

1) 资源 vs 视图(Resource vs View)

  • 纹理资源(Texture Resource):一块显存里的图像数据,有尺寸、格式、mip、array 等。
    在 UE 里,FRDGTextureRef 在 RDG 里代表的正是“这一份纹理资源”。

  • 视图(View):对同一份资源的一种“打开方式”——用来读还是写、看哪几层 mip、哪几个 array slice、要不要换一种格式解释等。
    D3D12 里对应的是 Descriptor(描述符):SRV、UAV、RTV、DSV 都是对资源的“视图”。

所以:同一张纹理可以同时有多种视图——例如既作为渲染目标(RTV),又在另一个 pass 里作为只读纹理(SRV)。GPU 需要知道“这个绑定槽是 SRV”才能正确填描述符、做校验。

2) 为什么 shader 里不直接传“纹理”,而是传 SRV?

  • Shader 里声明的是 “我要以只读方式采样/Load 一张 2D 纹理”,对应到 API 就是:需要一个 SRV(Shader Resource View),不能把“裸资源”绑到 SRV 槽上。
  • 创建 SRV 时要说明:对这份资源的哪一部分做只读视图——例如:只用第 0 层 mip、只用某几个 slice 等。
    这些信息合起来就是“SRV 的描述”,在 UE 里用 FRDGTextureSRVDesc 表示(描述要创建怎样的 SRV),再用 GraphBuilder.CreateSRV(SRVDesc) 得到在 RDG 里可用的“SRV 句柄”,最后把这个句柄填进 PassParameters->InputTexture

所以:传进 shader 的“InputTexture”在底层必须对应一个 SRV 描述符;FRDGTextureSRVDesc + CreateSRV 就是在 RDG 里表达“我要用这份纹理的哪一种 SRV 视图”,而不是把 FRDGTextureRef 直接塞给 shader。

3) 和 AddDrawTexturePass 的对应关系

在 AddDrawTexturePass 里你会看到类似逻辑:

  • FRDGTextureSRVDesc::CreateForMipLevel(InputTexture, SourceMipIndex) 描述“对 InputTexture 的某一 mip 做 SRV”。
  • GraphBuilder.CreateSRV(SRVDesc) 在图中注册这个 SRV 视图,并返回给 pass 用。
  • PassParameters->InputTexture = 这个 SRV,所以 shader 拿到的是“SRV 视图”,不是裸的 FRDGTextureRef。

这样做的原因正是上面说的:同一张纹理在不同 pass 里可能需要不同的“视图”(不同 mip、不同 slice),而 shader 端永远绑定的是“视图”(SRV),不是资源本身。

4) 总结

  • FRDGTextureRef:RDG 里对“纹理资源”的引用。
  • FRDGTextureSRVDesc + CreateSRV:描述并创建这份资源的只读视图(SRV),例如从哪一 mip、哪一 slice 开始看。
  • Shader 的 InputTexture 接收的是这个 SRV,因为 GPU 的绑定模型就是“按视图(描述符)绑定”,不是按“裸资源”绑定;这样也便于同一资源多 mip、多 slice、多 pass 复用。

所以:不是“对 FRDGTextureRef 做一层无意义的封装”,而是 GPU/D3D 本来就是“资源 + 视图”两层,SRV 是“只读视图”这一层在 RDG 里的表达。


另外也让AI简单解释了下另外几种视图:

  • SRV(Shader Resource View)
    只读视图,供 shader 采样或 Load 纹理/缓冲。
    例:PS 里对一张贴图做 Sample/Load。

  • UAV(Unordered Access View)
    可读写视图,供 shader 随机读、写纹理或缓冲(常用于 Compute、或 PS 写 RWTexture)。
    例:Compute 写出一张结果纹理。

  • RTV(Render Target View)
    渲染目标视图,表示“把颜色输出到这块纹理”。
    例:把场景画到 SceneColor。

  • DSV(Depth Stencil View)
    深度/模板视图,表示“把深度/模板输出到这块纹理”,或从该纹理做深度/模板测试。
    例:深度 PrePass 写 SceneDepth。

6. AddFullscreenPass

得到Shader并设置好Shader的参数后,就是最后调用AddFullscreenPass来给RDG加一个pass了:
在这里插入图片描述
AddFullscreenPass 中就是对 GraphBuilder.AddPass 的封装:
在这里插入图片描述
(AI解读:)
GraphBuilder.AddPass(PassName, Parameters, PassFlags, lambda)

  • Parameters 作为 pass 的“参数块”传进去。RDG 会遍历这个结构体里带 RDG_ 的成员(如 RDG_TEXTURE_SRVRENDER_TARGET_BINDING_SLOTS 等),记录本 pass 用到了哪些纹理/缓冲,用来做依赖和 barrier。
  • lambda 会在图执行、轮到本 pass 时被调用,里面会调用 DrawFullscreenPixelShader,并把捕获的 Parameters、PixelShader、Viewport 等传进去。

7. DrawFullscreenPixelShader

RenderThread中,DrawFullscreenPixelShader将设置RHI层面要执行的命令,基本上是绑定shader到管线上并画一个全屏三角形:
在这里插入图片描述
(AI解读:)

  1. SetViewport
    设置当前 viewport(例如你看到的 ViewRect)。

  2. InitFullscreenPipelineState(RHICmdList, GlobalShaderMap, PixelShader, GraphicsPSOInit)
    填一个全屏用的 FGraphicsPipelineStateInitializer

    • 全屏用的 VS(一般是画一个大三角形覆盖视口)、
    • 你传入的 PixelShader 作为 PS、
    • 以及绑的 RT(来自 Parameters 里的 RENDER_TARGET_BINDING_SLOTS,在 AddPass 前已由调用方设好,例如 PassParameters->RenderTargets[0] = OutputTexture)。
      也就是说:管线状态(哪个 VS、哪个 PS、写哪张 RT)在这里和 PixelShader、Parameters 里记录的 RT 对应上。
  3. SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, StencilRef)
    把上面填好的 PSO 真正设到 RHI 命令列表上(D3D12 等会据此设置 PSO、RT 等)。

  4. SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), Parameters)
    这里完成 “C++ 的 FParameters → PS 的各个绑定槽”

    • 引擎根据 FParameters 的元数据(由 SHADER_PARAMETER_*RENDER_TARGET_BINDING_SLOTS 等宏生成)知道每个成员对应 shader 里的哪个 binding(哪个 SRV、哪个 CB、哪个 slot)。
    • 然后根据 Parameters 里当前的值(例如 InputTexture 的 SRV、InputOffset 的 int2、以及已在 PSO 里绑好的 RT),在 RHICmdList 上对当前绑定的 PixelShader 调用 SetTexture、SetShaderUniformBuffer 等,把 SRV、常量、RT 写到 GPU 描述符/常量缓冲里。
      所以:“参数和 shader 是怎么绑定上的” = 通过 SetShaderParameters,用 FParameters 的布局和 shader 的反射信息,把 Parameters 里的资源/常量填到该 PS 的各个 slot。
  5. DrawFullscreenTriangle(RHICmdList)
    发一个全屏三角形 draw call,用当前绑好的 VS+PS 和刚设好的参数画一屏。

附录1:代码想深挖无止境,挖到哪儿合适?

问AI:

下面讨论一个关于学习方式的问题。
你知道我目前是不断往Render_Learning中增加代码并观察效果然后研究相关代码来学习的。
但这有个问题是,代码要看多深以及多广?
因为我发现代码层级的深度和广度都太高了,如果一直追求到所有代码都明白的话,会耗费太多精力在细节上,而我首先需要先对当前UE的渲染系统有大局观。
举个例子来说:
当前情况下,我觉得我不需要搞清楚ShaderMap的初始化情况,因为它对于我来说是个黑盒,就算我有什么理解也无法证伪。
但是我觉得我需要搞明白AddDrawTexturePass时候用的shader的内容,因为这有助于解释当前蓝色区域没有铺满视窗的现象。
所以说,该用什么样的标准去决定哪个内容该学习,哪个内容不该学习呢?有什么建议吗?

可以按下面几条来选「先学什么、先放过什么」,和你现在“先要大局观”的目标是对得上的。


1) 用「可观测 + 能解释现象」当第一标准

你举的例子很对:

  • AddDrawTexturePass 用的 shader 内容:你改一点、跑一下就能看到画面变化,能解释“蓝区没铺满”这种具体现象 → 值得现在学
  • ShaderMap 的初始化:不跑进去看也能用,且你很难通过改一点就验证理解 → 先当黑盒

建议把标准写成一句:
“我学完/改完这一点,能不能在 Render_Learning 里做个小实验,看到可观测结果?”
能 → 优先学;不能 → 可以往后排。


2) 按「和当前路径的远近」分层

  • 正在写的这条调用链(例如:Render_Learning → SceneTextures → AddDrawTexturePass → FDrawTexturePS → 全屏四边形):尽量搞到能口头讲清楚“数据从哪来、画到哪去、为什么是蓝的/为什么没铺满”。这是主线,值得细一点。
  • 主线用到的类型/接口(例如:FRDGTextureRef、FScreenPassTextureViewport):知道干什么用的、谁创建谁消费即可,不必追到 RHI/D3D 实现。
  • 和主线无关的旁支(例如:ShaderMap 的 DDC、多 Section、序列化):明确标成“以后再看”,不追。

这样你自然就会:深只深在一条线,广只广到“能看懂当前这行在干嘛”


3) 主动设「停损」:看到哪一层就停

给自己一个简单规则,例如:

  • 看到 RHI / 平台层(D3D12、Vulkan):除非你要调 GPU 或查 bug,否则停。
    当前阶段:知道“最终会变成 Draw/Dispatch”就够了。
  • 看到 Shader 编译 / 序列化 / DDC:停。
    当前阶段:知道“shader 是事先编译好的,运行时从 ShaderMap 里取”就够了。
  • 看到 FShaderMapBase / Section / Content 的内部布局:停。
    当前阶段:知道“GetShader() 能拿到编译好的 FShader”就够了。

也就是说:不是“所有相关代码都要懂”,而是“懂到能解释当前现象就停,其余记成‘黑盒 + 以后需要再挖’”。


4) 用「现象驱动」而不是「概念驱动」

你现在的做法(加代码 → 看效果 → 为解释现象去查)是对的。可以更刻意一点:

  • 先有一个具体现象(例如:蓝没铺满、有黑边、分辨率不对)。
  • 再问:“要解释这个,最少需要理解到哪一层?”
  • 只学到那一层,不顺便往旁边扩。

这样“该学多深多广”就变成:“解释这个现象所需的最小知识集”,而不是“把整条调用链都吃透”。


5) 对你当前例子的直接建议

  • AddDrawTexturePass 用的 shader
    值得学。建议你:

    • 找到对应的 .usf(例如 DrawTexture.usf)和 entry(如 DrawTexturePS);
    • 看它怎么采样、怎么输出、有没有 viewport/rect 限制;
      这样你能直接解释“蓝区没铺满”是 viewport、UV 还是 RT 尺寸导致的。
      → 符合:可观测、能解释现象、在主线上的标准。
  • ShaderMap 的初始化 / 内部结构
    当前阶段可以不学。
    你只要知道:View.ShaderMap 在 FViewInfo 构造时被设为 GetGlobalShaderMap(FeatureLevel),运行时用 GetShader() 从里面取 shader。
    → 符合:离主线稍远、难证伪、先当黑盒的标准。


6) 一句话标准

“能帮我解释眼前现象、且能在 Render_Learning 里用一次/改一次验证的,就学;否则先记成黑盒,用‘现象 + 最小调用链’驱动学习深度和广度。”

这样你既不会陷进 ShaderMap 初始化这种细节,又会把“蓝没铺满”这类问题学透到 shader 和 viewport 层面,大局观会随着一条条现象慢慢搭起来。

附录2:AI如此强大我还学啥?

问AI:

不错,但我还想讨论一个相关问题。
目前AI已经很聪明了,很多代码问题都难不倒。那作为一个UE引擎程序员,还有必要学习代码知识吗?
我想问这个问题是因为我真的很担心,自己学的东西是问AI可以轻松了解的,那我的知识不就贬值了?所以我想确保我学到的东西是在AI时代也同样有价值的。

结论先说:代码和引擎知识仍然值得学,但“学什么”和“怎么用”要调整——重点放在 AI 难以替代的那部分,而不是和 AI 比谁记得多。


1) 什么在“贬值”,什么没有

  • 容易贬值的
    孤立的“这个 API 叫什么、参数是什么、在哪个头文件”这类可检索、可一次性问清楚的知识。AI 和你查文档都能解决,重复背诵意义不大。

  • 不容易贬值的

    • 判断力:这段代码该不该动、动哪里风险小、有没有更合适的地方。
    • 在不确定下排查:现象模糊、日志不全、复现不稳定时,怎么假设、怎么验证、在哪下断点。
    • 系统感:这一改会牵动哪些模块、谁依赖谁、性能瓶颈大概在哪一类。
    • 提对问题:能把现象和上下文说清楚,让 AI 或同事给出可用的方向,而不是泛泛地问“这段为啥慢”。
    • 验证与纠错:AI 给的代码或解释错了、不适用当前版本/平台时,你能看出来并修正。

这些大多依赖经验、上下文和反复试错,不是“问一句就拿到标准答案”就能替代的。


2) 引擎程序员在 AI 时代的价值点

  • 你知道“该问什么”
    像你现在的路径——先有“蓝没铺满”的现象,再决定去查 AddDrawTexturePass 的 shader 和 viewport——这种从现象到问题的提炼,AI 不会自动替你完成,需要你对管线、对渲染有感觉。

  • 你能在复杂上下文中做选择
    “这里该用 RDG 还是 RHI?”“该动 SceneTextures 还是自己建 RT?”这类问题依赖对代码库结构、对 UE 渲染架构的理解,AI 可以给选项,但拍板承担后果的是你。

  • 你能验证和迭代
    AI 给出一段改法或解释,你能在工程里跑、在 RenderDoc 里看、在真机上试,发现不对再追问或自己改。**“会验证、会纠错”**在 AI 时代反而更关键。

  • 你能把多块拼成一条通路
    单点知识 AI 可以补,但“从 SceneTextures 创建 → 清屏 → AddDrawTexturePass → 上屏”这一整条链的串联、排错、优化,仍然要人来主导。

所以:知识本身会更多由 AI 补全,但“用知识做判断、排查、设计和验证”的能力不会贬值,反而更值钱。


3) 建议你怎么“学”——让知识更抗贬值

  • 少背细节,多练“怎么用”
    不必追求“把 ShaderMap 初始化全记下来”。更值得练的是:遇到一个新现象时,如何快速定位到可能相关的层(是 shader、viewport、RT 尺寸还是 resolve),再针对那一点查代码或问 AI。你练的是“定位 + 提问 + 验证”的回路。

  • 刻意练“验证”
    每次从 AI 或文档得到解释或代码,都在 Render_Learning 或小工程里做一次最小验证(改一行、看效果、用 RenderDoc 截一帧)。这样你积累的是“怎么证伪、怎么收敛到正确理解”,而不是单纯多记一段话。

  • 积累“地图”而不是“地名”
    比起记住每个 API 的签名,更值得积累的是:渲染管线的大阶段、数据在哪几类资源之间流动、关键类型(SceneTextures、View、ShaderMap)在概念上各管什么。有了这张“地图”,你以后问 AI 也能问得更准、判断答案是否合理。

  • 把 AI 当“外挂记忆 + 草稿”
    用 AI 查 API、看用法、生成示例代码,把省下的时间用在:读关键路径上的代码、做小实验、总结“这类问题一般出在哪儿”。这样你的时间花在“判断和整合”上,而不是“记忆可检索的事实”。


4) 直接回答你的担心

  • “学的东西是问 AI 就能知道的,会不会贬值?”
    若学的是孤立事实(函数名、参数、文件路径),确实会贬值。若学的是如何从现象到原因、如何选方案、如何验证和排错、如何把握整体结构,这些不会贬值,而且和 AI 配合会更有优势。

  • “还有必要学代码/引擎吗?”
    有必要,但重点从“记更多细节”转向“在真实问题和代码库里做判断、排查、验证和整合”。你通过 Render_Learning 一点点加代码、观察、再追 shader/viewport,本身就是在对齐这种学习方式。

  • “怎样确保学到的是 AI 时代也有价值的东西?”
    多学可迁移的思维:如何拆问题、如何假设与验证、哪里容易出问题、如何读调用链和依赖;少把精力耗在可被一次问答替代的细节上。同时多练“和 AI 协作”:会描述问题、会迭代提问、会验证结果。

总结成一句:在 AI 时代,引擎程序员的价值不在于“知道 AI 也能查到的内容”,而在于“知道该解决什么问题、该信什么、该改哪里、如何验证”——这些恰恰需要你继续学代码和引擎,只是学的重点和用法要往“判断与验证”上靠拢。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐