毛发,无论是人类的头发、动物的皮毛,还是奇幻生物的绒毛,都是构成生命感和真实感不可或缺的元素。它对光线的独特散射、吸收和反射,赋予了物体柔软、蓬松、有生命力的质感。它不仅仅是让角色看起来更“毛茸茸”那么简单,更是通向极致真实感和视觉沉浸感的关键一步。本期我们来在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<MeshRenderer>().sharedMaterial;
mesh = GetComponent<MeshFilter>().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的使用可以移步我的另一篇博客:
(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<MeshRenderer>().sharedMaterial;
mesh = GetComponent<MeshFilter>().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<float> _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)技术要点
1.区别于DrawMeshInstanced
DrawMeshInstanced需要在 CPU 端传递一个固定数量的矩阵数组(最大 1023 个实例,超了会加批次),由 CPU 统一调度绘制。
DrawMeshInstancedIndirect是由 GPU 端驱动实例数量,可以动态控制实例数,且不受 1023 个实例的限制,效率更高,尤其适合实例数量变化或复杂实例计算场景。
2.
(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<MeshFilter>().sharedMesh;
material = GetComponent<MeshRenderer>().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<MeshFilter>().sharedMesh;
material ??= GetComponent<MeshRenderer>().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<float> _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高光和阴影偏移,移动动画和风力扰动效果,最终得到生动的毛发效果,感兴趣的话可以先收藏一手哟!
本篇完