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(调试用)
  • 存底层 ResourceRHIFRHIResource*),即真正给 GPU 用的资源
  • 提供 GetRHI(),在 Pass 执行时拿到 RHI 指针

比喻:图里「某一项资源」的抽象:只知道「有个名字 + 背后有一块 RHI 资源」,不关心是纹理还是缓冲。

代码位置RenderGraphResources.h 约 130 行,class FRDGResource

2) FRDGViewableResource(可被“看”的资源)

是什么:继承自 FRDGResource,表示「可以被创建 SRV/UAV 等 View、并且由 RDG 跟踪生命周期和访问」的资源。

在 FRDGResource 之上多了

  • TypeERDGViewableResourceType):是 Texture 还是 Buffer,便于安全转换
  • bExternal / bExtracted / bProduced:是否外部注册、是否被提取、是否已被某 Pass 写入
  • HasBeenProduced():是否有 Pass 写过
  • GetParent():其上的 View(如 SRV/UAV)通过它找到「父资源」

比喻:在「有一块 RHI 资源」的基础上,加上「图会管谁写了它、谁读了它、能不能被 View 引用」——只有这类资源才能被当作纹理/缓冲来 bind 到 Pass。

关系FRDGViewableResource : public FRDGResource(约 289 行)。FRDGTextureFRDGBuffer 都继承自它。

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版在此处的行为是一致的:

  1. 上述log不断打出。
  2. 随着视窗尺寸调整,Extent值会变大,但是从不会缩小。
    在这里插入图片描述
  3. 进一步截帧,在RenderDoc中发现:
    这里的Extent的值确定就是SceneColor这个纹理的分辨率
    在这里插入图片描述
    而它与最终显示到视窗里的这个RT(名字叫BufferedRT)的分辨率并不一致。
    在这里插入图片描述
    即:
  • SceneTexturesConfig.Extent = SceneColor(或者说GBuffer)的分辨率 = 目前视窗里蓝色区域大小
  • 视窗总的分辨率 = BufferedRT的分辨率

那看起来我单纯使用AddDrawTexturePass这个接口将SceneColor画到视窗RT上发现大小不匹配的结果,是可以理解的。原版Render函数里这些GBuffer的分辨率同样也是不匹配视窗的,但是原版的图像能填满视窗,看起来要通过后续的学习才能理解了。
在这里插入图片描述

5*. RenderDoc中显示的资源名

我感兴趣为啥我在RenderDoc中能看到类似“SceneColor” “BufferedRT” 这样的资源的名字,就问了AI。
AI回答如下:(仅供参考)

  1. 条件
    • 编译时打开 RHI_USE_RESOURCE_DEBUG_NAME(通常 Development/Debug 会开),这样 D3D12 才会真的调 SetName
  2. RDG 纹理
    • GraphBuilder.CreateTexture(Desc, TEXT("你的名字"))CreateTextureMSAA(..., NameMS, NameResolve, ...) 等带 Name 的接口;这个名字会作为 Pool 的 Name 传入 FRHITextureCreateDesc,最后变成 D3D12 的 Debug Name。
  3. 非 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版在此处的行为是一致的:

  1. 上述log不断打出。
  2. 随着视窗尺寸调整,Extent值会变大,但是从不会缩小。
  3. 这个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,只能 ≥ 上一帧

第三步:Grow 下的“历史最大”逻辑(只增不减的关键)

  • 每帧会把当前 DesiredExtentLargestDesiredExtents[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 每帧都打 每帧都会调 InitializeViewFamilyFMinimalSceneTextures::InitializeViewFamily,Scene Color 每帧都会创建/复用,你的 Log 在创建路径上,所以会不断打。
Extent 只会变大、不会缩小 编辑器里强制用 Grow;Grow 下 Extent = max(上一帧 Extent, 本帧请求) 再与历史最大取 max,所以只会增不会减。
Extent ≠ 视窗大小 Extent 来自 GetDesiredInternalBufferSize(经过 Screen Percentage、动态分辨率、量化),再经 Grow 保留历史最大,所以既不是“当前视窗像素”,也可能比当前视窗大。

若想验证“按请求尺寸变化”:在非编辑器下设 r.SceneRenderTargetResizeMethod 0r.SceneRenderTargetResizeMethodForceOverride 1,Extent 会跟随“请求的缓冲尺寸”变化(可能随窗口缩小而缩小);在编辑器中仍会强制 Grow,行为不变。

后续

本篇已经学习了SceneTextures的基本概念,但是当前Render_Learning中还有内容值得学习,就放在下一篇了。

Logo

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

更多推荐