
TiXL SplitMeshVertices 算子详解网格顶点拆分实现 Flat Shading 与平滑几何散射【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3SplitMeshVertices 是 TiXLtooll3算子库Lib.mesh.modify中负责顶点拆分的网格修改算子它把索引化Indexed网格中每个三角形面片引用的顶点展开为三个相互独立的顶点从而解除面片之间共享顶点的关系为 Flat Shading平直着色、逐面片散射Scatter等操作扫清障碍。本文将以官方文档 SplitMeshVertices.md 为骨架结合其 C# 算子定义、节点图定义与 HLSL 计算着色器源码讲透它的参数含义、底层实现原理与典型实战用法帮助你直接在 VJ / 实时动态图形项目中控制网格的平滑度。一、算子定位为什么要拆分顶点在现代 GPU 渲染中三角形网格通常以索引化方式存储顶点缓冲Vertex Buffer存放去重的顶点索引缓冲Index Buffer用三个索引int3描述一个三角形面片。这种结构天然支持平滑着色Smooth Shading——相邻面片共享顶点共享顶点的法线是多个面片法线的平均值光照过渡柔和。但当你想做以下事情时共享顶点就成了阻碍散射Scatter平滑着色的几何体例如把每个面片沿各自方向炸开如果面片仍共享顶点顶点会被多个面片拉扯无法独立移动把着色改成 Flat平直每个面片需要拥有自己独立的法线而不是共享插值后的法线需要按面片为单位独立处理顶点属性法线、切线、副切线。SplitMeshVertices 的官方文档一句话点明了它的用途This can be relevant to scatter smooth shaded geometry or change shading to flat.详见 Lib.mesh.modify 算子目录 中对本算子的描述。它把每个面片 3 个顶点这一关系显式化让后续的逐面片操作成为可能。二、输入输出参数速览按照文档中给出的参数表该算子只有两个输入和一个输出参数类型 / 关联性说明InputMeshMeshBuffersRequired输入的网格缓冲。MeshBuffers封装了顶点缓冲、索引缓冲与块定义缓冲详见下文第五节必需连接否则算子无数据可处理ShadeFlatSingle即float控制着色平直程度的系数取值范围本质上是 0~1 的插值因子但也可以传入大于 1 的值以强化效果输出输出类型ResultT3.Core.DataTypes.MeshBuffers关于 ShadeFlat 的关键细节默认值 0从 SplitMeshVertices.t3 的DefaultValue可以看出ShadeFlat默认是0.0。此时输出顶点完全保留原始法线网格外观基本不变0~1介于两者之间时每个顶点的法线在原始法线与面片平均法线之间线性插值lerp即从平滑着色向平直着色过渡1完全平直着色同一面片的三个顶点法线收敛为同一个面片平均法线大于1在官方示例 ScatterMeshFacesExample.t3 中ShadeFlat被设为1.57说明可以超过 1 使用以在散射场景中进一步强化面片感HLSL 的lerp并不会限制系数范围。关于 InputMesh 与 Relevancy在 SplitMeshVertices.t3ui 的InputUis中InputMesh的Relevancy被标记为RequiredShadeFlat则为可选未标记。也就是说在 TiXL 编辑器中InputMesh端口会以必需连接的方式呈现未连接时算子不会产生有效输出。三、源码实现剖析从 C# 插槽到计算着色器3.1 C# 算子定义SplitMeshVertices.cs 定义了算子的骨架——两个输入插槽和一个输出插槽namespace Lib.mesh.modify; [Guid(3f0f0c40-482d-4d79-a201-b4651a0cd3a8)] internal sealed class SplitMeshVertices : InstanceSplitMeshVertices { [Output(Guid 7873a4c0-04ff-41b5-bf5e-66ae745c3918)] public readonly SlotMeshBuffers Result new(); [Input(Guid 22370faa-8381-4878-8653-2fe9297400da)] public readonly InputSlotMeshBuffers InputMesh new(); [Input(Guid 308f12dc-a308-472a-923e-20f0a20d54db)] public readonly InputSlotfloat ShadeFlat new(); ... }可见其输入输出类型严格对应文档InputMesh接收MeshBuffersShadeFlat接收floatResult输出MeshBuffers。类中还定义了SpacesPointSpace / ObjectSpace / WorldSpace与DirectionsWorldSpace / SurfaceNormal两组枚举为后续扩展例如按指定空间与方向计算拆分预留了空间——目前算子主体逻辑由节点图内的计算着色器完成。3.2 内部节点图GPU 流水线SplitMeshVertices 本质上是一个由多个基础算子组成的Symbol符号/节点图其图定义见 SplitMeshVertices.t3界面布局见 SplitMeshVertices.t3ui。从图中可以还原出完整的 GPU 处理流水线_MeshBufferComponents把输入的MeshBuffers拆解为顶点缓冲、索引缓冲等组成部分GetBufferComponents× 4分别取出源索引缓冲与源顶点缓冲的 SRV着色器资源视图及其尺寸属性ComputeShader加载着色器源码Lib:shaders/3d/mesh/mesh-SplitVertices.hlsl即仓库中的 mesh-SplitVertices.hlslGetSRVPropertiesCalcDispatchCount根据索引缓冲大小计算需要的线程调度数量并送入ComputeShaderStage的Dispatch图内预设为64, 1, 1IntValue节点固定为 64对应着色器[numthreads(64,1,1)]的线程组大小MultiplyIntB3因为每个面片拆分后要生成3 个独立顶点所以新顶点数量 面片数 × 3NewVertices/NewFaceIndices申请新的顶点缓冲Stride为 32即一个PbrVertex的大小与新的索引缓冲Stride为 12即一个int3FloatsToBuffer把ShadeFlat标量写入常量缓冲供着色器读取ExecuteBufferUpdate_AssembleMeshBuffers执行 GPU 计算后把新顶点缓冲与新索引缓冲重新组装成MeshBuffers从Result端口输出。整条链路完全在 GPU 上以 Compute Shader 完成不需要 CPU 回读数据因此即使网格面片数量很大性能开销也主要集中在一次Dispatch上。3.3 HLSL 核心逻辑一线程一面片mesh-SplitVertices.hlsl 是算子真正的心脏其关键结构如下cbuffer Params : register(b0) { float ShadeFlat; } StructuredBufferint3 SourceIndices : register(t0); StructuredBufferPbrVertex SourceVertices : register(t1); RWStructuredBufferint3 ResultIndices : register(u0); RWStructuredBufferPbrVertex ResultVertices : register(u1); [numthreads(64,1,1)] void main(uint3 i : SV_DispatchThreadID) { uint numFaces, stride; SourceIndices.GetDimensions(numFaces, stride); int faceIndex i.x; if(faceIndex numFaces) return; int3 indicesForFace SourceIndices[faceIndex]; int vertexIndex faceIndex * 3; float3 p1 SourceVertices[indicesForFace.x].Position; float3 p2 SourceVertices[indicesForFace.y].Position; float3 p3 SourceVertices[indicesForFace.z].Position; ... ResultVertices[vertexIndex0] SourceVertices[indicesForFace.x]; ResultVertices[vertexIndex1] SourceVertices[indicesForFace.y]; ResultVertices[vertexIndex2] SourceVertices[indicesForFace.z]; float3 n1 SourceVertices[indicesForFace.x].Normal; float3 n2 SourceVertices[indicesForFace.y].Normal; float3 n3 SourceVertices[indicesForFace.z].Normal; float3 n normalize(n1 * w.x n2 * w.y n3 * w.z); ResultVertices[vertexIndex0].Normal lerp(n1, n, flat); ResultVertices[vertexIndex1].Normal lerp(n2, n, flat); ResultVertices[vertexIndex2].Normal lerp(n3, n, flat); ... ResultIndices[faceIndex] int3(vertexIndex, vertexIndex1, vertexIndex2); }实现要点并行策略每个 GPU 线程处理一个面片faceIndex i.x线程数由面片数决定超出部分直接return源索引以int3形式按面片存储每面片 3 个索引顶点展开面片f的三个源顶点被复制到目标顶点缓冲的f*3、f*31、f*32位置新索引缓冲则直接写入(f*3, f*31, f*32)——这就是拆分的本质相同的顶点数据被复制多份面片之间不再共享法线处理先对三个顶点法线做加权平均得到面片法线nw为权重向量着色器中通过对三条边向量做点积来探测退化/共线边把对应权重置 0避免退化面片污染平均结果再用lerp把每个顶点法线从原始值向面片法线过渡过渡量就是ShadeFlat切线/副切线同步处理Bitangent与Tangent使用同样的加权平均 lerp逻辑保证在法线贴图Normal Mapping场景下着色空间依然自洽顶点位置、UV 等其余属性则原样复制不做任何修改。需要说明的是这里的面片法线是三个顶点法线的加权平均近似面片法线而不是用叉积重新计算的几何法线从源码结构看它旨在用最轻量的方式抹平顶点法线的平滑过渡而非做严格的几何法线重算。若需要基于几何形状重算法线可与同组的 RecomputeNormals 文档 所述算子配合使用。四、实战用法配合 ScatterMeshFaces 做面片散射SplitMeshVertices 最常见的实战场景是与ScatterMeshFaces搭配使用。官方示例 ScatterMeshFacesExample.t3 中同时出现了这两个算子ScatterMeshFaces见 ScatterMeshFaces 文档负责按面片中心把网格炸开并保持单个面片尺寸不变随后SplitMeshVertices以ShadeFlat 1.57的参数接入把散射后的网格进一步拆分为独立顶点并强制平直着色。典型的节点链如下Mesh 源 → ScatterMeshFaces → SplitMeshVertices(ShadeFlat) → 渲染为什么需要这样的组合ScatterMeshFaces会把每个面片沿面法线方向推开。如果网格仍是索引化、共享顶点的结构推开的动作会受到相邻面片共享顶点的牵制产生拉伸或撕裂先用SplitMeshVertices把顶点按面片拆开此时ShadeFlat0可保持外观不变每个面片就拥有了物理上独立的顶点再配合ShadeFlat1或示例中的1.57把法线统一为面片法线散射后的每个小面片以 Flat 方式独立受光视觉上碎片感极强——这正是 VJ 场景中常见的爆炸/解体/像素化碎裂效果的核心构成。同样的思路也适用于任何需要逐面片独立变换的场合例如结合DeformMesh、DisplaceMesh等变形类算子时先拆分顶点可以避免共享顶点被多次位移叠加。同组算子的完整清单与各自说明可查阅 Lib.mesh.modify 算子目录。五、输出 MeshBuffers 与下游连接Result输出的类型是T3.Core.DataTypes.MeshBuffers其定义见 MeshBuffers.cspublic class MeshBuffers: IDisposable { public BufferWithViews VertexBuffer new (); public BufferWithViews IndicesBuffer new (); public BufferWithViews ChunkDefsBuffer new (); ... public int FaceCount IndicesBuffer?.Srv ! null ? IndicesBuffer.Srv.Description.Buffer.ElementCount : 0; }它把渲染一个网格所需的**顶点缓冲、索引缓冲、块定义缓冲ChunkDefs**打包在一起拆分后的新顶点与新索引正是通过_AssembleMeshBuffers重新装入这三个缓冲后从Result输出的其ToString()实现会报告{vertexCount} vertices {faceCount} faces在编辑器中悬停即可直观确认拆分效果——拆分后顶点数会变为原来的约 3 倍具体取决于原网格的共享顶点比例而面片数不变输出可以继续连接到任何接受MeshBuffers的渲染/修改算子例如各类 Mesh 材质渲染算子、TransformMesh、FlipNormals等整条链仍然保持在 GPU 缓冲层面传递没有额外的 CPU 拷贝开销。六、总结SplitMeshVertices 是一个小而关键的网格预处理算子它通过一次 GPU Compute Shader 调度把索引化网格展开为面片独立的顶点集合并以一个ShadeFlat系数在平滑着色与平直着色之间自由过渡。无论是想对平滑几何体做面片散射、制作碎裂解体效果还是单纯想快速切换 Flat Shading 风格都可以把它插入到网格修改链中配合 ScatterMeshFaces、RecomputeNormals 等同组算子获得完整的处理流程。官方示例 ScatterMeshFacesExample.t3 是上手最快的参照直接打开即可观察ShadeFlat参数对散射网格视觉风格的实时影响。【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考