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

相关推荐
玖玥拾15 小时前
Unity3D RPG 入门项目(四)背包拖拽、物品悬浮提示、项目开发思维
3d·unity·游戏引擎
xcLeigh19 小时前
Unity基础:Start与Update方法——Unity脚本生命周期初探
java·unity·游戏引擎·教程
郝学胜-神的一滴21 小时前
[简化版 GAMES 104] 现代游戏引擎 05:游戏引擎世界构建核心机制深度解析
c++·程序人生·unity·游戏引擎·计算机图形学·opengl
松树戈2 天前
godot项目【宝石捕手】04~添加音效、记分
游戏引擎·godot
LONGZETECH2 天前
无人机实训高成本痛点解法:虚拟仿真实现 70% 耗材损耗下降
大数据·算法·unity·架构·无人机
五仁烧饼2 天前
Unity Addressables 静默资源策略
游戏·unity·addressables
ue星空3 天前
【Godot】更换编辑器外观主题
编辑器·游戏引擎·godot
fanfan_hongyun3 天前
unity 与cad 坐标系映射
unity·游戏引擎
神码编程3 天前
【Unity】 HTFramework框架(七十)MultiChoiceDropdown可多选的下拉菜单
unity·游戏引擎·ugui·dropdown·下拉菜单
ue星空3 天前
Godot 2D像素游戏移动抖动、模糊问题解决方案
游戏·游戏引擎·godot