ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity GPU Instacne动画解决方案

Unity GPU Instacne动画解决方案

背景:Unity 的 GPU Instancing 是一种优化渲染性能的技术,特别适用于需要大量重复渲染相同或相似物体的场景(如植被、子弹、建筑群等)。其核心优势在于通过减少 CPU 和
GPU 的开销显著提升性能。

一.优势分析

1. 性能优化:减少 Draw Calls

  • 传统渲染:每个独立物体会触发一次 Draw Call(绘制调用),大量重复物体会导致 CPU 频繁向 GPU 发送指令,成为性能瓶颈。
  • GPU Instancing:通过单次 Draw Call 批量渲染多个实例,即使物体数量成千上万,也能将 Draw Calls 降至最低,极大减轻 CPU 负担。
  • 适用场景:适用于大量重复的静态或动态物体(如树木、子弹、人群)

2. 节省内存与带宽

  • 共享材质与网格:所有实例共享同一材质和网格数据,避免重复加载资源,减少内存占用。
  • 实例数据压缩:每个实例的个性化数据(位置、旋转、缩放等)通过紧凑的 GPU 缓冲区传递,而非单独存储,降低数据传输带宽需求。

3. 支持动态属性

  • 逐实例数据:允许每个实例拥有独立属性,包括:
    • 变换矩阵(位置、旋转、缩放)
    • 自定义材质属性(颜色、UV 偏移、动画进度等)
  • 动态更新:属性可通过脚本实时修改(如动态调整位置或颜色),保持高性能渲染。

二.原理分析

1.大多数人形动画其模型上用的都是SkinnedMeshRenderer,但是SkinnedMeshRenderer是不支持GPU Instance,我们需要将人形动画的SkinnedMeshRenderer组件修改成MeshRenderer。

2.该动画方案的关键是将人形动画的每一帧网格的顶点存储到一张图片中,用rgb三通道表示顶点的位置,在shader中只需要读取图片的像素信息就可以获得当前网格的顶点,这样游戏在运行时就不会频繁的提取网格信息通知给GPU绘制,减少Draw call的产生。

3.shader中需要开启实例化编译选项

#pragma multi_compile_instancing

并且通过实例化属性来控制shader变量

UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float, _TimeOffset) UNITY_INSTANCING_BUFFER_END(Props)

同时在C#脚本中通过MaterialPropertyBlock类来修改shader变量。

在这里解释一下MaterialPropertyBlock的作用:直接修改Material属性会导致 Unity 自动创建新的材质实例(new Material()),增加内存开销和管理成本。MaterialPropertyBlock通过将属性值存储在独立的“属性块”中,避免材质实例化,从而节省资源。使用 MaterialPropertyBlock 的物体仍可参与GPU Instancing动态批处理(若属性相同),因为它不会破坏材质的共享状态。其工作原理在于将自定义属性值(如颜色、纹理偏移)绑定到物体的渲染器(如MeshRenderer)上,渲染时 Unity 会优先使用这些覆盖值,而非材质本身的默认值。同时属性值存储在 GPU 缓冲区中,通过渲染器传递到 Shader,不修改原始材质的元数据。数千个物体可共享同一材质,仅通过属性块差异化配置,避免因材质实例化导致的内存爆炸。

注意:GPU Instacne一定要通过MaterialPropertyBlock设置shader的属性,如果使用传统的material.SetFloat等直接修改shader属性的方法会导致Unity重新创建一份新的材质导致合批失败

三.功能代码

编译器工具

using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; public class AnimationMeshExporter : EditorWindow { /// <summary> /// 导出面板类 /// </summary> [Serializable] public class GPUAnimationCreateClip { public AnimationClip aniclip; public int samplesPerSecond = 60; //采样帧数 } private float progressValue = 0; public GameObject character; public SkinnedMeshRenderer renderer; public Animator animator; public List<GPUAnimationCreateClip> clipList = new List<GPUAnimationCreateClip>(); private SerializedObject serializedObject; private SerializedProperty clipsProp; private List<Vector3[]> vertexs = new List<Vector3[]>(); private List<Vector3[]> normals = new List<Vector3[]>(); private Vector3 minPos; private Vector3 range; private static string ExportPath = "Assets/Plug/GPUInstance/Export"; private static string FilePath = $"{ExportPath}/{{0}}"; private static string clipsDataPath = $"{ExportPath}/{{0}}/{{1}}.asset"; private static string TexturePath = $"{ExportPath}/{{0}}/Texture"; private static string ImagePath = $"{ExportPath}/{{0}}/Texture/{{1}}.png"; private static string MaterialPath = $"{ExportPath}/{{0}}/{{1}}.mat"; private static string PrefabPath = $"{ExportPath}/{{0}}/{{1}}.prefab"; private const string MAT_VAR_VERTEXCOUNT = "_VertexCount"; private const string MAT_VAR_MAXMEASURE = "_Range"; private const string MAT_VAR_MINPOS = "_MinPos"; private const string MAT_VAR_MAINTEXTURE = "_MainTex"; private const string MAT_VAR_POSTEXTURE = "_PosTex"; private const string MAT_VAR_NORMALTEXTURE = "_NormalTex"; [MenuItem("Tools/Export Animation Meshes")] private static void Init() { // 步骤2:获取或创建窗口实例 var window = GetWindow<AnimationMeshExporter>(); window.titleContent = new GUIContent("Export Animation Meshes"); window.minSize = new Vector2(800, 300); // 设置最小窗口尺寸 } private void OnEnable() { serializedObject = new SerializedObject(this); clipsProp = serializedObject.FindProperty(nameof(clipList)); } private void OnGUI() { serializedObject.Update(); // 显示数组标题 EditorGUILayout.LabelField("动画", EditorStyles.boldLabel); // 显示数组元素 EditorGUILayout.PropertyField(clipsProp, new GUIContent("动画参数"), true); // 应用修改 if (serializedObject.ApplyModifiedProperties()) { EditorUtility.SetDirty(this); // 标记为需要保存 } character = (GameObject)EditorGUILayout.ObjectField("角色模型", character, typeof(GameObject), true); animator = (Animator)EditorGUILayout.ObjectField("animator", animator, typeof(Animator), true); renderer = (SkinnedMeshRenderer)EditorGUILayout.ObjectField("renderer", renderer, typeof(SkinnedMeshRenderer), true); if (GUILayout.Button("导出网格序列")) { // 执行操作逻辑 EditorApplication.delayCall += ExportMeshes; } // 进度条显示 EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(), progressValue, "处理进度"); } private void UpdateProgress() { progressValue += 0.01f; if (progressValue >= 1) { EditorApplication.update -= UpdateProgress; } Repaint(); // 强制刷新界面 } private void ExportMeshes() { if (!character || clipList == null || clipList.Count <= 0) return; // 模拟进度更新 progressValue = 0; EditorApplication.update += UpdateProgress; //创建Export文件夹 if (!Directory.Exists(ExportPath)) { Directory.CreateDirectory(ExportPath); } //创建角色文件夹 string filePath = string.Format(FilePath, character.name); Directory.CreateDirectory(filePath); //创建Texture文件夹 string texture = string.Format(TexturePath, character.name); Directory.CreateDirectory(texture); CreateMeshVertex(); Texture2D vertex = CreateVertexTex(); Texture2D normal = CreateNormalTex(); CreateMaterial(vertex,normal); CreatePrefab(); Debug.Log($"创建成功,文件路径:{filePath}"); } //创建顶点 private void CreateMeshVertex() { GpuAnimationData gpuAnimationData = ScriptableObject.CreateInstance<GpuAnimationData>(); foreach (var clip in clipList) { GPUAnimationClip gPUAnimationClip = new GPUAnimationClip(); gPUAnimationClip.aniclip = clip.aniclip; gPUAnimationClip.samplesPerSecond = clip.samplesPerSecond; gPUAnimationClip.StartFrame = gpuAnimationData.totalFrame; //当前动画的起始帧数 float clipLength = clip.aniclip.length; int totalFrames = Mathf.FloorToInt(clipLength * clip.samplesPerSecond); for (int i = 0; i < totalFrames; i++) { float time = i / (float)clip.samplesPerSecond; // 采样动画到当前时间 clip.aniclip.SampleAnimation(character, time); // 在编辑器运行时更新游戏逻辑但又不想打断编辑器的常规更新循环时 UnityEditor.EditorApplication.QueuePlayerLoopUpdate(); // 烘焙网格 Mesh mesh = new Mesh(); renderer.BakeMesh(mesh, true); Vector3[] vertexs = mesh.vertices; Vector3[] normals = mesh.normals; this.vertexs.Add(vertexs); this.normals.Add(normals); } gpuAnimationData.totalFrame += totalFrames; //计算总帧数 gPUAnimationClip.EndFrame = gpuAnimationData.totalFrame; //当前片段的结束帧数 gpuAnimationData.AddClips(gPUAnimationClip); } string filePath = string.Format(clipsDataPath, character.name, $"{character.name}_data"); // 保存为Asset文件 AssetDatabase.CreateAsset(gpuAnimationData, filePath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } //创建顶点贴图 private Texture2D CreateVertexTex() { int height = vertexs.Count; int weight = vertexs[0].Length; // 创建Texture2D,使用RGBA32格式,关闭mipmap Texture2D tex = new Texture2D(weight, height, TextureFormat.RGBA32, false); float xmin, ymin, zmin; float xmax, ymax, zmax; (xmin, ymin, zmin) = FindExtremum(true); minPos = new Vector3(xmin, ymin, zmin); (xmax, ymax, zmax) = FindExtremum(false); float xrange = xmax - xmin; float yrange = ymax - ymin; float zrange = zmax - zmin; range = new Vector3(xrange, yrange, zrange); // 设置每个像素的颜色 Color[] colors = new Color[height * weight]; for (int y = 0; y < height; y++) { for (int x = 0; x < weight; x++) { Vector3 pos = vertexs[y][x]; //将坐标压缩到0-1 float r = (pos.x - xmin) / xrange; float g = (pos.y - ymin) / yrange; float b = (pos.z - zmin) / zrange; colors[y * weight + x] = new Color(r, g, b, 1f); } } tex.SetPixels(colors); tex.Apply(); // 可选:设置过滤模式为点过滤(像素风格) tex.filterMode = FilterMode.Point; // 保存为PNG文件 byte[] pngData = tex.EncodeToPNG(); string filePath = string.Format(ImagePath, character.name, $"{character.name}_vertex"); File.WriteAllBytes(filePath, pngData); // 刷新资源数据库并调整导入设置 AssetDatabase.Refresh(); TextureImporter importer = AssetImporter.GetAtPath(filePath) as TextureImporter; if (importer != null) { importer.sRGBTexture = false; // 关闭 sRGB 颜色空间转换 importer.filterMode = FilterMode.Point; // 设置过滤模式为Point importer.mipmapEnabled = false; // 关闭Mipmap importer.textureCompression = TextureImporterCompression.Uncompressed; //禁用压缩 importer.maxTextureSize = 8192; importer.npotScale = TextureImporterNPOTScale.None; //禁止对非二次幂(NPOT)纹理进行自动缩放 importer.SaveAndReimport(); // 应用更改 } tex = AssetDatabase.LoadAssetAtPath<Texture2D>(filePath); return tex; } private (float, float, float) FindExtremum(bool findMin) { int width = vertexs.Count; int height = vertexs[0].Length; float initial = findMin ? float.MaxValue : float.MinValue; float x = initial, y = initial, z = initial; Func<float, float, float> compare = findMin ? (a, b) => MathF.Min(a, b) : (a, b) => MathF.Max(a, b); for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { Vector3 pos = vertexs[j][i]; x = compare(x, pos.x); y = compare(y, pos.y); z = compare(z, pos.z); } } return (x, y, z); } private (float, float, float) FindMinValue() => FindExtremum(true); private (float, float, float) FindMaxValue() => FindExtremum(false); /// <summary> /// 创建法线贴图 /// </summary> private Texture2D CreateNormalTex() { int height = normals.Count; int weight = normals[0].Length; // 创建Texture2D,使用RGBA32格式,关闭mipmap Texture2D tex = new Texture2D(weight, height, TextureFormat.RGBA32, false); // 设置每个像素的颜色 Color[] colors = new Color[height * weight]; for (int y = 0; y < height; y++) { for (int x = 0; x < weight; x++) { Vector3 normal = normals[y][x]; // 示例:左上角红色,右下角蓝色,中间渐变 float max = Mathf.Max(normal.x, normal.y,normal.z); float r = normal.x / max; float g = normal.y / max; float b = normal.z / max; colors[y * weight + x] = new Color(r, g, b, 1f); } } tex.SetPixels(colors); tex.Apply(); // 可选:设置过滤模式为点过滤(像素风格) tex.filterMode = FilterMode.Point; // 保存为PNG文件 byte[] pngData = tex.EncodeToPNG(); string filePath = string.Format(ImagePath, character.name, $"{character.name}_normal"); File.WriteAllBytes(filePath, pngData); // 刷新资源数据库并调整导入设置 AssetDatabase.Refresh(); TextureImporter importer = AssetImporter.GetAtPath(filePath) as TextureImporter; if (importer != null) { // 关闭 sRGB 颜色空间转换 importer.sRGBTexture = false; importer.filterMode = FilterMode.Point; // 设置过滤模式为Point importer.mipmapEnabled = false; // 关闭Mipmap importer.textureCompression = TextureImporterCompression.Uncompressed; // 可选:禁用压缩 importer.npotScale = TextureImporterNPOTScale.None; //禁止对非二次幂(NPOT)纹理进行自动缩放 importer.maxTextureSize = 8192; importer.SaveAndReimport(); // 应用更改 } tex = AssetDatabase.LoadAssetAtPath<Texture2D>(filePath); return tex; } private void CreatePrefab() { GameObject newPlayer = GameObject.Instantiate(character); newPlayer.name = character.name + "(GPUInstance)"; GPUAnimation gpu = newPlayer.AddComponent<GPUAnimation>(); //加载数据文件 string GpuAnimationDataPath = string.Format(clipsDataPath, character.name, $"{character.name}_data"); gpu.data = AssetDatabase.LoadAssetAtPath<GpuAnimationData>(GpuAnimationDataPath); SkinnedMeshRenderer[] renderers = newPlayer.GetComponentsInChildren<SkinnedMeshRenderer>(); for (int i = 0;i < renderers.Length;i++) { GameObject obj = renderers[i].gameObject; MeshFilter meshFilter = obj.AddComponent<MeshFilter>(); meshFilter.mesh = renderers[i].sharedMesh; MeshRenderer meshRenderer = obj.AddComponent<MeshRenderer>(); string newMaterialPath = string.Format(MaterialPath, character.name, $"{character.name}_mat"); Material newMaterial = AssetDatabase.LoadAssetAtPath<Material>(newMaterialPath); meshRenderer.sharedMaterial = newMaterial; gpu.mesh = meshRenderer; DestroyImmediate(renderers[i]); } Animator a = newPlayer.GetComponentInChildren<Animator>(); DestroyImmediate(a); // 3. 将GameObject保存为预制体 string filePath = string.Format(PrefabPath, character.name,newPlayer.name); PrefabUtility.SaveAsPrefabAsset(newPlayer, filePath); DestroyImmediate(newPlayer); } private void CreateMaterial(Texture posTexture,Texture normalTexture) { // 1. 创建新材质 Shader standardShader = Shader.Find("Custom/InstancedShader"); Material newMaterial = new Material(standardShader) { name = $"{character.name}_mat", }; //设置主纹理 if (renderer.sharedMaterial != null) { Texture mainTex = renderer.sharedMaterial.GetTexture(MAT_VAR_MAINTEXTURE); if (mainTex != null) { newMaterial.SetTexture(MAT_VAR_MAINTEXTURE, mainTex); } } newMaterial.SetInt(MAT_VAR_VERTEXCOUNT, renderer.sharedMesh.vertexCount); newMaterial.SetVector(MAT_VAR_MINPOS, minPos); newMaterial.SetVector(MAT_VAR_MAXMEASURE, range); //设置顶点纹理 newMaterial.SetTexture(MAT_VAR_POSTEXTURE, posTexture); //设置法线纹理 newMaterial.SetTexture(MAT_VAR_NORMALTEXTURE, normalTexture); newMaterial.enableInstancing = true; string filePath = string.Format(MaterialPath, character.name,$"{character.name}_mat"); // 保存材质 AssetDatabase.CreateAsset(newMaterial, filePath); AssetDatabase.Refresh(); AssetDatabase.SaveAssets(); } }

Shader

Shader "Custom/InstancedShader" { Properties { _MainTex ("Texture", 2D) = "white" {} _PosTex("PosTex",2D) = "white"{} _NormalTex("_NormalTex",2D) ="white"{} _VertexCount("_VertexCount",int) = 6000 _MinPos ("_MinPos",Vector) = (0,0,0,0) _Range ("_Range",Vector) = (0,0,0,0) } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing // 启用实例化编译选项 //#pragma target 3.5 #include "UnityCG.cginc" // 实例化属性 UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float, _TimeOffset) UNITY_INSTANCING_BUFFER_END(Props) struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID // 实例ID }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float3 normal : TEXCOORD1; UNITY_VERTEX_INPUT_INSTANCE_ID }; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _PosTex; sampler2D _NormalTex; int _VertexCount; float4 _Range; float4 _MinPos; v2f vert (appdata v, uint vid : SV_VertexID) { UNITY_SETUP_INSTANCE_ID(v); // 设置实例ID v2f o; o.uv = TRANSFORM_TEX(v.uv, _MainTex); float x = (float)vid + 0.5; float y = UNITY_ACCESS_INSTANCED_PROP(Props, _TimeOffset); float3 pro =(tex2Dlod(_PosTex, float4(x / _VertexCount, y, 0, 0))).xyz; v.vertex.xyz = pro * _Range.xyz + _MinPos.xyz; o.vertex = UnityObjectToClipPos(v.vertex); float3 normal = (tex2Dlod(_NormalTex, float4(x / _VertexCount, y, 0, 0))).xyz; o.normal = mul((float3x3)unity_ObjectToWorld, normal); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); col *= dot(normalize(i.normal), normalize(float3(0, 1, 1))) * 0.4 + 0.6; return col; } ENDCG } } }

GPU动画调用

using System.Collections.Generic; using UnityEngine; public class GPUAnimation : MonoBehaviour { public MeshRenderer mesh; public GpuAnimationData data; private float currentFrame = 0; private float baseRate = 60; private float speed = 1; //动画播放速率 private int totalFrames { get => data.totalFrame; } private GPUAnimationClip currentClip; //当前播放的动画 private MaterialPropertyBlock block; [SerializeField] private GPUAnimationEventManager eventManager; public GPUAnimationEventManager EventManager { get { return eventManager; } } private void Awake() { eventManager = new GPUAnimationEventManager(this); } private void Start() { block = new MaterialPropertyBlock(); data.Init(); } private void Update() { base.MyUpdate(); InvokeAnimation(); mesh.SetPropertyBlock(block); } public void Play(string name,bool loop = true) { if (!data.ContainClip(name)) { Debug.LogError($"{this.gameObject.name}不包含{name}动画"); return; } currentClip = data.GetClip(name); currentClip.Loop = loop; currentFrame = currentClip.StartFrame; eventManager.InvokeAniBeginEvent(name); } public void SetSpeed(float speed) { this.speed = speed; } /// <summary> /// 是否包含动画片段 /// </summary> /// <param name="name"></param> /// <returns></returns> public bool ContainClip(string name) { return data.ContainClip(name); } public List<GPUAnimationClip> GetAllClip() => data.clips; /// <summary> /// 执行动画片段 /// </summary> private void InvokeAnimation() { if (currentClip == null) return; currentFrame += Time.deltaTime * baseRate * speed; eventManager.InvokeAniUpdateEvent(currentClip.Name); if (currentFrame >= currentClip.EndFrame) { eventManager.InvokeAniEndEvent(currentClip.Name); if (currentClip.Loop) { eventManager.InvokeAniBeginEvent(currentClip.Name); currentFrame = currentClip.StartFrame; } else currentFrame = currentClip.EndFrame; } block.SetFloat("_TimeOffset", currentFrame / totalFrames); } }
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; [Serializable] public class GPUAnimationEventManager { [Serializable] public class GPUAnimaitonEvent { public string animationName; public UnityEvent OnAniBeginEvent = new UnityEvent(); public UnityEvent OnAniUpdateEvent = new UnityEvent(); public UnityEvent OnAniEndEvent = new UnityEvent(); public GPUAnimaitonEvent(string animationName) { this.animationName = animationName; } } public GPUAnimationEventManager(GPUAnimation gPUAnimation) { this.gPUAnimation = gPUAnimation; List<GPUAnimationClip> clips = gPUAnimation.GetAllClip(); for (int i = 0;i < clips.Count;i++) { if (!ContainEvent(clips[i].Name)) { gPUAnimaitonEvents.Add(new GPUAnimaitonEvent(clips[i].Name)); } } } private GPUAnimation gPUAnimation; [SerializeField] private List<GPUAnimaitonEvent> gPUAnimaitonEvents = new List<GPUAnimaitonEvent>(); public bool ContainEvent(string animationName) { for (int i = 0;i < gPUAnimaitonEvents.Count;i++) { if (gPUAnimaitonEvents[i].animationName == animationName) return true; } return false; } public void AddAniBeginEvent(string animationName, UnityAction unityAction) { if (!gPUAnimation.ContainClip(animationName)) { Debug.LogError($"{gPUAnimation.gameObject}没有{animationName}动画"); return; } foreach (var aniEvent in gPUAnimaitonEvents) { if (animationName == aniEvent.animationName) { aniEvent.OnAniBeginEvent.AddListener(unityAction); return; } } } public void AddAniUpdateEvent(string animationName, UnityAction unityAction) { if (!gPUAnimation.ContainClip(animationName)) { Debug.LogError($"{gPUAnimation.gameObject}没有{animationName}动画"); return; } foreach (var aniEvent in gPUAnimaitonEvents) { if (animationName == aniEvent.animationName) { aniEvent.OnAniUpdateEvent.AddListener(unityAction); return; } } } public void AddAniEndEvent(string animationName, UnityAction unityAction) { if (!gPUAnimation.ContainClip(animationName)) { Debug.LogError($"{gPUAnimation.gameObject}没有{animationName}动画"); return; } foreach (var aniEvent in gPUAnimaitonEvents) { if (animationName == aniEvent.animationName) { aniEvent.OnAniEndEvent.AddListener(unityAction); return; } } } public void RemoveAniBeginEvent(string animationName, UnityAction unityAction) { foreach (var aniEvent in gPUAnimaitonEvents) { if (animationName == aniEvent.animationName) { aniEvent.OnAniBeginEvent.RemoveListener(unityAction); return; } } } public void RemoveAniUpdateEvent(string animationName, UnityAction unityAction) { foreach (var aniEvent in gPUAnimaitonEvents) { if (animationName == aniEvent.animationName) { aniEvent.OnAniUpdateEvent.RemoveListener(unityAction); return; } } } public void RemoveAniEndEvent(string animationName, UnityAction unityAction) { foreach (var aniEvent in gPUAnimaitonEvents) { if (animationName == aniEvent.animationName) { aniEvent.OnAniEndEvent.RemoveListener(unityAction); return; } } } public void InvokeAniBeginEvent(string animationName) { if (!gPUAnimation.ContainClip(animationName)) { Debug.LogError($"{gPUAnimation.gameObject}没有{animationName}动画"); return; } foreach (var aniEvent in gPUAnimaitonEvents) { if (animationName == aniEvent.animationName) { aniEvent.OnAniBeginEvent?.Invoke(); return; } } } public void InvokeAniUpdateEvent(string animationName) { if (!gPUAnimation.ContainClip(animationName)) { Debug.LogError($"{gPUAnimation.gameObject}没有{animationName}动画"); return; } foreach (var aniEvent in gPUAnimaitonEvents) { if (animationName == aniEvent.animationName) { aniEvent.OnAniUpdateEvent?.Invoke(); return; } } } public void InvokeAniEndEvent(string animationName) { if (!gPUAnimation.ContainClip(animationName)) { Debug.LogError($"{gPUAnimation.gameObject}没有{animationName}动画"); return; } foreach (var aniEvent in gPUAnimaitonEvents) { if (animationName == aniEvent.animationName) { aniEvent.OnAniEndEvent?.Invoke(); return; } } } }
using System; using UnityEngine; [Serializable] public class GPUAnimationClip { public AnimationClip aniclip; public int samplesPerSecond = 60; //采样帧数 public int StartFrame; //起始帧数 public int EndFrame; //结束帧数 public bool Loop; public string Name { get => aniclip.name; } }
using System.Collections.Generic; using UnityEngine; public class GpuAnimationData : ScriptableObject { public int totalFrame = 0; //总帧数 public List<GPUAnimationClip> clips = new List<GPUAnimationClip>(); public Dictionary<string, GPUAnimationClip> gpudic; // 在加载或初始化时重建字典 public void Init() { gpudic = new Dictionary<string, GPUAnimationClip>(); foreach (var clip in clips) { gpudic.Add(clip.Name, clip); gpudic[clip.Name] = clip; } } public void AddClips(GPUAnimationClip clip) { clips.Add(clip); } public bool ContainClip(string name) => gpudic.ContainsKey(name); public GPUAnimationClip GetClip(string name) => gpudic[name]; public float GetStartFrame(string name) => gpudic[name].StartFrame; public float GetEndFrame(string name) => gpudic[name].EndFrame; }

四.代码关键点分析

1.由于rgb通道只能存储0到1的浮点数,为了我们需要将顶点位置压缩到0到1的范围内进行存储。为了防止压缩时精度丢失过大,算法通过遍历所有顶点,找到模型在播放所有动作时的顶点范围的最大值和最小值,将顶点数据在这个最大值和最小值之间平均分布,并对应0到1的范围。

2.关于保存Texture的参数设置

// 关闭 sRGB 颜色空间转换 importer.sRGBTexture = false; importer.filterMode = FilterMode.Point; // 设置过滤模式为Point importer.mipmapEnabled = false; // 关闭Mipmap importer.textureCompression = TextureImporterCompression.Uncompressed; // 可选:禁用压缩 importer.maxTextureSize = 8192; importer.npotScale = TextureImporterNPOTScale.None; //禁止对非二次幂(NPOT)纹理进行自动缩放 importer.SaveAndReimport(); // 应用更改

其目的是防止存储顶点数据后unity对图片进行压缩和转换,导致在shader中提取图片颜色后无法还原到对应的顶点位置信息

3.本人还将法线信息提取出来并存储到Texture中,方便shader提取

4.本插件对于多动画的处理,是将人物的所有动画打包到一张贴图上,通过监听shader动画播放到多少帧来识别当前播放到了哪个动画。并且封装了动画播放和动画事件,方便动画调用已经事件监听

五.总结

GPU Instancing 是 Unity 中优化大规模重复物体渲染的利器,通过减少 Draw Calls、节省资源、支持动态属性,显著提升性能表现。结合合理的 Shader 设计和脚本控制,可以在不牺牲视觉效果的前提下实现高效渲染。

完整代码已经在上文中公开,只需要导入到unity中就会出现插件。

因为这个是需要从本人的项目工程中单独剥离出来,后续本人如果有时间会将完整工程上传到Github上,太累了,偷个懒后面再说哈哈哈

返回列表