资讯动态

UE5大规模动画角色渲染:GPU实例化骨骼网格实现方案

发布时间:2026/8/12 12:08:27 来源:尧图企业网站定制
1. 项目概述与核心价值在UE5项目中当场景中需要渲染大量相同或相似的动画角色时比如一支庞大的军队、一群游动的鱼群或者一片随风摇曳的树林传统的逐角色CPU蒙皮与Draw Call调用方式很快就会成为性能瓶颈。每个Skeletal Mesh都需要CPU计算骨骼变换、更新顶点缓冲区然后提交独立的绘制指令这带来了巨大的CPU开销和GPU指令压力。GPU Instanced RenderingGPU实例化渲染正是解决这一痛点的关键技术。它允许我们在一次Draw Call中绘制多个共享相同几何体但具有不同变换位置、旋转、缩放甚至部分材质参数的网格体将大量重复的变换计算从CPU转移到GPU并行处理从而极大地提升渲染效率。然而UE5默认的Skeletal Mesh渲染管线并不直接支持GPU实例化。这是因为Skeletal Mesh的顶点数据依赖于每帧由CPU计算的骨骼变换矩阵即蒙皮矩阵每个实例的动画姿态可能完全不同这打破了传统静态网格实例化中“共享相同顶点数据”的前提。因此实现“GPU Instanced Skeletal Mesh Rendering”的核心挑战在于如何将每个实例独有的、每帧变化的骨骼动画数据高效地打包并传递给GPU并在着色器中完成最终的蒙皮计算。这个项目就是深入UE5渲染底层构建一套能够支持大量动画角色同屏高效渲染的完整解决方案。它不仅适用于游戏中的大规模单位渲染在数字孪生、虚拟仿真、影视预演等领域中需要呈现海量动态角色的场景里也具有极高的应用价值。2. 核心思路与架构设计实现这个功能我们不能停留在蓝图或简单的材质层面必须深入到UE5的渲染线程和渲染管线中。核心思路是绕过引擎默认的SkeletalMeshComponent渲染路径创建一套自定义的渲染组件和代理将实例数据主要是每帧的骨骼变换矩阵通过结构化的缓冲区如Structured Buffer上传至GPU并在自定义的顶点工厂和着色器中读取这些数据完成基于GPU的实例化蒙皮计算。整个架构可以拆解为以下几个核心层2.1 运行时逻辑层GameThread这一层由我们自定义的UInstancedSkeletalMeshComponent类继承自UPrimitiveComponent负责。它的主要职责是管理实例维护一个实例数组存储每个实例的世界变换、自定义数据如颜色、生命值等以及最重要的——对其动画状态的引用如一个UAnimInstance或精简的姿势数据。驱动动画更新在TickComponent中驱动或获取每个实例的动画更新。这里的一个关键优化点是我们可能不需要为每个实例运行完整的动画蓝图逻辑。对于大量重复的简单动画如待机、行走循环可以共享一个动画状态或者使用程序化生成的姿势。收集渲染数据在渲染线程开始前将本帧所有实例所需的渲染数据收集、打包好。最核心的数据是每个实例的“骨骼变换矩阵数组”。我们需要将每个实例的骨骼变换通常是在其局部空间或相对于某个根骨骼的空间计算好并扁平化成一个大的浮点数数组。2.2 渲染线程数据层RenderThread这一层由FInstancedSkeletalMeshSceneProxy类继承自FPrimitiveSceneProxy负责。它是连接游戏线程和渲染管线的桥梁。数据中转与上传接收从组件传来的实例数据打包好的矩阵数组和其他参数。它的核心任务是在渲染线程中创建并更新GPU资源主要是Structured Buffer。我们将所有实例的骨骼矩阵数据按顺序存入这个缓冲区。假设一个角色有100根骨骼有1000个实例那么这个缓冲区就需要存储 100 * 1000 * 16一个4x4矩阵的浮点数个数个浮点数。数据上传通常通过FRHICommandList的UpdateStructuredBuffer或类似指令完成。管理渲染资源创建和维护与实例化渲染相关的Vertex Factory、材质实例等。2.3 渲染管线层RHI/Shader这是实现的核心包含自定义的FVertexFactory和Shader。自定义顶点工厂 (Vertex Factory)顶点工厂定义了顶点数据的布局和如何传递给着色器。我们需要创建一个新的顶点工厂例如FInstancedSkeletalMeshVertexFactory它除了包含原始Skeletal Mesh的静态顶点属性位置、法线、UV等外更重要的是声明并管理一个SRV (Shader Resource View)用于绑定我们在SceneProxy中创建的那个包含所有实例骨骼数据的Structured Buffer。自定义着色器 (Shader)我们需要修改蒙皮着色器代码。传统的蒙皮着色器从Uniform Buffer中读取当前绘制对象的骨骼矩阵。在我们的实例化版本中着色器需要通过InstanceID确定当前像素/顶点属于哪个实例。使用这个InstanceID作为索引从我们通过顶点工厂绑定的Structured Buffer中读取该实例对应的那组骨骼矩阵。用读取到的矩阵对顶点进行蒙皮变换。其余的光照、材质计算则与标准流程无异。2.4 材质与参数传递为了保持艺术家的 workflow我们通常希望通过材质实例来调节每个实例的外观如颜色、粗糙度偏移。这可以通过将每个实例的自定义数据如一个包含颜色、标量参数的FVector4打包到另一个缓冲区或者在实例数据中预留Per-Instance参数并通过DrawCall的InstanceData传递给着色器。在UE5中可以巧妙利用Primitive Uniform Buffer或自定义的Data Interface来实现。关键设计抉择动画数据来源这是架构设计的重中之重。方案一每实例完整动画。每个实例拥有独立的UAnimInstanceCPU开销极大仅适用于实例数量较少但需要复杂独立动画的情况。方案二共享动画状态。所有实例共享同一套骨骼姿势数据CPU只计算一次动画所有实例复用。适用于同步动画的群体如整齐划一的士兵。方案三程序化/噪声驱动动画。动画由Shader Time、World Position等参数通过数学公式如正弦波、噪声图在着色器中实时计算CPU几乎零开销适用于鸟群、鱼群、植被摇摆。本项目通常聚焦于方案二和方案三的混合实现以达到性能与效果的平衡。3. 核心实现细节拆解3.1 自定义组件UInstancedSkeletalMeshComponent这个组件是逻辑的起点。我们需要在头文件中定义实例数据结构和必要的缓冲区。// InstancedSkeletalMeshComponent.h UCLASS(ClassGroup(Custom), meta(BlueprintSpawnableComponent)) class UInstancedSkeletalMeshComponent : public UPrimitiveComponent { GENERATED_BODY() public: // 设置要使用的Skeletal Mesh和主材质 UFUNCTION(BlueprintCallable, CategoryInstanced Skeletal Mesh) void SetSkeletalMeshAndMaterial(USkeletalMesh* InMesh, UMaterialInterface* InMaterial); // 添加一个实例传入初始变换和动画状态索引或指向共享动画数据的引用 UFUNCTION(BlueprintCallable, CategoryInstanced Skeletal Mesh) int32 AddInstance(const FTransform InstanceTransform, int32 AnimationStateIndex 0); // 更新特定实例的变换 UFUNCTION(BlueprintCallable, CategoryInstanced Skeletal Mesh) bool UpdateInstanceTransform(int32 InstanceIndex, const FTransform NewTransform, bool bWorldSpace true); // 每帧更新驱动动画和收集数据 virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; protected: // 创建场景代理 virtual FPrimitiveSceneProxy* CreateSceneProxy() override; private: // 实例数据 struct FInstanceData { FTransform Transform; int32 AnimStateIndex; // 指向共享动画状态的索引 FVector4 CustomData; // 可用于传递颜色、强度等自定义参数 }; TArrayFInstanceData InstanceDataArray; // 共享的动画姿势数据池简化表示 TArrayTArrayFMatrix SharedAnimationPoses; USkeletalMesh* SkeletalMesh nullptr; UMaterialInterface* Material nullptr; // 渲染线程需要的数据副本用于双缓冲或直接传递 TArrayfloat BoneMatricesFlatArray; // 所有实例所有骨骼的矩阵扁平化存储 FRWBuffer BoneMatricesBuffer; // 渲染线程的GPU缓冲区资源 };在TickComponent中我们需要遍历所有实例根据其AnimStateIndex从SharedAnimationPoses中获取对应的骨骼变换矩阵将其与实例的Transform结合计算出最终的世界空间骨骼矩阵然后压扁到BoneMatricesFlatArray中。这个过程是主要的CPU开销点必须高度优化考虑使用多线程并行计算。3.2 自定义场景代理FInstancedSkeletalMeshSceneProxy场景代理在渲染线程中工作它负责将组件准备好的数据上传到GPU。// InstancedSkeletalMeshSceneProxy.h class FInstancedSkeletalMeshSceneProxy : public FPrimitiveSceneProxy { public: FInstancedSkeletalMeshSceneProxy(const UInstancedSkeletalMeshComponent* InComponent); virtual ~FInstancedSkeletalMeshSceneProxy(); // 获取动态数据在每帧渲染前被调用 virtual void GetDynamicMeshElements(const TArrayconst FSceneView* Views, const FSceneViewFamily ViewFamily, uint32 VisibilityMap, FMeshElementCollector Collector) const override; // 更新渲染数据从游戏线程接收新数据 void UpdateBoneMatrices_RenderThread(const TArrayfloat InBoneMatricesData, uint32 InNumInstances); private: // 顶点工厂 FInstancedSkeletalMeshVertexFactory* VertexFactory; // 骨骼矩阵的Structured Buffer渲染线程资源 FRHIShaderResourceView* BoneMatricesSRV nullptr; // 实例数量 uint32 NumInstances 0; // 引用原始Mesh的渲染数据LOD模型等 const FSkeletalMeshRenderData* SkeletalMeshRenderData; };在GetDynamicMeshElements中我们需要为每个视图View和每个LOD等级构建一个FMeshBatch。关键步骤是设置FMeshBatch的VertexFactory为我们自定义的实例化顶点工厂并设置实例数量NumInstances。同时我们需要将BoneMatricesSRV通过FMeshDrawShaderBindings绑定到着色器参数上。UpdateBoneMatrices_RenderThread函数通常由组件通过ENQUEUE_RENDER_COMMAND宏调用将游戏线程中准备好的BoneMatricesFlatArray数据安全地传递到渲染线程并更新BoneMatricesSRV。3.3 自定义顶点工厂FInstancedSkeletalMeshVertexFactory顶点工厂是连接CPU数据和着色器输入的关键。我们需要定义新的数据流。// InstancedSkeletalMeshVertexFactory.h class FInstancedSkeletalMeshVertexFactory : public FVertexFactory { DECLARE_VERTEX_FACTORY_TYPE(FInstancedSkeletalMeshVertexFactory); public: struct FDataType { // 标准的骨骼网格顶点流 FVertexStreamComponent PositionComponent; FVertexStreamComponent TangentBasisComponents[2]; FVertexStreamComponent TextureCoordinatesComponent; FVertexStreamComponent ColorComponent; // 我们添加的流骨骼索引和权重如果几何体缓存中不包含 FVertexStreamComponent BoneIndicesComponent; FVertexStreamComponent BoneWeightsComponent; }; // 设置数据流 void SetData(const FDataType InData); // 关键声明一个SRV参数用于绑定骨骼矩阵缓冲区 SHADER_PARAMETER_SRV(StructuredBufferfloat4, BoneMatricesBuffer) // 初始化RHI资源创建Shader参数布局 virtual void InitRHI() override; };在InitRHI中我们需要调用FVertexFactory::InitRHI()并特别声明我们的BoneMatricesBuffer参数。顶点工厂的GetType和ShouldCompilePermutation等静态函数也需要正确实现以确保着色器在编译时包含对应的支持。3.4 着色器实现修改蒙皮逻辑这是GPU端的核心。我们需要编写自定义的顶点着色器通常通过修改引擎的BasePassVertexShader.usf或创建新的着色器变种。// 在.usf文件中或通过HLSLCC编译的代码中 // 假设我们通过顶点工厂绑定了缓冲区 StructuredBufferfloat4x4 BoneMatricesBuffer; // 修改蒙皮函数 float3 SkinVertexPosition(int32 InstanceId, int32 BoneIndex0, int32 BoneIndex1, int32 BoneIndex2, int32 BoneIndex3, float4 BoneWeights, float3 InPosition) { // 计算该实例骨骼矩阵数据的起始索引 uint boneMatrixOffset InstanceId * MaxBoneCount; // MaxBoneCount需在C端定义并传入 // 从缓冲区中读取该实例的骨骼矩阵 float4x4 BoneMatrix0 BoneMatricesBuffer[boneMatrixOffset BoneIndex0]; // ... 读取 BoneMatrix1,2,3 // 进行蒙皮计算 float3 SkinnedPosition mul(float4(InPosition, 1.0), BoneMatrix0).xyz * BoneWeights.x; SkinnedPosition mul(float4(InPosition, 1.0), BoneMatrix1).xyz * BoneWeights.y; // ... 累加其他权重 return SkinnedPosition; } // 在顶点着色器主函数中 VS_OUTPUT MainVS(..., uint InstanceId : SV_InstanceID) { // 原有的顶点属性获取... // 使用InstanceId和自定义的蒙皮函数进行变换 float3 SkinnedWorldPos SkinVertexPosition(InstanceId, Input.BoneIndices, Input.BoneWeights, Input.Position); // 后续的MVP变换... }我们需要修改着色器编译环境确保我们的自定义顶点工厂类型能够编译出对应的着色器变种并正确链接到我们的蒙皮函数。4. 完整实现流程与关键代码4.1 步骤一项目设置与基础类创建创建插件或模块建议在UE5中创建一个新的Runtime模块如InstancedSkeletalMeshRenderer来管理所有相关代码保持引擎的纯净。创建核心类按照第3节的规划创建UInstancedSkeletalMeshComponent、FInstancedSkeletalMeshSceneProxy、FInstancedSkeletalMeshVertexFactory的.h和.cpp文件。修改Build.cs文件在新模块的.Build.cs文件中添加必要的依赖特别是RenderCore,RHI,Renderer,SkeletalMeshRenderer等渲染相关模块。4.2 步骤二实现组件逻辑与数据准备在UInstancedSkeletalMeshComponent::TickComponent中实现数据准备循环void UInstancedSkeletalMeshComponent::TickComponent(float DeltaTime, ...) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (!SkeletalMesh || InstanceDataArray.IsEmpty()) return; // 1. 更新共享动画姿势例如根据时间推进一个主动画序列 UpdateSharedAnimationPoses(DeltaTime); // 2. 并行计算所有实例的最终骨骼矩阵并扁平化 BoneMatricesFlatArray.Reset(); const int32 NumBones /* 从SkeletalMesh获取骨骼数 */; const int32 TotalFloats InstanceDataArray.Num() * NumBones * 16; BoneMatricesFlatArray.AddUninitialized(TotalFloats); ParallelFor(InstanceDataArray.Num(), [](int32 InstanceIdx) { const FInstanceData InstData InstanceDataArray[InstanceIdx]; const TArrayFMatrix PoseMatrices SharedAnimationPoses[InstData.AnimStateIndex]; FTransform InstanceToWorld InstData.Transform * GetComponentTransform(); int32 BaseIndex InstanceIdx * NumBones * 16; for (int32 BoneIdx 0; BoneIdx NumBones; BoneIdx) { // 将骨骼的局部姿势矩阵转换为世界空间矩阵 FMatrix FinalBoneMatrix PoseMatrices[BoneIdx] * InstanceToWorld.ToMatrixWithScale(); // 将FMatrix的16个float存入扁平化数组 const float* MatrixData (const float*)FinalBoneMatrix; FMemory::Memcpy(BoneMatricesFlatArray[BaseIndex BoneIdx * 16], MatrixData, 16 * sizeof(float)); } }); // 3. 将数据发送到渲染线程 if (SceneProxy) { ENQUEUE_RENDER_COMMAND(UpdateInstancedBoneMatrices)( [Proxy (FInstancedSkeletalMeshSceneProxy*)SceneProxy, DataCopy BoneMatricesFlatArray, NumInst InstanceDataArray.Num()](FRHICommandListImmediate RHICmdList) { Proxy-UpdateBoneMatrices_RenderThread(DataCopy, NumInst); }); } }4.3 步骤三渲染线程资源更新与绘制在FInstancedSkeletalMeshSceneProxy::UpdateBoneMatrices_RenderThread中更新GPU缓冲区void FInstancedSkeletalMeshSceneProxy::UpdateBoneMatrices_RenderThread(const TArrayfloat InBoneMatricesData, uint32 InNumInstances) { check(IsInRenderingThread()); NumInstances InNumInstances; uint32 TotalDataSize InBoneMatricesData.Num() * sizeof(float); if (!BoneMatricesBuffer.IsValid() || BoneMatricesBuffer.NumBytes ! TotalDataSize) { // 重新创建缓冲区 BoneMatricesBuffer.Release(); FRHIResourceCreateInfo CreateInfo(TEXT(InstancedSkeletalMeshBoneMatrices)); BoneMatricesBuffer RHICreateStructuredBuffer(sizeof(float), TotalDataSize, BUF_ShaderResource | BUF_Dynamic, CreateInfo); BoneMatricesSRV RHICreateShaderResourceView(BoneMatricesBuffer); } // 更新缓冲区数据 float* Data (float*)RHILockBuffer(BoneMatricesBuffer, 0, TotalDataSize, RLM_WriteOnly); FMemory::Memcpy(Data, InBoneMatricesData.GetData(), TotalDataSize); RHIUnlockBuffer(BoneMatricesBuffer); }在GetDynamicMeshElements中设置绘制批次void FInstancedSkeletalMeshSceneProxy::GetDynamicMeshElements(...) const { for (int32 ViewIndex 0; ViewIndex Views.Num(); ViewIndex) { if (VisibilityMap (1 ViewIndex)) { const FSceneView* View Views[ViewIndex]; // 获取合适的LOD索引 int32 LODIndex /* 基于距离计算LOD */; const FSkeletalMeshLODRenderData LODData *SkeletalMeshRenderData-LODRenderData[LODIndex]; FMeshBatch MeshBatch Collector.AllocateMesh(); // 配置MeshBatch顶点工厂、材质、索引/顶点缓冲区等 MeshBatch.VertexFactory VertexFactory; MeshBatch.MaterialRenderProxy Material-GetRenderProxy(); MeshBatch.Type PT_TriangleList; MeshBatch.LODIndex LODIndex; // 关键设置实例数量 MeshBatch.Elements[0].NumInstances NumInstances; // 将BoneMatricesSRV绑定到MeshBatch的ShaderBindings上 // 这里需要操作FMeshDrawShaderBindings是一个相对底层的设置过程 // ... Collector.AddMesh(ViewIndex, MeshBatch); } } }4.4 步骤四着色器绑定与编译确保顶点工厂正确地将BoneMatricesSRV绑定到着色器。这通常在顶点工厂的GetShaderBindings方法中完成void FInstancedSkeletalMeshVertexFactory::GetShaderBindings( const FScene* Scene, ERHIFeatureLevel::Type FeatureLevel, const FPrimitiveSceneProxy* PrimitiveSceneProxy, const FMaterialRenderProxy MaterialRenderProxy, const FMaterial Material, const FMeshPassProcessorRenderState DrawRenderState, const TUniformBufferRefFViewUniformShaderParameters ViewUniformBuffer, FMeshDrawSingleShaderBindings ShaderBindings, FVertexInputStreamArray VertexStreams) const { // 调用父类绑定标准数据流 FVertexFactory::GetShaderBindings(...); // 绑定我们的自定义缓冲区 const FInstancedSkeletalMeshSceneProxy* InstancedProxy (const FInstancedSkeletalMeshSceneProxy*)PrimitiveSceneProxy; if (InstancedProxy InstancedProxy-BoneMatricesSRV.IsValid()) { // 假设我们在Shader中定义了一个名为 InstancedBoneMatrices 的 StructuredBuffer 参数 ShaderBindings.Add(Shader-GetUniformBufferParameterFInstancedSkeletalMeshUniformParameters(), InstancedProxy-GetUniformBuffer()); // 更底层的绑定可能需要通过SetSRVParameter } }同时需要创建对应的着色器变种。这通常涉及修改引擎的GlobalShaderMap.cpp或创建自定义的ShaderType并确保在材质编译时包含我们的顶点工厂类型。5. 性能优化与疑难排查5.1 性能优化要点CPU端优化并行计算如上述代码所示使用ParallelFor并行计算每个实例的骨骼矩阵是必须的。动画共享与简化这是减少CPU负担最有效的方法。对于不需要独立复杂动画的实例使用共享动画状态。甚至可以考虑使用更少的骨骼LOD动画或完全在GPU端通过顶点动画贴图Vertex Animation Texture, VAT来驱动。数据更新频率并非所有实例每帧都需要更新动画。可以实现按距离或重要性分级的更新策略。双缓冲与增量更新避免每帧全量更新整个骨骼矩阵缓冲区。如果只有少数实例的动画或位置变化可以只更新缓冲区中对应的部分。GPU端优化缓冲区管理确保Structured Buffer的大小适中且内存对齐如以256字节对齐。避免频繁的创建和销毁。着色器优化在着色器中通过InstanceID索引缓冲区是高效的但要确保访问是合并的coalesced。将骨骼索引和权重存储在顶点缓冲区中而不是通过常量缓冲区传递。剔除Culling在提交Draw Call前进行视锥体剔除和遮挡剔除。可以在CPU端进行粗略的包围盒剔除也可以在GPU端通过计算着色器进行更精细的剔除生成一个经过筛选的实例索引列表只绘制可见的实例。LOD细节层次为Skeletal Mesh设置多个LOD。在自定义的GetDynamicMeshElements中根据实例到相机的距离选择不同的LOD模型进行绘制。高LOD模型可以使用更多骨骼低LOD模型使用更少骨骼甚至简化为静态网格体能大幅减少顶点处理和骨骼计算量。内存优化骨骼矩阵精度考虑使用half4或float16_t来存储旋转和缩放部分位置部分使用float以减小缓冲区大小。压缩动画数据如果使用共享动画存储的骨骼姿势矩阵可以使用相对变换相对于父骨骼而非绝对的世界矩阵并在着色器中重建。5.2 常见问题与排查实例不显示或显示错乱检查数据流首先确认自定义顶点工厂的数据流Position, Normal, UV等是否与原始Skeletal Mesh的顶点缓冲区布局完全匹配。一个不匹配的流会导致顶点着色器读取到错误数据。调试着色器使用RenderDoc或PIX捕获一帧检查Draw Call的参数是否正确。重点查看NumInstances是否正确。自定义的Structured Buffer是否成功绑定到着色器槽位Register。缓冲区内的数据是否正确。可以在着色器中输出InstanceID或读取的矩阵数据到颜色上进行可视化调试。矩阵乘法顺序这是最常见的错误。确保在CPU端组合实例变换和骨骼姿势矩阵的顺序是正确的通常是LocalBonePose * InstanceTransform并且在着色器中应用矩阵的顺序行主序 vs 列主序mul参数顺序与UE的HLSL约定一致。性能提升不明显甚至下降Profile工具使用UE5的stat unit、stat gpu或Unreal Insights进行性能分析。确认瓶颈是从CPU的DrawCall转移到了哪里是CPU的矩阵计算还是GPU的顶点着色器Draw Call数使用stat scenerendering查看Draw Call数量是否真的减少了。如果自定义管线设置不当可能会导致每个实例反而产生了多个Draw Call。GPU瓶颈如果实例数量极多数万GPU的顶点着色器工作量会剧增。此时瓶颈在GPU。解决方案是启用GPU裁剪并积极使用LOD减少实际处理的顶点数量。动画不同步或卡顿线程同步确保从游戏线程到渲染线程的数据传递是安全的。使用ENQUEUE_RENDER_COMMAND是标准做法。检查是否有竞态条件例如在渲染线程读取数据时游戏线程正在写入。数据延迟由于双缓冲或命令队列GPU渲染的数据可能比逻辑帧晚一帧。在快速移动的相机下可能感觉到“拖影”。可以考虑使用预测或增加帧间插值来缓解。与引擎特性冲突如阴影、贴花、后期处理自定义的渲染路径可能不会自动被引擎的阴影投射、深度预通道、自定义深度等Pass处理。你需要在GetDynamicMeshElements中为这些特殊的Mesh PassEMeshPass::Type也生成对应的FMeshBatch并配置正确的着色器。材质中的某些节点如世界位置偏移可能需要访问我们自定义的实例数据这需要你扩展材质系统创建自定义的材质节点和HLSL函数。实现GPU实例化的Skeletal Mesh渲染是一个深入渲染管线的复杂任务需要对UE5的渲染架构有深刻理解。它带来的性能收益是巨大的特别是在追求大规模动态场景的现代项目中。从简单的共享动画群体开始逐步扩展到支持GPU裁剪、LOD和更复杂的每实例数据交互可以构建出一套非常强大的大规模角色渲染方案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价