[学习记录]Unity毛发渲染[URP]-Shell基础版_毛发渲染合批、
毛发,无论是人类的头发、动物的皮毛,还是奇幻生物的绒毛,都是构成生命感和真实感不可或缺的元素。它对光线的独特散射、吸收和反射,赋予了物体柔软、蓬松、有生命力的质感。它不仅仅是让角色看起来更“毛茸茸”那么简单,更是通向极致真实感和视觉沉浸感的关键一步。本期我们来在Unity6的UDRP项目中实现一个Shell外壳技术的Fur毛发的基础版渲染效果,最终效果如下图所示。
[基础版包含纹理+Shell+AO+边缘光效果]
使用Unity版本:6000.0.43f1
我会先实现UDRP下单Pass(不应用GPUInstancing)+DrawMesh绘制API的方案,后面会再用GPUInstancing(分别使用两种实例绘制API)改进性能。
一.为什么不使用多Pass渲染
1.高DrawCall&打断SRPBatcher
在URP中,Pass由手动控制,一般在不开启GPU Instancing时,每个Pass都是一次DrawCall,且Shader内多Pass(大部分情况)会导致渲染状态的切换,导致SRP Batcher 无法合批,所以URP中提倡主Pass渲染。
2.RenderFeature实现难度大
可以使用RenderFeature虽然可以避开Shader内多pass,但是存在注入难度大 & 与多 Pass Shader 协调困难的问题。所以一般是利用RenderFeature插入额外效果,而不是实现Shader 多 Pass。
3.追求优良性能
多 Pass 意味着多次顶点变换、光照计算、纹理采样 对草,毛发这样一组“重复结构”的对象很不划算,使用URP鼓励的主通道渲染配合GPUInstancing是性能最优的选择。
二.Shell基本原理
Shell算法可以说是很经典了,网上一搜有大量的介绍,下方的原理图就很直观了,这里我用白话总结一些关键点。
1.顶点沿法线偏移做多壳层
渲染多层壳(Shell,本质是把一个Mesh渲染多次),根据壳层序号作为偏移算子,将顶点沿法线方向偏移一定距离。
2.壳层透明度递减做体积感
采样一张噪声纹理图(仅需读取单通道的黑白信息),根据壳层序号与总壳数的比值 ,将则遮罩图的黑白信息应用到每层壳的透明通道,使外层壳透明度逐渐降低,从而形成多层的体积感。
使用到的毛发噪声及法线贴图:
三.3种Shell_Fur实现方式
我采用UnlitShader+C#控制脚本的形式实现。在Shader内部,我修改了默认的CGPROGRAM,使用HLSLPROGRAM,对应HLSL语法。
注意引入基本库
#include \"Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl\"
1.单通道(非GPUInstancing)+DrawMesh实现基础绘制
(1)Property声明
根据需求我定义了以下属性:
(1)基础Shell:定义了毛发噪声纹理, 毛发(壳层偏移)长度 和 壳层总数;
(1)AO:定义了毛发的根部颜色 和 末端颜色;
(2)边缘光:定义了边缘光颜色 和 菲涅尔强度;
(3)为了便于控制噪声效果,定义剔除阈值;
Properties { //毛发噪声纹理 _FurTex(\"Fur Texture\", 2D) = \"white\" {} //毛发根部颜色 [HDR]_RootColor(\"RootColor\",Color)=(0,0,0,1) //毛发末端颜色 [HDR]_FurColor(\"FurColor\",Color)=(1,1,1,1) //凹凸纹理 _BumpTex(\"Normal Map\", 2D) = \"bump\" {} //凹凸强度 _BumpIntensity(\"Bump Intensity\",Range(0,2))=1 //毛发长度 _FurLength(\"Fur Length\", Float) = 0.2 //壳层总数 _ShellCount(\"Shell Count\", Float) = 16 //边缘光颜色 [HDR]_FresnelColor(\"Fresnel Color\", Color) = (1,1,1,1) //菲涅尔强度 _FresnelPower(\"Fresnel Power\", Float) = 5 //噪声剔除阈值 _FurAlphaPow(\"Fur AlphaPow\", Range(0,6)) = 1 }
(2)顶点沿法线偏移
在顶点着色器中对顶点进行法线方向的偏移
v2f vert(appdata v){ v2f o; xxx xxx float shellIndex = _ShellIndex; float shellFrac = shellIndex / _ShellCount; float3 worldNormal = TransformObjectToWorldNormal(v.normal); float3 worldPos = TransformObjectToWorld(v.vertex.xyz); worldPos += worldNormal * (_FurLength * shellFrac); xxx xxx return o;}
(3)噪声图透明度递减
half4 frag(v2f i) : SV_Target { xxx xxx float mask = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv).r; float alpha= saturate(mask - pow(shellFrac,_FurAlphaPow)); xxx xxx xxx col.a = alpha; xxx return col; }
(4)控制脚本传入壳层序号
在 Shader 中 没有直接的方法 获取实例编号(比如第几次调用的实例),比如SV_InstanceID在 Unity 的标准 Shader HLSL 里不暴露。UNITY_GET_INSTANCE_ID
UNITY_GET_INSTANCE_ID()
通常不生效,或者需要使用特殊渲染管线(如 HDRP + DOTS)。
所以,如果你不传 ShellIndex
,Shader 就 不知道当前是第几层 Shel
(4)完整代码
C#控制脚本
[RequireComponent((typeof(MeshRenderer)))][ExecuteAlways]public class ShellFurController_NonGpuInstancing : MonoBehaviour{ Mesh mesh; Material material; public int shellCount = 16; private Matrix4x4[] matrices; private MaterialPropertyBlock[] props; void Start() { //不调用 .material,这会创建一个新实例,浪费内存 material = GetComponent().sharedMaterial; mesh = GetComponent().sharedMesh; matrices = new Matrix4x4[shellCount]; props = new MaterialPropertyBlock[shellCount]; for (int i = 0; i < shellCount; i++) { matrices[i] = transform.localToWorldMatrix; Debug.Log(matrices[i]); props[i] = new MaterialPropertyBlock(); props[i].SetFloat(\"_ShellIndex\", i); } } void Update() { //同步更新壳层世界位置 for (int i = 0; i < shellCount; i++) { matrices[i] = transform.localToWorldMatrix; Debug.Log(matrices[i]); props[i].SetFloat(\"_ShellIndex\", i); } //使用DrawMesh API渲染多壳层 for (int i = 0; i < shellCount; i++) { Graphics.DrawMesh( mesh, matrices[i], material, 0, null, 0, props[i], UnityEngine.Rendering.ShadowCastingMode.Off, false ); } }}
UnlitShader
Shader \"Unlit/Base_Shell_Fur_NonGpuIns\"{ Properties { //毛发噪声纹理 _FurTex(\"Fur Texture\", 2D) = \"white\" {} //毛发根部颜色 [HDR]_RootColor(\"RootColor\",Color)=(0,0,0,1) //毛发末端颜色 [HDR]_FurColor(\"FurColor\",Color)=(1,1,1,1) //凹凸纹理 _BumpTex(\"Normal Map\", 2D) = \"bump\" {} //凹凸强度 _BumpIntensity(\"Bump Intensity\",Range(0,2))=1 //毛发长度 _FurLength(\"Fur Length\", Float) = 0.2 //壳层总数 _ShellCount(\"Shell Count\", Float) = 16 //外发光颜色 [HDR]_FresnelColor(\"Fresnel Color\", Color) = (1,1,1,1) //菲涅尔强度 _FresnelPower(\"Fresnel Power\", Float) = 5 //噪声剔除阈值 _FurAlphaPow(\"Fur AlphaPow\", Range(0,6)) = 1 } SubShader { Tags { \"Queue\"=\"Transparent\" \"RenderType\"=\"Transparent\" } LOD 200 ZWrite Off Cull Back Blend SrcAlpha OneMinusSrcAlpha Pass { Name \"FurPass\" Tags { \"LightMode\" = \"UniversalForward\" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include \"Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl\" TEXTURE2D(_FurTex); SAMPLER(sampler_FurTex); float4 _FurTex_ST; TEXTURE2D(_BumpTex); SAMPLER(sampler_BumpTex); float _FurLength; float _ShellCount; float4 _FresnelColor; float _FresnelPower; float _FurAlphaPow; float4 _RootColor; float4 _FurColor; float _ShellIndex;//壳层序号,由C#控制脚本传入 struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 viewDir : TEXCOORD1; float3 worldNormal : TEXCOORD2; float shellIndex : TEXCOORD3; }; v2f vert(appdata v) { float shellIndex = _ShellIndex; float shellFrac = shellIndex / _ShellCount; v2f o; float3 worldNormal = TransformObjectToWorldNormal(v.normal); float3 worldPos = TransformObjectToWorld(v.vertex.xyz); worldPos += worldNormal * (_FurLength * shellFrac); o.pos = TransformWorldToHClip(worldPos); o.uv = TRANSFORM_TEX(v.uv, _FurTex); o.viewDir = normalize(_WorldSpaceCameraPos - worldPos); o.worldNormal = worldNormal; o.shellIndex = shellIndex; return o; } half4 frag(v2f i) : SV_Target { half4 col = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv); float shellFrac = i.shellIndex / _ShellCount; float mask = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv).r; float alpha= saturate(mask - pow(shellFrac,_FurAlphaPow)); float3 bump = UnpackNormal(SAMPLE_TEXTURE2D(_BumpTex, sampler_BumpTex, i.uv)); float3 normalWS = normalize(i.worldNormal + bump * 0.5); //边缘光 float fresnel = pow(1.0 - saturate(dot(i.viewDir, normalWS)), _FresnelPower); //AO col*=lerp(_RootColor,_FurColor,shellFrac); col.a = alpha; col.rgb += _FresnelColor.rgb * fresnel * alpha; return col;} ENDHLSL } }}
(5)效果展示
(6)性能压力!!
当将控制脚本中的ShellCount设置到比较大的数值,我这里发现100层就比较卡了,试想如果这是真实的游戏场景,这性能压力简直是简直了,所以我决定采用GPUInstancing来优化我的项目。
运行前
运行后(场景内漫游时帧率急剧下降)
2.单通道(GPUInstancing)+DrawMeshInstansed实现优良性能
这里我采用Shader内StructruedBuffer搭配C#控制脚本内ComputeBuffer方案实现。
关于GPUInstancing的内容可以移步我的另一篇博客:Unity性能优化-渲染模块(1)-CPU侧(2)-DrawCall优化(2)GPUInstancing-CSDN博客
回顾绘制API的使用可以移步我的另一篇博客:
[学习记录]Unity中的绘制API-CSDN博客
(1)技术要点
1.合并 Draw Call:将所有实例的绘制合并成一个 Draw Call。
2.CPU 准备实例数据: 你需要在 CPU 上准备一个所有实例(壳层)的变换矩阵数组,使用ComputeBuffer作为一个所有实例的额外数据数组(例如,包含每个壳层索引的float数组)。通过material.SetBuffer传递给 Shader。
3.Shader 获取实例 ID: Shader 中会启用实例化,并通过内置的SV_InsatnceID获取当前正在处理的实例(壳层)的 ID。
(2)完整代码
C#控制脚本
using UnityEngine;[RequireComponent((typeof(MeshRenderer)))][ExecuteAlways]public class ShellFurController_DrawInstanced : MonoBehaviour{ Mesh mesh; Material material; [Header(\"壳层数\")]public int shellCount = 16; private Matrix4x4[] matrices; //使用DrawInstanced(),为了正确合批,使用统一的MPB,一次绘制所有实例 private MaterialPropertyBlock props; private ComputeBuffer shellIndexBuffer; float[] shellIndices; void Start() { material = GetComponent().sharedMaterial; mesh = GetComponent().sharedMesh; if (!material.enableInstancing) { Debug.LogWarning(\"Fur material must enable GPU Instancing\"); } // 所有实例使用同一个 props,用数组传 ShellIndex matrices = new Matrix4x4[shellCount]; props = new MaterialPropertyBlock(); shellIndices = new float[shellCount]; for (int i = 0; i < shellCount; i++) { matrices[i] = transform.localToWorldMatrix; shellIndices[i] = i; } shellIndexBuffer = new ComputeBuffer(shellCount, sizeof(float)); shellIndexBuffer.SetData(shellIndices); material.SetBuffer(\"_ShellIndexBuffer\", shellIndexBuffer); } void Update() { // 实例位置更新 for (int i = 0; i < shellCount; i++) { matrices[i] = transform.localToWorldMatrix; } // 使用真正的 GPU Instancing 调用 Graphics.DrawMeshInstanced( mesh, 0, material, matrices, shellCount, props, UnityEngine.Rendering.ShadowCastingMode.Off, false ); }}
UnlitShader
Shader \"Unlit/Base_Shell_Fur_GpuIns\"{ Properties { //毛发噪声纹理 _FurTex(\"Fur Texture\", 2D) = \"white\" {} //毛发根部颜色 [HDR]_RootColor(\"RootColor\",Color)=(0,0,0,1) //毛发末端颜色 [HDR]_FurColor(\"FurColor\",Color)=(1,1,1,1) //凹凸纹理 _BumpTex(\"Normal Map\", 2D) = \"bump\" {} //凹凸强度 _BumpIntensity(\"Bump Intensity\",Range(0,2))=1 //毛发长度 _FurLength(\"Fur Length\", Float) = 0.2 //壳层总数 _ShellCount(\"Shell Count\", Float) = 16 //外发光颜色 [HDR]_FresnelColor(\"Fresnel Color\", Color) = (1,1,1,1) //菲涅尔强度 _FresnelPower(\"Fresnel Power\", Float) = 5 //噪声剔除阈值 _FurAlphaPow(\"Fur AlphaPow\", Range(0,6)) = 1 } SubShader { Tags { \"Queue\"=\"Transparent\" \"RenderType\"=\"Transparent\" } LOD 200 ZWrite Off Cull Back Blend SrcAlpha OneMinusSrcAlpha Pass { Name \"FurPass\" Tags { \"LightMode\" = \"UniversalForward\" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include \"Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl\" TEXTURE2D(_FurTex); SAMPLER(sampler_FurTex); float4 _FurTex_ST; TEXTURE2D(_BumpTex); SAMPLER(sampler_BumpTex); float4 _BumpTex_ST; float _FurLength; float _ShellCount; float _WindStrength; float4 _FresnelColor; float _FresnelPower; float _FurAlphaPow; float4 _RootColor; float4 _FurColor; StructuredBuffer _ShellIndexBuffer; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; uint id: SV_InstanceID; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 viewDir : TEXCOORD1; float3 worldNormal : TEXCOORD2; float shellIndex : TEXCOORD3; }; v2f vert(appdata v) { float shellIndex = _ShellIndexBuffer[v.id]; float shellFrac = shellIndex / _ShellCount; v2f o; float3 worldNormal = TransformObjectToWorldNormal(v.normal); float3 worldPos = TransformObjectToWorld(v.vertex.xyz); float windOffset = sin(worldPos.x * 5 + _Time.y * 2 + shellIndex) * _WindStrength; worldPos += worldNormal * (_FurLength * shellFrac + windOffset); o.pos = TransformWorldToHClip(worldPos); o.uv = TRANSFORM_TEX(v.uv, _FurTex); o.viewDir = normalize(_WorldSpaceCameraPos - worldPos); o.worldNormal = worldNormal; o.shellIndex = shellIndex; return o; } half4 frag(v2f i) : SV_Target { half4 col = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv); float shellFrac = i.shellIndex / _ShellCount; float mask = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv).r; float alpha= saturate(mask - pow(shellFrac,_FurAlphaPow)); float3 bump = UnpackNormal(SAMPLE_TEXTURE2D(_BumpTex, sampler_BumpTex, i.uv)); float3 normalWS = normalize(i.worldNormal + bump * 0.5); float fresnel = pow(1.0 - saturate(dot(i.viewDir, normalWS)), _FresnelPower); //AO col*=lerp(_RootColor,_FurColor,shellFrac); col.a = alpha; col.rgb += _FresnelColor.rgb * fresnel * alpha; return col;} ENDHLSL } }}
(3)效果展示
渲染100层前后对比:
savebybatching:0->99
setPass Call : 24->25
这说明我们的GPUInstancng应用成功了。
3.单通道(GPUInstancing)+DrawMeshInstancedIndirect()实现极致性能
(1)技术要点
区别于DrawMeshInstanced:
DrawMeshInstanced需要在 CPU 端传递一个固定数量的矩阵数组(最大 1023 个实例,超了会加批次),由 CPU 统一调度绘制。
DrawMeshInstancedIndirect是由 GPU 端驱动实例数量,可以动态控制实例数,且不受 1023 个实例的限制,效率更高,尤其适合实例数量变化或复杂实例计算场景。
(2)完整代码
C#控制脚本
using UnityEngine;using UnityEngine.Rendering;[ExecuteAlways]public class ShellFurController_DrawInstancedIndirect : MonoBehaviour{ [Header(\"壳层数(动态可调)\")] public int shellCount = 32; private Mesh mesh; private Material material; private ComputeBuffer argsBuffer; private ComputeBuffer shellIndexBuffer; private int lastShellCount = -1; private Camera mainCam; void Start() { mesh = GetComponent().sharedMesh; material = GetComponent().sharedMaterial; mainCam = Camera.main; InitBuffers(); // 初次初始化 } void Update() { /*Camera cam = Camera.current; if (!Application.isPlaying && UnityEditor.SceneView.currentDrawingSceneView != null) cam = UnityEditor.SceneView.currentDrawingSceneView.camera;*/ if (shellCount != lastShellCount || argsBuffer == null || shellIndexBuffer == null) { InitBuffers(); lastShellCount = shellCount; } Graphics.DrawMeshInstancedIndirect( mesh, 0, material, new Bounds(transform.position, Vector3.one * 100f), argsBuffer, 0, null, ShadowCastingMode.On, true, gameObject.layer, mainCam,//这里绑的是Game窗口里的主相机,只会在Game窗口中渲染,场景视图中会不渲染,可以替换成上方的Scene窗口里的cam LightProbeUsage.Off ); } void InitBuffers() { // 清理旧 buffer argsBuffer?.Release(); shellIndexBuffer?.Release(); // 初始化 mesh/material mesh ??= GetComponent().sharedMesh; material ??= GetComponent().sharedMaterial; // 创建 DrawMeshInstancedIndirect 参数 buffer uint[] args = new uint[5] { (uint)mesh.GetIndexCount(0), (uint)shellCount, (uint)mesh.GetIndexStart(0), (uint)mesh.GetBaseVertex(0), 0 }; argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments); argsBuffer.SetData(args); // 创建 shell index buffer,传给 Shader float[] shellIndices = new float[shellCount]; for (int i = 0; i < shellCount; i++) shellIndices[i] = i; shellIndexBuffer = new ComputeBuffer(shellCount, sizeof(float)); shellIndexBuffer.SetData(shellIndices); // 设置材质参数 material.SetBuffer(\"_ShellIndexBuffer\", shellIndexBuffer); material.SetInt(\"_ShellCount\", shellCount); } void OnDisable() { argsBuffer?.Release(); shellIndexBuffer?.Release(); argsBuffer = null; shellIndexBuffer = null; }#if UNITY_EDITOR void OnValidate() { lastShellCount = -1; // 强制重建 buffer }#endif}
UnlitShader和上面使用DrawMeshInstanced绘制的保持一致
Shader \"Unlit/Base_Shell_Fur_GpuIns\"{ Properties { //毛发噪声纹理 _FurTex(\"Fur Texture\", 2D) = \"white\" {} //毛发根部颜色 [HDR]_RootColor(\"RootColor\",Color)=(0,0,0,1) //毛发末端颜色 [HDR]_FurColor(\"FurColor\",Color)=(1,1,1,1) //凹凸纹理 _BumpTex(\"Normal Map\", 2D) = \"bump\" {} //凹凸强度 _BumpIntensity(\"Bump Intensity\",Range(0,2))=1 //毛发长度 _FurLength(\"Fur Length\", Float) = 0.2 //壳层总数 _ShellCount(\"Shell Count\", Float) = 16 //外发光颜色 [HDR]_FresnelColor(\"Fresnel Color\", Color) = (1,1,1,1) //菲涅尔强度 _FresnelPower(\"Fresnel Power\", Float) = 5 //噪声剔除阈值 _FurAlphaPow(\"Fur AlphaPow\", Range(0,6)) = 1 } SubShader { Tags { \"Queue\"=\"Transparent\" \"RenderType\"=\"Transparent\" } LOD 200 ZWrite Off Cull Back Blend SrcAlpha OneMinusSrcAlpha Pass { Name \"FurPass\" Tags { \"LightMode\" = \"UniversalForward\" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include \"Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl\" TEXTURE2D(_FurTex); SAMPLER(sampler_FurTex); float4 _FurTex_ST; TEXTURE2D(_BumpTex); SAMPLER(sampler_BumpTex); float4 _BumpTex_ST; float _FurLength; float _ShellCount; float _WindStrength; float4 _FresnelColor; float _FresnelPower; float _FurAlphaPow; float4 _RootColor; float4 _FurColor; StructuredBuffer _ShellIndexBuffer; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; uint id: SV_InstanceID; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 viewDir : TEXCOORD1; float3 worldNormal : TEXCOORD2; float shellIndex : TEXCOORD3; }; v2f vert(appdata v) { float shellIndex = _ShellIndexBuffer[v.id]; float shellFrac = shellIndex / _ShellCount; v2f o; float3 worldNormal = TransformObjectToWorldNormal(v.normal); float3 worldPos = TransformObjectToWorld(v.vertex.xyz); float windOffset = sin(worldPos.x * 5 + _Time.y * 2 + shellIndex) * _WindStrength; worldPos += worldNormal * (_FurLength * shellFrac + windOffset); o.pos = TransformWorldToHClip(worldPos); o.uv = TRANSFORM_TEX(v.uv, _FurTex); o.viewDir = normalize(_WorldSpaceCameraPos - worldPos); o.worldNormal = worldNormal; o.shellIndex = shellIndex; return o; } half4 frag(v2f i) : SV_Target { half4 col = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv); float shellFrac = i.shellIndex / _ShellCount; float mask = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv).r; float alpha= saturate(mask - pow(shellFrac,_FurAlphaPow)); float3 bump = UnpackNormal(SAMPLE_TEXTURE2D(_BumpTex, sampler_BumpTex, i.uv)); float3 normalWS = normalize(i.worldNormal + bump * 0.5); float fresnel = pow(1.0 - saturate(dot(i.viewDir, normalWS)), _FresnelPower); //AO col*=lerp(_RootColor,_FurColor,shellFrac); col.a = alpha; col.rgb += _FresnelColor.rgb * fresnel * alpha; return col;} ENDHLSL } }}
(3)效果展示
(4)与DrawInstanced性能对比
这里我分别使用Graphics.DrawInstanced()和Graphics.DrawInstancedIndirect() 绘制100000层实例。并通过Stats面板比较运行时的性能。
下图为Graphics.DrawInstancedIndirect() 运行时Stats面板
下图为Graphics.DrawInstanced() 运行时Stats面板
(*)Stats面板上反映性能的指标
(1)CPU: main (ms) / FPS: 直接反映主线程和整体游戏循环的流畅度。主线程耗时越低越好。(2)Batches: 直接反映 Draw Call 数量,越低越好。
(3)Tris / Verts: 直接反映 GPU 需要处理的几何体数量,越低越好。这是这次对比中最关键的性能差异点。
(4)render thread (ms):反映了渲染命令提交的效率。
(5)SetPass calls: 反映了 GPU 状态切换开销。
经过对比我们可以发现,Graphics.DrawInstancedIndirect()之所以能实现“极致性能”,不仅仅因为它减少了 CPU 的 Draw Calls (Batches),更因为它在几何体生成和渲染数量上实现了巨大的优化,将渲染的三角形和顶点数量从千万级别降低到了千级别。
四.总结
OK,至此我们实现了基本的Shell毛发效果并分别使用两种实例绘制API优化了性能,下一期我们将会完善渲染效果,加入漫反射,kajiya高光和阴影偏移,移动动画和风力扰动效果,最终得到生动的毛发效果,感兴趣的话可以先收藏一手哟!
本篇完