[学习记录]Unity毛发渲染[UDRP]-Shell基础版

发布于:2025-07-04 ⋅ 阅读:(22) ⋅ 点赞:(0)

        毛发,无论是人类的头发、动物的皮毛,还是奇幻生物的绒毛,都是构成生命感和真实感不可或缺的元素。它对光线的独特散射、吸收和反射,赋予了物体柔软、蓬松、有生命力的质感。它不仅仅是让角色看起来更“毛茸茸”那么简单,更是通向极致真实感和视觉沉浸感的关键一步。本期我们来在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的使用可以移步我的另一篇博客:

[学习记录]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,用数组传 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高光和阴影偏移,移动动画和风力扰动效果,最终得到生动的毛发效果,感兴趣的话可以先收藏一手哟!

 

本篇完


网站公告

今日签到

点亮在社区的每一天
去签到