15.CSharp传参与VAT动画播放

15.具体实现-渲染部分-CSharp传参与动画播放实现


15.1 知识点

主要目标和步骤

在 C# 里把 VAT 的配置信息传给材质,驱动 Shader 采样。

  1. 创建带 MeshRendererMeshFilter 的对象,关联网格与材质。
  2. 在 C# 里拿到材质和 AnimationInfos(及内部的 AnimationInfo)。
  3. 封装播放动画 API:用动画状态名播放。
    • 通过名字找到对应的 AnimationInfo
    • AnimationInfo 里的数据传给材质;部分MaterialPropertyBlock 传递(每实例可不同)。

创建材质并关联纹理

创建材质,选择项目里的 VAT Shader,关联主纹理和 VAT 纹理,并开启 GPU Instancing

为何要用 MeshRenderer + 预制体

自带蒙皮动画走的是 SkinnedMeshRendererGPU 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动画
    }

替换预制体并对比

替换创建的预制体并对比,可以看到批处理数量大幅降低,帧率大幅提升。


遗留问题

  1. 远看可以,近看不流畅
    蒙皮网格渲染器走骨骼矩阵,中间时刻可以插值,观感相对顺滑;当前 VAT 实现按帧取顶点,近看时帧与帧之间的变化会更显眼。
    解决思路:提高烘焙采样率;在 Shader 里对相邻两帧做插值;或采用「远景 VAT、近景蒙皮」分级。

  2. 动画切换、循环时比较麻烦(需自行封装)
    没有 Animator 那套状态机与过渡,切换等于改 VAT 片段参数;循环若首尾姿态不接会跳变——这些都要自己在逻辑层封装(状态、过渡、何时重置随机偏移等)。
    解决思路:自研短混合或衔接片段;循环段烘焙时对齐首尾;需要严丝合缝起播时关闭随机偏移、从首帧起播。

  3. 多纹理问题
    动画条数多、总帧数大时,单张 VAT 的宽高或精度装不下全部顶点动画数据,必须拆成多张纹理;Shader 与 C# 就要处理「采哪一张、UV/索引怎么对应」,比教程里一张 VAT 跑通要复杂。
    解决思路:按片段或按容量拆多张 VAT,用 Texture2DArray / 图集统一绑定,实例或材质上带「第几张 VAT」的索引;或降低单帧精度、合并动画以控制单张体积。

  4. 法线、切线或更多数据
    流程里优先解决顶点位置;光照若仍按静态法线/切线,会和变形不一致。
    解决思路:烘焙阶段增加法线/切线等通道;或简化光照、近景主角单独用高精度方案。

  5. 自定义渲染规则
    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

×

喜欢就点赞,疼爱就打赏