unity的GPUInstance和GPU动画

先说GPU动画之前,我们得先了解人物骨骼

Mesh是人物的网格,通过这个网格来渲染人物

SkinnedMeshRenderer是人物蒙皮,有了这个才能看见人物的外观

Bones是人物骨骼,他只是一个Transform,用来表示这个骨骼在哪个位置用的

Avatar是这个人物的骨骼控制器,他负责绑定哪个Bones是手臂,哪个Bounes是腿部,并且会控制骨骼怎么移动

人物动画要播放,必须要有Avatar ,不然只有Animator,他不知道骨骼的对应关系,也就没法控制动画,会永远保持T-Pose形状

当我们需要烘焙动画,我们是先将这个人物用SampleAnimation固定到某一帧,得到这一帧的顶点信息。所以要烘焙时也必须要有Avatar,不然烘焙没效果

现在开始实现效果(只实现最最基础的功能,其他的不写)

1.C#部分

有2个类,代码如下

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

//窗口,目前只做一个烘焙按钮就行了
public class MyWindow : EditorWindow
{

    [MenuItem("My/BakeWindow")]
    private static void CreateW()
    {
        CreateWindow<MyWindow>();
    }

    private void OnGUI()
    {
        if(GUILayout.Button("Bake"))
        {
            GPUInstanceTest test = FindObjectOfType<GPUInstanceTest>();
            Texture2D d2 = test.BuildTexture();
            AssetDatabase.CreateAsset(d2, "Assets/Build/test.asset");
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        }
    }
}
csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GPUInstanceTest : MonoBehaviour
{
    public AnimationClip clip;
    public GameObject renderParent;
    public SkinnedMeshRenderer skinRender;

    public Texture2D BuildTexture()
    {
        int vertexCount = 0;
        if(skinRender != null)
        {
            vertexCount = skinRender.sharedMesh.vertexCount;
        }

        int totalFrame = (int)(clip.length * clip.frameRate);
        float perFrameTime = 1.0f / totalFrame; //每帧所花时间

        //注意这里的TextureFormat 必须是RGBAHalf!!用RGBA32贴图完全错乱
        Texture2D tex = new Texture2D(vertexCount, totalFrame, TextureFormat.RGBAHalf, false);

        for(int frameIndex = 0; frameIndex < totalFrame; frameIndex++)
        {
            //计算时应该避免用除法,除法有误差的情况会导致贴图错误
            float currentTime = frameIndex * perFrameTime;
            clip.SampleAnimation(renderParent, currentTime);

            Mesh mesh = new Mesh();
            if(skinRender != null)
            {
                skinRender.BakeMesh(mesh);
            }
            Vector3[] currentVerts = mesh.vertices;
            for (int vertIndex = 0; vertIndex < currentVerts.Length; vertIndex++)
            {
                Vector3 pos = currentVerts[vertIndex];
                Color col = new Color(pos.x, pos.y, pos.z, 1);
                tex.SetPixel(vertIndex, frameIndex, col);    //u是顶点索引,v是帧
            }
        }
        tex.Apply();
        return tex;
    }
}

注意这里的一些细节:

1.每一帧对renderParent采样,采样后得到当前帧顶点坐标

2.(重要!)Texture2D的构造函数中,一定是用RGBAHalf格式,用其他格式都会导致贴图错误

3.设置像素时,以顶点坐标作为uv的u索引,帧数作为v索引

4.保存贴图时,应当设置为.asset后缀的文件

接下来写shader

clike 复制代码
Shader "My/GPUAnim"
{
    Properties
    {
        // 动画纹理
        _AnimTex ("Animation Texture", 2D) = "white" {}
        _CurTime ("Time", Float) = 0
        _AnimLen("AnimLen", Float) = 0
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        Cull off
        LOD 100
        
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            
            sampler2D _AnimTex;
            float4 _AnimTex_ST;
            float4 _AnimTex_TexelSize;  //x = 1 / width
            float _CurTime;
            float _AnimLen;
            
            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            v2f vert (appdata v, uint vertexID : SV_VertexID)
            {
                v2f o;
                
                float texU = (vertexID + 0.5) * _AnimTex_TexelSize; //0.5是纹理采样偏移,避免采样时的边缘问题
                float texV = _CurTime / _AnimLen; 
                
                float4 animData = tex2Dlod(_AnimTex, float4(texU, texV, 0, 0));
                // 恢复原始位置
                float3 animatedPos = animData.xyz;
                o.pos = UnityObjectToClipPos(float4(animatedPos, 1.0));
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

这里也有几个注意事项必须要看的

1._AnimTex_TexelSize是float4,他的x分量,结果为1/width,意思是每个像素占多大位置

2.计算u坐标时,是(vertexID + 0.5) * _AnimTex_TexelSize;这个0.5表示的是纹理采样的偏移量,不能让u坐标刚好贴紧像素边缘,会导致贴图错误

[图1]

举个例子如图1:比如第0个像素,范围是0.0到0.25,那么u坐标应该是(0.0+0.25)/2=0.125。所以+0.5就表示的是在中间位置

3.使用tex2Dlod采样

这样就可以开始烘焙了,人物如图2:

[图2]

这里有一只猫,CatMesh上挂着SkinnedMeshRenderer,准备好AnimationClip

创建一个GameObject叫做GPUTest,挂上GPUInstanceTest脚本,然后绑定如图3

[图3]

这里的RenderParent,表示的就是可以播放Animation的那个父物体,因为我们要做SampleAnimation,必须要有能播AnimationClip的对象

SkinRender就是需要烘焙的网格了

设置好后,点击My/BakeWindow,就可以烘焙了,烘焙完成就可以把贴图挂到材质球上了,如图4

[图4]

调整材质球的Time值,顶点成功渲染!

相关推荐
凡情13 小时前
android隐私合规检测
android·unity
小贺儿开发13 小时前
Unity3D 本地 Stable Diffusion 文生图效果演示
人工智能·unity·stable diffusion·文生图·ai绘画·本地化
Swift社区14 小时前
传统游戏引擎 vs 鸿蒙 System 架构
架构·游戏引擎·harmonyos
mxwin1 天前
Unity Shader 半透明物体为什么不能写入深度缓冲?
unity·游戏引擎·shader
晚枫歌F1 天前
三层时间轮的实现
网络·unity·游戏引擎
咸鱼永不翻身1 天前
Lua脚本事件检查工具
unity·lua·工具
leo__5201 天前
单载波中继系统资源分配算法MATLAB仿真程序
算法·matlab·unity
努力长头发的程序猿1 天前
Unity使用ScriptableObject序列化资源
unity·游戏引擎
mxwin1 天前
Unity Shader 手写基于 PBR 的 URP Lit Shader 核心光照计算
unity·游戏引擎·shader
小贺儿开发1 天前
Unity3D 智能云端数字标牌系统
unity·阿里云·人机交互·视频·oss·广告·互动