15.具体实现-渲染部分-CSharp传参与动画播放实现
15.1 知识点
主要目标和步骤
在 C# 里把 VAT 的配置信息传给材质,驱动 Shader 采样。
- 创建带
MeshRenderer和MeshFilter的对象,关联网格与材质。 - 在 C# 里拿到材质和
AnimationInfos(及内部的AnimationInfo)。 - 封装播放动画 API:用动画状态名播放。
- 通过名字找到对应的
AnimationInfo。 - 把
AnimationInfo里的数据传给材质;部分用MaterialPropertyBlock传递(每实例可不同)。
- 通过名字找到对应的
创建材质并关联纹理
创建材质,选择项目里的 VAT Shader,关联主纹理和 VAT 纹理,并开启 GPU Instancing。

为何要用 MeshRenderer + 预制体
自带蒙皮动画走的是 SkinnedMeshRenderer,GPU Instancing 不支持这种路径。所以需要新建物体,用 MeshRenderer + MeshFilter,关联对应网格和材质,做成预制体再批量用。


脚本挂载与声明
把 C# 脚本挂到预制体上,声明材质和动画配置 SO,并在 Inspector 里赋好引用。
public class GPUInstancingAnimation : MonoBehaviour
{
public Material material; // 用于渲染的材质(需挂载VAT Shader)
public AnimationInfos animationInfos; // 动画数据配置文件
}

创建顶点动画播放 API
根据动画名查配置,把共享的顶点相关参数写到 Material,把每实例不同的帧率、偏移、像素索引、帧数等通过 MaterialPropertyBlock 设好,再 SetPropertyBlock 交给渲染器,驱动 GPU Instancing 下的 VAT 动画。
/// <summary>
/// 动画播放API
/// </summary>
/// <param name="name">动画名</param>
/// <param name="isRandom">是否有随机偏移 可以带来相同对象相同动画的不同表现</param>
public void Play(string name, bool isRandom = true)
{
//为了避免浪费内存 materialPropertyBlock主要是用来传递参数的 一个就行
// [说明] MaterialPropertyBlock是引用类型,静态全局只需创建一次
if (materialPropertyBlock == null)
{
materialPropertyBlock = new MaterialPropertyBlock();
}
// 根据动画名查找对应的动画配置数据
AnimationInfo animationInfo = null;
for (int i = 0; i < animationInfos.allAnimationinfo.Length; i++)
{
if (animationInfos.allAnimationinfo[i].animationName == name)
{
animationInfo = animationInfos.allAnimationinfo[i];
break;
}
}
// 安全检查:如果没找到对应动画,输出错误日志并返回
if (animationInfo == null)
{
Debug.LogError("没有找到对应名字为" + name + "的动画");
return;
}
//对公共属性进行赋值
// [说明] 这些属性是所有动画共享的,直接设置到Material上
material.SetInt("_VertexCount", animationInfos.vertexCount); // 设置模型顶点总数
material.SetInt("_VertexMax", animationInfos.vertexMax); // 设置顶点坐标最大值(用于解压缩)
material.SetInt("_VertexMin", animationInfos.vertexMin); // 设置顶点坐标最小值(用于解压缩)
//设置每个对象可能不同的数据
// [说明] 这些属性是每个实例独有的,通过MaterialPropertyBlock设置
materialPropertyBlock.SetInt("_FrameRate", animationInfo.frameRate); // 设置动画帧率(如30fps、60fps)
//如果有随机 才有偏移 可以做到相同对象播放相同动画时有不同表现
//但是有的动画需要从第一帧开始播 不需要随机
// [说明] 随机偏移:让多个相同物体的动画不同步,避免"复制粘贴"感
materialPropertyBlock.SetInt("_OffsetIndex", isRandom ? Random.Range(0, 100) : 0); // 设置帧偏移(随机或0)
materialPropertyBlock.SetInt("_PixelIndex", animationInfo.pixelIndex); // 设置该动画在VAT纹理中的起始像素位置
materialPropertyBlock.SetInt("_FrameCount", animationInfo.frameCount); // 设置该动画的总帧数
//传递materialPropertyBlock相关差异性数据给到对应对象的 材质(Shader)
// [说明] 将属性块应用到渲染器,实现GPU Instancing的差异化渲染
renderer.SetPropertyBlock(materialPropertyBlock);
}
Start中取渲染器并播待机动画
void Start()
{
renderer = this.GetComponent<Renderer>(); // 获取当前物体的渲染器组件
Play("Ice_Idle"); // 启动时默认播放Idle动画
}
替换预制体并对比
替换创建的预制体并对比,可以看到批处理数量大幅降低,帧率大幅提升。


遗留问题
远看可以,近看不流畅
蒙皮网格渲染器走骨骼矩阵,中间时刻可以插值,观感相对顺滑;当前 VAT 实现按帧取顶点,近看时帧与帧之间的变化会更显眼。
解决思路:提高烘焙采样率;在 Shader 里对相邻两帧做插值;或采用「远景 VAT、近景蒙皮」分级。动画切换、循环时比较麻烦(需自行封装)
没有 Animator 那套状态机与过渡,切换等于改 VAT 片段参数;循环若首尾姿态不接会跳变——这些都要自己在逻辑层封装(状态、过渡、何时重置随机偏移等)。
解决思路:自研短混合或衔接片段;循环段烘焙时对齐首尾;需要严丝合缝起播时关闭随机偏移、从首帧起播。多纹理问题
动画条数多、总帧数大时,单张 VAT 的宽高或精度装不下全部顶点动画数据,必须拆成多张纹理;Shader 与 C# 就要处理「采哪一张、UV/索引怎么对应」,比教程里一张 VAT 跑通要复杂。
解决思路:按片段或按容量拆多张 VAT,用 Texture2DArray / 图集统一绑定,实例或材质上带「第几张 VAT」的索引;或降低单帧精度、合并动画以控制单张体积。法线、切线或更多数据
流程里优先解决顶点位置;光照若仍按静态法线/切线,会和变形不一致。
解决思路:烘焙阶段增加法线/切线等通道;或简化光照、近景主角单独用高精度方案。自定义渲染规则
GPU Instancing + 自定义 Shader 后,阴影、深度、排序等不能默认与内置蒙皮一致。
解决思路:在管线里补齐需要的 Pass(如 ShadowCaster、DepthOnly);用 Layer / Rendering Layer 区分对象类型。
15.2 知识点代码
GPUInstancingAnimation.cs
using UnityEngine;
public class GPUInstancingAnimation : MonoBehaviour
{
public Material material; // 用于渲染的材质(需挂载VAT Shader)
public AnimationInfos animationInfos; // 动画数据配置文件
private Renderer renderer; // 渲染器组件引用
private static MaterialPropertyBlock materialPropertyBlock; // 材质属性块(静态,用于GPU Instancing参数传递)
void Start()
{
renderer = this.GetComponent<Renderer>(); // 获取当前物体的渲染器组件
Play("Ice_Idle"); // 启动时默认播放Idle动画
}
/// <summary>
/// 动画播放API
/// </summary>
/// <param name="name">动画名</param>
/// <param name="isRandom">是否有随机偏移 可以带来相同对象相同动画的不同表现</param>
public void Play(string name, bool isRandom = true)
{
//为了避免浪费内存 materialPropertyBlock主要是用来传递参数的 一个就行
// [说明] MaterialPropertyBlock是引用类型,静态全局只需创建一次
if (materialPropertyBlock == null)
{
materialPropertyBlock = new MaterialPropertyBlock();
}
// 根据动画名查找对应的动画配置数据
AnimationInfo animationInfo = null;
for (int i = 0; i < animationInfos.allAnimationinfo.Length; i++)
{
if (animationInfos.allAnimationinfo[i].animationName == name)
{
animationInfo = animationInfos.allAnimationinfo[i];
break;
}
}
// 安全检查:如果没找到对应动画,输出错误日志并返回
if (animationInfo == null)
{
Debug.LogError("没有找到对应名字为" + name + "的动画");
return;
}
//对公共属性进行赋值
// [说明] 这些属性是所有动画共享的,直接设置到Material上
material.SetInt("_VertexCount", animationInfos.vertexCount); // 设置模型顶点总数
material.SetInt("_VertexMax", animationInfos.vertexMax); // 设置顶点坐标最大值(用于解压缩)
material.SetInt("_VertexMin", animationInfos.vertexMin); // 设置顶点坐标最小值(用于解压缩)
//设置每个对象可能不同的数据
// [说明] 这些属性是每个实例独有的,通过MaterialPropertyBlock设置
materialPropertyBlock.SetInt("_FrameRate", animationInfo.frameRate); // 设置动画帧率(如30fps、60fps)
//如果有随机 才有偏移 可以做到相同对象播放相同动画时有不同表现
//但是有的动画需要从第一帧开始播 不需要随机
// [说明] 随机偏移:让多个相同物体的动画不同步,避免"复制粘贴"感
materialPropertyBlock.SetInt("_OffsetIndex", isRandom ? Random.Range(0, 100) : 0); // 设置帧偏移(随机或0)
materialPropertyBlock.SetInt("_PixelIndex", animationInfo.pixelIndex); // 设置该动画在VAT纹理中的起始像素位置
materialPropertyBlock.SetInt("_FrameCount", animationInfo.frameCount); // 设置该动画的总帧数
//传递materialPropertyBlock相关差异性数据给到对应对象的 材质(Shader)
// [说明] 将属性块应用到渲染器,实现GPU Instancing的差异化渲染
renderer.SetPropertyBlock(materialPropertyBlock);
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com