UE开发

UE 中 SceneCapture2D 捕获 Render Target 的底层原理

2026-08-25

5 min

分类

UE开发

在 UE 中,小地图、镜子、监控画面、缩略图生成、离屏渲染等功能,经常会用到 USceneCaptureComponent2D 和 UTextureRenderTarget2D。刚开始接触时,很容易把 SceneCapture2D 理解成“截一张当前屏幕,然后存到 Render Target”。这种理解虽然直观,但并不准确。

SceneCapture2D 真正做的事情,更接近于创建一个额外的相机视角,让 UE 的 Renderer 从这个视角重新渲染一次场景,然后把结果写入一张 Render Target 纹理。因此它不是 Screen Capture,而是一次额外的场景渲染。

整个过程可以先简单理解成:

SceneCaptureComponent2D
        ↓
构建一个额外的 View
        ↓
Renderer 再渲染一次场景
        ↓
Render Target
        ↓
   ┌────┴────┐
   ↓         ↓
Shader 使用  ReadPixels()
              ↓
             CPU

理解这条主线以后,CaptureScene()、Render Target、Mip、ReadPixels() 这些看起来比较零散的概念,其实都能串起来。


SenceCaptureComponent2D流程图.png
SenceCaptureComponent2D流程图.png

1. Render Target 本质上是什么

正常情况下,UE 渲染一帧画面时,会经过 SceneColor、Lighting、PostProcess 等阶段,最终把结果输出到用于显示的 Back Buffer。Render Target 的区别只在于:渲染结果不一定非要输出到屏幕,也可以输出到一张 GPU Texture。

普通游戏画面:

Scene
  ↓
Renderer
  ↓
SceneColor / PostProcess
  ↓
BackBuffer
  ↓
屏幕


SceneCapture:

Scene
  ↓
Renderer
  ↓
Render Target Texture

所以 Render Target 这个名字本身已经描述了它的含义:它是一张可以作为“渲染目标”的纹理。

在 UE C++ 层,我们平时接触到的是:

UTextureRenderTarget2D

不过 UTextureRenderTarget2D 本身并不是显存中的那张 GPU Texture,它是 UObject 层用于描述和管理 Render Target 的对象。真正的资源关系大致是:

UTextureRenderTarget2D
        ↓
FTextureRenderTarget2DResource
        ↓
FRHITexture
        ↓
D3D12 Texture / Vulkan Image / ...
        ↓
GPU 显存

因此可以把 UTextureRenderTarget2D 理解成上层对象,而 FRHITexture 才更接近 Renderer 最终操作的 GPU 纹理资源。

这也解释了为什么 RT 捕获完成以后,图片默认仍然留在 GPU 上。如果只是想在材质或者 Shader 中使用这张纹理,完全没有必要先把它读取到 CPU。


2. SceneCapture2D 到底是怎么“捕获”的

SceneCapture2D 最重要的一点是:它不是把主相机已经渲染好的画面复制一份,而是自己创建一个 View。

可以把主相机和 SceneCapture 看成两个观察同一个 Scene 的视角:

                 Scene
                /     \
               /       \
          主相机       SceneCapture
             ↓              ↓
           View A          View B
             ↓              ↓
          Renderer        Renderer
             ↓              ↓
           屏幕             RT

因此 SceneCaptureComponent2D 有自己的 Transform、FOV、Projection、ShowFlags、PostProcessSettings、CaptureSource、HiddenActors 等配置。即使主相机和 SceneCapture 位于同一个位置,如果这些设置不一致,最终看到的画面仍然可能不同。

SceneCapture 有几种常见的捕获触发方式:

bCaptureEveryFrame
bCaptureOnMovement
CaptureScene()
CaptureSceneDeferred()

bCaptureEveryFrame = true 时会每帧更新,bCaptureOnMovement = true 时组件移动会触发更新。如果这两个都关闭,那么就可以在真正需要的时候手动调用:

SceneCapture->CaptureScene();

这里需要注意,CaptureScene() 并不能简单理解为“这个函数内部直接把 GPU 渲染完成了”。它发生在 Game Thread,主要负责准备本次 SceneCapture 需要的数据,并把真正的渲染工作提交给 Render Thread。

从 UE 的逻辑结构来看,大致会经历:

Game Thread

USceneCaptureComponent2D::CaptureScene()
        ↓
FScene::UpdateSceneCaptureContents()
        ↓
根据 SceneCapture 参数构建 View
        ↓
FSceneViewFamily + FSceneView
        ↓
创建 FSceneRenderer
        ↓
ENQUEUE_RENDER_COMMAND
        ↓
=============================
Render Thread
        ↓
UpdateSceneCaptureContent_RenderThread()
        ↓
FSceneRenderer::Render()

其中 FSceneView 可以理解成 Renderer 眼中的“相机信息”。它包含当前从哪里看、使用什么投影矩阵、显示哪些东西、使用哪些渲染设置等信息。Renderer 并不特别关心这个 View 是来自玩家摄像机还是 SceneCapture,只需要根据 View 去渲染 Scene。

这里还有一个比较值得注意的底层细节:FSceneRenderer 是在 Game Thread 侧创建的,然后通过 ENQUEUE_RENDER_COMMAND 提交给 Render Thread。并不是进入 Render Thread 后才开始创建 FSceneRenderer。

因此更准确的线程关系是:

Game Thread

创建 View
   ↓
创建 FSceneRenderer
   ↓
ENQUEUE_RENDER_COMMAND
   ↓
=============================
Render Thread
   ↓
FSceneRenderer::Render()

这个细节对于理解 UE 中 Game Thread 负责准备渲染数据、Render Thread 负责执行渲染工作非常有帮助。


3. Renderer 怎么把结果写进 RT

进入 Render Thread 后,SceneCapture 实际上还是使用 UE 正常的 Renderer 去渲染场景。为了便于理解,可以把典型过程简化成:

Visibility / Culling
        ↓
Depth PrePass
        ↓
BasePass
        ↓
Lighting
        ↓
Translucency
        ↓
PostProcess

不过这个流程只是概念上的简化。UE5 内部大量使用 RDG,也就是 Render Dependency Graph,实际渲染并不是永远按照这几个阶段固定线性执行。Deferred、Forward、Nanite、Lumen、ShowFlags 以及 CaptureSource 等配置都会影响具体执行哪些 Render Pass。

所以更准确的理解是:FSceneRenderer::Render() 会建立大量 RDG Pass,描述各个渲染任务之间的资源依赖,最终由 RDG 和 RHI 去执行真正的 GPU 工作。

SceneCapture 还有一个非常重要的参数:

CaptureSource

它决定了最终希望得到渲染管线中的哪一种结果。例如:

SCS_SceneColorHDR
SCS_FinalColorLDR
SCS_FinalColorHDR
SCS_FinalToneCurveHDR
SCS_SceneDepth
SCS_Normal
SCS_BaseColor

这里不能简单地把 SCS_FinalColorHDR 理解成“屏幕最终颜色的 HDR 版本”。它表示的是 Final Color HDR,处于 Linear Working Color Space;而 SCS_FinalToneCurveHDR 则包含 Tone Curve,两者并不是一回事。

SCS_Normal 和 SCS_BaseColor 也有一个经常被忽略的限制:它们依赖 Deferred Renderer 的 GBuffer,因此只适用于 Deferred Renderer。Forward Renderer 下无法按照正常的 GBuffer 路径获取这两类数据。

所以 SceneCapture 最终并不是简单地执行:

Renderer
   ↓
直接画进 RT

更准确的流程应该理解为:

Renderer
   ↓
产生 SceneColor / Depth / GBuffer /
PostProcess 等中间结果
   ↓
根据 CaptureSource 选择目标结果
   ↓
Resolve / Copy / Composite
   ↓
Render Target

这也是为什么改变 CaptureSource 后,即使 SceneCapture 的位置完全没变,最终 RT 的颜色、Alpha、深度或者后处理效果都可能发生明显变化。

Render Target 本质是一张 Texture,所以同样可以拥有 Mip。假设 RT 分辨率为 1024×1024,那么它可能拥有:

Mip 0    1024 × 1024
Mip 1     512 × 512
Mip 2     256 × 256
Mip 3     128 × 128
...

SceneCapture 正常输出的主要是 RT 的 Mip 0。如果开启了自动生成 Mip,例如 bAutoGenerateMips,后续 GPU 可以继续根据 Mip 0 向下生成更小的 Mip。

因此 Mip 和 SceneCapture 的关系并不复杂:SceneCapture 负责把当前场景画进 RT,Mip 负责给这张 Texture 提供多个分辨率等级。


4. CaptureScene 和 ReadPixels 是两件完全不同的事

调用:

SceneCapture->CaptureScene();

完成的是:

Scene
  ↓
Renderer
  ↓
GPU Render Target

此时像素主要仍然位于 GPU。

如果我们的目的只是:

Render Target
     ↓
Material
     ↓
Texture Sample
     ↓
Shader

那么整个过程都可以留在 GPU,不需要 CPU 参与,这也是 Render Target 最正常的使用方式。

但如果后面需要把 RT 保存成 PNG、转换成 Base64、上传服务器或者做 CPU 图像处理,就需要真正把 GPU 数据拿出来。例如:

FTextureRenderTargetResource* Resource =
    RenderTarget->GameThread_GetRenderTargetResource();

TArray<FColor> Pixels;
Resource->ReadPixels(Pixels);

此时流程就变成:

GPU Render Target
        ↓
FRenderTarget::ReadPixels()
        ↓
RHIReadSurfaceData()
        ↓
具体 RHI Backend
        ↓
GPU → CPU Readback
        ↓
TArray<FColor>

这里也可以解释为什么 ReadPixels() 往往比想象中昂贵。

正常渲染时,CPU 可以不断向 GPU 提交命令,GPU 在后面异步执行,CPU 不需要每次都等 GPU 完成。但 ReadPixels() 的目标恰恰是让 CPU 获取 GPU 刚刚生成的数据,因此通常会涉及 GPU→CPU 的同步和 Readback。

具体到了 D3D12、Vulkan 或 Metal 后,底层可能使用 staging resource、readback heap 等机制,但这些属于各个 RHI Backend 的实现细节。在 UE Renderer/RHI 抽象层,更重要的调用链是:

ReadPixels
   ↓
RHIReadSurfaceData
   ↓
RHI Backend
   ↓
GPU → CPU

所以如果每帧都做 SceneCapture,再每帧 ReadPixels(),成本可能会很高。前者意味着场景多渲染一次,后者又增加了一次 GPU→CPU 读回以及潜在同步,两部分性能开销会叠加在一起。


5. 把整个流程串起来

理解完前面的内容以后,一次完整 SceneCapture 可以整理成下面这条链:

                         Game Thread
                              │
                  USceneCaptureComponent2D
                              │
                       CaptureScene()
                              │
                              ▼
                 FScene::UpdateSceneCaptureContents
                              │
                              ▼
                 FSceneViewFamily + FSceneView
                              │
                              ▼
                    创建 FSceneRenderer
                              │
                              ▼
                  ENQUEUE_RENDER_COMMAND
                              │
==============================│================
                         Render Thread
                              │
                              ▼
           UpdateSceneCaptureContent_RenderThread
                              │
                              ▼
                  FSceneRenderer::Render()
                              │
                              ▼
                             RDG
                              │
               典型 Render Pass / GPU 工作
                              │
                              ▼
               Resolve / Copy / Composite
                              │
                              ▼
                   Render Target Mip 0
                              │
                ┌─────────────┴─────────────┐
                │                           │
                ▼                           ▼
          GPU Shader 使用               ReadPixels()
                                             │
                                             ▼
                                     RHIReadSurfaceData
                                             │
                                             ▼
                                        GPU → CPU
                                             │
                                             ▼
                                      TArray<FColor>

因此,SceneCapture2D、Render Target 和 ReadPixels 可以分别用一句话理解:

SceneCapture2D 负责创建额外 View 并请求重新渲染场景;Render Target 负责接收 GPU 的渲染结果;ReadPixels 则负责把结果从 GPU 重新读取到 CPU。

这三个步骤看起来经常一起出现,但本质上属于不同阶段。理解这一点以后,很多 UE 中和 RT 有关的问题都会变得清晰,例如为什么 SceneCapture 会增加 GPU 开销、为什么 RT 和屏幕颜色不同、为什么 SceneCapture 通常写 Mip 0,以及为什么 ReadPixels() 很容易导致性能下降。

如果只想记住一个核心结论,可以记成:

SceneCapture2D 不是“截屏”,而是创建一个额外 View,让 UE Renderer 再渲染一次场景,并根据 CaptureSource 将结果输出到 GPU Render Target。只有在调用 ReadPixels 等读取接口时,像素数据才会真正从 GPU 回到 CPU。

评论

参与讨论,分享你的看法。评论审核通过后展示。

暂无评论,来抢沙发吧。