UE开发
UE 中 SceneCapture2D 捕获 Render Target 的底层原理
在 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() 这些看起来比较零散的概念,其实都能串起来。

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。
评论
参与讨论,分享你的看法。评论审核通过后展示。
暂无评论,来抢沙发吧。