AI教我UE渲染-学习笔记(8):StaticMesh渲染数据初步学习

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 上的数据。


职责(做什么)

子类通过虚函数参与整条渲染管线,典型包括:

方向 代表接口
可见性与通道 GetViewRelevanceGetLightRelevanceIsShown
几何提交 DrawStaticElements(创建时一次)、GetDynamicMeshElements(每帧/每视图收集 FMeshBatch
资源生命周期 CreateRenderThreadResources / DestroyRenderThreadResources
变换与包围体 基类里已有 LocalToWorldBounds 等,变换更新可走 OnTransformChanged
光线追踪等扩展 GetDynamicRayTracingInstancesHasRayTracingRepresentation 等(#if RHI_RAYTRACING

GetDynamicMeshElements 的注释也强调了:渲染线程不能碰 UObject,需要的状态必须在 Proxy 上有一份


数据结构(本体里有什么)

Proxy 很大,可以理解成几层:

  1. 身份与归属FPrimitiveComponentIdFSceneInterface* Scene、指回 FPrimitiveSceneInfo* PrimitiveSceneInfo (场景里真正挂接、索引、八叉树等用的是 SceneInfo,Proxy 是 SceneInfo 里"可渲染的那一面")。
  2. 几何与变换LocalToWorldBounds / LocalBoundsActorPosition 等。
  3. 着色与管线状态 :大量 uint8 位域(静态/可移动、阴影、Nanite、皮肤、Spline......)、FPrimitiveUniformShaderParametersUniformBufferCombinedMaterialRelevance 等。
  4. 调试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 填给渲染器。


一句话FPrimitiveSceneProxyUPrimitiveComponent 在渲染线程上的安全替身 + 多态渲染入口 ;场景里和 FPrimitiveSceneInfoFScene::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 == truenew Nanite::FSceneProxy(...)不是 FStaticMeshSceneProxy
  • bCreateNanite == falsenew 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::FSceneProxyBaseFPrimitiveSceneProxy不继承 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上会有

  1. 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::RenderDataTUniquePtr<FStaticMeshRenderData>)持有;里面典型包括:

  • LODResources :每个 LOD 的顶点缓冲、索引缓冲、Section 等(FStaticMeshLODResources
  • LODVertexFactories:与各 LOD 对应的顶点工厂(材质/着色路径用)
  • ScreenSize:LOD 切换距离
  • NaniteResourcesPtrRayTracingProxy 等扩展表示

也就是说:静态网格在 GPU/渲染侧的几何与管线相关数据,都集中在这份 FStaticMeshRenderData,而不是散落在组件上每次去 UObject 上现查。


为啥要给 FStaticMeshSceneProxyGetRenderData()

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 / SectionVertexBuffersIndexBuffer 等,去绑 RHI、发 DrawIndexed
  • 这些数据都在 RenderData->LODResources[LOD].... 下面。

RenderDataFStaticMeshSceneProxy 里是 protected 成员、又没有公开 getter ,渲染器里就只能写 friend 、强转或重复从 UStaticMesh 取------都不合适。加一个 const FStaticMeshRenderData* GetRenderData() const ,等于承认:外部合法用法就是读这份与 Proxy 生命周期一致的渲染数据指针(和 Proxy 内部用法一致,只是从 private 变成可控的只读出口)。


原始流程里 RenderData 是怎么被访问的?

主路径全是 FStaticMeshSceneProxy 自己的实现内部 用成员 RenderData,不经过你新加的接口。例如:

  1. 构造 :校验 RenderData、按 LOD 建 LODs、距离场、RayTracing 等(前面 grep 里从 259 行起一整段)。
  2. FMeshBatchGetMeshElement / GetShadowMeshElement / SetMeshElementGeometrySource 等会从
    RenderData->LODResources[LODIndex]RenderData->LODVertexFactories[LODIndex] 取 section、索引缓冲、顶点工厂(例如 678--679、743--744 行附近)。
  3. 资源创建/销毁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 流送块等) UStaticMeshFStaticMeshRenderData 一份网格资产,被很多 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_LearningDrawInfo.LocalToWorld = SMProxy->GetLocalToWorld();
    来自 FStaticMeshSceneProxy (组件/实例在场景中的变换),不是从 UStaticMesh 资产读。

5.2. FBufferRHIRef PositionVB

  • 类型FBufferRHIRef(对 FRHIBuffer 的引用计数句柄,一般表示顶点缓冲)。
  • 含义仅位置 的顶点流(Stream 0),对应 FPositionVertexBuffer 上传到 GPU 的那份 RHI 缓冲。
  • Render_LearningDrawInfo.PositionVB = PosVB.VertexBufferRHI;,其中
    PosVB = LOD.VertexBuffers.PositionVertexBufferLOD0

5.3. FBufferRHIRef TangentsVB

  • 类型 :同上,FBufferRHIRef
  • 含义 :静态网格顶点缓冲里 Tangent 基底 所在的那块 RHI 缓冲(与引擎里 FStaticMeshVertexBuffer 的布局一致);你在 Stream 1 上只按偏移取了 TangentZ 当法线用。
  • Render_LearningDrawInfo.TangentsVB = SMVertBuf.TangentsVertexBuffer.VertexBufferRHI;
    SMVertBuf = LOD.VertexBuffers.StaticMeshVertexBuffer

5.4. FBufferRHIRef IndexBuffer

  • 类型FBufferRHIRef(索引缓冲的 RHI 句柄)。
  • 含义 :该 LOD 的三角形索引(uint32 索引缓冲的 GPU 侧)。
  • Render_LearningDrawInfo.IndexBuffer = LOD.IndexBuffer.IndexBufferRHI;

5.5. uint32 PositionStride

  • 类型:无符号 32 位整数。
  • 含义位置顶点 每个顶点占多少 字节 (stride),用于构建 FVertexElement 的 Stream 0,使 RHI 与顶点声明和 VS 输入一致。
  • Render_LearningDrawInfo.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_Float3VET_PackedNormal 等)。
  • 含义 :Stream 1 上 TangentZ 那一段 的顶点元素类型,必须与 GPU 里实际存储格式一致,否则解释错误。
  • Render_Learning :高精度:VET_Short4N;低精度:VET_PackedNormal

5.9. uint32 FirstIndex

  • 类型uint32
  • 含义 :在 本 LOD 的索引缓冲 里,该 Section 的 起始索引下标(从哪个 index 开始画)。
  • Render_LearningDrawInfo.FirstIndex = Section.FirstIndex;Section = LOD.Sections[0]

5.10. uint32 NumTriangles

  • 类型uint32
  • 含义 :该 Section 要画的 三角形个数 (传给 DrawIndexedPrimitive 的三角形数量参数)。
  • Render_LearningDrawInfo.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 里"怎么拿到")

  1. 遍历 Scene->PrimitivesFPrimitiveSceneInfoProxy
  2. IsStaticMeshProxy() 过滤后 static_cast<FStaticMeshSceneProxy*>
  3. GetRenderData()FStaticMeshRenderData ,取 LODResources[0]
  4. 顶点/索引:LOD.VertexBuffers (Position + StaticMeshVertexBuffer)和 LOD.IndexBuffer 上的 VertexBufferRHI / IndexBufferRHI
  5. Section 范围:LOD.Sections[0]FirstIndexNumTrianglesMinVertexIndexMaxVertexIndex
  6. 变换:SMProxy->GetLocalToWorld()
  7. Tangent 布局:GetUseHighPrecisionTangentBasis() 决定 stride / offset / EVertexElementType 三分支。

整体上,几何与索引来自资产的 FStaticMeshRenderData(LOD0)实例矩阵来自 FStaticMeshSceneProxyFSceneMeshDrawInfo 只是把 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 来自每个 FSceneMeshDrawInfoFMatrix44f(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.PositionStrideTangentZOffsetDrawInfo.TangentElementTypeDrawInfo.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 里连续) DrawIndexedPrimitiveDrawInfo.IndexBuffer + FirstIndex / NumTriangles / MinVertexIndex / MaxVertexIndex

索引与顶点范围相关参数 全部来自 FSceneMeshDrawInfo


FSceneMeshDrawInfo 的对应关系(仅列本 pass 用到的字段)

用途 FSceneMeshDrawInfo 成员
VS 实例变换 LocalToWorld
顶点声明 PositionStrideTangentStrideTangentZOffsetTangentElementType
绑定的 RHI 缓冲 PositionVBTangentsVBIndexBuffer
DrawIndexedPrimitive FirstIndexNumTrianglesMinVertexIndexMaxVertexIndex

总结

场景中每个 StaticMeshComponent 都有一个 FStaticMeshSceneProxy 作为渲染线程的代理人。

本篇的代码中,是从Scene->Primitives中得到所有的SceneProxy,遍历他们并逐个构建出对应的 FSceneMeshDrawInfo,里面有模型的顶点数据和世界矩阵。

然后用 FSceneMeshDrawInfo 中的数据去添加绘制pass。

流程相对清晰,不过顶点数据还有些疑问,毕竟之前画三角形时是硬编码,细节是完全透明的,但是现在每个模型的顶点数据的构建构建过程还是黑盒。这也许是后续的学习目标了。

相关推荐
YakSue7 天前
AI教我UE渲染-学习笔记(6):启用深度缓冲
unreal engine
YakSue7 天前
AI教我UE渲染-学习笔记(7):PS输出到多个RT
unreal engine
YakSue11 天前
AI教我UE渲染-学习笔记(5):测试画一个3D三角形
unreal engine
暗光之痕14 天前
Unreal5 研究笔记 蓝图自定义节点
笔记·unreal engine
YakSue17 天前
AI教我UE渲染-学习笔记(4):测试新写个全屏复制贴图的shader与pass
unreal engine
JCHwa21 天前
UE5 GAS 源码深度解析 | 第2篇:AttributeSet 源码导读
游戏开发·unreal engine
侑虎科技1 个月前
在UE5中,预测脚步IK实现-PredictFootIK
性能优化·unreal engine
暗光之痕2 个月前
Unreal5研究笔记 Actor的生命周期函数
笔记·unreal engine
哎呦哥哥和巨炮叔叔2 个月前
Unreal Engine 是否支持光线追踪?UE5 光线追踪原理与性能解析
ue5·unreal engine·光线追踪·lumen·实时渲染·渲染101云渲染·ue云渲染