毛发,无论是人类的头发、动物的皮毛,还是奇幻生物的绒毛,都是构成生命感和真实感不可或缺的元素。它对光线的独特散射、吸收和反射,赋予了物体柔软、蓬松、有生命力的质感。它不仅仅是让角色看起来更“毛茸茸”那么简单,更是通向极致真实感和视觉沉浸感的关键一步。本期我们来在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;xxxxxxfloat shellIndex = _ShellIndex;float shellFrac = shellIndex / _ShellCount;float3 worldNormal = TransformObjectToWorldNormal(v.normal);float3 worldPos = TransformObjectToWorld(v.vertex.xyz);worldPos += worldNormal * (_FurLength * shellFrac);xxxxxxreturn o;
}
(3)噪声图透明度递减
half4 frag(v2f i) : SV_Target{xxxxxxfloat mask = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv).r;float alpha= saturate(mask - pow(shellFrac,_FurAlphaPow));xxxxxxxxxcol.a = alpha;xxxreturn 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 200ZWrite OffCull BackBlend SrcAlpha OneMinusSrcAlphaPass{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);//AOcol*=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<MeshRenderer>().sharedMaterial;mesh = GetComponent<MeshFilter>().sharedMesh;if (!material.enableInstancing){Debug.LogWarning("Fur material must enable GPU Instancing");}// 所有实例使用同一个 props,用数组传 ShellIndexmatrices = 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 200ZWrite OffCull BackBlend SrcAlpha OneMinusSrcAlphaPass{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);//AOcol*=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窗口里的camLightProbeUsage.Off);}void InitBuffers(){// 清理旧 bufferargsBuffer?.Release();shellIndexBuffer?.Release();// 初始化 mesh/materialmesh ??= GetComponent<MeshFilter>().sharedMesh; material ??= GetComponent<MeshRenderer>().sharedMaterial;// 创建 DrawMeshInstancedIndirect 参数 bufferuint[] 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,传给 Shaderfloat[] 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_EDITORvoid 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 200ZWrite OffCull BackBlend SrcAlpha OneMinusSrcAlphaPass{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);//AOcol*=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高光和阴影偏移,移动动画和风力扰动效果,最终得到生动的毛发效果,感兴趣的话可以先收藏一手哟!
本篇完