来源:Mesh Shader 学习笔记 + Nanite 硬件光栅源码阅读
引擎版本:UE 5.8
面向读者:UE 图形程序员
Mesh shader 用一个像 compute shader 那样协作的线程组,替掉了 IA + VS(以及 GS):线程组自己取几何、自己变换,最后往三个并排的输出数组——顶点、索引、逐图元属性——写出一个 meshlet,直接交给固定功能光栅器。
Nanite 的硬件光栅入口 HWRasterizeMS 是这套模型现成的活教材。UE 5.8 里它固定 32 线程一组,一组处理的不是整个 cluster,而是 cluster 里的一个顶点复用批次(≤32 个三角形、≤32 个唯一顶点);visibility buffer 和 VSM 需要的逐三角形信息全部走逐图元属性,顶点变换则复用一份跑在 mesh shader 里的「顶点着色器」CommonRasterizerVS。
目录
- 传统几何管线卡在哪
- Mesh Shader 的执行模型
- 三个输出数组
- Nanite 何时走硬件光栅
- 从可见 cluster 到 DispatchMesh
- HWRasterizeMS 逐段拆解
- HWRasterizePS:片元落到 VSM 物理页
- 三条路径对照
- 坑点速记
- 参考链接
一、传统几何管线卡在哪
Vertex Buffer + Index Buffer
│
[Input Assembler] 固定功能:按索引逐个取顶点喂给 VS
│
[Vertex Shader] 一个顶点进,一个顶点出
│
[Hull / Domain / Geometry Shader](可选)
│
[Rasterizer] 固定功能:三角形 setup、覆盖、插值
│
[Pixel Shader]
对 GPU-driven 渲染来说,光栅器之前的这半段处处受限:
| 环节 | 限制 | 后果 |
|---|---|---|
| Input Assembler | 只认「顶点缓冲 + 索引缓冲」的固定格式 | 压缩存储、由 GPU 自己决定画哪些的几何(比如 Nanite 的 cluster page)喂不进去 |
| Vertex Shader | 1 进 1 出,看不到邻居,不能增删几何 | 共享顶点的复用只能指望硬件 post-transform cache,shader 自己控制不了 |
| Geometry Shader | 能增删几何,但输出必须保序落地,放大量又不可预测 | 实现上要额外缓冲,性能差,热路径基本不用 |
| 整条前端 | 线程之间没有 groupshared,也没有同步 | 解码、去重、剔除这类需要协作的工作做不了 |
二、Mesh Shader 的执行模型
2.1 能直接喂光栅器的 compute shader
[Amplification Shader](可选) 决定启动多少个 mesh 线程组,可以先做粗剔除
│ DispatchMesh
[Mesh Shader] 一个线程组协作产出一个 meshlet
│
[Rasterizer] → [Pixel Shader] 与传统管线相同
- 线程组语义:
[numthreads(X, Y, Z)],有 groupshared,可以GroupMemoryBarrierWithGroupSync(),和 compute shader 一样能协作。 - 没有 IA:输入只有线程 ID,外加 Amplification shader 传下来的 payload。从哪个 buffer、按什么格式取几何,压缩与否,甚至程序化生成,全由 shader 决定。
- 线程与输出元素没有隐式对应:一个线程可以写多个顶点和图元,也可以一个都不写,负载怎么分由作者安排。
2.2 D3D12 的硬上限
| 项 | 上限 |
|---|---|
线程组大小 X*Y*Z | ≤ 128 |
| 每组输出顶点 | ≤ 256 |
| 每组输出图元 | ≤ 256 |
| groupshared | ≤ 28 KB(compute shader 为 32 KB) |
| 输出属性 | 顶点属性行 + 图元属性行合计 ≤ 32 个 4 分量行,两类属性不能打包进同一行;按字节还要满足 vert_attr_bytes × ALIGN32(n_verts) + prim_attr_bytes × ALIGN32(n_prims) ≤ 32 KB |
DispatchMesh | 每一维 < 64k,三维乘积 ≤ 2²² |
2.3 心智模型
一个 mesh shader 线程组 = 一个 meshlet = 一次自带顶点缓冲和索引缓冲的迷你 indexed draw。这份 VB / IB 由线程组在片上现造,造完直接交给光栅器。
Nanite 的几何本来就按 cluster 存储,上限是 NANITE_MAX_CLUSTER_TRIANGLES = 128、NANITE_MAX_CLUSTER_VERTICES = 256(NaniteDefinitions.h:21-26),正好落在 D3D12 的输出上限之内,天然就是 meshlet 的形状。不过 5.8 并没有采用「一组 = 一个 cluster」,第五、六节会看到它把 cluster 进一步切成了更小的批次。
三、三个输出数组
Mesh shader 不像 VS 那样 return 一个顶点,而是往三个线程组共享的输出数组里写。UE 在平台头文件里用宏把它们包了一层:
// Engine/Shaders/Public/Platform/D3D/D3DCommon.ush:90(节选)
#if PLATFORM_SUPPORTS_MESH_SHADERS_TIER0
#define MESH_SHADER_TRIANGLE_ATTRIBUTES(InThreadsX) [numthreads(InThreadsX, 1, 1)][outputtopology("triangle")]
// ...
#define MESH_SHADER_VERTEX_EXPORT(VertexType, InMaxVertexCount) out vertices VertexType OutVertices[InMaxVertexCount]
#define MESH_SHADER_TRIANGLE_EXPORT(InMaxTriangleCount) out indices uint3 OutTriangles[InMaxTriangleCount]
// ...
#define MESH_SHADER_WRITE_VERTEX(InVertexIndex, Value) OutVertices[InVertexIndex] = Value;
#define MESH_SHADER_WRITE_TRIANGLE(InTriangleIndex, Value) OutTriangles[InTriangleIndex] = Value;
// ...
#endif
#if PLATFORM_SUPPORTS_MESH_SHADERS_TIER1
#define MESH_SHADER_PRIMITIVE_EXPORT(PrimitiveType, InMaxPrimitiveCount) out primitives PrimitiveType OutPrimitives[InMaxPrimitiveCount]
#define MESH_SHADER_WRITE_PRIMITIVE(InPrimitiveIndex, Value) OutPrimitives[InPrimitiveIndex] = Value;
#endif| 数组 | 传统管线里的对应物 | 装的是什么 |
|---|---|---|
OutVertices[] | VS 的输出(post-transform 顶点) | 每项一个 VSOut,必须含 SV_Position,外加要插值给 PS 的量 |
OutTriangles[] | Index Buffer | 每项一个 uint3,引用的是本组 OutVertices 的槽号,不是网格里的原始顶点号 |
OutPrimitives[] | 没有对应物 | 每个三角形一份常量属性,PS 直接读 |
图 1:一个线程组导出两个共享一条边的三角形。cluster 本地顶点号先压成连续槽号,索引数组引用槽号,逐图元属性每个三角形一份。
3.1 先声明数量:SetMeshOutputCounts
写任何输出数组之前,线程组必须先调用 SM 6.5 的内建函数 SetMeshOutputCounts(NumVertices, NumPrimitives),声明本次实际导出多少顶点和图元。数组声明的长度只是容量,这里给出的才是有效数量。规范对它的约束:
- 每个 shader 只能调用一次,而且必须支配(dominate)所有对输出数组的写入;
- 只采用第一个活跃线程传入的值;
- 在发散控制流里调用,或者写入超出声明数量的槽位,都是未定义行为,硬件不负责裁掉;
- 始终没调用,等于这个线程组什么都不输出。
❗ 规范明确允许「在 group-uniform 条件下先 return,再调用 SetMeshOutputCounts」,Nanite 却在源码里写明这样做会出 corruption,而且编译器不报错:
// Engine/Shaders/Private/Nanite/NaniteRasterizer.usf:3056
// NOTE: Doing a simple early out here doesn't work. Likely because divergent control
// flow is not allowed around SetMeshOutputCounts, even if the condition is uniform for
// the group. The compiler succeeds but corruption occurs.所以 HWRasterizeMS 从不提前返回:所有取数逻辑都包在 if (bValidIndex) 里,无效的线程组照样走到 SetMeshOutputCounts(0, 0)。
3.2 顶点和索引为什么分开
为了顶点复用。一个被 6 个三角形共享的顶点,在 OutVertices 里只变换、只导出一次,被索引引用 6 次;如果每个三角形独立吐 3 个顶点,同一个顶点就要变换 6 次,占 6 份导出带宽。
3.3 逐图元属性解决了什么
传统管线想给「每个三角形」挂一份数据,只能复制到它的 3 个顶点上,或者动用 GS。Mesh shader 原生支持逐图元导出:PS 按语义名匹配输入,逐图元属性在一个三角形内恒定,插值修饰符对它既不需要、也不起作用。Nanite 把 visibility buffer 的逐三角形 ID PixelValue、ViewId,以及 VSM 的 mip、页表偏移、页矩形都放在这里,布局见 §6.5。
❗ PrimitiveAttributesPacked 刻意用 uint4,并标了 nointerpolation(NaniteRasterizer.usf:2476-2484)。源码注释写明用 uint4 是为了防止编译器把逐顶点和逐图元属性错误地打包到一起——规范本来就禁止两类属性共用一行;nointerpolation 在 mesh shader 路径里不起作用,它服务的是 VS 回退路径,那里同一个结构作为普通顶点输出传给 PS。
3.4 UE 的 Tier0 / Tier1
UE 把 mesh shader 能力分成两档:PLATFORM_SUPPORTS_MESH_SHADERS_TIER0 提供顶点和索引导出,TIER1 再加上逐图元导出。这是 UE 自己的划分——D3D12 只有 D3D12_MESH_SHADER_TIER_1 一档,D3D12 RHI 在该能力成立时把两档一起置位:
// Engine/Source/Runtime/D3D12RHI/Private/D3D12Adapter.cpp:1372
GRHISupportsMeshShadersTier0 = GRHISupportsMeshShadersTier1 = (D3D12Caps7.MeshShaderTier >= D3D12_MESH_SHADER_TIER_1);Nanite 的 mesh shader 路径要求 Tier1,UseMeshShader() 里的注释就是 “We require tier1 support to utilize primitive attributes”(NaniteShared.cpp:138)。
四、Nanite 何时走硬件光栅
4.1 按 cluster 分流
软件光栅(compute shader)处理像素级的小三角形更快,三角形一大,固定功能光栅器更划算。分流在 cluster 剔除阶段按 cluster 决定:
// Engine/Shaders/Private/Nanite/NaniteClusterCulling.usf:301
if (RenderFlags & NANITE_RENDER_FLAG_FORCE_HW_RASTER)
{
bUseHWRaster = true;
}
else
{
float HWEdgeScale = InstanceData.NonUniformScale.w * Bounds.NodeMaxDeformScale;
bUseHWRaster |= ProjectedEdgeScale < HWEdgeScale * abs( EdgeLength ) * NaniteView.LODScaleHW; // TODO: EdgeLength shouldn't have sign
}LODScaleHW = ViewToPixels / MinPixelsPerEdgeHW(NaniteShared.cpp:200),这个不等式的含义是「cluster 的边长(乘上实例的最大缩放)投影到屏幕超过 r.Nanite.MinPixelsPerEdgeHW 个像素」。之后的分桶阶段还会按材质再改一次判定:
| 条件 | 位置 | 结果 |
|---|---|---|
投影边长超过 r.Nanite.MinPixelsPerEdgeHW | NaniteClusterCulling.usf:308 | HW |
cluster 需要裁剪(Cull.bNeedsClipping) | NaniteClusterCulling.usf:860 | HW |
材质需要有限差分(!bNoDerivativeOps) | NaniteRasterBinning.usf:421 | 强制 HW:SW 光栅的一条 lane 处理的是任意一个三角形,算不了导数 |
| 半透明材质 | NaniteRasterBinning.usf:427 | 强制 HW |
| 可编程像素材质,且平台最小 wave 宽度 < 32 | NaniteRasterBinning.usf:415 | 强制 HW |
| 位移(displacement)、体素、曲线 | NaniteRasterBinning.usf:440-457 | 强制 SW |
4.2 选哪条硬件路径
GetRasterHardwarePath(NaniteShared.cpp:158)按顺序挑:
- Mesh shader:
UseMeshShader()成立,即平台 DDPI 声明支持 Tier1、NaniteMeshShadersSupported通过、r.Nanite.MeshShaderRasterization打开、运行时GRHISupportsMeshShadersTier1为真;如果开了r.AllowGlobalClipPlane,平台还必须支持 mesh shader 输出 clip distance。再细分三种:平台最大 MS 线程组为 32 时走MeshShaderNV;平台要求不 wrap 的派发参数时走MeshShader;其余走MeshShaderWrapped,D3D12 属于这一种。 - Primitive shader:
GRHISupportsPrimitiveShaders且r.Nanite.PrimShaderRasterization打开,只在支持 primitive shader 的平台上存在。 - Vertex shader:以上都不满足时,回退到
HWRasterizeVS+DrawPrimitiveIndirect。
4.3 相关开关
| CVar | 默认 | 作用 |
|---|---|---|
r.Nanite.MeshShaderRasterization | 1 | 条件满足时用 mesh shader 做 HW 光栅 |
r.Nanite.PrimShaderRasterization | 1 | 条件满足时用 primitive shader 做 HW 光栅 |
r.Nanite.MinPixelsPerEdgeHW | 32 | 三角形边长超过多少像素开始走 HW 光栅 |
r.AllowGlobalClipPlane | — | 打开后,若平台不支持 MS 输出 clip distance,mesh shader 路径被禁用 |
PC 上把 r.Nanite.MeshShaderRasterization 设为 0 就会落到 VS 路径,可以拿来对比两条路径的开销;前提是平台没有剥离 Nanite 的 VS 排列,否则 GetRasterHardwarePath 里的 checkf 会直接报致命错误(NaniteShared.cpp:185)。
五、从可见 cluster 到 DispatchMesh
HW 光栅要启动多少个线程组只有 GPU 知道,所以只能间接派发。可见 cluster 进光栅之前先经过一轮 GPU 分桶,全部在 FRenderer::AddPass_Binning(NaniteCullRaster.cpp:4895)里,RDG 事件依次是:
RasterGroupInit → RasterBinCount → RasterGroupReserve → (RasterGroupDepthBlock) → RasterBinScatter → RasterBinFinalize先按 raster group 计数、分配区间,再把工作项 scatter 进去,最后写间接参数。关键在于:一条 HW 工作项对应的是一个顶点复用批次,而不是一个 cluster。
图 2:cluster 在离线阶段切成顶点复用批次;每帧分桶时每个 HW 批次写一条 RasterBinData,批次总数写进 DispatchMesh 参数,一个线程组消费一个批次。
5.1 批次在离线阶段切好
NaniteBuilder 在编码阶段对每个 cluster 的每个材质段贪心切分,唯一顶点数或三角形数任一到 32 就开新批次,只记录每批的三角形数:
// Engine/Source/Developer/NaniteBuilder/Private/Encode/NaniteEncodeVertReuseBatch.cpp:88(节选)
static void BuildVertReuseBatches(FCluster& Cluster)
{
for (FMaterialRange& MaterialRange : Cluster.MaterialRanges)
{
// ...
const uint32 MaxBatchVerts = 32;
const uint32 MaxBatchTris = 32;
// ...
for (uint32 TriIndex = MaterialRange.RangeStart; TriIndex < TriIndexEnd; ++TriIndex)
{
// ...
// If adding this tri to the current batch will result in too many unique verts, start a new batch
const uint32 NumNewUniqueVerts = uint32(!Bit0) + uint32(!Bit1) + uint32(!Bit2);
if (NumUniqueVerts + NumNewUniqueVerts > MaxBatchVerts)
{
check(NumTris > 0);
MaterialRange.BatchTriCounts.Add(uint8(NumTris));
// ...(清空计数与掩码,这个三角形留给下一批)
--TriIndex;
continue;
}
// ...
++NumTris;
if (NumTris == MaxBatchTris)
{
MaterialRange.BatchTriCounts.Add(uint8(NumTris));
// ...
}
}
// ...
}
}每批的三角形数以 5 bit(存 count - 1)写进 cluster 数据:材质段不超过 3 个时内联在 cluster 的打包数据里,否则放进页数据,运行时由 DecodeVertReuseBatchInfo(NaniteRasterBinning.usf:292)解出。
5.2 运行时:HW cluster 拆成批次写进 raster group
// Engine/Shaders/Private/Nanite/NaniteRasterBinning.usf:433(节选)
#if FORCE_BATCHING
const bool bForceBatching = true;
#else
const bool bForceBatching = false;
#endif
bool bUseBatch = !bSoftware && bUsePrimOrMeshShader && (bForceBatching || MaterialFlags.bVertexProgrammable || MaterialFlags.bTranslucent);
// ...
BatchCount = CondMask(bUseBatch, BatchCount, 1u);
// ...
for (uint BatchIndex = 0; BatchIndex < BatchCount; ++BatchIndex)
{
if (bUseBatch)
{
uint BatchTriCount = DecodeVertReuseBatchInfo(Cluster, BatchInfoOffset, BatchIndex, bBatchInfoInlined);
RangeEnd = RangeStart + BatchTriCount;
}
const uint RangeNum = RangeEnd - RangeStart;
OutRasterBinData[GroupClusterMapping + BatchIndex].x = ClusterIndex;
OutRasterBinData[GroupClusterMapping + BatchIndex].y = (RasterBin << 16) | (RangeStart << 8) | RangeNum;
RangeStart = RangeEnd;
}FORCE_BATCHING 由 C++ 按平台决定:不支持 primitive shader 的平台一律为 1(NaniteCullRaster.cpp:1199)。所以在 PC 上只要走 mesh shader,每个 HW cluster 的每个材质段都会拆成批次,每个批次一条 RasterBinData:.x 是可见 cluster 索引,.y 打包了 raster bin、三角形起点和三角形数;raster group 的 HWCount 累加的也是批次数。
5.3 间接参数与 64k 限制
// Engine/Shaders/Private/Nanite/NaniteRasterBinning.usf:205
if (FinalizeMode == 0 && bMeshShader) // Wrapping
{
#if !PLATFORM_REQUIRES_UNWRAPPED_MESH_SHADER_ARGS
// Need to span the launch size across X,Y,Z so any single dimension does not exceed the 64k
// limit as per https://microsoft.github.io/DirectX-Specs/d3d/MeshShader.html#dispatchmesh-api
// The product of X*Y*Z can be 2^22, so we convert 1D->3D->1D to work around this limitation.
const uint DimensionLimit = (1u << 16u) - 1u;
const uint3 GroupCount = GetGroupCountWrapped(ClampedHWClusterCount, DimensionLimit.xx);
// HW: ThreadGroupCountXYZ
OutRasterGroupArgsSWHW[Offset + 4u] = GroupCount.x;
OutRasterGroupArgsSWHW[Offset + 5u] = GroupCount.y;
OutRasterGroupArgsSWHW[Offset + 6u] = GroupCount.z;
#else
OutRasterGroupArgsSWHW[Offset + 4u] = ClampedHWClusterCount;
#endif
}每个 raster group 占 8 个 uint 的参数(NANITE_RASTERIZER_ARG_COUNT):第 0 个是 SW 工作项数,第 4~6 个是 DispatchMesh 的三维线程组数,所以 C++ 侧的偏移是 +16 字节:
// Engine/Source/Runtime/Renderer/Private/Nanite/NaniteCullRaster.cpp:3661
if (IsMeshShaderRasterPath(HardwarePath))
{
RHICmdList.DispatchIndirectMeshShader(Parameters.IndirectArgs->GetIndirectRHICallBuffer(), Group.IndirectOffset + 16);
}
else
{
RHICmdList.DrawPrimitiveIndirect(Parameters.IndirectArgs->GetIndirectRHICallBuffer(), Group.IndirectOffset + 16);
}一次 DispatchIndirectMeshShader 覆盖一个 raster group 里全部 view 的全部 HW 批次。shader 里的 GetUnWrappedDispatchGroupId 把三维组号还原成一维,wrap 之后多出来的组靠 bValidIndex 输出空 meshlet。
❗ DispatchMesh 的线程组数是 HW 批次数,不是可见 cluster 数。一个满 128 三角形的 cluster 至少对应 4 个线程组,在 PIX / RenderDoc 里看 HW Rasterize (Triangles) 下的调用时要按批次理解。
六、HWRasterizeMS 逐段拆解
6.1 编译配置:32 线程的批次模式
// Engine/Source/Runtime/Renderer/Private/Nanite/NaniteCullRaster.cpp:2038(FHWRasterizeMS::ModifyCompilationEnvironment)
OutEnvironment.SetDefine(TEXT("NANITE_MESH_SHADER"), 1);
OutEnvironment.SetDefine(TEXT("NANITE_HW_COUNTER_INDEX"), 4); // Mesh and primitive shaders use an index of 4 instead of 5
OutEnvironment.SetDefine(TEXT("NANITE_MULTI_VIEW"), 1);
const uint32 MSThreadGroupSize = 32;
OutEnvironment.SetDefine(TEXT("NANITE_VERT_REUSE_BATCH"), 1);
OutEnvironment.SetDefine(TEXT("NANITE_MESH_SHADER_TG_SIZE"), MSThreadGroupSize);
OutEnvironment.CompilerFlags.Add(CFLAG_Wave32);入口签名于是展开成 [numthreads(32, 1, 1)][outputtopology("triangle")],三个输出数组各 32 项:
// Engine/Shaders/Private/Nanite/NaniteRasterizer.usf:3023
MESH_SHADER_TRIANGLE_ATTRIBUTES(NANITE_MESH_SHADER_TG_SIZE)
void HWRasterizeMS(
uint GroupThreadID : SV_GroupThreadID,
uint3 GroupID : SV_GroupID,
#if WORKGRAPH_NODE
DispatchNodeInputRecord<FShaderBundleNodeRecord> InputRecord,
#endif
#if NANITE_VERT_REUSE_BATCH
MESH_SHADER_VERTEX_EXPORT(VSOut, 32),
MESH_SHADER_TRIANGLE_EXPORT(32),
MESH_SHADER_PRIMITIVE_EXPORT(PrimitiveAttributesPacked, 32)
#else
MESH_SHADER_VERTEX_EXPORT(VSOut, 256),
MESH_SHADER_TRIANGLE_EXPORT(128),
MESH_SHADER_PRIMITIVE_EXPORT(PrimitiveAttributesPacked, 128)
#endif
)❗ 源码里还留着 #else 分支的 256 / 128 声明,以及函数末尾 #if NANITE_MESH_SHADER_TG_SIZE == 128 的「每条 lane 再写一个 +128 顶点」,但 FHWRasterizeMS 编译时写死了上面的配置,这两段不会进入任何 mesh shader 排列。照着它们去读,会误以为一组处理整个 cluster、每条 lane 负责两个顶点。同一入口还会编成 work graph 的 mesh 节点(FHWRasterizeWGMS,NaniteCullRaster.cpp:2084),编译配置相同。
线程与几何的映射:
| 维度 | 映射 |
|---|---|
| 1 个线程组 | 1 个顶点复用批次(≤32 三角形、≤32 唯一顶点) |
| 导出三角形时的 1 条 lane | 1 个三角形:TriIndex = TriRange.Start + GroupThreadID |
| 导出顶点时的 1 条 lane | 批内第 GroupThreadID 个唯一顶点 |
| 1 个 wave | 整个线程组(CFLAG_Wave32),去重可以全靠 wave intrinsic 完成 |
6.2 取活儿:从 GroupID 到 FClusterRef
// Engine/Shaders/Private/Nanite/NaniteRasterizer.usf:3041(节选)
bool bValidIndex = true;
// ...
// Avoid overflowing the 64k limit on single dimension of SV_GroupID
uint DrawClusterIndex = GetUnWrappedDispatchGroupId(GroupID);
BRANCH
if (GroupID.y > 0 || GroupID.z > 0)
{
// Due to wrapping, the visible index can be out of range
bValidIndex = (DrawClusterIndex < RasterGroupMeta[GetRasterGroup()].HWCount);
}
// ...
FClusterRef ClusterRef = FetchClusterRef<true>(DrawClusterIndex);
// ...
BRANCH
if (bValidIndex)
{
TriRange = ClusterRef.TriRange;
VisibleCluster = GetVisibleCluster(ClusterRef.VisibleIndex, VIRTUAL_TEXTURE_TARGET);
GetNaniteMaterialSceneData(VisibleCluster, PrimitiveData, InstanceData);
NaniteView = GetNaniteView(VisibleCluster.ViewId);
bReverseWindingOrder = ReverseWindingOrder(NaniteView, PrimitiveData, InstanceData);
// ...
Cluster = GetCluster(VisibleCluster.PageIndex, VisibleCluster.ClusterIndex, NANITE_CLUSTER_TYPE_TRIANGLES);
TriIndex = TriRange.Start + GroupThreadID;
bool bTriValid = GroupThreadID < TriRange.Num;
if (bTriValid)
{
VertIndexes = DecodeTriangleIndices(Cluster, TriIndex);
if( bReverseWindingOrder )
VertIndexes.yz = VertIndexes.zy;
}
#if NANITE_VERT_REUSE_BATCH
DeduplicateVertIndexes(VertIndexes, GroupThreadID, bTriValid, NumUniqueVerts, LaneVertIndex, VertIndexes);
#else
LaneVertIndex = GroupThreadID;
NumUniqueVerts = Cluster.NumVerts;
#endif
}FetchClusterRef<true>(NaniteRasterizer.usf:155)读RasterBinData,解出VisibleIndex、TriRange.Start(8 bit)、TriRange.Num(8 bit)和RasterBin(高 16 bit)。GetVisibleCluster取回的只是一条打包记录(ViewId、PageIndex、ClusterIndex、剔除 flag、VSM 的vPage等)。几何始终留在ClusterPageData里,GetCluster只解出 cluster 头,顶点和索引都是按需从页数据里解码。ReverseWindingOrder(NaniteRasterizer.usf:221)在实例变换行列式为负(奇数个缩放分量为负,即镜像)时为真,primitive 开了反向剔除时再取反一次;为真时交换yz保持绕序。
6.3 批内顶点去重
// Engine/Shaders/Private/Nanite/NaniteRasterizationCommon.ush:147(节选:只保留 wave 宽度 ≥ 32 的分支)
void DeduplicateVertIndexes( uint3 VertIndexes, uint GroupThreadIndex, bool bTriValid, out uint OutNumUniqueVerts, out uint OutLaneVertIndex, out uint3 OutCornerLaneIndexes )
{
// ① 以批内最小顶点号为基准
uint BaseVertIndex = WaveActiveMin(bTriValid ? VertIndexes.x : 0xFFFFFFFFu); // VertIndexes.x is always smallest
// ② 每条 lane 把自己三角形的 3 个顶点标进 64 bit 掩码
uint2 TriangleVertMask = 0;
if( bTriValid )
{
VertIndexes -= BaseVertIndex;
UNROLL
for (uint i = 0; i < 3; i++)
{
bool bDstLow = VertIndexes[i] < 32;
uint DstMask = 1u << ( VertIndexes[i] & 31 );
if( bDstLow )
TriangleVertMask.x |= DstMask;
else
TriangleVertMask.y |= DstMask;
}
}
// ③ 整个 wave 求并集,得到本批用到的顶点集合
uint2 UsedVertMask = uint2(0,0);
UsedVertMask.x = WaveActiveBitOr(TriangleVertMask.x);
UsedVertMask.y = WaveActiveBitOr(TriangleVertMask.y);
// ④ 角 → 槽号:掩码里比它低的 1 的个数
OutCornerLaneIndexes.x = MaskedBitCount(UsedVertMask, VertIndexes.x);
OutCornerLaneIndexes.y = MaskedBitCount(UsedVertMask, VertIndexes.y);
OutCornerLaneIndexes.z = MaskedBitCount(UsedVertMask, VertIndexes.z);
// ⑤ lane → 顶点:第 i 条 lane 负责掩码里的第 i 个 1
OutLaneVertIndex = FindNthSetBit(UsedVertMask, GroupThreadIndex) + BaseVertIndex;
OutNumUniqueVerts = CountBits(UsedVertMask);
}VertIndexes.x一定是三个顶点里最小的:Nanite 的三角形索引按(base, base+a, base+b)(a、b > 0)的形式压缩存储,HWRasterizePS恢复重心坐标顺序时也用到了这个性质(NaniteRasterizer.usf:3397)。- 调用处把第三个输出参数直接写回
VertIndexes(NaniteRasterizer.usf:3116),此后三角形的三个角存的就是槽号。 - 两个方向用的是同一套编号:角 → 槽号数「掩码里比它低的 1」,lane → 顶点取「掩码里第 i 个 1」。第 i 条 lane 变换的顶点正好落在槽 i,索引数组和顶点数组天然对齐。
- ⚠️ 64 bit 掩码之所以装得下,靠的是 builder 的约束:每个三角形引用的顶点号与此前出现过的最大顶点号相差不到 32(
NANITE_CONSTRAINED_CLUSTER_CACHE_SIZE,由NaniteEncodeConstrain.cpp:330起的check校验),批内新增的唯一顶点又不超过 32,批内顶点号的跨度因而落在 64 以内。 - wave 宽度不足 32 的硬件上,函数退回用 groupshared 求最小值和并集。
6.4 声明数量,三类导出
// Engine/Shaders/Private/Nanite/NaniteRasterizer.usf:3123
SetMeshOutputCounts(NumUniqueVerts, TriRange.Num);
BRANCH
if (bValidIndex)
{
uint PrimExportIndex = GroupThreadID;
if (PrimExportIndex < TriRange.Num)
{
MESH_SHADER_WRITE_TRIANGLE(PrimExportIndex, VertIndexes);
const uint PixelValue = PackVisPixelX(ClusterRef.VisibleIndex, TriIndex);
PrimitiveAttributes Attributes = MakePrimitiveAttributes(NaniteView, VisibleCluster, PixelValue, bReverseWindingOrder, MaterialCBIndex);
PrimitiveAttributesPacked AttributesPacked = PackPrimitiveAttributes(Attributes);
MESH_SHADER_WRITE_PRIMITIVE(PrimExportIndex, AttributesPacked);
}
uint VertExportIndex = GroupThreadID;
if (VertExportIndex < Cluster.NumVerts)
{
VSOut VertexOutput = CommonRasterizerVS(NaniteView, PrimitiveData, InstanceData, VisibleCluster, Cluster, LaneVertIndex, 0u, bReverseWindingOrder, MaterialCBIndex, ClusterRef.RasterBin);
MESH_SHADER_WRITE_VERTEX(VertExportIndex, VertexOutput);
}
#if NANITE_MESH_SHADER_TG_SIZE == 128
// ...(TG = 32 时不编译)
#endif
}- 三角形:
VertIndexes此时已是槽号,直接写进OutTriangles。 - 图元:
PixelValue = PackVisPixelX(VisibleIndex, TriIndex) = ((VisibleIndex + 1) << 8) | TriIndex(NaniteDataDecode.ush:890)。低 8 位是 cluster 内三角形号,高 24 位存VisibleIndex + 1,读回时减 1(UnpackVisPixelX,:895),值 0 因而不会和 0 号 cluster 混淆。它连同 view、VSM 信息一起打包写进OutPrimitives。 - 顶点:第 i 条 lane 用
LaneVertIndex(批内第 i 个唯一顶点的 cluster 本地号)调用CommonRasterizerVS,结果写进槽 i。第 7 个参数PixelValue传0u:mesh shader 排列下VSOut根本没有PrimitivePacked成员(NaniteRasterizer.usf:2494),CommonRasterizerVS只在!NANITE_MESH_SHADER时才用这个参数(:2659)。VS 回退路径正相反,PixelValue逐顶点传入(:3213),每个角各带一份。
6.5 逐图元属性的布局
PackPrimitiveAttributes(NaniteRasterizer.usf:2525)把 PrimitiveAttributes 压进一个 uint4,VSM 排列下:
| 分量 | 内容 |
|---|---|
x | PixelValue |
y | ViewId(bit 0-15)· bSwapVW(bit 16)· MipLevel(bit 18-22)· ArrayIndex(bit 23 起) |
z | LevelOffset:目标 VSM 这一级 mip 在页表里的偏移 |
w | vPage.x(13 bit)· vPage.y(13 bit)· 窗口宽高方向的页数差 vPageDelta(各 3 bit) |
非 VSM 排列时 z / w 改存 16 bit 打包的 ViewRect;bindless 且材质像素可编程时,还会多一个 uint4 MaterialCBIndex。
6.6 CommonRasterizerVS:跑在 mesh shader 里的顶点着色器
// Engine/Shaders/Private/Nanite/NaniteRasterizer.usf:2617(节选)
VSOut CommonRasterizerVS(FNaniteView NaniteView, FPrimitiveSceneData PrimitiveData, FInstanceSceneData InstanceData, FVisibleCluster VisibleCluster, FCluster Cluster, uint VertIndex, uint PixelValue, bool bReverseWindingOrder, uint MaterialCBIndex, uint RasterBin)
{
VSOut Out;
FNanitePostDeformVertex InputVert = FetchAndDeformLocalNaniteVertex(PrimitiveData, InstanceData, GetInstanceViewData(InstanceData.InstanceId, NaniteView.SceneRendererPrimaryViewId), Cluster, VisibleCluster, VertIndex, NANITE_NUM_TEXCOORDS_TO_DECODE_HW_VS);
float3 WorldPositionOffset = 0.0f;
// ...(初始化 FMaterialShader)
BRANCH
if ( (VisibleCluster.Flags & NANITE_CULLING_FLAG_ENABLE_WPO) != 0 )
{
WorldPositionOffset = MaterialShader.EvaluateWorldPositionOffset();
}
float3 PointTranslatedWorld = DFTransformLocalToTranslatedWorld(InputVert.Position, InstanceData.LocalToWorld, NaniteView.PreViewTranslation).xyz + WorldPositionOffset;
MaterialShader.ApplyFirstPersonTransform(PointTranslatedWorld);
float4 PointClip = mul( float4( PointTranslatedWorld, 1 ), NaniteView.TranslatedWorldToClip );
float2 UnwarpedClip = PointClip.xy;
#if VIRTUAL_TEXTURE_TARGET
PointClip.xy = NaniteView.ClipSpaceScaleOffset.xy * PointClip.xy + NaniteView.ClipSpaceScaleOffset.zw * PointClip.w;
// Offset 0,0 to be at vPage for a 0, VSM_PAGE_SIZE * VSM_RASTER_WINDOW_PAGES viewport.
PointClip.xy += PointClip.w * ( float2(-2, 2) / VSM_RASTER_WINDOW_PAGES ) * VisibleCluster.vPage;
#else
PointClip.xy = NaniteView.ClipSpaceScaleOffset.xy * PointClip.xy + NaniteView.ClipSpaceScaleOffset.zw * PointClip.w;
#endif
#if !NANITE_MESH_SHADER
Out.PrimitivePacked = PackPrimitiveAttributes(MakePrimitiveAttributes(NaniteView, VisibleCluster, PixelValue, bReverseWindingOrder, MaterialCBIndex));
#endif
// ...(TexCoords、Material Cache)
#if !PIXELSHADER
Out.Position = PointClip;
#if STORE_CLIP_IN_INTERPOLATOR
Out.Clip.xy = UnwarpedClip;
Out.Clip.zw = PointClip.zw;
#endif
const bool bNearClip = ((NaniteView.Flags & NANITE_VIEW_FLAG_NEAR_CLIP) != 0u);
if (!bNearClip)
{
// Shader workaround to avoid HW depth clipping. Should be replaced with rasterizer state ideally.
Out.Position.z = 0.5f * Out.Position.w;
}
#endif
// ...
return Out;
}它干的就是传统 VS 的活:从 ClusterPageData 解码顶点并做形变、算 WPO、变换到裁剪空间。VSM 分支的两行把顶点挪进 HW 光栅窗口,这是整条 VSM HW 路径里最容易看糊涂的地方。
图 3:VSM 的 HW 光栅窗口。剔除阶段把 cluster 覆盖的页矩形按 4×4 页切窗口;VS 把窗口平移到 512×512 视口原点;PS 加回 ViewRect.xy 得到虚拟纹素,再经页表落到物理页。
VSM 的 HW 光栅并不直接画 16k × 16k 的虚拟阴影图,视口只有 VSM_PAGE_SIZE × VSM_RASTER_WINDOW_PAGES = 128 × 4 = 512 像素见方(NaniteCullRaster.cpp:6105):
- 剔除阶段切窗口:HW cluster 覆盖的页矩形按 4×4 页一格切开,格子里只要有需要渲染的页,就单独发射一个可见 cluster,
vPage/vPageEnd记录格内有效页的包围框(NaniteClusterCulling.usf:890-935)。 - VS 平移:
ClipSpaceScaleOffset先把整张虚拟阴影图的裁剪空间放大到窗口尺度,倍率为VirtualMaxResolutionXY / (PageSize × RasterWindowPages),mip 0 时为 16384 / 512 = 32(VirtualShadowMapArray.cpp:5147);再按vPage平移,让窗口左上角对准视口原点。 - PS 还原:
PixelPos += ViewRect.xy(ViewRect.xy = vPage × VSM_PAGE_SIZE)把窗口内坐标还原成虚拟纹素坐标,然后才查页表(第七节)。
VS 阶段完全不碰物理页,虚拟到物理的重定向留给 PS 逐像素做。同一个 cluster 跨多个窗口时会被发射多次,顶点也跟着重复变换——批内去重管不到跨批次、跨窗口、跨 view 的重复。
VSOut 里装什么取决于排列。VSM 深度 pass、非可编程材质时只有两项:SV_Position,以及 Clip(TEXCOORD0,xy 是窗口平移前的裁剪坐标,zw 是裁剪空间的 z、w)。可编程像素材质再加 TexCoords 和重心坐标相关的量(按平台能力选 VertexID、SV_Barycentrics 或显式导出),开了全局裁剪平面还有 SV_ClipDistance(NaniteRasterizer.usf:2451-2523)。
末尾的 near clip 分支也值得一看。方向光 clipmap 的 VSM 视图 flag 起始就是 0(VirtualShadowMapArray.cpp:4964),局部光默认打开 NANITE_VIEW_FLAG_NEAR_CLIP(:5056)。不开 near clip 时 Position.z 被固定成 0.5 × w 来绕开硬件深度裁剪,真正写入的深度取自插值的 Clip.zw,写入前还要 saturate(NaniteWritePixel.ush:82)——近平面前方的遮挡物被压到最近深度,照样投影。
七、HWRasterizePS:片元落到 VSM 物理页
// Engine/Shaders/Private/Nanite/NaniteRasterizer.usf:3246(节选)
#if NANITE_MESH_SHADER
MESH_SHADING_COMPATIBLE_PS_ENTRY_POINT
#endif
void HWRasterizePS(VSOut In
#if NANITE_MESH_SHADER
, PrimitiveAttributesPacked PrimitivePacked
#endif
#if MATERIAL_TWOSIDED
, bool bFrontFace : SV_IsFrontFace
#endif
)
{
PrimitiveAttributes Primitive;
#if NANITE_MESH_SHADER
Primitive = UnpackPrimitiveAttributes(PrimitivePacked);
#else
Primitive = UnpackPrimitiveAttributes(In.PrimitivePacked);
#endif
// ...
#if NANITE_HW_RASTER_INTERPOLATE_DEPTH
// ...
float4 SvPosition = float4(In.Position.xy, In.Clip.z / In.Clip.w, In.Clip.w);
#else
float4 SvPosition = In.Position;
#endif
uint2 PixelPos = (uint2)SvPosition.xy;
#if VIRTUAL_TEXTURE_TARGET
// ...(用 In.Clip.xy 重建窗口平移前的 SvPosition,供材质求值)
PixelPos += Primitive.ViewRect.xy;
if (all(PixelPos < Primitive.ViewRect.zw))
#else
// In multi-view mode every view has its own scissor, so we have to scissor manually.
if( all( (PixelPos >= Primitive.ViewRect.xy) & (PixelPos < Primitive.ViewRect.zw) ) )
#endif
{
// ...
FVisBufferPixel Pixel = CreateVisBufferPixel( PixelPos, PixelValue, SvPosition.z );
// ...
#if VIRTUAL_TEXTURE_TARGET
// ...
if( !VirtualToPhysicalTexelForRendering( FVirtualSMLevelOffset::Unpack(LevelOffset), MipLevel, Pixel.Position, Pixel.PhysicalPosition.xy ) )
{
// Not committed or should not be rendered into
return;
}
Pixel.PhysicalPosition.z = ArrayIndex;
#endif
// ...
#if ENABLE_EARLY_Z_TEST
BRANCH
if( !QuadActiveAnyTrue( Pixel.EarlyDepthTest() ) )
{
return;
}
#endif
// ...(可编程材质:Masked / PDO 求值)
BRANCH
if (MaterialMask >= 0)
{
Pixel.Write();
}
}
}按执行顺序:
- 取逐图元属性:mesh shader 排列下它是 PS 的独立输入参数,VS 排列下从
VSOut里取。D3D 上MESH_SHADING_PER_PRIMITIVE_ATTRIBUTE和MESH_SHADING_COMPATIBLE_PS_ENTRY_POINT都是空宏(Platform.ush:1551-1556);Vulkan 上前者展开成PerPrimitiveEXT修饰,后者给 PS 入口声明SPV_EXT_mesh_shader扩展和 capability(VulkanCommon.ush:141-142)。 - 深度来源:深度 pass(
NANITE_HW_RASTER_INTERPOLATE_DEPTH = DEPTH_ONLY)用插值的Clip.z / Clip.w,xy 仍取SV_Position。源码注释给的理由:手动插值比让硬件准备SV_Position寄存器快得多,但在极细长的三角形上某些硬件有精度问题;折中方案是 xy 始终用SV_Position保证命中正确的像素,深度只在 HW 光栅更重的阴影 pass 里走插值,visibility buffer pass 连深度也用SV_Position。 - 还原虚拟纹素 + 手动 scissor:VSM 下
PixelPos += ViewRect.xy,只需判上界;非 VSM 的多 view 模式下每个 view 有自己的 scissor,同样得在 shader 里判。 - 页表重定向:
VirtualToPhysicalTexelForRendering(VirtualShadowMapPageAccessCommon.ush:316)用VirtualTexelAddress >> VSM_LOG2_PAGE_SIZE查页表,物理页号 × 128 再加页内偏移;页没有提交或本帧不该渲染就直接return,不写深度。PhysicalPosition.z选物理页池的数组层:静态缓存层或动态层。 - 软件 early-Z:只存在于可编程像素材质的排列(
ENABLE_EARLY_Z_TEST = NANITE_PIXEL_PROGRAMMABLE,NaniteRasterizationCommon.ush:33),为的是在跑 Masked / PDO 材质之前先剔掉被挡住的像素。QuadActiveAnyTrue让整个 quad 都没通过时才返回,材质求值用到的导数不受影响。 - 原子写深度:
FVisBufferPixel::Write()。
// Engine/Shaders/Private/Nanite/NaniteWritePixel.ush:79
void Write()
{
// When near clipping is disabled we need to move the geometry
Depth = saturate( Depth );
const uint DepthInt = asuint( Depth );
#if VIRTUAL_TEXTURE_TARGET
InterlockedMax( OutDepthBufferArray[ PhysicalPosition ], DepthInt );
#else
WritePixel( OutVisBuffer64, Value, Position, DepthInt );
#if VISUALIZE
WritePixel( OutDbgBuffer64, VisualizeValues.x, Position, DepthInt );
#endif
#endif // VIRTUAL_TEXTURE_TARGET
}深度 saturate 之后是非负 float,按 uint 比较和按 float 比较同序,于是直接 asuint 做整数原子 max;reverse-Z 下值越大越近,InterlockedMax 本身就是深度测试。非 VSM 的 visibility buffer 写 64 bit:PackUlongType(uint2(PixelValue, DepthInt)) 在 D3D 上把深度放在高 32 位(D3DCommon.ush:122),一次 ImageInterlockedMaxUInt64 同时完成深度测试和三角形 ID 写入。
八、三条路径对照
| 传统 VS 管线 | Nanite VS 回退:HWRasterizeVS | Nanite MS:HWRasterizeMS | |
|---|---|---|---|
| 派发 | CPU 发 DrawIndexed | DrawPrimitiveIndirect:每个 instance 固定 128 × 3 = 384 个顶点(NaniteDataDecode.ush:1183),InstanceCount = HW cluster 数 | DispatchIndirectMeshShader:线程组数 = HW 批次数 |
| 取几何 | IA 读 VB / IB | 空 vertex declaration;SV_InstanceID → FClusterRef,SV_VertexID / 3 → 三角形,余数 → 角 | SV_GroupID → FClusterRef,lane → 三角形 / 唯一顶点 |
| 顶点复用 | 硬件 post-transform cache | 没有:每个角单独变换一次 | 批内去重,每个唯一顶点变换一次 |
| 逐三角形数据 | 复制到顶点,或用 GS | PrimitiveAttributesPacked 作为顶点输出,每个角各带一份 | OutPrimitives,每个三角形一份 |
| 多余的工作 | — | TriRange 之外的三角形也要跑 VS,输出 (0,0,0,1) 退化掉 | SetMeshOutputCounts 只声明有效数量 |
| 拓扑数量 | CPU 决定 | 固定 384 顶点 / instance | 运行时由 shader 决定 |
VS 回退路径同样没有绑定 VB / IB(mesh shader 路径不设 vertex declaration,VS 路径用空的,NaniteCullRaster.cpp:3460),所以 mesh shader 相对它的收益不在「bindless」,而在三件事:原生逐图元属性、批内顶点去重、只导出有效几何。支持 primitive shader 的平台还有第三条硬件路径:同一个 HWRasterizeVS 编成 primitive shader(CFLAG_VertexToPrimitiveShader,NaniteCullRaster.cpp:1891),以 point list 拓扑派发,PC 的 D3D12 上不存在这条路径。
一句话概括 mesh shader:把取几何、变换顶点、定义拓扑、挂逐图元属性全揽进一个协作式线程组,产出一个 meshlet 的顶点数组 + 索引数组 + 逐图元数组,SetMeshOutputCounts 声明有效数量后交给固定功能光栅器。
九、坑点速记
SetMeshOutputCounts之前不要提前 return,哪怕条件对整个线程组 uniform、规范也允许;Nanite 实测会 corruption,改用「无效组声明 0 个输出」的写法(§3.1)。- 5.8 的
HWRasterizeMS是 32 线程的批次模式;源码里的 256 / 128 输出声明和VertExportIndex += 128分支不会被编译(§6.1)。 DispatchMesh的线程组数 = HW 批次数,不是可见 cluster 数;wrap 之后多出来的组靠bValidIndex输出空 meshlet(§5.3)。- 顶点复用只在批内生效,同一顶点跨批次、跨 VSM 窗口、跨 view 都会重复变换(§6.3、§6.6)。
- 逐图元属性用
uint4与逐顶点属性隔开,防止编译器把两类属性打包进同一行(§3.3)。 - 深度 pass 的深度来自插值的
Clip.zw;没有 near clip 的视图里SV_Position.z被固定为0.5 × w,不能当深度用(§6.6、§七)。 - PS 里的软件 early-Z 只存在于可编程像素材质的排列(§七)。
PixelValue的高 24 位是VisibleIndex + 1,低 8 位是三角形号,解包时记得减 1(§6.4)。
十、参考链接
规范与官方文档
🔗 DirectX-Specs:Mesh Shader — D3D12 mesh / amplification shader 规范:线程组与输出上限、SetMeshOutputCounts 的约束、DispatchMesh 的维度限制、逐图元属性与 PS 的匹配规则
🔗 Coming to DirectX 12 — Mesh Shaders and Amplification Shaders: Reinventing the Geometry Pipeline(Microsoft DirectX Blog,2019) — D3D12 引入 mesh / amplification shader 的动机与管线概览
🔗 Nanite Virtualized Geometry in Unreal Engine — Nanite 官方文档
🔗 Virtual Shadow Maps in Unreal Engine — VSM 官方文档,含 Nanite 几何渲染进 VSM 的 pass 结构
演讲与文章
🔗 Introduction to Turing Mesh Shaders(NVIDIA,2018) — meshlet 与 task / mesh 两级管线的入门讲解
🔗 A Deep Dive into Nanite Virtualized Geometry(Karis 等,SIGGRAPH 2021) — Nanite cluster、软硬件光栅分工与 visibility buffer 的设计出处
引擎源码索引(UE 5.8,相对路径)
| 主题 | 相对路径 | 关键符号 |
|---|---|---|
| Mesh shader 宏(D3D) | Engine/Shaders/Public/Platform/D3D/D3DCommon.ush | MESH_SHADER_* (:90-110) · PackUlongType (:122) |
| 逐图元属性修饰宏 | Engine/Shaders/Public/Platform.ush、Engine/Shaders/Public/Platform/Vulkan/VulkanCommon.ush | MESH_SHADING_PER_PRIMITIVE_ATTRIBUTE (:1551 / :141) |
| HW 光栅 shader | Engine/Shaders/Private/Nanite/NaniteRasterizer.usf | FetchClusterRef (:155) · ReverseWindingOrder (:221) · PrimitiveAttributesPacked (:2476) · VSOut (:2488) · CommonRasterizerVS (:2617) · HWRasterizeMS (:3024) · HWRasterizeVS (:3161) · HWRasterizePS (:3249) |
| 批内去重、early-Z 开关 | Engine/Shaders/Private/Nanite/NaniteRasterizationCommon.ush | ENABLE_EARLY_Z_TEST (:33) · DeduplicateVertIndexes (:147) |
| 深度 / visibility buffer 写入 | Engine/Shaders/Private/Nanite/NaniteWritePixel.ush | WritePixel (:20) · FVisBufferPixel::Write (:79) |
PixelValue 打包 | Engine/Shaders/Private/Nanite/NaniteDataDecode.ush | PackVisPixelX (:890) · UnpackVisPixelX (:895) |
| 分桶与间接参数 | Engine/Shaders/Private/Nanite/NaniteRasterBinning.usf | RasterGroupFinalize (:182) · DecodeVertReuseBatchInfo (:292) · ExportRasterGroup (:406) |
| HW / SW 分流、VSM 窗口 | Engine/Shaders/Private/Nanite/NaniteClusterCulling.usf | SmallEnoughToDraw (:287) · 窗口循环 (:890) |
| VSM 页表 | Engine/Shaders/Private/VirtualShadowMaps/VirtualShadowMapPageAccessCommon.ush | VirtualToPhysicalTexelForRendering (:316) |
| VSM 常量 | Engine/Shaders/Shared/VirtualShadowMapDefinitions.h | VSM_PAGE_SIZE (:14) · VSM_RASTER_WINDOW_PAGES (:21) |
| Nanite 常量 | Engine/Shaders/Shared/NaniteDefinitions.h | NANITE_CONSTRAINED_CLUSTER_CACHE_SIZE (:19) · NANITE_MAX_CLUSTER_* (:21-26) · NANITE_RASTERIZER_ARG_COUNT (:292) |
| shader 配置与派发 | Engine/Source/Runtime/Renderer/Private/Nanite/NaniteCullRaster.cpp | r.Nanite.MinPixelsPerEdgeHW (:140) · FHWRasterizeMS (:2030) · DispatchIndirectMeshShader (:3663) · AddPass_Binning (:4895) |
| 路径选择与 CVar | Engine/Source/Runtime/Renderer/Private/Nanite/NaniteShared.cpp | r.Nanite.MeshShaderRasterization (:102) · UseMeshShader (:121) · GetRasterHardwarePath (:158) |
| 批次构建 | Engine/Source/Developer/NaniteBuilder/Private/Encode/NaniteEncodeVertReuseBatch.cpp | BuildVertReuseBatches (:88) |
| 索引局部性约束 | Engine/Source/Developer/NaniteBuilder/Private/Encode/NaniteEncodeConstrain.cpp | VerifyClusterConstraints (:316) |
| D3D12 能力探测 | Engine/Source/Runtime/D3D12RHI/Private/D3D12Adapter.cpp | GRHISupportsMeshShadersTier0/1 (:1372) |
| VSM 视图参数 | Engine/Source/Runtime/Renderer/Private/VirtualShadowMaps/VirtualShadowMapArray.cpp | clipmap 视图 flag (:4964) · 局部光视图 flag (:5056) · ClipSpaceScale (:5147) |