0. 写一段代码用到基础的Mesh数据
按照之前AI给的学习路线,现在该引入mesh的绘制了。原本它的建议是花一章专门去学习相关的概念

但是我让他讲解完概念后,依旧很抽象。所以为了加深理解,我还是让他写出可运行的代码来验证相关的概念。
与AI进行了一番讨论后,最终让他这样写代码:
请你实现吧。
在Render_Learning画当前的场景中的mesh,,但是只用他们的顶点数据,不用原始的材质,用一个我新写的只输出法线方向颜色的shader。
只限定于StaticMesh。
如果多个StaticMesh比较麻烦的话,可以只画一个。
如果多个Section比较麻烦的话,可以只画Section0
如果多个LOD比较麻烦的话,可以只画LOD0
以及其他会增加不必要的复杂度的逻辑,我现在只想验证最基础的Mesh绘制流程。
Opus4.6 经过一些时间后,给出了代码。验证没问题:

下面以代码改动入手,来学习相关概念。
1. FPrimitiveSceneProxy介绍
问AI:
很好,代码跑通了,下面从代码入手学习相关知识吧。首先是FPrimitiveSceneProxy这是什么概念?请简单介绍它的定位,职责,数据结构,实体等基础信息。
下面结合引擎里的类注释和结构,把 FPrimitiveSceneProxy 说清楚。
定位
FPrimitiveSceneProxy 是 渲染线程一侧 对 UPrimitiveComponent(游戏线程组件) 的镜像/代理:把"要用来画的东西"从组件里拷出来(或持有指向 RHI 资源的指针),让渲染代码 不必在渲染线程上解引用 UObject。
引擎在头文件里写得很直白:
291:294:d:\UnrealEngine\Engine\Source\Runtime\Engine\Public\PrimitiveSceneProxy.h
/**
* Encapsulates the data which is mirrored to render a UPrimitiveComponent parallel to the game thread.
* This is intended to be subclassed to support different primitive types.
*/
也就是说:游戏线程改组件 → 通过注册/更新把变化同步到场景;渲染只认 Proxy 上的数据。
职责(做什么)
子类通过虚函数参与整条渲染管线,典型包括:
| 方向 | 代表接口 |
|---|---|
| 可见性与通道 | GetViewRelevance、GetLightRelevance、IsShown 等 |
| 几何提交 | DrawStaticElements(创建时一次)、GetDynamicMeshElements(每帧/每视图收集 FMeshBatch) |
| 资源生命周期 | CreateRenderThreadResources / DestroyRenderThreadResources |
| 变换与包围体 | 基类里已有 LocalToWorld、Bounds 等,变换更新可走 OnTransformChanged |
| 光线追踪等扩展 | GetDynamicRayTracingInstances、HasRayTracingRepresentation 等(#if RHI_RAYTRACING) |
GetDynamicMeshElements 的注释也强调了:渲染线程不能碰 UObject,需要的状态必须在 Proxy 上有一份。
数据结构(本体里有什么)
Proxy 很大,可以理解成几层:
- 身份与归属 :
FPrimitiveComponentId、FSceneInterface* Scene、指回FPrimitiveSceneInfo* PrimitiveSceneInfo(场景里真正挂接、索引、八叉树等用的是 SceneInfo,Proxy 是 SceneInfo 里"可渲染的那一面")。 - 几何与变换 :
LocalToWorld、Bounds/LocalBounds、ActorPosition等。 - 着色与管线状态 :大量
uint8位域(静态/可移动、阴影、Nanite、皮肤、Spline......)、FPrimitiveUniformShaderParameters的UniformBuffer、CombinedMaterialRelevance等。 - 调试 :
ComponentForDebuggingOnly------ 仅调试 ,注释写明 渲染线程不应解引用。
1728:1771:d:\UnrealEngine\Engine\Source\Runtime\Engine\Public\PrimitiveSceneProxy.h
/** Pointer back to the PrimitiveSceneInfo that owns this Proxy. */
FPrimitiveSceneInfo* PrimitiveSceneInfo;
...
* This should not be dereferenced on the rendering thread. The game thread can be modifying UObject members at any time.
* Use PrimitiveComponentId instead when a component identifier is needed.
*/
const UPrimitiveComponent* ComponentForDebuggingOnly;
"实体"关系(和场景里谁一对)
FScene(实现里)用TArray<FPrimitiveSceneInfo*> Primitives管理所有图元;遍历场景时你拿到的往往是FPrimitiveSceneInfo*,再通过SceneInfo->Proxy(或等价访问)得到FPrimitiveSceneProxy*。FPrimitiveSceneProxy:描述怎么画、画什么(类型相关,多态)。FPrimitiveSceneInfo:描述在场景图里的条目(索引、更新脏标记、与其它系统交互等)。
所以:组件 →(注册)→ SceneInfo + Proxy;渲染器主要和 Proxy/SceneInfo 打交道。
常见子类
按图元类型派生不同 Proxy,例如 FStaticMeshSceneProxy (静态网格)、骨骼网格、样条网格、粒子等------各自实现 DrawStaticElements / GetDynamicMeshElements 等,把 FMeshBatch 填给渲染器。
一句话 :FPrimitiveSceneProxy 是 UPrimitiveComponent 在渲染线程上的安全替身 + 多态渲染入口 ;场景里和 FPrimitiveSceneInfo 、FScene::Primitives 一起构成"场景图元"的完整实体。你写的 IsStaticMeshProxy() / 从 Proxy 取 RenderData,正是在这个抽象上针对 StaticMesh 这一种 Proxy 子类 做识别和数据读取。
2. FPrimitiveSceneProxy 的 bIsStaticMesh
因为当前只考虑了普通StaticMesh,即bIsStaticMesh成员是True的FPrimitiveSceneProxy。而这个成员外部访问不了,所以AI增加了一个用于访问的接口:

我也顺便问了下bIsStaticMesh是怎么设置的,以及我发现当StaticMesh启用了Nanite时这个值也是false,我希望确认下:
AI回答:(仅供参考)
你的推断对 :启用 Nanite 且走 Nanite 专用 Proxy 时,bIsStaticMesh 不会 被置为 true。
bIsStaticMesh 是怎么来的?
它不是 运行时根据"是否启用 Nanite"算出来的,而是 只有 在 FStaticMeshSceneProxy 的构造函数里无条件 写成 true:
269:269:d:\UnrealEngine\Engine\Source\Runtime\Engine\Private\StaticMeshSceneProxy.cpp
bIsStaticMesh = true;
基类里默认是 false(在 FPrimitiveSceneProxy 构造函数初始化列表里):
357:358:d:\UnrealEngine\Engine\Source\Runtime\Engine\Private\PrimitiveSceneProxy.cpp
, bIsStaticMesh(false)
, bIsNaniteMesh(false)
所以:只有实际类型是 FStaticMeshSceneProxy(并执行了这段构造)时,IsStaticMeshProxy() 才会为真。
和 Nanite 的关系:先选 Proxy 类型,再设标志位
UStaticMeshComponent 创建 Proxy 时,FStaticMeshComponentHelper::CreateSceneProxy 会判断是否用 Nanite:
479:486:d:\UnrealEngine\Engine\Source\Runtime\Engine\Public\StaticMeshComponentHelper.h
const bool bUseNanite = Component.ShouldCreateNaniteProxy(&NaniteMaterials);
if (bUseNanite)
{
// Nanite is fully supported
return Component.CreateStaticMeshSceneProxy(NaniteMaterials, true);
}
CreateStaticMeshSceneProxy 里:
bCreateNanite == true→new Nanite::FSceneProxy(...)(不是FStaticMeshSceneProxy)bCreateNanite == false→new FStaticMeshSceneProxy(...)
2899:2915:d:\UnrealEngine\Engine\Source\Runtime\Engine\Private\StaticMeshSceneProxy.cpp
FPrimitiveSceneProxy* UStaticMeshComponent::CreateStaticMeshSceneProxy(Nanite::FMaterialAudit& NaniteMaterials, bool bCreateNanite)
{
// Default implementation: Nanite::FSceneProxy or FStaticMeshSceneProxy
LLM_SCOPE(ELLMTag::StaticMesh);
if (bCreateNanite)
{
return ::new Nanite::FSceneProxy(NaniteMaterials, this);
}
auto* Proxy = ::new FStaticMeshSceneProxy(this, false);
Nanite::FSceneProxy 的基类是 Nanite::FSceneProxyBase → FPrimitiveSceneProxy ,不继承 FStaticMeshSceneProxy,因此不会 执行 bIsStaticMesh = true 那段逻辑。
Nanite 路径在 FSceneProxyBase 里只把 bIsNaniteMesh = true:
271:275:d:\UnrealEngine\Engine\Source\Runtime\Engine\Public\NaniteSceneProxy.h
FSceneProxyBase(const FPrimitiveSceneProxyDesc& Desc)
: FPrimitiveSceneProxy(Desc)
{
bIsNaniteMesh = true;
小结
| 情况 | Proxy 类型 | bIsStaticMesh |
bIsNaniteMesh |
|---|---|---|---|
| 正常 Nanite 渲染 | Nanite::FSceneProxy |
false(默认) | true |
| 传统静态网格 / Nanite 回退到普通 SM Proxy | FStaticMeshSceneProxy |
true | false |
因此:若你的学习代码用 IsStaticMeshProxy() 过滤,启用 Nanite 且成功创建 Nanite::FSceneProxy 的物体不会被选中 ;若要包含 Nanite 网格,需要同时判断 IsNaniteMesh() (或 dynamic_cast / 类型分支等你项目里约定的写法)。
3. FStaticMeshSceneProxy 的 RenderData
AI还在FStaticMeshSceneProxy中增加了一个访问RenderData的接口:

于是我问AI:
我注意到你在FStaticMeshSceneProxy中增加了一个访问RenderData的接口。
我想问下RenderData是什么概念,你为啥要访问他。
另外在原始的流程中,这个RenderData是如何被访问的?
此外还补充了下问题:
有几个问题:
1)不是说渲染相关的数据应该在SceneProxy吗,为啥UStaticMesh上会有
- RenderData是拷贝过来的吗?还是只是拿到了指针,如果是拷贝过来的话岂不是有重复数据?
3)你为啥不直接用UStaticMesh的GetRenderData接口,还特地要在SceneProxy中加接口。
AI的回答:(仅供参考)
下面分三部分说明:RenderData 是什么 、学习代码为何要拿到它 、引擎里原本怎么用它。
RenderData 是什么?
FStaticMeshRenderData 在引擎里的定义写得很直白:"渲染静态网格所需的全部数据":
770:772:d:\UnrealEngine\Engine\Source\Runtime\Engine\Public\StaticMeshResources.h
/**
* FStaticMeshRenderData - All data needed to render a static mesh.
*/
class FStaticMeshRenderData
它挂在资源 UStaticMesh 上,由 UStaticMesh::RenderData(TUniquePtr<FStaticMeshRenderData>)持有;里面典型包括:
LODResources:每个 LOD 的顶点缓冲、索引缓冲、Section 等(FStaticMeshLODResources)LODVertexFactories:与各 LOD 对应的顶点工厂(材质/着色路径用)ScreenSize:LOD 切换距离NaniteResourcesPtr、RayTracingProxy等扩展表示
也就是说:静态网格在 GPU/渲染侧的几何与管线相关数据,都集中在这份 FStaticMeshRenderData 里,而不是散落在组件上每次去 UObject 上现查。
为啥要给 FStaticMeshSceneProxy 加 GetRenderData()?
FStaticMeshSceneProxy 在构造时就已经把 UStaticMesh::GetRenderData() 拷进成员 RenderData:
229:231:d:\UnrealEngine\Engine\Source\Runtime\Engine\Private\StaticMeshSceneProxy.cpp
FStaticMeshSceneProxy::FStaticMeshSceneProxy(const FStaticMeshSceneProxyDesc& InProxyDesc, bool bForceLODsShareStaticLighting)
: FPrimitiveSceneProxy(InProxyDesc, InProxyDesc.GetStaticMesh()->GetFName())
, RenderData(InProxyDesc.GetStaticMesh()->GetRenderData())
你在 DeferredShadingRenderer 的学习 Pass 里需要:
- 指定 LOD / Section 的
VertexBuffers、IndexBuffer等,去绑 RHI、发DrawIndexed; - 这些数据都在
RenderData->LODResources[LOD]....下面。
若 RenderData 在 FStaticMeshSceneProxy 里是 protected 成员、又没有公开 getter ,渲染器里就只能写 friend 、强转或重复从 UStaticMesh 取------都不合适。加一个 const FStaticMeshRenderData* GetRenderData() const ,等于承认:外部合法用法就是读这份与 Proxy 生命周期一致的渲染数据指针(和 Proxy 内部用法一致,只是从 private 变成可控的只读出口)。
原始流程里 RenderData 是怎么被访问的?
主路径全是 FStaticMeshSceneProxy 自己的实现内部 用成员 RenderData,不经过你新加的接口。例如:
- 构造 :校验
RenderData、按 LOD 建LODs、距离场、RayTracing 等(前面 grep 里从 259 行起一整段)。 - 填
FMeshBatch:GetMeshElement/GetShadowMeshElement/SetMeshElementGeometrySource等会从
RenderData->LODResources[LODIndex]、RenderData->LODVertexFactories[LODIndex]取 section、索引缓冲、顶点工厂(例如 678--679、743--744 行附近)。 - 资源创建/销毁 :
CreateRenderThreadResources/DestroyRenderThreadResources里对RenderData->LODVertexFactories等注册 SpeedTree 风、RayTracing 句柄等。
也就是说:标准渲染管线从不"从场景里随便拿个 Proxy 再 GetRenderData",而是:
- 要么 Proxy 内部 一直用已缓存的
RenderData; - 要么 其它引擎代码 通过
UStaticMesh::GetRenderData()(编辑器、流送、DDCI 等)在游戏线程或工具路径上访问资源对象上的数据。
你的学习代码属于 在自定义渲染路径里复用同一份几何 ,所以需要和 Proxy 内部一样看到 FStaticMeshLODResources ,因此才会用到指向 FStaticMeshRenderData 的指针。
小结
| 概念 | 说明 |
|---|---|
| RenderData | UStaticMesh 上的 FStaticMeshRenderData:LOD 几何、顶点工厂、Nanite/RT 等"可渲染数据"的聚合。 |
| 为何从 Proxy 取 | Proxy 构造时已持有与网格一致的指针;自定义 Pass 要绑 VB/IB,需要访问 LODResources。 |
| 原流程 | FStaticMeshSceneProxy 内部 大量使用 RenderData 构建 FMeshBatch;一般不从外部遍历 Proxy 去取,除非像你这样的自定义渲染/调试代码。 |
1) 不是说渲染相关数据在 SceneProxy 吗,为什么 UStaticMesh 上还有?
这里要区分 两类"渲染相关":
| 类型 | 典型放在哪 | 含义 |
|---|---|---|
| 资源级、可共享、大体量(顶点/索引、LOD 烘焙、Nanite 流送块等) | UStaticMesh → FStaticMeshRenderData |
一份网格资产,被很多 Actor/组件实例引用,只存一份,流送、Cook、多实例共享都自然。 |
| 实例级、每组件一份(世界变换、可见性、材质覆盖、本帧 LOD 选择、PrimitiveUniform 等) | FPrimitiveSceneProxy / FStaticMeshSceneProxy |
每个 UStaticMeshComponent 一个 Proxy,镜像游戏线程状态,渲染线程安全读。 |
引擎注释里说的 Proxy 上"镜像"的,主要是 组件上的可变/实例属性 ,避免渲染线程去碰 UObject。静态网格本身的几何大块数据 从设计上是 资产管线烘焙进 FStaticMeshRenderData,和"Proxy 代表组件实例"是分工,不是矛盾。
2) RenderData 是拷贝还是指针?会不会重复一份数据?
在 FStaticMeshSceneProxy 构造里的是:
231:231:d:\UnrealEngine\Engine\Source\Runtime\Engine\Private\StaticMeshSceneProxy.cpp
, RenderData(InProxyDesc.GetStaticMesh()->GetRenderData())
RenderData 成员类型是 const FStaticMeshRenderData* (指向 UStaticMesh 里那份 TUniquePtr<FStaticMeshRenderData> 所拥有的对象)。也就是说:
- 拷贝的是指针 ,不是整份
FStaticMeshRenderData深拷贝。 - 顶点/索引等大数据只有一份 ,由 StaticMesh 资产持有;多个组件实例的多个
FStaticMeshSceneProxy可以 共享指向同一份FStaticMeshRenderData。
不会因为每个 Proxy 就多复制一整套网格数据。
3) 为什么不直接用 UStaticMesh::GetRenderData(),还要在 SceneProxy 上加接口?
核心原因:渲染路径里不应依赖在渲染线程上读 UStaticMesh(或任意 UObject)。
FPrimitiveSceneProxy的注释写得很清楚:渲染线程 不应解引用 组件上的 UObject;游戏线程可能随时改对象。GetRenderData()若在渲染代码里写成Component->GetStaticMesh()->GetRenderData(),就等于在渲染侧又通过 UObject 链条 去拿数据,线程安全与生命周期都更难保证(何时 StaticMesh 仍有效、是否与 Scene 注册一致等)。- Proxy 在注册时已经
RenderData = Mesh->GetRenderData(),把 "这份实例当前关联的资产渲染数据" 固化成 仅数据指针 ,后续渲染代码只认 Proxy +FStaticMeshRenderData,符合 "渲染线程只碰 Proxy 已缓存的、非 UObject 的数据" 的习惯用法。
所以在 Renderer 里 加 FStaticMeshSceneProxy::GetRenderData(),是 从已知的 Proxy 取已缓存指针 ,而不是在 DeferredShadingRenderer 里再去找 UStaticMeshComponent / UStaticMesh。
补充 :若代码明确跑在 游戏线程 、且持有合法的 UStaticMesh*,用 UStaticMesh::GetRenderData() 是正常用法(工具、Cook、编辑器)。自定义渲染 Pass 若在渲染线程执行,则更应避免从 UObject 取,优先走 Proxy 上已存的指针。
一句话 :资产几何在 UStaticMesh/FStaticMeshRenderData (共享一份);实例状态在 Proxy ;Proxy 里对 RenderData 只是 指针引用 ,并在渲染侧 避免再碰 UObject。
4. 着色器修改
usf中的修改,增加新的顶点着色器和像素着色器:
cpp
// ========== 场景 StaticMesh 用的 VS/PS ==========
float4x4 LocalToWorldMatrix;
void SceneMeshVS(
in float3 InPosition : ATTRIBUTE0,
in float4 InTangentZ : ATTRIBUTE1,
out float3 OutWorldNormal : TEXCOORD0,
out float4 OutPosition : SV_POSITION)
{
// 局部空间 → 世界空间 → 裁剪空间
float4 WorldPos = mul(float4(InPosition, 1.0f), LocalToWorldMatrix);
OutPosition = mul(WorldPos, ViewProjectionMatrix);
// 法线变换:用 LocalToWorld 的上 3x3(适用于均匀缩放的刚体变换)
float3 WorldNormal = mul(InTangentZ.xyz, (float3x3)LocalToWorldMatrix);
OutWorldNormal = normalize(WorldNormal);
}
void SceneMeshPS(
in float3 InWorldNormal : TEXCOORD0,
out float4 OutColor : SV_Target0)
{
// 世界法线 [-1,1] → [0,1],直接作为颜色输出
OutColor = float4(normalize(InWorldNormal) * 0.5f + 0.5f, 1.0f);
}
和之前类似,只是增加了LocalToWorldMatrix这样一个从局部空间变换到世界空间的矩阵计算。
C++中也对应定义了新的着色器:
cpp
class FSceneMeshLearningVS : public FGlobalShader
{
DECLARE_GLOBAL_SHADER(FSceneMeshLearningVS);
SHADER_USE_PARAMETER_STRUCT(FSceneMeshLearningVS, FGlobalShader);
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER(FMatrix44f, ViewProjectionMatrix)
SHADER_PARAMETER(FMatrix44f, LocalToWorldMatrix)
END_SHADER_PARAMETER_STRUCT()
};
IMPLEMENT_GLOBAL_SHADER(FSceneMeshLearningVS, "/Engine/Private/Tools/SceneLearning.usf", "SceneMeshVS", SF_Vertex);
class FSceneMeshLearningPS : public FGlobalShader
{
DECLARE_GLOBAL_SHADER(FSceneMeshLearningPS);
SHADER_USE_PARAMETER_STRUCT(FSceneMeshLearningPS, FGlobalShader);
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
};
IMPLEMENT_GLOBAL_SHADER(FSceneMeshLearningPS, "/Engine/Private/Tools/SceneLearning.usf", "SceneMeshPS", SF_Pixel);
5. 构建绘制mesh的数据(FSceneMeshDrawInfo)
接着,AI定义了一个结构用于存储绘制每个mesh时的数据:
(需要注意,这个数据是AI写代码自己新定义的数据,不是原生UE的,所以不用太关注,可以把它当成一个临时存储当前所需数据的结构)
cpp
struct FSceneMeshDrawInfo
{
FMatrix LocalToWorld;
FBufferRHIRef PositionVB;
FBufferRHIRef TangentsVB;
FBufferRHIRef IndexBuffer;
uint32 PositionStride;
uint32 TangentStride;
uint32 TangentZOffset;
EVertexElementType TangentElementType;
uint32 FirstIndex;
uint32 NumTriangles;
uint32 MinVertexIndex;
uint32 MaxVertexIndex;
};
这个数据是在 Render_Learning 函数中构建的:
cpp
TArray<FSceneMeshDrawInfo> MeshDrawInfos;
for (int32 i = 0; i < Scene->Primitives.Num(); i++)
{
FPrimitiveSceneInfo* PrimInfo = Scene->Primitives[i];
if (!PrimInfo || !PrimInfo->Proxy || !PrimInfo->Proxy->IsStaticMeshProxy())
{
continue;
}
FStaticMeshSceneProxy* SMProxy = static_cast<FStaticMeshSceneProxy*>(PrimInfo->Proxy);
const FStaticMeshRenderData* RenderData = SMProxy->GetRenderData();
if (!RenderData || RenderData->LODResources.Num() == 0)
{
continue;
}
const FStaticMeshLODResources& LOD = RenderData->LODResources[0];
if (LOD.Sections.Num() == 0)
{
continue;
}
const FPositionVertexBuffer& PosVB = LOD.VertexBuffers.PositionVertexBuffer;
const FStaticMeshVertexBuffer& SMVertBuf = LOD.VertexBuffers.StaticMeshVertexBuffer;
if (!IsValidRef(PosVB.VertexBufferRHI) ||
!IsValidRef(SMVertBuf.TangentsVertexBuffer.VertexBufferRHI) ||
!IsValidRef(LOD.IndexBuffer.IndexBufferRHI))
{
continue;
}
const FStaticMeshSection& Section = LOD.Sections[0];
FSceneMeshDrawInfo DrawInfo;
DrawInfo.LocalToWorld = SMProxy->GetLocalToWorld();
DrawInfo.PositionVB = PosVB.VertexBufferRHI;
DrawInfo.TangentsVB = SMVertBuf.TangentsVertexBuffer.VertexBufferRHI;
DrawInfo.IndexBuffer = LOD.IndexBuffer.IndexBufferRHI;
DrawInfo.PositionStride = PosVB.GetStride();
DrawInfo.FirstIndex = Section.FirstIndex;
DrawInfo.NumTriangles = Section.NumTriangles;
DrawInfo.MinVertexIndex = Section.MinVertexIndex;
DrawInfo.MaxVertexIndex = Section.MaxVertexIndex;
if (SMVertBuf.GetUseHighPrecisionTangentBasis())
{
DrawInfo.TangentStride = 16;
DrawInfo.TangentZOffset = 8;
DrawInfo.TangentElementType = VET_Short4N;
}
else
{
DrawInfo.TangentStride = 8;
DrawInfo.TangentZOffset = 4;
DrawInfo.TangentElementType = VET_PackedNormal;
}
MeshDrawInfos.Add(DrawInfo);
}
if (MeshDrawInfos.Num() > 0)
{
AddSceneMeshPass_Learning(GraphBuilder, View.ShaderMap, ViewProjectionMatrix,
MoveTemp(MeshDrawInfos), SceneTextures.Color.Target, DepthTexture);
}
让AI逐项解释 FSceneMeshDrawInfo 每个成员的数据类型,当前的含义,以及在Render_Learning函数中是如何拿到对应的数据的:(仅供参考)
5.1. FMatrix LocalToWorld
- 类型 :
FMatrix(4×4,世界变换,引擎里通常为 double 精度)。 - 含义 :该 StaticMesh 实例的 局部空间 → 世界空间 矩阵;顶点着色器里用它把物体空间顶点变到世界空间(你的
SceneMeshVS里与ViewProjection配套)。 - 在
Render_Learning里 :DrawInfo.LocalToWorld = SMProxy->GetLocalToWorld();
来自FStaticMeshSceneProxy(组件/实例在场景中的变换),不是从UStaticMesh资产读。
5.2. FBufferRHIRef PositionVB
- 类型 :
FBufferRHIRef(对FRHIBuffer的引用计数句柄,一般表示顶点缓冲)。 - 含义 :仅位置 的顶点流(Stream 0),对应
FPositionVertexBuffer上传到 GPU 的那份 RHI 缓冲。 - 在
Render_Learning里 :DrawInfo.PositionVB = PosVB.VertexBufferRHI;,其中
PosVB = LOD.VertexBuffers.PositionVertexBuffer,LOD0。
5.3. FBufferRHIRef TangentsVB
- 类型 :同上,
FBufferRHIRef。 - 含义 :静态网格顶点缓冲里 Tangent 基底 所在的那块 RHI 缓冲(与引擎里
FStaticMeshVertexBuffer的布局一致);你在 Stream 1 上只按偏移取了 TangentZ 当法线用。 - 在
Render_Learning里 :DrawInfo.TangentsVB = SMVertBuf.TangentsVertexBuffer.VertexBufferRHI;,
SMVertBuf = LOD.VertexBuffers.StaticMeshVertexBuffer。
5.4. FBufferRHIRef IndexBuffer
- 类型 :
FBufferRHIRef(索引缓冲的 RHI 句柄)。 - 含义 :该 LOD 的三角形索引(
uint32索引缓冲的 GPU 侧)。 - 在
Render_Learning里 :DrawInfo.IndexBuffer = LOD.IndexBuffer.IndexBufferRHI;。
5.5. uint32 PositionStride
- 类型:无符号 32 位整数。
- 含义 :位置顶点 每个顶点占多少 字节 (stride),用于构建
FVertexElement的 Stream 0,使 RHI 与顶点声明和 VS 输入一致。 - 在
Render_Learning里 :DrawInfo.PositionStride = PosVB.GetStride();
(来自FPositionVertexBuffer,一般为 12,即 3×float)。
5.6. uint32 TangentStride
- 类型 :
uint32。 - 含义 :Tangent 顶点缓冲里 每个顶点 占用的字节数;高精度与低精度打包时 stride 不同。
- 在
Render_Learning里 :- 若
SMVertBuf.GetUseHighPrecisionTangentBasis()为真:TangentStride = 16; - 否则:
TangentStride = 8。
与引擎里 StaticMesh 顶点布局一致。
- 若
5.7. uint32 TangentZOffset
- 类型 :
uint32。 - 含义 :在 Tangent 缓冲的一个顶点 内,Tangent Z(用作法线) 相对该顶点起始位置的 字节偏移 ;用于
FVertexElement(1, DrawInfo.TangentZOffset, ...),让 Input Assembler 只把"法线那部分"喂给着色器语义。 - 在
Render_Learning里 :高精度:8;低精度:4(与TangentStride分支配套)。
5.8. EVertexElementType TangentElementType
- 类型 :
EVertexElementType(枚举,如VET_Float3、VET_PackedNormal等)。 - 含义 :Stream 1 上 TangentZ 那一段 的顶点元素类型,必须与 GPU 里实际存储格式一致,否则解释错误。
- 在
Render_Learning里 :高精度:VET_Short4N;低精度:VET_PackedNormal。
5.9. uint32 FirstIndex
- 类型 :
uint32。 - 含义 :在 本 LOD 的索引缓冲 里,该 Section 的 起始索引下标(从哪个 index 开始画)。
- 在
Render_Learning里 :DrawInfo.FirstIndex = Section.FirstIndex;,Section = LOD.Sections[0]。
5.10. uint32 NumTriangles
- 类型 :
uint32。 - 含义 :该 Section 要画的 三角形个数 (传给
DrawIndexedPrimitive的三角形数量参数)。 - 在
Render_Learning里 :DrawInfo.NumTriangles = Section.NumTriangles;。
5.11. uint32 MinVertexIndex / uint32 MaxVertexIndex
- 类型 :
uint32。 - 含义 :该 Section 在索引里引用的顶点索引的 最小值 / 最大值 ;RHI 的
DrawIndexedPrimitive用它们描述 本次 draw 可能触及的顶点索引范围 (NumVertices = Max - Min + 1),用于驱动/验证顶点流范围。 - 在
Render_Learning里 :
MinVertexIndex = Section.MinVertexIndex;
MaxVertexIndex = Section.MaxVertexIndex;
调用处:
DrawIndexedPrimitive(..., MinVertexIndex, MaxVertexIndex - MinVertexIndex + 1, FirstIndex, NumTriangles, 1)。
数据流小结(Render_Learning 里"怎么拿到")
- 遍历
Scene->Primitives→FPrimitiveSceneInfo→Proxy。 - 用
IsStaticMeshProxy()过滤后static_cast<FStaticMeshSceneProxy*>。 GetRenderData()→FStaticMeshRenderData,取LODResources[0]。- 顶点/索引:
LOD.VertexBuffers(Position + StaticMeshVertexBuffer)和LOD.IndexBuffer上的VertexBufferRHI/IndexBufferRHI。 - Section 范围:
LOD.Sections[0]的FirstIndex、NumTriangles、MinVertexIndex、MaxVertexIndex。 - 变换:
SMProxy->GetLocalToWorld()。 - Tangent 布局:
GetUseHighPrecisionTangentBasis()决定 stride / offset /EVertexElementType三分支。
整体上,几何与索引来自资产的 FStaticMeshRenderData(LOD0) ,实例矩阵来自 FStaticMeshSceneProxy ;FSceneMeshDrawInfo 只是把 AddSceneMeshPass_Learning 里建 PSO、绑流、DrawIndexedPrimitive 所需的 RHI 参数打成一个包。
6. 添加绘制mesh的pass
当构建好了 FSceneMeshDrawInfo 之后,就来到了最终在AddSceneMeshPass_Learning函数中使用了:
cpp
static void AddSceneMeshPass_Learning(
FRDGBuilder& GraphBuilder,
const FGlobalShaderMap* ShaderMap,
const FMatrix& ViewProjectionMatrix,
TArray<FSceneMeshDrawInfo>&& MeshDrawInfos,
FRDGTextureRef ColorTexture,
FRDGTextureRef DepthTexture)
{
TShaderMapRef<FSceneMeshLearningVS> VertexShader(ShaderMap);
TShaderMapRef<FSceneMeshLearningPS> PixelShader(ShaderMap);
FSceneMeshLearningPS::FParameters* PSParameters = GraphBuilder.AllocParameters<FSceneMeshLearningPS::FParameters>();
PSParameters->RenderTargets[0] = FRenderTargetBinding(ColorTexture, ERenderTargetLoadAction::ELoad);
PSParameters->RenderTargets.DepthStencil = FDepthStencilBinding(
DepthTexture,
ERenderTargetLoadAction::ELoad,
ERenderTargetLoadAction::ENoAction,
FExclusiveDepthStencil::DepthWrite_StencilNop);
const FIntPoint OutputExtent = ColorTexture->Desc.Extent;
const FMatrix44f ViewProjMatrix44f(ViewProjectionMatrix);
GraphBuilder.AddPass(
RDG_EVENT_NAME("LearningSceneMesh"),
PSParameters,
ERDGPassFlags::Raster,
[VertexShader, PixelShader, PSParameters, ViewProjMatrix44f,
OutputExtent, Meshes = MoveTemp(MeshDrawInfos)]
(FRDGAsyncTask, FRHICommandList& RHICmdList)
{
RHICmdList.SetViewport(0.0f, 0.0f, 0.0f, (float)OutputExtent.X, (float)OutputExtent.Y, 1.0f);
for (const FSceneMeshDrawInfo& DrawInfo : Meshes)
{
FVertexDeclarationElementList Elements;
Elements.Add(FVertexElement(0, 0, VET_Float3, 0, DrawInfo.PositionStride));
Elements.Add(FVertexElement(1, DrawInfo.TangentZOffset, DrawInfo.TangentElementType, 1, DrawInfo.TangentStride));
FVertexDeclarationRHIRef VertexDecl = PipelineStateCache::GetOrCreateVertexDeclaration(Elements);
FGraphicsPipelineStateInitializer GraphicsPSOInit;
RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
GraphicsPSOInit.RasterizerState = TStaticRasterizerState<FM_Solid, CM_None>::GetRHI();
GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<true, CF_DepthNearOrEqual>::GetRHI();
GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = VertexDecl;
GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
GraphicsPSOInit.PrimitiveType = PT_TriangleList;
SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0);
FSceneMeshLearningVS::FParameters VSParameters;
VSParameters.ViewProjectionMatrix = ViewProjMatrix44f;
VSParameters.LocalToWorldMatrix = FMatrix44f(DrawInfo.LocalToWorld);
SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), VSParameters);
SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), *PSParameters);
RHICmdList.SetStreamSource(0, DrawInfo.PositionVB, 0);
RHICmdList.SetStreamSource(1, DrawInfo.TangentsVB, 0);
RHICmdList.DrawIndexedPrimitive(
DrawInfo.IndexBuffer,
0,
DrawInfo.MinVertexIndex,
DrawInfo.MaxVertexIndex - DrawInfo.MinVertexIndex + 1,
DrawInfo.FirstIndex,
DrawInfo.NumTriangles,
1);
}
}
);
}
接着,让AI解释下这里的代码,特别是与上一篇里画三角形时候的pass的区别,以及重点提一下使用FSceneMeshDrawInfo的部分:(仅供参考)
6.1. 顶点着色器参数
- 三角形 :VS 只有
ViewProjectionMatrix(世界顶点已在 CPU 侧顶点数据里)。 - 场景 Mesh :VS 多
LocalToWorldMatrix,且LocalToWorld来自每个FSceneMeshDrawInfo(FMatrix44f(DrawInfo.LocalToWorld)),因为顶点在 物体空间 ,要先乘DrawInfo.LocalToWorld再乘 ViewProjection。 LocalToWorld 来自于 FSceneMeshDrawInfo。
6.2. 顶点声明(Vertex Declaration)与 PSO
- 三角形 :固定 全局声明
GLearningVertexDeclaration.VertexDeclarationRHI,与GLearningTriangleVB的布局一致,整段 pass 一套 PSO。 - 场景 Mesh :每个
FSceneMeshDrawInfo可能不同 (位置 stride、Tangent 高精度/低精度),在for (const FSceneMeshDrawInfo& DrawInfo : Meshes)里用DrawInfo.PositionStride、TangentZOffset、DrawInfo.TangentElementType、DrawInfo.TangentStride
拼FVertexDeclarationElementList,再PipelineStateCache::GetOrCreateVertexDeclaration(Elements),因此 每个 DrawInfo 可能换一次VertexDecl并重设 PSO(双流:stream0 位置,stream1 TangentZ)。
数据来源:与 VB/stride/offset/type 相关的字段全部来自 FSceneMeshDrawInfo。
6.3. 顶点流与 DrawCall
AddTrianglePass_Learning |
AddSceneMeshPass_Learning |
|
|---|---|---|
| 缓冲 | 单一全局 GLearningTriangleVB.VertexBufferRHI |
DrawInfo.PositionVB + DrawInfo.TangentsVB(两条 stream) |
SetStreamSource |
只设 stream 0 | stream 0 + stream 1 |
| 绘制 | DrawPrimitive(非索引,2 个三角形,顶点在 VB 里连续) |
DrawIndexedPrimitive (DrawInfo.IndexBuffer + FirstIndex / NumTriangles / MinVertexIndex / MaxVertexIndex) |
索引与顶点范围相关参数 全部来自 FSceneMeshDrawInfo。
和 FSceneMeshDrawInfo 的对应关系(仅列本 pass 用到的字段)
| 用途 | FSceneMeshDrawInfo 成员 |
|---|---|
| VS 实例变换 | LocalToWorld |
| 顶点声明 | PositionStride,TangentStride,TangentZOffset,TangentElementType |
| 绑定的 RHI 缓冲 | PositionVB,TangentsVB,IndexBuffer |
DrawIndexedPrimitive |
FirstIndex,NumTriangles,MinVertexIndex,MaxVertexIndex |
总结
场景中每个 StaticMeshComponent 都有一个 FStaticMeshSceneProxy 作为渲染线程的代理人。
本篇的代码中,是从Scene->Primitives中得到所有的SceneProxy,遍历他们并逐个构建出对应的 FSceneMeshDrawInfo,里面有模型的顶点数据和世界矩阵。
然后用 FSceneMeshDrawInfo 中的数据去添加绘制pass。
流程相对清晰,不过顶点数据还有些疑问,毕竟之前画三角形时是硬编码,细节是完全透明的,但是现在每个模型的顶点数据的构建构建过程还是黑盒。这也许是后续的学习目标了。