【GPU Instance】从入门到过载

接上一篇:曾志伟:【Unity游戏开发】合批优化汇总我们简单了解了一下 GPU Instance,多实例渲染,主要用来解决DC压力,减少CPU向GPU发送渲染命令(DrawCall)的次数,提升渲染效率。

这篇文章主要是进一步实践和深入了解的 GPU Instance 使用原理和优化我们实操了一下 GPU Instance 的使用一、简单接口调用【入门】1.1 Shader支持Shader "GPUInstanceShader"{ Properties{ _Color("Color", Color) = (1, 1, 1, 1) } SubShader{ Tags { "RenderType" = "Opaque" } Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing // 开启多实例的变量编译 #include "UnityCG.cginc" struct appdata{ float4 vertex : POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID //顶点着色器的 InstancingID 定义 }; struct v2f{ float4 vertex : SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID //片元着色器的 InstancingID 定义 }; UNITY_INSTANCING_BUFFER_START(Props) // 定义多实例变量数组 UNITY_DEFINE_INSTANCED_PROP(float4, _Color) UNITY_INSTANCING_BUFFER_END(Props) v2f vert(appdata v){ v2f o; UNITY_SETUP_INSTANCE_ID(v); //装配 InstancingID UNITY_TRANSFER_INSTANCE_ID(v, o); //输入到结构中传给片元着色器 o.vertex = UnityObjectToClipPos(v.vertex); // 顶点的世界坐标转屏幕裁剪坐标 return o; } fixed4 frag(v2f i) : SV_Target{ UNITY_SETUP_INSTANCE_ID(i); //装配 InstancingID return UNITY_ACCESS_INSTANCED_PROP(Props, _Color); //提取多实例中的当前实例的Color属性变量值 } ENDCG } } }。

新建材质,使用上面shader,勾选支持Enable GPU Instancing

1.2 C#接口调用usingUnityEngine;publicclassTestGPUInstance:MonoBehaviour{publicGameObjectprefab;publicintInstanceCount

=10;privateMeshmesh;privateMaterialmaterial;privateMatrix4x4[]matrix;privateMeshFilter[]meshFilters;private

Renderer[]renders;privateVector4[]colors;privateMaterialPropertyBlockmaterialPropertyBlock;voidAwake()

{if(prefab==null)return;varmeshFilter=prefab.GetComponent();if(meshFilter){mesh=prefab.GetComponent

().sharedMesh;material=prefab.GetComponent().sharedMaterial;}matrix=newMatrix4x4

[InstanceCount];colors=newVector4[InstanceCount];materialPropertyBlock=newMaterialPropertyBlock();for

(inti=0;i

(-50,50);matrix[i]=Matrix4x4.identity;//设置位置 matrix[i].SetColumn(3,newVector4(x,y,z,1));//设置缩放,矩阵缩放 matrix

[i].m00=Mathf.Max(1,x);matrix[i].m11=Mathf.Max(1,y);matrix[i].m22=Mathf.Max(1,z);// 材质 colors[i]=newVector4

(Random.Range(0f,1f),Random.Range(0f,1f),Random.Range(0f,1f),1);materialPropertyBlock.SetVectorArray(

"_Color",colors);}}voidUpdate(){// 传入mesh、材质、矩阵 // 可以使用 materialPropertyBlock 覆盖 material Graphics.DrawMeshInstanced

(mesh,0,material,matrix,matrix.Length,materialPropertyBlock);}}dUpdate(){Graphics.DrawMeshInstanced(mesh

,0,mat,matrix,matrix.Length);}}这里注意一下 matrix 这个参数,就是一个矩阵,用来控制生成实例的位置和缩放,复习一下矩阵的知识(曾志伟:【GAMES101现代图形学入门】P3 二维变换 笔记

)DrawMeshInstanced API 可以达到合批的作用,但是无法设置不同的参数同时需要注意一次 DrawInstance 调用最多绘制 1023 个物体的限制1.3 新建场景,生成测试把 TestGPUInstance 脚本挂到camera,随便建个Cube,拖到 TestGPUInstance 的Prefab上,点击运行,就能看到实例化的多个Cube。

二、使用 MaterialPropertyBlock 避免创建 Material 实例【进阶】上面是基于材质一致的 GPU Instance 简单调用,下面需求来了:我们要绘制相同 mesh,材质的某些参数不同的多实例。

比如颜色DrawMeshInstanced 这个接口有多个扩展,其中支持传入MaterialPropertyBlock 来覆盖传入的 Material,实现不同实例,不同材质参数。

2.1 多实例参数传递方式:2.1.1 DrawMeshInstanced 接口传入 MaterialPropertyBlock 实现Shader 层定义:新增多实例定义 UNITY_INSTANCING_BUFFER_START(Props) // 定义多实例变量数组 UNITY_DEFINE_INSTANCED_PROP(float4, _Color) UNITY_INSTANCING_BUFFER_END(Props)

C# 层交互:设置每个实力颜色到数组,然后调用 DrawMeshInstanced 接口的时候,将材质颜色数组传入materialPropertyBlock.SetVectorArray("_Color", colors); Graphics.DrawMeshInstanced(mesh, 0, material, matrix, matrix.Length, materialPropertyBlock);

Shader 层调用:UNITY_ACCESS_INSTANCED_PROP(Props, _Color)2.1.2 使用 StructuredBuffer 传入Shader 层:新增 StructuredBuffer 数组定义

StructuredBuffer _instancing_color;C# 层交互:privateComputeBuffercb_color_;cb_color_=newComputeBuffer

(total_count,sizeof(float)*4);lst_color_.Add(newfloat4(1,1,0.0f,1.0f));cb_color_.SetData(lst_color_);

mtrl_work_.SetBuffer(Shader.PropertyToID("_instancing_color"),cb_color_);Shader 层调用: #ifdef UNITY_INSTANCING_ENABLED col *= _instancing_color[unity_InstanceID]; #endif

三、GPU Instance Skin 蒙皮动画实现【过载】项目有用到GPU蒙皮动画来实现大批量的球场观众背景实现,(想到这个也可以用在SLG那种千人同屏的背景渲染上)这边进行简单学习一下这个涉及到多方面知识,动画、骨骼蒙皮、矩阵、shader,有点猪脑过载了。

高能预警!

就是这种密密麻麻的背景,不求高精度渲染,但是要求不同动作,不同材质等需求3.1 What is Skin?要实现GPU蒙皮动画,首先要明白几个概念:骨骼:理解骨骼动画 :骨骼动画的基本原理可概括为:在骨骼控制下,通过顶点混合动态计算蒙皮网格的顶点,而骨骼的运动相对于其父骨骼,并由动画关键帧数据驱动。

涉及到几个关键知识点的学习:骨骼、蒙皮、骨骼动画、bindposes!!!建议先学习一下这篇:博主自己下海实践的简单骨骼动画demo:曾志伟:【动画】Unity骨骼动画 Skinned Mesh组件实现原理

原文链接:Unity-利用SkinnedMeshRenderer和Mesh的BindPose实现骨骼动画_鹅厂程序小哥的博客-CSDN博客_unity bindpose什么是蒙皮?要了解蒙皮,我们需要了解一下动作设计师的工作流

以 3dmax 为例,动画师绑骨骼的流程大致如下建模架设骨骼到模型内添加蒙皮修改器,刷顶点(受骨骼影响的)权重绑好骨骼后,我们就可以通过控制骨骼,来控制整个模型的姿势变化

3.2 Why GPU Skin?问题:主要瓶颈之一是角色动画处理集中在CPU端那么一个简单的想法就是我们能不能把这部分开销转移给GPU呢?因为GPU的计算能力是它的强项第二个瓶颈是CPU和GPU之间的Draw Call问题。

这个问题可以使用批处理(包括Static Batching和Dynamic Batching)或者Unity5.4之后引入的GPU Instancing来解决然而,不幸的是,这些技术都不支持动画角色的 SkinnedMeshRender。

CPU Skinning 与 GPU Skinning :GPU Skinning 结合 Instanced 高效实现大量单位动画解决方案:把动画相关的过程从CPU端转移到GPU端同时,由于CPU不需要处理动画计算,SkinnedMeshRender也可以换成普通的MeshRender,这样我们就可以愉快的使用GPU Instancing减少Draw Call的次数。

3.3 How To Use GPU Skin?方案选定后,我们要做的是在CPU端进行骨骼变换,在GPU端进行蒙皮首先我们要解决的是如何在GPU进行蒙皮动画?大致的步骤是这样的:以固定的频率对角色动画进行采样,记录角色的骨骼矩阵,然后将矩阵的4行分成4个像素存入一张。

贴图A中(空间换时间)运行时,将当前帧索引 Index 和贴图A传入GPUGPU在顶点着色器进行蒙皮,通过 Shader 实时计算顶点坐标3.3.1 提取骨骼动画数据Unity的动画数据都在AnimationClip里面,研究半天,看到2种实现方案:

Animator接口进行采样,回放:unity gpu instance skin mesh骨骼动画AnimationUtility.GetCurveBindings:Jims GameDev Blog

(最后有demo)本质都是想拿到骨骼在该帧的世界坐标,能实现需求的都是好猫我们将分析使用第一种工具方案遇到一个问题: Cant playback from recorder, no recorded data found 的问题,网上没找的合适的解决方案。

最后改成用GameObject.SetActive(false)进行动画重置,animator.Update(t); update到指定时间,获取对应帧骨骼的位置坐标这部分主要要拿的是骨骼的变换矩阵计算骨骼变换矩阵大概原理就是:。

骨骼有根节点, 每个骨骼节点有父子关系,子节点相对父节点有相对的旋转平移,记录为根据矩阵的结合律,把子节点依次按父节点回溯,对应的依次相乘,就可以得到该节点在世界空间中的新的变换矩阵

for(intbone_idx=0;bone_idx

lst_parent=newList();lst_parent.Add(tmp_bone);while(tmp_bone.name!=root_name){tmp_bone

=tmp_bone.parent;if(!tmp_bone)break;lst_parent.Add(tmp_bone);}// 骨骼有根节点, 每个骨骼节点有父子关系,子节点相对父节点有相对的旋转平移,记录为

// 根据矩阵的结合律,把子节点依次按父节点回溯,对应的依次相乘,就可以得到该节点在世界空间中的新的变换矩阵 Matrix4x4tmp_mtx=bindposes[bone_idx];foreach

(vartNodeinlst_parent){varnodeTransform=tNode.transform;Matrix4x4mat=Matrix4x4.TRS(nodeTransform.localPosition

,nodeTransform.localRotation,nodeTransform.localScale);Matrix4x4tm=tmp_mtx;tmp_mtx=mat*tm;}// 分4个像素存入贴图

for(introw_idx=0;row_idx<4;++row_idx){varrow=tmp_mtx.GetRow(row_idx);tex_clr_identity[texel_index_identity

++]=newColor(row.x,row.y,row.z,row.w);}}3.3.1.1 bindposes 绑定的姿势作用:bindposes的主要作用在骨骼变换前预制一些骨骼变换,使得人物可以在同一动画上有不同的骨骼位置表现,简化工作流等

公式:BindPose是如何参与在骨骼蒙皮运算中的 根据Unity文档, Unity中BindPose的算法如下:OneBoneBindPose = bone.worldToLocalMatrix * transform.localToWorldMatrix; OneBoneBindPose = 骨骼的世界转局部坐标系矩阵 * Mesh的局部转世界矩阵

bindPose是骨骼的逆转换矩阵, 在这里我们可以让这个矩阵相对与root, 这样我们就能自由地移动root物件了.公式推导:bindpose定义 - wantnon - 博客园3.3.2 将当前帧索引 Index

和贴图A传入GPU这部分的重点就是帧索引的计算:假设动画有10帧,骨骼有20个,那么生成的动画矩阵个数为200个,一个矩阵占4个像素要显示某一帧动画,在shader根据 帧数*(20*4) +(bone_idx*4)索引到该节点的矩阵

CPU层只要把当前帧数传入,剩下的就交给GPU进行计算采样纹理拿到矩阵了3.3.3 GPU在顶点着色器进行蒙皮,通过 Shader 实时计算顶点坐标利用传入的当前帧数,采样贴图A的指定位置(帧数*(bone_count*4) +(bone_idx*4))的4个像素,拿到矩阵索引的开始位置,继续向下采样4个像素,拼成一个骨骼变换矩阵。

然后每个顶点最多受4个骨骼影响,分别计算4个骨骼的变换矩阵和权重,矩阵1*权重1+矩阵2*权重2+矩阵3*权重3+矩阵4*权重4,得到最后的矩阵顶点和骨骼变换矩阵相乘,得出顶点在骨骼变换矩阵影响下的坐标。

这就是所谓的GPU蒙皮 // 贴图纹理:每帧、每个骨骼矩阵,一个矩阵4个像素 // 计算骨骼矩阵 float4x4 cal_bone_mtx(const uint bone_idx){ // 拿到 C# 层传入的骨骼矩阵索引 uint idx_1dx = (int)UNITY_ACCESS_INSTANCED_PROP(Props, _ani_matrix_index) + bone_idx * 4; uint x = idx_1dx % _tex_ani_side_length; uint y = idx_1dx / _tex_ani_side_length; float2 ani_xy = float2(0, 0); // 为啥这里要再除一次呀?UV 以(0,1) 表示? ani_xy.x = (float)x / _tex_ani_side_length; ani_xy.y = (float)y / _tex_ani_side_length; // 采样4个像素 float4 mat1 = SAMPLE_TEXTURE2D_LOD(_tex_ani, sampler_PointClamp, ani_xy + float2(0, 0), 0); float4 mat2 = SAMPLE_TEXTURE2D_LOD(_tex_ani, sampler_PointClamp, ani_xy + float2(1.0 / _tex_ani_side_length, 0), 0); float4 mat3 = SAMPLE_TEXTURE2D_LOD(_tex_ani, sampler_PointClamp, ani_xy + float2(2.0 / _tex_ani_side_length, 0), 0); float4 mat4 = SAMPLE_TEXTURE2D_LOD(_tex_ani, sampler_PointClamp, ani_xy + float2(3.0 / _tex_ani_side_length, 0), 0); float4x4 mtx_x = float4x4(mat1,mat2,mat3,float4(0, 0, 0, 1)); return mtx_x; }

// 拿到骨骼变换矩阵 float4x4 mtx_1 = cal_bone_mtx(v.vBones.x); float4x4 mtx_2 = cal_bone_mtx(v.vBones.y); float4x4 mtx_3 = cal_bone_mtx(v.vBones.z); float4x4 mtx_4 = cal_bone_mtx(v.vBones.w); float4x4 mtx = mtx_1 * v.vWeights.x + mtx_2 * v.vWeights.y + mtx_3 * v.vWeights.z + mtx_4 * v.vWeights.w; obj_vertex = mul(mtx, float4(v.vertex.xyz, 1.0f)).xyz;

3.3.4 优缺点优缺点都写在 天欲雪:浅析 Unity 中骨骼动画的各种实现 的”将骨骼动画烘焙至贴图“part了这个方案是利用空间换时间,如果模型顶点数据特别多或动画时长特别长的时候,这时就会遇到内存瓶颈。

3.3.5 BLENDINDICES、BLENDWEIGHTS struct appdata{ float4 vertex : POSITION; float2 uv : TEXCOORD0; uint4 vBones : BLENDINDICES; float4 vWeights : BLENDWEIGHTS; UNITY_VERTEX_INPUT_INSTANCE_ID };

还有个黑盒,还没整明白,BLENDINDICES、BLENDWEIGHTS 这个是哪里来的?顶点格式从模型的顶点格式上讲,一般模型顶点数据包括Position(顶点位置)、Normal(顶点法线)、Tangent(顶点切线)、TexCoord(uv坐标)、Color(顶点色)等。

而骨骼模型的话,则多了BlendIndices(混合索引),BlendWeight(混合权重)骨骼蒙皮动画_野生的声威的博客-CSDN博客_blendweightBlendIndices是个int4类型,它存储了4个数组下标,对应4个影响它的骨骼。

因为我们在处理Skeleton(骨架、骨骼树)的时候,会遍历所有Skeleton上的骨骼,然后进行标号,存到数组里,所以BlendIndices也就对应的是这个数组里的下标了这里虽然可以存储4个索引,但实际上经常用不满,大部分情况一个顶点只受一个骨骼影响。

BlendWeight则是上面BlendIndices提到的,该顶点受每个影响它的骨骼的权重,权重占得多的话,影响得就越多BlendWeight和BlendIndices是一一对应关系的,有几个索引就有几个权重,所以BlendWeight是一个float4类型。

源工程源博客工具:unity gpu instance skin mesh骨骼动画自己实验用的demo:TeddyFrameWork/Assets/_Scenes/Scenes_test/GPUInstance at main · Aver58/TeddyFrameWork

最后效果:实现了基本的GPU Instance Skin参考GPU 实例化 - Unity 手册大佬的文章就是简洁通透:How to Render 10,000 Animated Characters With 20 Draw Calls in Unity | Jiadong Chen

GPU Skinning 结合 Instanced 高效实现大量单位动画基于GPU Skin的骨骼动画Instance的实现游戏开发

UWA:GPU Skinning 加速骨骼动画

主题测试文章,只做测试使用。发布者:飞翔的荷兰人,转转请注明出处:http://www.301seo.cn/index.php?m=home&c=View&a=index&aid=8946

联系我们

在线咨询:点击这里给我发消息

邮件:209087445@qq.com