总结:
- 作用:速度缓冲区(Velocity) 渲染,用于 TAA、运动模糊、延迟抗锯齿。
- 区分:不透明物体速度、半透明物体速度两个分支。
const bool bIsTranslucentClippedDepthPass = VelocityPass == EVelocityPass::TranslucentClippedDepth; const bool bSupportsTranslucentClippedDepth = SupportsTranslucentClippedDepth(ShaderPlatform); const bool bIsTranslucentClippedDepthEnabled = CVarVelocityOutputTranslucentClippedDepthEnabled.GetValueOnRenderThread() != 0; if (bIsTranslucentClippedDepthPass && (!bSupportsTranslucentClippedDepth || !bIsTranslucentClippedDepthEnabled)) { return; }判断当前通道类型
bIsTranslucentClippedDepthPass检查当前的VelocityPass是否等于枚举值TranslucentClippedDepth,即是否为处理半透明裁剪物体的速度通道。检查平台支持
bSupportsTranslucentClippedDepth通过SupportsTranslucentClippedDepth(ShaderPlatform)查询当前着色器平台(如PC、主机、移动端)是否支持这一特性。检查用户设置
bIsTranslucentClippedDepthEnabled从控制台变量CVarVelocityOutputTranslucentClippedDepthEnabled读取运行时值,判断用户是否启用了该功能(非0为启用)。条件返回
如果当前是半透明裁剪深度通道,但平台不支持或用户未启用,则直接return,跳过该通道的执行。
这里的clip不是屏幕裁剪,而是材质的半透明或者mask
为什么需要专门的“速度通道”?
这就是你之前看到的代码要处理的核心问题。
挑战:对于普通的半透明物体,渲染它们的速度信息本身就很复杂且性能开销大。而对于“半透明裁剪”物体,因为它的形状是靠“裁剪”形成的,边缘的像素在“透明”与“不透明”之间剧烈变化,计算其运动速度会更加困难和不可靠,容易产生视觉噪点或错误。
解决方案:因此,引擎专门设计了一个名为
TranslucentClippedDepth的速度通道来处理这类物体。为了精确计算它们的速度,这个通道会额外利用深度信息(Depth)来辅助判断
问:所以如果我半透明度为非0,我的速度信息该怎么表达,因为半透明物体一个是半透明物体本身的速度,另一个是半透明物体后面的速度
答:
Unreal Engine 的“单选题”策略
对于真正的混合半透明(Opacity ≠ 1),UE 并不试图融合两者,而是提供两种策略,由开发者决定谁优先:
策略 A:默认行为(选背景——半透明物体“摆烂”)
大多数半透明材质默认不写入速度缓冲区(也不写入主深度)。
此时,速度缓冲里保留的是背后不透明物体的速度。
后果:TAA/运动模糊在处理这个像素时,认为“画面运动等于背景速度”。因此,背景是清晰的,但移动的半透明物体(如旋转的玻璃杯)会因为重投影错位,出现严重的拖尾、重影或抖动。这是UE里半透明物体动态画质差的常见原因。
策略 B:强制覆盖(选前景——半透明物体“抢麦”)
开发者可以在材质中勾选
Output Velocity,或者通过 CVar(r.Translucency.Velocity)强制开启。此时,半透明物体会在渲染通道中将自己的速度写入缓冲区,暴力覆盖(Overwrite)掉背景速度。
后果:半透明物体本身变得清晰稳定,运动模糊正确。但透过半透明物体看背景时,TAA会使用前景的速度去卷绕背景像素,导致背景出现扭曲、撕裂或错误的模糊。
RDG_CSV_STAT_EXCLUSIVE_SCOPE(GraphBuilder, RenderVelocities); SCOPED_NAMED_EVENT(FSceneRenderer_RenderVelocities, FColor::Emerald); SCOPE_CYCLE_COUNTER(STAT_RenderVelocities);后面两个,之前的章节已经多次讲过,这里就不再赘述,重点讲第一个
作用:面向自动化性能测试(CSV 统计)的专属计时器。
RDG指渲染依赖图(Render Dependency Graph),这个宏会将计时数据嵌入到 RDG 的执行流中。
CSV指逗号分隔值(Comma-Separated Values),引擎会将这个作用域的耗时输出到
.csv日志文件中,供 QA 或性能测试团队离线分析。EXCLUSIVE表示独占统计,即这个计时器只计算
RenderVelocities本身的耗时,不包含其内部调用的子模块时间,防止重复累加。用途:帮助开发者在长时间运行的游戏录像中,精准定位这个函数是否在某些地图场景下突然变慢。
uint32 bNeedsClearMask = HasBeenProduced(SceneTextures.Velocity) ? 0 : ((1u << GNumExplicitGPUsForRendering) - 1);核心逻辑:查重与跳过
HasBeenProduced(SceneTextures.Velocity)是 RDG 系统的一个查询接口:
返回
true:意味着在当前帧的渲染图谱中,在此之前已经有其他 Pass(比如不透明物体渲染通道)向速度纹理写入过有效数据了。此时
bNeedsClearMask = 0(无清除掩码)。不执行清除操作,直接保留现有数据。这是一个巨大的性能优化,避免了重复清空显存,节省带宽。
返回
false:说明当前帧还没人碰过这张速度纹理,里面残留的是上一帧的“垃圾数据”或未初始化的显存脏值。此时必须执行清除,并将所有像素的速度值归零(代表“静止”),防止 TAA 或运动模糊读取到上一帧的错乱数据导致画面撕裂。
RDG_EVENT_SCOPE_STAT(GraphBuilder, RenderVelocities, "RenderVelocities(%s)",GetVelocityPassName(VelocityPass)); RDG_GPU_STAT_SCOPE(GraphBuilder, RenderVelocities);RDG_EVENT_SCOPE_STAT(...)
作用:在 GPU 命令流中插入“命名标签”,并将这个标签挂钩到 CPU 统计系统。
给 GPU “贴标签”:当使用RenderDoc、PIX或Unreal Insights(GPU 追踪)抓取帧时,这个宏会在 GPU 时间线上生成一个名为
RenderVelocities(具体通道名)的彩色/分层区块。比如,当VelocityPass是TranslucentClippedDepth时,你在性能分析工具里会看到一个醒目的长条,写着RenderVelocities(TranslucentClippedDepth)。动态命名:
GetVelocityPassName(VelocityPass)让你能一目了然地区分当前执行的是“普通速度通道”还是“半透明裁剪速度通道”,方便技美针对性优化。CPU 关联:虽然它贴的是 GPU 标签,但它同时将这段 GPU 工作挂载到了 CPU 的
RenderVelocities统计组下,确保在stat命令中能关联起来。
2.RDG_GPU_STAT_SCOPE(GraphBuilder, RenderVelocities)
作用:实际开启 GPU 硬件计时器,精确测量这段 GPU 代码的执行时长。
开启 GPU 查询:这个宏会向 GPU 命令队列插入一对“开始/结束”时间戳查询(Timestamp Queries)。GPU 内部有专门的硬件计数器来记录这些时间点。
填充
stat GPU数据:当你在控制台输入stat GPU或stat RHI时,屏幕上显示的“RenderVelocities”那行的毫秒数(ms),主要就是由这个宏负责累加计算出来的。独立于 CPU:CPU 发命令可能只花 0.1ms,但 GPU 画这些速度可能花掉 2ms(因为要等待纹理采样和显存读写)。这个宏专门捕捉这 2ms 的 GPU 墙钟时间。
const EMeshPass::Type MeshPass = GetMeshPassFromVelocityPass(VelocityPass); const bool bIsOpaquePass = VelocityPass == EVelocityPass::Opaque; FExclusiveDepthStencil ExclusiveDepthStencil = (bIsOpaquePass && !(Scene->EarlyZPassMode == DDM_AllOpaqueNoVelocity)) ? FExclusiveDepthStencil::DepthRead_StencilWrite : FExclusiveDepthStencil::DepthWrite_StencilWrite; ExclusiveDepthStencil = bIsTranslucentClippedDepthPass ? FExclusiveDepthStencil::DepthRead_StencilNop : ExclusiveDepthStencil;MeshPass = GetMeshPassFromVelocityPass(VelocityPass):将速度通道枚举转换为具体的网格渲染通道类型(如Opaque、Masked等)。bIsOpaquePass:标记当前是不是普通不透明物体的速度通道。
场景 A(常见情况):
bIsOpaquePass == true且EarlyZPassMode不是DDM_AllOpaqueNoVelocity。这意味着引擎在“不透明物体渲染”阶段已经提前运行过 Early-Z 通道,深度缓冲(Depth Buffer)里已经有了有效的深度数据。
因此,速度通道只需要读取(Read)深度值用于计算像素的世界位置和速度,无需再写一遍;但为了后续的遮挡剔除或标记,需要写入(Write)模板(Stencil)。
权限设为:
DepthRead_StencilWrite(深度只读,模板可写)。
场景 B(罕见/特殊):如果不透明物体没有提前写入深度(
EarlyZPassMode == DDM_AllOpaqueNoVelocity),意味着当前深度缓冲是无效的或空的。速度通道如果只读,就会读到垃圾数据,导致计算错误。
所以,速度通道必须同时写入深度(Write)和模板(Write),把深度值补上。
权限设为:
DepthWrite_StencilWrite(深度和模板均可写)。
bool bHasAnyPixelShaderMotionVectorWorldOffsetMaterials = false; GetMotionVectorOutputFlag(InViews, MeshPass, bForceVelocity, bHasAnyPixelShaderMotionVectorWorldOffsetMaterials); const bool bSupportPixelShaderMotionVectorWorldOffset = SupportsPixelShaderMotionVectorWorldOffset(ShaderPlatform) && bIsOpaquePass && bHasAnyPixelShaderMotionVectorWorldOffsetMaterials; //Only opaque pass supports per pixel override. FRDGTextureRef MotionVectorWorldOffsetTexture = nullptr; if (bSupportPixelShaderMotionVectorWorldOffset) { MotionVectorWorldOffsetTexture = GraphBuilder.CreateTexture(SceneTextures.Velocity->Desc,TEXT("MotionVectorWorldOffsetTexture")); AddClearRenderTargetPass(GraphBuilder, MotionVectorWorldOffsetTexture); }它要解决什么特殊问题?
通常情况下,物体的运动速度是由CPU/顶点着色器计算物体顶点位置在前后帧的偏移量得出的(比如角色骨骼动画、刚体位移)。
但有些材质效果无法通过顶点动画实现,只能在像素着色器里扰动画面,比如:
流动的水面波纹(顶点没动,但法线和UV在动)。
飘动的旗帜上的局部褶皱。
带有滚动纹理的熔岩或能量护盾。
如果只靠顶点算速度,这些像素在TAA眼里就是“静止”的,但画面内容却在剧烈变化,会导致重影和撕裂。因此,材质允许开发者直接在像素着色器里输出一个“额外偏移量”来修正速度。
为什么要单独建一张纹理(MotionVectorWorldOffsetTexture)?
这是最关键的设计点:
主速度纹理(
SceneTextures.Velocity)已经在当前帧的不透明速度通道中被写入了基础速度(基于顶点计算)。如果像素着色器偏移直接覆盖主纹理,没有偏移的像素数据就会被清空,导致大面积画面出错。
因此,引擎的策略是“分步合成”:
第一步(当前代码):判断场景里是否有材质开启了像素着色器偏移(
bHasAny...)。如果有,就新建一张独立的、全黑的临时纹理(MotionVectorWorldOffsetTexture)作为“增量累加器”。第二步(在后面未贴出的合成Pass中):引擎会读取主速度纹理 + 这张偏移纹理,将两者相加,得到最终速度,再写回主纹理,这个主纹理就是velocitypass最后渲染出来的速度场。
AddClearRenderTargetPass的作用:把这整张新纹理刷成纯黑(0,0)。这样,后续像素着色器在计算时,只在需要偏移的地方写入非零值;不需要偏移的地方保持0,相加后不影响主速度。
相当于单独建立一个纹理,然后清空,清空过后计算屏幕中材质的像素偏移度(计算PS导致的速度)然后累加在这张新纹理,做完后累加到主velocity图中
velocitypass也是Pass它执行了vs才能获取到顶点速度
像素着色器偏移 (Pixel Shader Offset):这才是你问题中真正关心的“像素级别”偏移。它发生在像素着色器阶段,能够实现顶点着色器无法完成的、更精细的屏幕空间效果,例如:
基于UV动画的流动效果,如水流。
基于屏幕空间坐标的扭曲效果,如热浪。
材质的颜色或亮度在移动,但几何体本身静止(可以理解为一种“光流”)。
这种情况会计算到临时创建的速度图
两个概念的区别
| 特性 | World Position Offset (WPO) | 像素着色器偏移(你引用的那段描述) |
|---|---|---|
| 执行阶段 | 顶点着色器 | 像素着色器 |
| 本质 | 直接移动模型的顶点世界坐标 | 在屏幕空间或纹理空间进行采样偏移,不改变几何体 |
| 例子 | 树叶摆动、顶点动画、物体变形 | UV 平移动画(水流)、热浪扭曲(折射偏移)、基于屏幕坐标的采样扰动 |
| 产生运动向量? | 会,因为前后帧顶点位置不同 | 不会,因为几何体实际上没动,只是贴图或光照在动(“光流”效果) |
| 渲染时是否进入 Velocity Pass | 是(如果开了相关选项) | 否,它只是一般材质着色,不写入速度缓冲 |
for (int32 ViewIndex = 0; ViewIndex < InViews.Num(); ViewIndex++) { FViewInfo& View = InViews[ViewIndex]; checkf(!(View.Family->EngineShowFlags.StereoMotionVectors && PlatformSupportsOpenXRMotionVectors(View.GetShaderPlatform())), TEXT("Normal velocity rend遍历所有视图
InViews是当前帧需要渲染的所有视图(例如左眼、右眼、场景捕获等),循环逐一检查。获取每个视图的配置
View.Family->EngineShowFlags包含各种渲染功能开关。StereoMotionVectors是旧版立体渲染中的运动向量(motion vector)支持标志。检查平台能力
PlatformSupportsOpenXRMotionVectors(View.GetShaderPlatform())判断当前 RHI/Shader 平台是否支持 OpenXR 的运动向量扩展。
if (View.ShouldRenderView()) { const bool bHasAnyDraw = HasAnyDraw(View.ParallelMeshDrawCommandPasses[MeshPass]); if (!bHasAnyDraw && !bForceVelocity) { continue; }View.ShouldRenderView()
检查该视图是否需要被实际渲染(例如:视图不可见、被完全遮挡、或为无效视角时可跳过)。HasAnyDraw(View.ParallelMeshDrawCommandPasses[MeshPass])ParallelMeshDrawCommandPasses是预先收集好的、按 MeshPass 分类的绘制命令数组。MeshPass在这里应该是速度 Pass(例如EMeshPass::Velocity)。HasAnyDraw判断这个 Pass 中是否有任何需要绘制的可见网格体。bForceVelocity
一个外部传入的布尔标志,表示“即使没有任何可见的网格体绘制,也必须强制输出速度纹理”。
典型场景:启用了TAA/DLSS/TSR等需要历史运动向量的特性,即便是全屏后处理也需要一个“零速度”或前一帧运动向量来维持历史缓冲。
某些平台或 VR 场景要求始终提供运动向量。
Velocity Pass 并不是所有物体都会参与
ParallelMeshDrawCommandPasses[MeshPass]里收集的是当前视图中需要输出运动向量的网格体绘制命令。物体能被收录,需要同时满足多个条件:
物体本身需要写入运动向量
只有被标记为“可能运动”或渲染设置需要输出速度的 Primitive 才会生成速度绘制命令。例如:使用了World Position Offset(WPO)或骨骼动画的动态物体。
开启了物体运动向量(Per-Object Motion Blur)的静态网格体。
场景中移动的粒子、贴花等。
完全静态且从未移动的物体通常不会生成速度 Pass 命令。
RDG_GPU_MASK_SCOPE(GraphBuilder, View.GPUMask);RDG_GPU_MASK_SCOPE
是 UE 的 RDG(Render Dependency Graph)提供的一个宏,用于设置当前 RDG 构建过程中后续 Pass 的 GPU 掩码。
离开这个作用域(即宏生成的for循环结束或}结束后),GPU 掩码会恢复为之前的值。GraphBuilder
当前的 RDG 构建器,所有 Pass 通过它添加到帧图中。View.GPUMask
一个位掩码(通常是FRHIGPUMask),表示该视图应该被哪些 GPU 渲染。
在单 GPU 系统上,这个掩码通常是1(仅 GPU 0)。
在多 GPU 渲染(如分屏、VR 每眼不同 GPU、交叉 GPU 渲染)时,不同视图可能有不同的 GPUMask,确保每个视图只在分配给的 GPU 上执行相应的工作。
RDG_GPU_MASK_SCOPE(GraphBuilder, View.GPUMask); const bool bIsParallelVelocity = FVelocityRendering::IsParallelVelocity(ShaderPlatform); // Clear velocity render target explicitly when velocity rendering in parallel or no draw but force to. // Avoid adding a separate clear pass in non parallel rendering. const bool bExplicitlyClearVelocity = (bNeedsClearMask & View.GPUMask.GetNative()) && (bIsParallelVelocity || (bForceVelocity && !bHasAnyDraw)); if (bExplicitlyClearVelocity) { AddClearRenderTargetPass(GraphBuilder, SceneTextures.Velocity); bNeedsClearMask &= ~View.GPUMask.GetNative(); }bNeedsClearMask是一个 GPU 掩码,表示哪些 GPU 上的 Velocity RT 需要清除。View.GPUMask.GetNative()是当前视图所绑定的 GPU 索引掩码。两者按位与,确保只有该视图真正使用的 GPU 才执行清除,避免跨 GPU 污染或多余操作。
View.BeginRenderView();View.BeginRenderView()在 Velocity Pass 中的作用是初始化该视图的渲染环境,确保后续的绘制命令能够正确输出到 Velocity Render Target。
FParallelMeshDrawCommandPass& ParallelMeshPass = *View.ParallelMeshDrawCommandPasses[MeshPass]; FVelocityPassParameters* PassParameters = GraphBuilder.AllocParameters<FVelocityPassParameters>(); PassParameters->View = View.GetShaderParameters(); ParallelMeshPass.BuildRenderingCommands(GraphBuilder, Scene->GPUScene, PassParameters->InstanceCullingDrawParams); PassParameters->SceneTextures = SceneTextures.GetSceneTextureShaderParameters(View.FeatureLevel); PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding( SceneTextures.Depth.Resolve, ERenderTargetLoadAction::ELoad, ERenderTargetLoadAction::ELoad, ExclusiveDepthStencil); if (bBindRenderTarget) { ERenderTargetLoadAction LoadAction = (bNeedsClearMask & View.GPUMask.GetNative()) ? ERenderTargetLoadAction::EClear : ERenderTargetLoadAction::ELoad; if (MotionVectorWorldOffsetTexture) { // Switch Velocity and Offset texture to avoid an additional copy // // Write Velocity into the Offset texture and Offset into the Velocity texture so that // When we resolve (e.g., RWOffset[Position]+= Velocity[ResolvedPosition]), the resolved velocity // is stored in the Velocity texture (RWOffset) instead of Offset texture to avoid an additional copy // from Offset texture to Velocity texture. // From // V = v // Offset = o // Offset[p] += V[rp] // V = Offset // To // Offset = v // V = o // V[p] += Offset[rp] PassParameters->RenderTargets[0] = FRenderTargetBinding(MotionVectorWorldOffsetTexture,LoadAction); PassParameters->RenderTargets[1] = FRenderTargetBinding(SceneTextures.Velocity, LoadAction); } else { PassParameters->RenderTargets[0] = FRenderTargetBinding(SceneTextures.Velocity, LoadAction); } bNeedsClearMask &= ~View.GPUMask.GetNative(); }这段代码是 Velocity Pass实际提交绘制命令前的参数配置,最核心的设计是注释里解释的“交换 Velocity 纹理与 Offset 纹理的绑定”。
基础顶点速度直接渲染到SceneTextures.Velocity,后续直接使用即可。
RT0(原本应该是 Velocity)→ 绑定为
MotionVectorWorldOffsetTexture(临时偏移纹理)。RT1(额外绑定)→ 绑定为
SceneTextures.Velocity(原始主速度纹理)。
合成阶段(后面未显示的 Pass)会将V作为“累加目标”,把Offset的内容(现在是基础速度)加进去。
最终结果原地留在
SceneTextures.Velocity中,省去了从 Offset 拷贝回 Velocity 的步骤。这要求像素着色器在写入
V时要处理好初始值(通常需要 Load 而非 Clear,以保证保留像素偏移速度的正确合成)。
处理完当前视图后,将对应的 GPU 掩码位清零,避免重复清除(多视图可能共享某些 GPU 掩码)。这是典型的掩码消费模式。
PassParameters->VelocityClippedDepth = BindTranslucentVelocityClippedDepthPassUniformParameters(GraphBuilder,SceneTextures, bIsTranslucentClippedDepthPass, ShaderPlatform);提供深度信息用于裁剪
将当前场景的深度纹理(SceneTextures)以及平台相关的参数打包,传入 Velocity Pass 的着色器参数中。解决半透明物体背后的速度错误
当场景中存在半透明物体(例如玻璃、水面)时,其背后移动的不透明物体会产生运动向量。如果直接使用这些向量做运动模糊,会导致半透明区域也跟着模糊,视觉效果错误。
通过启用bIsTranslucentClippedDepthPass,着色器可以在计算速度时利用深度纹理进行比对:如果当前像素的深度比半透明物体的深度更远(即位于半透明之后),则该速度被丢弃或置零。
只有位于半透明物体前面的不透明物体,其运动向量才会被保留。
场景举例
背景:一个快速向右移动的不透明立方体。
前景:一块静止的半透明玻璃板,遮挡了部分立方体。
渲染速度时:
不透明 Velocity Pass 先执行:立方体被绘制,它在屏幕上的所有像素(包括被玻璃挡住的部分)都写入了向右的运动向量。
如果直接使用这张速度图:玻璃区域也会继承立方体的运动向量。
运动模糊/TAA 会错误地把玻璃也向右拖拽,尽管玻璃本身是静止的。
问:如果玻璃和车都运动呢?
直接答案:如果玻璃和车都运动,最终屏幕像素的速度完全由玻璃的运动决定,汽车的运动向量在玻璃遮挡区域会被彻底抛弃。
所以这样的话就是汽车虽然在跑,但是看到半透明的玻璃深度在其之前,速度被置0,然后累加上玻璃的速度,所以就是玻璃速度了
if (bIsParallelVelocity) { GraphBuilder.AddDispatchPass( RDG_EVENT_NAME("VelocityParallel"), PassParameters, ERDGPassFlags::Raster, [&View, &ParallelMeshPass, PassParameters](FRDGDispatchPassBuilder& DispatchPassBuilder) { ParallelMeshPass.Dispatch(DispatchPassBuilder, &PassParameters->InstanceCullingDrawParams); }); } else { GraphBuilder.AddPass( RDG_EVENT_NAME("Velocity"), PassParameters, ERDGPassFlags::Raster, [&View, &ParallelMeshPass, PassParameters](FRDGAsyncTask, FRHICommandList& RHICmdList) { SetStereoViewport(RHICmdList, View); ParallelMeshPass.Draw(RHICmdList, &PassParameters->InstanceCullingDrawParams); }); }并行路径 (bIsParallelVelocity == true)
非并行路径 (bIsParallelVelocity == false)
这段代码根据bIsParallelVelocity选择调用Dispatch(并行、多线程命令录制)或Draw(传统、单线程命令执行),并确保非并行路径正确设置了立体视口。它标志着 Velocity Pass 的 CPU 端工作完成,将控制权交给 GPU 去实际执行速度渲染。
这里加入Velocity Pass 自己的光栅化类型
if (bSupportPixelShaderMotionVectorWorldOffset) { for (int32 ViewIndex = 0; ViewIndex < InViews.Num(); ViewIndex++) { FViewInfo& View = InViews[ViewIndex]; if (View.ShouldRenderView()) { const bool bHasAnyDraw = HasAnyDraw(View.ParallelMeshDrawCommandPasses[MeshPass]); if ((!bHasAnyDraw && !bForceVelocity) || !View.bUsesMotionVectorWorldOffset) { continue; } RDG_GPU_MASK_SCOPE(GraphBuilder, View.GPUMask); // Resolve { typedef FMotionVectorWorldOffsetVelocityResolveCS SHADER; SHADER::FParameters* PassParameters = GraphBuilder.AllocParameters<SHADER::FParameters>(); PassParameters->View = View.GetShaderParameters(); PassParameters->DepthTexture = GraphBuilder.CreateSRV(SceneTextures.Depth.Resolve); // Switch back to avoid an additional copy. PassParameters->VelocityTexture = GraphBuilder.CreateSRV(MotionVectorWorldOffsetTexture); PassParameters->RWMotionVectorWorldOffset = GraphBuilder.CreateUAV(SceneTextures.Velocity); TShaderMapRef<SHADER> ComputeShader(View.ShaderMap); FIntVector GroupCount = FComputeShaderUtils::GetGroupCount(View.ViewRect.Size(), SHADER::GetGroupSize()); FComputeShaderUtils::AddPass( GraphBuilder, RDG_EVENT_NAME("MotionVectorWorldOffsetVelocityResolve %dx%d", View.ViewRect.Width(), View.ViewRect.Height()), ComputeShader, PassParameters, GroupCount); } } } }这段代码正是我们之前讨论的“合成 Pass”的实际实现。它的作用是将像素着色器输出的运动向量偏移累加到主速度纹理上,完成最终速度图的生成。
#if !(UE_BUILD_SHIPPING) const bool bForwardShadingEnabled = IsForwardShadingEnabled(ShaderPlatform); if (!bForwardShadingEnabled) { FRenderTargetBindingSlots VelocityRenderTargets; VelocityRenderTargets[0] = FRenderTargetBinding(SceneTextures.Velocity, bNeedsClearMask ? ERenderTargetLoadAction::EClear : ERenderTargetLoadAction::ELoad); VelocityRenderTargets.DepthStencil = FDepthStencilBinding( SceneTextures.Depth.Resolve, ERenderTargetLoadAction::ELoad, ERenderTargetLoadAction::ELoad, ExclusiveDepthStencil); StampDeferredDebugProbeVelocityPS(GraphBuilder, InViews, VelocityRenderTargets); } #endif前置条件:仅在非前向着色(即延迟渲染)模式下执行。因为前向渲染中速度缓冲的管理方式可能不同,这个调试工具只针对延迟渲染路径。
配置 Render Target 绑定:
VelocityRenderTargets[0]绑定主速度纹理SceneTextures.Velocity,加载动作根据bNeedsClearMask决定清除或保留。绑定深度模板缓冲为场景深度(只读加载),因为调试着色器可能需要深度信息。
调用
StampDeferredDebugProbeVelocityPS:这是一个调试辅助函数,它会向速度纹理中绘制调试标记(例如在特定区域写入特殊的颜色/向量值)。
典型的用途:通过控制台命令(如
vis Velocity)或调试探头(Debug Probe)在屏幕上指定一个像素区域,然后在这个区域覆盖写入醒目的运动向量值,帮助开发人员可视化某一点的速度信息。函数名中的Deferred指延迟渲染,Probe暗示与调试探针相关(允许实时查看屏幕某像素的运动向量数值)。