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值,顶点成功渲染!

相关推荐
沉默金鱼19 小时前
Unity实用技能-模型
unity·游戏引擎
阿里云云原生20 小时前
AgentRun:如何利用 AI Agent 构建现代化的舆情分析解决方案?
人工智能·unity·游戏引擎
在路上看风景21 小时前
2.8 预渲染
unity
老朱佩琪!21 小时前
Unity代理模式
unity·游戏引擎·代理模式
技术小甜甜1 天前
【Godot】【入门】GDScript 快速上手(只讲游戏里最常用的 20% 语法)
android·游戏·编辑器·游戏引擎·godot
老朱佩琪!1 天前
Unity命令模式
unity·游戏引擎·命令模式
世洋Blog1 天前
Unity编辑器基础
unity·c#·编辑器·游戏引擎
老朱佩琪!1 天前
Unity责任链模式
unity·设计模式·责任链模式
WarPigs1 天前
Unity NetCode for GameObject笔记
笔记·unity·游戏引擎