AI教我UE渲染-学习笔记(2):使用SceneTextures
0. 使用SceneTextures
上一篇《AI教我UE渲染-学习笔记(1):清理视窗RT》中我直接操纵的是 ViewFamily.RenderTarget,这是最后显示在视窗上的RT。而原版的Render过程实际上是不断地操纵中间场景纹理即FSceneTextures的,最后才将结果显示到ViewFamily.RenderTarget上。本篇我想仿照类似的过程,即创建并清空SceneTextures,最后拷贝到ViewFamily.RenderTarget上。
让AI实现了我的目标:(实际上它实现的第一版代码命中了scene textures 的config是空的check,修复后又命中了D3D12的一个崩溃,再修复后才是下面能跑通的代码)
void FDeferredShadingSceneRenderer::Render_Learning(FRDGBuilder& GraphBuilder, const FSceneRenderUpdateInputs* SceneUpdateInputs)
{
// 当 SceneUpdateInputs 为空时,OnRenderBegin 中不会执行 PrepareSceneTexturesConfigTask,
// SceneTexturesConfig 与 FamilySize 未被初始化,在此按原版路径补做一次。
if (!GetActiveSceneTexturesConfig().IsValid())
{
PrepareViewRectsForRendering();
InitializeSceneTexturesConfig(GetActiveSceneTexturesConfig(), ViewFamily);
}
// 步骤 1:创建 SceneTextures(与 Render_Original 一致),清空 SceneColor,再拷贝到 ViewFamilyTexture。
FSceneTextures::InitializeViewFamily(GraphBuilder, ViewFamily, FamilySize);
FSceneTextures& SceneTextures = GetActiveSceneTextures();
const FLinearColor DarkBlue(0.02f, 0.02f, 0.12f, 1.0f);
AddClearRenderTargetPass(GraphBuilder, SceneTextures.Color.Target, DarkBlue);
if (SceneTextures.Config.NumSamples > 1)
{
AddResolveSceneColorPass(GraphBuilder, Views, SceneTextures.Color);
}
FRDGTextureRef ViewFamilyTexture = TryCreateViewFamilyTexture(GraphBuilder, ViewFamily);
if (ViewFamilyTexture && Views.Num() > 0)
{
// AddCopyTexturePass 要求格式一致(D3D12 CopyResource/CopyTextureRegion),当 SceneColor 为 HDR(如 FloatRGBA)
// 而 ViewFamily 为后缓冲(如 8bit)时会失败,故改用基于绘制的拷贝,由 shader 做格式/尺寸转换。
AddDrawTexturePass(GraphBuilder, Views[0].ShaderMap, SceneTextures.Color.Resolve, ViewFamilyTexture, FRDGDrawTextureInfo());
}
}
执行后发现,虽然它是完成了预期的逻辑,但清理的深蓝色的范围并非全屏。本篇希望能随着对相关代码的学习最终能找到这个问题的原因
1. FSceneTextures 概念
熟悉“延迟渲染”的概念的话一定知道 GBuffer 的概念。FSceneTextures其实就是存储GBuffer的结构:
让AI介绍下FSceneTextures,它的回答:(仅供参考)
可以把它想成 “这一帧场景渲染的共享工作台”: 不负责算光照、不负责画三角形,而是集中存放这一帧里各个 Pass 要读写的中间纹理。 谁要写深度、谁要写颜色、谁要读 GBuffer,都到这一套纹理里来拿/写。
职责定位:
- 统一管理本帧的“场景用”RT:在 RDG 里创建、持有,并随管线一步步被填满。
- 作为各阶段的约定接口:深度 Pass 写 Depth,Base Pass 写 Color + GBuffer,光照 Pass 读 GBuffer + Depth 再写 Color,后处理读 Color 等,都通过 FSceneTextures 的成员来约定“写哪张、读哪张”。
- 和最终屏幕解耦:所有 Pass 只和 FSceneTextures 打交道;最后再有人把 Color(或后处理结果)拷贝/合成到 ViewFamily 的后缓冲上屏。
所以:FSceneTextures = 本帧延迟渲染(及后处理)的“中间结果仓库”,不是最终要 Present 的那张图。
2. FRDGTextureMSAA相关概念
FSceneTextures继承自FMinimalSceneTextures,后者是包含所有渲染配置中通用的最小场景纹理集合的RDG结构(不像FSceneTextures主要用于延迟渲染)。FMinimalSceneTextures拥有成员Color
// Texture containing scene color information with lighting but without post processing. Will be two textures if MSAA.
FRDGTextureMSAA Color{};
根据注释可知它是屏幕的颜色在光照之后后处理之前的状态。Color类型是FRDGTextureMSAA。我让AI介绍了下相关的概念,他的回答:(仅供参考)
1) FRDGResource(最底层)
是什么:RDG 里「带名字、且对应一块 RHI 资源」的基类。
职责:
- 存一个 Name(调试用)
- 存底层 ResourceRHI(
FRHIResource*),即真正给 GPU 用的资源 - 提供 GetRHI(),在 Pass 执行时拿到 RHI 指针
比喻:图里「某一项资源」的抽象:只知道「有个名字 + 背后有一块 RHI 资源」,不关心是纹理还是缓冲。
代码位置:RenderGraphResources.h 约 130 行,class FRDGResource。
2) FRDGViewableResource(可被“看”的资源)
是什么:继承自 FRDGResource,表示「可以被创建 SRV/UAV 等 View、并且由 RDG 跟踪生命周期和访问」的资源。
在 FRDGResource 之上多了:
- Type(
ERDGViewableResourceType):是 Texture 还是 Buffer,便于安全转换 - bExternal / bExtracted / bProduced:是否外部注册、是否被提取、是否已被某 Pass 写入
- HasBeenProduced():是否有 Pass 写过
- GetParent():其上的 View(如 SRV/UAV)通过它找到「父资源」
比喻:在「有一块 RHI 资源」的基础上,加上「图会管谁写了它、谁读了它、能不能被 View 引用」——只有这类资源才能被当作纹理/缓冲来 bind 到 Pass。
关系:FRDGViewableResource : public FRDGResource(约 289 行)。FRDGTexture 和 FRDGBuffer 都继承自它。
3) FRDGTexture(具体的一种:纹理)
是什么:继承自 FRDGViewableResource,表示图里的一张纹理。
在 FRDGViewableResource 之上多了:
- Desc / Flags:尺寸、格式、MSAA、创建标志等(
FRDGTextureDesc) - GetRHI() 返回 FRHITexture*
- 子资源(Mip、Slice)的 Layout、State 等,用于 Barrier/过渡
- 与池化/临时分配相关的 RenderTarget、TransientTexture 等
比喻:就是「图里那一张 2D/3D 纹理」:创建、被哪个 Pass 写、被哪个 Pass 读、何时做 Transition,都由 RDG 通过这个类型管理。
关系:class FRDGTexture final : public FRDGViewableResource(约 569 行)。
平时用的 FRDGTextureRef 就是 FRDGTexture*(RenderGraphFwd.h)。
4) FRDGTextureMSAA(MSAA 的一对纹理,不是继承)
是什么:一个结构体,不是类继承。里面放两个 FRDGTextureRef:
- Target:多采样渲染目标(写 MSAA 的那张)
- Resolve:解析后的单采样纹理(给后续 Pass 读的那张)
用途:开 MSAA 时,渲染先写到 Target,再 Resolve 到 Resolve;不用 MSAA 时可以让 Target 和 Resolve 指向同一张 FRDGTexture。
比喻:像一个「双格收纳盒」:要么两格各放一张纹理(MSAA),要么两格都指向同一张(非 MSAA)。它不是 FRDGTexture 的子类,而是「对 FRDGTexture 的引用的一种组合」。
关系:与上面三个没有继承关系。
- 它内部持有两个 FRDGTextureRef(即两个
FRDGTexture*)。 - 所以:FRDGTextureMSAA 使用 FRDGTexture,而不是继承它。
关系小结(从简单到复杂)
FRDGResource ← 最基础:名字 + RHI 指针
│
└── FRDGViewableResource ← 可被 View 引用、图跟踪读写与生命周期
│
├── FRDGTexture ← 具体的一种:图里的一张纹理(FRDGTextureRef = FRDGTexture*)
└── FRDGBuffer ← 另一种:图里的一块缓冲
FRDGTextureMSAA ← 独立结构体:包两个 FRDGTextureRef(Target + Resolve),用于 MSAA 流程
一句话:FRDGResource 是图里资源的根;FRDGViewableResource 是「可被 View 且被图管理」的那一类;FRDGTexture 是其中的「纹理」这一种;FRDGTextureMSAA 则是用两个 FRDGTexture 引用组成的 MSAA 配对,不参与继承链。
3. FSceneTexturesConfig 概念
SceneTexturesConfig 看起来是创建SceneTextures时的配置,让AI介绍下,它的回答:(仅供参考)
FSceneTexturesConfig 是创建 FSceneTextures 的配置;其中 Extent 就是场景纹理的分辨率(宽×高),和 NumSamples、ColorFormat、ShadingPath 等一起决定这一帧会创建哪些、多大、什么格式的 Scene 纹理。成员举例:
成员 含义 是否关键 Extent 全分辨率场景纹理的 (宽, 高),即分辨率 ✅ 最关键之一 ShadingPath / FeatureLevel / ShaderPlatform 管线与平台,决定格式与 IsValid() ✅ 关键 NumSamples Color/Depth 的 MSAA 采样数 ✅ 关键 ColorFormat / ColorCreateFlags / DepthCreateFlags SceneColor/Depth 的格式与创建标志 ✅ 关键 ColorClearValue / DepthClearValue 默认清除值 常用 GBufferParams / GBufferBindings 延迟 GBuffer 布局 延迟管线关键 SmallDepthDownsampleFactor 小深度图相对 Extent 的下采样倍数 常用
其中Extent是最关键的成员之一,它代表着纹理的分辨率。
4. 创建纹理处log
使用Extent为尺寸创建SceneTextures的纹理的地方在FSceneTextures::InitializeViewFamily中
让AI在此处加了一句log:
UE_LOG(LogRenderer, Log, TEXT("[Render_Learning] 使用尺寸 %d x %d 创建了 SceneTextures 的 Color"), Config.Extent.X, Config.Extent.Y);
测试发现,原版和Learning版在此处的行为是一致的:
- 上述log不断打出。
- 随着视窗尺寸调整,Extent值会变大,但是从不会缩小。

- 进一步截帧,在RenderDoc中发现:
这里的Extent的值确定就是SceneColor这个纹理的分辨率
而它与最终显示到视窗里的这个RT(名字叫BufferedRT)的分辨率并不一致。
即:
- SceneTexturesConfig.Extent = SceneColor(或者说GBuffer)的分辨率 = 目前视窗里蓝色区域大小
- 视窗总的分辨率 = BufferedRT的分辨率
那看起来我单纯使用AddDrawTexturePass这个接口将SceneColor画到视窗RT上发现大小不匹配的结果,是可以理解的。原版Render函数里这些GBuffer的分辨率同样也是不匹配视窗的,但是原版的图像能填满视窗,看起来要通过后续的学习才能理解了。
5*. RenderDoc中显示的资源名
我感兴趣为啥我在RenderDoc中能看到类似“SceneColor” “BufferedRT” 这样的资源的名字,就问了AI。
AI回答如下:(仅供参考)
- 条件:
- 编译时打开 RHI_USE_RESOURCE_DEBUG_NAME(通常 Development/Debug 会开),这样 D3D12 才会真的调
SetName。- RDG 纹理:
- 用
GraphBuilder.CreateTexture(Desc, TEXT("你的名字"))或CreateTextureMSAA(..., NameMS, NameResolve, ...)等带 Name 的接口;这个名字会作为 Pool 的Name传入FRHITextureCreateDesc,最后变成 D3D12 的 Debug Name。- 非 RDG / 直接 RHI:
- 用
FRHITextureCreateDesc::Create2D(DebugName)(或等价接口)在创建时传入 DebugName,D3D12 会在创建后对该资源调用SetName(CreateDesc.DebugName)。所以:RenderDoc 里能显示 “SceneColor”“BufferedRT” 等,是因为 UE 在创建或复用这些纹理时,把名字写进了 RHI 的 CreateDesc.DebugName,D3D12 再通过
ID3D12Object::SetName设到资源上;RenderDoc 只是读取 D3D12 里已经设好的名字。
当我修改视窗大小时,可以看到视窗的RT被重新创建,此时这个名字被传入:
6. Config.Extent的计算方式
我感兴趣这个Extent的计算方式,于是问AI
回到之前讨论Extent的问题。加了那个Extent的Log后,我发现:
原版和Learning版在此处的行为是一致的:
- 上述log不断打出。
- 随着视窗尺寸调整,Extent值会变大,但是从不会缩小。
- 这个Extent值不等于视窗的大小。
这让我有些困惑,我以为这个Extent应该是视窗大小的。所以请告诉我这个Extent是怎样计算的,以及为什么它只会变大不会缩小?
AI的回答:(仅供参考)
结论先说:Extent 不是“当前视窗大小”,而是“本帧/历史里算出来的内部缓冲期望尺寸”;在编辑器下会强制用“只增不减”的 Grow 策略,所以只会变大、不会缩小。
下面按你关心的三点说明:Extent 怎么算、为什么只增不减、为什么和视窗不一致。
1. Extent 是怎么算出来的
整体在 FSceneTextureExtentState::Compute(ViewFamily)(SceneTextures.cpp 约 154–336 行)里完成。
第一步:算“本帧期望尺寸” DesiredFamilyExtent
- 调用
FSceneRenderer::GetDesiredInternalBufferSize(ViewFamily)(SceneRendering.cpp 约 3144 行)。 - 这里不是直接用视窗的宽高,而是:
- 用每个 View 的 UnconstrainedViewRect(或在不支持 Screen Percentage 时用 UnscaledViewRect)做基准;
- 乘上 ResolutionFractionUpperBound(屏幕百分比、动态分辨率上界、SecondaryViewFraction、Lens 畸变、Overscan 等);
- 得到
FamilySizeUpperBound,再 QuantizeSceneBufferSize 量化。
- 所以 DesiredFamilyExtent = 经过 Screen Percentage / 动态分辨率 / 量化后的“内部缓冲期望尺寸”,已经和“裸视窗像素”不一样了。
第二步:按“缩放策略”得到 DesiredExtent
- 使用
r.SceneRenderTargetResizeMethod(0/1/2)选策略,但在编辑器里会被覆盖(见下):- RequestedSize (0):
DesiredExtent = DesiredFamilyExtent(每帧跟请求尺寸走,可以随窗口变小而变小)。 - ScreenRes (1):
DesiredExtent = (GSystemResolution.ResX, GSystemResolution.ResY)(固定为屏幕分辨率)。 - Grow (2):
DesiredExtent = max(LastExtent, DesiredFamilyExtent),即至少保持上一帧的 Extent,只能 ≥ 上一帧。
- RequestedSize (0):
第三步:Grow 下的“历史最大”逻辑(只增不减的关键)
- 每帧会把当前
DesiredExtent和 LargestDesiredExtents[0…2](最近几帧出现的最大请求)做 ComponentMax,再参与后续计算(约 323–328 行)。 - 最终会 LastExtent = DesiredExtent 写回历史(约 334 行)。
- 因此在 Grow 模式下,Extent 实际上是“历史出现过的最大请求尺寸”,窗口缩小不会让 Extent 缩小。
2. 为什么“只会变大、不会缩小”
- 在编辑器里(
GIsEditor == true)代码强制使用 Grow,不用 CVar:
else if (GIsEditor)
{
// Always grow scene render targets in the editor.
SceneTargetsSizingMethod = Grow;
-
所以你在编辑器中看到的 Extent 一定走 Grow:
- 当前帧:
DesiredExtent = max(LastExtent, DesiredFamilyExtent); - 再和
LargestDesiredExtents[]取 max; - 结果只会 ≥ 上一帧,不会因为窗口缩小而减小。
- 当前帧:
-
设计目的:减少因 RT 尺寸变化导致的重新分配和卡顿;用“曾经要过的最大尺寸”作为当前 RT 尺寸,避免频繁 resize。
-
若希望“缩小窗口时 Extent 也跟着缩小”,需要:
- 在非编辑器下使用 r.SceneRenderTargetResizeMethod 0(RequestedSize),并且确保没有别处强制 Grow;或
- 调 ResetSceneTextureExtentHistory(例如
r.ResetSceneTextureExtentHistory)清空历史,下一帧会按当前请求重新算(可能变小)。
3. 为什么 Extent ≠ 视窗大小
-
DesiredFamilyExtent 本身就不是“视窗像素”:
- 视窗大小 → 会转换成 View 的 UnconstrainedViewRect 等;
- 再乘 Screen Percentage / 动态分辨率上界 / SecondaryViewFraction / Lens / Overscan;
- 再 QuantizeSceneBufferSize。
-
所以 Extent 是“内部场景渲染用的缓冲尺寸”,和“窗口客户区像素”本来就可以不同(例如 80% 分辨率时会更小,或量化后略大一点)。
-
再加上 Grow + 历史最大:
- 一旦某帧视窗或分辨率比例变大过,Extent 就会涨上去并保留;
- 之后即使把窗口拉小,Extent 也不会降下来,所以会看到 Extent 比当前视窗大 的情况。
Extent小结(直接回答你的三点)
| 现象 | 原因 |
|---|---|
| Log 每帧都打 | 每帧都会调 InitializeViewFamily → FMinimalSceneTextures::InitializeViewFamily,Scene Color 每帧都会创建/复用,你的 Log 在创建路径上,所以会不断打。 |
| Extent 只会变大、不会缩小 | 编辑器里强制用 Grow;Grow 下 Extent = max(上一帧 Extent, 本帧请求) 再与历史最大取 max,所以只会增不会减。 |
| Extent ≠ 视窗大小 | Extent 来自 GetDesiredInternalBufferSize(经过 Screen Percentage、动态分辨率、量化),再经 Grow 保留历史最大,所以既不是“当前视窗像素”,也可能比当前视窗大。 |
若想验证“按请求尺寸变化”:在非编辑器下设 r.SceneRenderTargetResizeMethod 0 和 r.SceneRenderTargetResizeMethodForceOverride 1,Extent 会跟随“请求的缓冲尺寸”变化(可能随窗口缩小而缩小);在编辑器中仍会强制 Grow,行为不变。
后续
本篇已经学习了SceneTextures的基本概念,但是当前Render_Learning中还有内容值得学习,就放在下一篇了。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)