tajourney tajourney
  • 首页
  • 渲染
    • PBR
    • NPR
  • 引擎
    • Unity
    • UE
  • DCC
    • Houdini
    • PhotoShop
    • Blender
  • 优化
  • 工具
  • 杂谈
    • 游戏
  • 友链
  • 关于
    • About Me
    • 网站公告
    • 维护记录
    • QA
  • 注册
  • 登录
首页 › 渲染 › Unity URP - Shell GS Shader

Unity URP - Shell GS Shader

糯米
3年前渲染阅读 2,977

前言

Shells Shader是我大概半年前实现的一个小Demo,本篇实现综合借鉴了许多内外网文章,详情见引用列表,印象中最深的是王者分享的阿狸那篇文章,不过本篇是以另一种方式实现的。Shells Shader用途很多,主要用于毛发/草地/地毯材质的渲染(但是毛发如果用纯Shell表现并不好,需要夹杂Fin,纯Shell形式表现地毯会相对好些),本篇会主要讲地毯形式渲染,并会扩展补充一些Shells Shader的其他用途(比如Shells Shader体积云,见WorkingFat前辈写的相关文章)。

这篇文章我会边重写一遍Shader(untiy2021.2+,urp12.0+),边回忆一下当时的思路。

效果

Todo

  • 自定义阴影
  • 支持GPU Instance
  • Shell 体积云

什么是Shells

Shells就是沿几何体的法线方向生成 n 层网格并且每次将网格顶点沿法线挤出并裁剪(有点类似于外描线中顶点外扩的方式,所以顶点外扩的部分优化方法对Shells Shader同样适用),总共生成n 层网格。每层网格都会适度的采样MainTex。Shells Shader其实就是实现Shells方式的GS(Geometry Shader),类似于下图(PS鼠绘灵魂画手)。

实线部分表示Geometry Surface,虚线就是Shells.

实现

1.

对于王者分享的那篇制作方案本身我就不细讲了,因为我的实现方式不同于那篇分享,有很多文章非常详细的讲解了那篇分享的实现方式,比如:https://zhuanlan.zhihu.com/p/87443345。王者那篇分享里原理相关部分讲的非常详细,但是这种方式生成的毛发比较凌乱,而且多Pass性能消耗问题很大。本篇实现只需要通过一个Pass就可以实现Shells的几何效果,当然后面优化上加上深度、阴影共3个Pass。

因为我目前只在pc下看实现的效果,所以去除了gl版本的编译,如果想在移动端跑的同学可以在我写的基础上优化(但是手机上要想跑GS性能就。。。)

#pragma exclude_renderers gles gles3 glcore

下面的分开讲

1.Pass 1

首先要生成Shells,我们需要确定两个参数,一个是layer num,表示生成多少层shell,一个是layer space,表示层间距。

[IntRange]_LayerNum("Layer Number",Range(1,50)) = 15
_LayerSpace("Layer Space",Range(0.0, 10.0)) = 1.0

我们需要在管线的vs、gs、fs三个阶段里传递数据,所以先定义三种结构体,Pass定义大致如下

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma vertex vert
            #pragma require geometry
            #pragma geometry geom 
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"

            struct appdata
            {
                //todo..
            };

            struct v2g
            {
                //todo..
            };

            //已经在UnlitInput.hlsl中定义过,不需要重定义 
            //TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); float4 _BaseMap_ST;

            v2g vert(appdata v)
            {
                v2g o = (v2g)0;
                //todo..
                return o;
            }

            struct g2f
            {
                //todo..
            };

            [maxvertexcount(90)]
            void geom(triangle v2g i[3], inout TriangleStream stream)
            {
               //todo..
            }

            float4 frag(g2f i) : SV_Target
            {
                //todo..

                float4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);
                float3 color = baseColor;
                return float4(color, 1.0);
            }

            ENDHLSL
        }

可能会有人不懂[maxvertexcount(90)]这个标签的作用(90这里我随便先写的),简单的讲就声明着色器调用将输出的最大顶点数。这里就顺便说下DX11的GS和GS Instance吧:因为DX11中默认声明的instanceCount为32(bit),即1024;在GS Instance中每个Instance * 输出通道不能超过1024,比如假设我的g2f定义如下:

                float2 uv : TEXCOORD0;
                float4 positionCS : SV_POSITION;
                float4 positionWS : TEXCOORD1;
                float3 normalWS : TEXCOORD2;
                float3 tangentWS : TEXCOORD3;
                float  layer : TEXCOORD4;

共有17(bit)通道,那么我能够定义的maxvertexcount最大值不能超过1024/17 = 60.235 -> 60,即[maxvertexcount(60)];记住我这个算法就足够了,其实也不需要太过纠结原理,不影响使用。

如果还有疑问参考下面的几个链接的资料吧:
1.https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-geometry-shader

2.https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#inst_vGSInstanceIDDCL

如何生成Shells呢?

首先我们想到的重要步骤是在GS中生成Shells,即使顶点沿着物体的N方向偏移,假如我们需要生成Index层Shell,那么第一层Shell我们移动 0 * N * _LayerSpace,第二层移动 1 * N * _LayerSpace,....,以此类推:(对,就是绘制outline最常用的顶点外扩的方式,只不过是在GS中写的,所以顶点外扩的优化方案对此同样适用,这里就不细讲了)

float3 positionWS = vertexInput.positionWS + normalInput.normalWS * (_LayerSpace * Index);

但是如果只是简单的移动顶点肯定不行,我们需要表现合适的密度,因为我这里模拟的是地毯毛绒的密度,所以可以用一张白噪声来表现(单通道就够),给定一个阈值,低于阈值并且LayerNum>0的部分我们自定义进行裁剪。

                float cut = SAMPLE_TEXTURE2D(_FurNoise, sampler_FurNoise, i.uv2).r;
                if (i.layer > 0.0 && cut< _CutThreshold) discard;

效果如下(分别是调整_CutThreshold、layerNum、layerSpace的效果),完整代码见后

Shader "Shell"
{
    Properties
    {
        _BaseMap("BaseMap", 2D) = "white" {}
        _FurNoise("FurNoise", 2D) = "white" {}

        [IntRange]_LayerNum("Layer Number",Range(1,50)) = 15
        _LayerSpace("Layer Space",Range(0.0, 0.1)) = 0.001
        _CutThreshold("_CutThreshold", Range(0.0, 1.0)) = 0.1
    }

    SubShader
    {
        Tags 
        { 
            "RenderType"="Opaque"
            "RenderPipeline" = "UniversalPipeline"  
        }

        LOD 100

        ZWrite On
        Cull Back

        // Convention:
        // space at the end of the variable name
        // WS: world space
        // RWS: Camera-Relative world space. A space where the translation of the camera have already been substract in order to improve precision
        // VS: view space
        // OS: object space
        // CS: Homogenous clip spaces
        // TS: tangent space
        // TXS: texture space

        // use capital letter for regular vector, vector are always pointing outward the current pixel position (ready for lighting equation)
        // capital letter mean the vector is normalize, unless we put 'un' in front of it.
        // V: View vector  (no eye vector)
        // L: Light vector
        // N: Normal vector
        // H: Half vector

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma vertex vert
            #pragma require geometry
            #pragma geometry geom 
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"

            int _LayerNum;
            float _LayerSpace;
            float _CutThreshold;
            //已经在UnlitInput.hlsl中定义过,不需要重定义 
            //TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); float4 _BaseMap_ST;
            TEXTURE2D(_FurNoise); SAMPLER(sampler_FurNoise);float4 _FurNoise_ST;

            struct appdata
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float4 tangentOS : TANGENT;
                float2 uv : TEXCOORD0;
            };

            struct v2g
            {
                float2 uv : TEXCOORD0;
                float4 positionOS : SV_POSITION;
                float3 normalOS : TEXCOORD1;
                float4 tangentOS : TEXCOORD2;
            };

            v2g vert(appdata v)
            {
                v2g o = (v2g)0;
                o.positionOS = v.positionOS;
                o.normalOS = v.normalOS;
                o.tangentOS = v.tangentOS;
                o.uv = v.uv;
                return o;
            }

            struct g2f
            {
                float2 uv : TEXCOORD0;
                float2 uv2 : TEXCOORD1;
                float4 positionCS : SV_POSITION;
                float  layer : TEXCOORD3;
            };

            //GeometryShader
            [maxvertexcount(96)]
            void geom(triangle v2g i[3], inout TriangleStream stream)
            {
                [loop] for (float j = 0; j < _LayerNum; ++j)
                {
                    [unroll] for (float k = 0; k < 3; ++k)
                    {
                        g2f o = (g2f)0;
                        //pos and N 
                        VertexPositionInputs vertexInput = GetVertexPositionInputs(i[k].positionOS.xyz);
                        VertexNormalInputs normalInput = GetVertexNormalInputs(i[k].normalOS, i[k].tangentOS);
                        //沿顶点偏移
                        float3 positionWS = vertexInput.positionWS + normalInput.normalWS * (_LayerSpace * j);
                        o.positionCS = TransformWorldToHClip(positionWS);
                        //uv
                        o.uv = TRANSFORM_TEX(i[k].uv, _BaseMap);
                        o.uv2 = TRANSFORM_TEX(i[k].uv, _FurNoise);
                        //layer
                        o.layer = (float)j/_LayerNum;
                        //append data
                        stream.Append(o);
                    }
                    stream.RestartStrip();
                }
            }

            float4 frag(g2f i) : SV_Target
            {
                float cut = SAMPLE_TEXTURE2D(_FurNoise, sampler_FurNoise, i.uv2).r;
                if (i.layer > 0.0 && cut< _CutThreshold) discard;
                float4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);
                return color;
            }

            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode" = "DepthOnly" }

            ZWrite On
            ColorMask 0

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma vertex DepthOnlyVertex
            #pragma fragment DepthOnlyFragment
            #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
            ENDHLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags {"LightMode" = "ShadowCaster" }

            ZWrite On
            ZTest LEqual
            ColorMask 0

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma target 4.5
            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment
            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
            ENDHLSL
        }
    }
}

为了体验毛茸茸的感觉,我们需要把间距拉小,层数适中。标签[maxvertexcount(N)]来定义几何着色器单次调用输出的最大顶点个数,这个输出顶点个数应当是输出类型对应的顶点数的整数倍。因为我们裁剪中丢弃了部分顶点,所以需要在return前使用stream.RestartStrip()来保证输出流的格式正确。

不过目前这个效果一点也不好,只能看到一个基础形状,我们需要在此基础上继续优化我们的shader:

1.添加PBR渲染下的光照效果

这里我偷懒就使用Unity官方写好的URP Lit了,详情可以参考URP ShaderLab的Lit.Shader源码。这里我参照的是Metallic workflow。

Reference Chart for Metallic settings

本来花费了一个星期整理了一下Unity urp的新PBR(其实和之前没区别),以及URP12.0之后添加的新特性(下面代码中的一些用不到的Key都没有删,为了方便自己后面扩展),但是再放在这里讲篇幅就太太太长了,等我整理一篇新的吧。这里就简单粘贴一下代码(可以先参照URP的Lit.shader理解一些新特性)

Shader "Shell"
{
    Properties
    {
        [Header(Base)][Space]
        [MainColor] _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1)
        _AmbientColor("Ambient Color", Color) = (0.0, 0.0, 0.0, 1)
        _BaseMap("Albedo", 2D) = "white" {}
        _FurScale("Fur Scale", Range(0.0, 10.0)) = 1.0
        _FurNoise("FurNoise", 2D) = "white" {}
        _Occlusion("Occlusion", Range(0.0, 1.0)) = 0.5
        [IntRange]_LayerNum("Layer Number",Range(1,50)) = 15
        _LayerSpace("Layer Space",Range(0.0, 0.1)) = 0.001
        _CutThreshold("_CutThreshold", Range(0.0, 1.0)) = 0.1

        [Space][Header(UnityPBR)][Space]
        [Gamma]_Metallic("Metallic", Range(0.0, 1.0)) = 0.5
        _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5
        [Normal] _NormalMap("Normal", 2D) = "bump" {}
        _NormalScale("Normal Scale", Range(0.0, 2.0)) = 1.0
        [Header(Lighting)][Space]
        _RimLightPower("Rim Light Power", Range(1.0, 20.0)) = 6.0
        _RimLightIntensity("Rim Light Intensity", Range(0.0, 1.0)) = 0.5
    }

    SubShader
    {
        Tags 
        { 
            "RenderType"="Opaque"
            "RenderPipeline" = "UniversalPipeline"  
        }

        LOD 100

        ZWrite On
        Cull Back

        // Convention:
        // space at the end of the variable name
        // WS: world space
        // RWS: Camera-Relative world space. A space where the translation of the camera have already been substract in order to improve precision
        // VS: view space
        // OS: object space
        // CS: Homogenous clip spaces
        // TS: tangent space
        // TXS: texture space

        // use capital letter for regular vector, vector are always pointing outward the current pixel position (ready for lighting equation)
        // capital letter mean the vector is normalize, unless we put 'un' in front of it.
        // V: View vector  (no eye vector)
        // L: Light vector
        // N: Normal vector
        // H: Half vector

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM

            // Universal Pipeline keywords
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
            #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
            #pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING
            #pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION
            #pragma multi_compile_fragment _ _SHADOWS_SOFT
            #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
            #pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3
            #pragma multi_compile_fragment _ _LIGHT_LAYERS
            #pragma multi_compile_fragment _ _LIGHT_COOKIES
            #pragma multi_compile _ _CLUSTERED_RENDERING

            // -------------------------------------
            // Unity defined keywords
            #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
            #pragma multi_compile _ SHADOWS_SHADOWMASK
            #pragma multi_compile _ DIRLIGHTMAP_COMBINED
            #pragma multi_compile _ LIGHTMAP_ON
            #pragma multi_compile _ DYNAMICLIGHTMAP_ON
            #pragma multi_compile_fog
            #pragma multi_compile_fragment _ DEBUG_DISPLAY

            #pragma exclude_renderers gles gles3 glcore
            #pragma vertex vert
            #pragma require geometry
            #pragma geometry geom 
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"

            int _LayerNum;
            float _LayerSpace;
            float _CutThreshold;
            float _Occlusion;
            float _FurScale;
            float _NormalScale;
            float _RimLightPower;
            float _RimLightIntensity;
            float _AmbientColor;
            //已经在LitInput.hlsl中定义过,不需要重定义 
            //TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); float4 _BaseMap_ST;
            TEXTURE2D(_FurNoise); SAMPLER(sampler_FurNoise);float4 _FurNoise_ST;
            TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap);float4 _NormalMap_ST;

            struct appdata
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float4 tangentOS : TANGENT;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2g
            {
                float2 uv : TEXCOORD0;
                float4 positionOS : SV_POSITION;
                float3 normalOS : TEXCOORD1;
                float4 tangentOS : TEXCOORD2;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2g vert(appdata v)
            {
                v2g o = (v2g)0;
                o.positionOS = v.positionOS;
                o.normalOS = v.normalOS;
                o.tangentOS = v.tangentOS;
                o.uv = v.uv;
                return o;
            }

            struct g2f
            {
                float2 uv : TEXCOORD0;
                float4 positionCS : SV_POSITION;
                float4 positionWS : TEXCOORD1;
                float3 normalWS : TEXCOORD2;
                float3 tangentWS : TEXCOORD3;
                float  layer : TEXCOORD4;
                DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 5);
                float4 fogFactorAndVertexLight : TEXCOORD6; // x: fogFactor, yzw: vertex light
                UNITY_VERTEX_OUTPUT_STEREO
            };

            //GeometryShader
            //maxvertexcount
            [maxvertexcount(40)]
            void geom(triangle v2g i[3], inout TriangleStream stream)
            {
                [loop] for (float j = 0; j < _LayerNum; ++j)
                {
                    [unroll] for (float k = 0; k < 3; ++k)
                    {
                        g2f o = (g2f)0;
                        //pos and N 
                        VertexPositionInputs vertexInput = GetVertexPositionInputs(i[k].positionOS.xyz);
                        VertexNormalInputs normalInput = GetVertexNormalInputs(i[k].normalOS, i[k].tangentOS);
                        o.normalWS = normalInput.normalWS;
                        o.tangentWS = normalInput.tangentWS;
                        //vertex offset
                        o.positionWS.xyz = vertexInput.positionWS + normalInput.normalWS * (_LayerSpace * j);
                        o.positionCS = TransformWorldToHClip(o.positionWS);
                        //uv
                        o.uv = TRANSFORM_TEX(i[k].uv, _BaseMap);
                        //o.uv2 = TRANSFORM_TEX(i[k].uv, _FurNoise);
                        //layer
                        o.layer = (float)j/_LayerNum;
                        //Lighting
                        float3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS);
                        float fogFactor = ComputeFogFactor(vertexInput.positionCS.z);
                        o.fogFactorAndVertexLight = float4(fogFactor, vertexLight);
                        OUTPUT_LIGHTMAP_UV(i.lightmapUV, unity_LightmapST, o.lightmapUV);
                        OUTPUT_SH(o.normalWS.xyz, o.vertexSH);
                        //append data
                        stream.Append(o);
                    }
                    stream.RestartStrip();
                }
            }

            void ApplyRimLight(inout float3 color, float3 posWS, float3 viewDirWS, float3 normalWS)
            {
                float VDotN = abs(dot(viewDirWS, normalWS));
                float normalFactor = pow(abs(1.0 - VDotN), _RimLightPower);

                Light light = GetMainLight();
                float LDotV = dot(light.direction, viewDirWS);
                float intensity = pow(max(-LDotV , 0.0), _RimLightPower);
                intensity *= _RimLightIntensity * normalFactor;
            #if defined(MAIN_LIGHT_CALCULATE_SHADOWS)
                float4 shadowCoord = TransformWorldToShadowCoord(posWS);
                intensity *= MainLightRealtimeShadow(shadowCoord);
            #endif 
                color += intensity * light.color;

            #ifdef _ADDITIONAL_LIGHTS
                int additionalLightsCount = GetAdditionalLightsCount();
                for (int i = 0; i < additionalLightsCount; ++i)
                {
                    int index = GetPerObjectLightIndex(i);
                    Light light = GetAdditionalPerObjectLight(index, posWS);
                    float LDotV = dot(light.direction, viewDirWS);
                    float intensity = max(-LDotV , 0.0);
                    intensity *= _RimLightIntensity * normalFactor;
                    intensity *= light.distanceAttenuation;
            #if defined(MAIN_LIGHT_CALCULATE_SHADOWS)
                    //support Point Light shadows
                    // returns 0.0 if position is in light's shadow
                    // returns 1.0 if position is in light
                    intensity *= AdditionalLightRealtimeShadow(index, posWS, light.direction);
            #endif 
                    color += intensity * light.color;
                }
            #endif
            }
            
            float4 frag(g2f i) : SV_Target
            {
                float2 uv2 = i.uv / _BaseMap_ST.xy * _FurScale;
                float cut = SAMPLE_TEXTURE2D(_FurNoise, sampler_FurNoise, uv2).r;
                if (i.layer > 0.0 && cut< _CutThreshold) discard;

                float3 viewDirWS = SafeNormalize(GetCameraPositionWS() - i.positionWS);
                float3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uv2), _NormalScale);
                float3 bitangent = SafeNormalize(viewDirWS.y * cross(i.normalWS, i.tangentWS));
                float3 normalWS = SafeNormalize(TransformTangentToWorld(
                    normalTS, 
                    float3x3(i.tangentWS, bitangent, i.normalWS)));

                SurfaceData surfaceData = (SurfaceData)0;
                InitializeStandardLitSurfaceData(i.uv, surfaceData);
                surfaceData.occlusion = lerp(1.0 - _Occlusion, 1.0, i.layer);
                surfaceData.albedo *= surfaceData.occlusion;

                InputData inputData = (InputData)0;
                inputData.positionWS = i.positionWS;
                inputData.normalWS = normalWS;
                inputData.viewDirectionWS = viewDirWS;
            #if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF)
                inputData.shadowCoord = TransformWorldToShadowCoord(i.positionWS);
            #else
                inputData.shadowCoord = float4(0, 0, 0, 0);
            #endif
                inputData.fogCoord = i.fogFactorAndVertexLight.x;
                inputData.vertexLighting = i.fogFactorAndVertexLight.yzw;
                inputData.bakedGI = SAMPLE_GI(i.lightmapUV, i.vertexSH, normalWS);

                float4 color = UniversalFragmentPBR(inputData, surfaceData);
                //RimLight 
                ApplyRimLight(color.rgb, i.positionWS, viewDirWS, normalWS);
                //float4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);
                color.rgb += _AmbientColor;
                color.rgb = MixFog(color.rgb, inputData.fogCoord);
                return color;
            }

            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode" = "DepthOnly" }

            ZWrite On
            ColorMask 0

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma vertex DepthOnlyVertex
            #pragma fragment DepthOnlyFragment
            #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
            ENDHLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags {"LightMode" = "ShadowCaster" }

            ZWrite On
            ZTest LEqual
            ColorMask 0

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma target 4.5
            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment
            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
            ENDHLSL
        }
    }
}

效果图如下:

(1)PBR(Metallic workflow)

_Smoothness = 0.5
 
 
_Smoothness = 0.2
 

(2)高性能 | Only One DC for Gemotry

(3)支持多光照/多点光源

总览(添加后处理等):

Unity URP - Shell GS Shader-tajourney

2.更加风格化的细节

通常毛绒距离表面越远(密度)越细,意味着裁剪程度越大,所以我们做一下处理:

float cut = SAMPLE_TEXTURE2D(_FurNoise, sampler_FurNoise, i.uv2).r * (1.0 - i.layer);

3.顶点动画

根据Shell层级设置不同的偏移值

            float3 shellAnimation(int index,float3 posOS)
            {
                float moveParam = pow(abs((float)index / _LayerNum), _BaseMove.w);
                float3 windAngle = _Time.w * _WindFreq.xyz;
                float3 windMove = moveParam * _WindMove.xyz * sin(windAngle + posOS * _WindMove.w);
                float3 move = moveParam * _BaseMove.xyz + windMove;
                return move;
            }

可能还需要做的优化

LOD

如果是草地这种要写LOD,写更好的裁剪方案

GPU Instancing

支持GPU Instancing,可以支持大世界铺满这种,一般也是绘制草地才需要

Tessellation

利用曲面细分平滑,详见 https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/

Mesh shader(DirectX12 Ultimate /Vulkan)

采用新的shader pipeline,这个实际上是在硬件层面优化,我也在学习中,就不献丑了

Unity URP - Shell GS Shader-tajourney

引用&参考(不分引用先后)

1.https://mp.weixin.qq.com/s/aIWMEO5Qa2gNn2yCmnHbOg

2.https://xbdev.net/directx3dx/specialX/Fur/

3.https://zhuanlan.zhihu.com/p/378015611

4.https://developer.download.nvidia.com/whitepapers/2007/SDK10/FurShellsAndFins.pdf

5.https://docs.unity3d.com/Manual/StandardShaderMaterialParameterMetallic.html

6.https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-geometry-shader

7.http://walkingfat.com/%e5%9f%ba%e4%ba%8e%e8%a7%86%e5%b7%ae%e8%b4%b4%e5%9b%be%e7%9a%84%e6%af%9b%e5%8f%91%e6%95%88%e6%9e%9c/

8.http://walkingfat.com/bump-noise-cloud-3d%e5%99%aa%e7%82%b9gpu-instancing%e5%88%b6%e4%bd%9c%e5%9f%ba%e4%ba%8e%e6%a8%a1%e5%9e%8b%e7%9a%84%e4%bd%93%e7%a7%af%e4%ba%91/

9.http://walkingfat.com/%e5%9f%ba%e4%ba%8etessellation%e7%9a%84%e4%bd%93%e7%a7%af%e4%ba%91/

10.https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/

赞(5)

评论已关闭

搜索
近期文章
  • 终末地人物渲染(更新中) 2025年3月17日
  • Unity APV体素化光栅化实现 2025年2月5日
  • Unity 不同 ReflectionProbe 打断 Instancing 解决方案 2025年1月15日
  • Renderdoc 原神截帧记录 2024年8月22日
  • Houdini VAT:Vellum Cltoh笔记 2 2024年7月17日
归档
  • 2025年3月 (1)
  • 2025年2月 (1)
  • 2025年1月 (1)
  • 2024年8月 (1)
  • 2024年7月 (2)
  • 2024年1月 (1)
  • 2023年10月 (3)
  • 2023年9月 (4)
  • 2023年8月 (5)
  • 2023年7月 (4)
  • 2023年6月 (4)
  • 2023年5月 (1)
  • 2023年4月 (3)
  • 2023年3月 (11)
  • 2023年2月 (11)
  • 2023年1月 (1)
  • 2022年7月 (1)
  • 2022年6月 (2)
  • 2022年5月 (1)
  • 2022年4月 (1)
  • 2022年3月 (1)
  • 2022年2月 (1)
  • 2022年1月 (7)
  • 5
Copyright © 2022-2025 tajourney. Dev by nuomi 版权所有.
鲁ICP备19015245号
  • 首页
  • 渲染
    • PBR
    • NPR
  • 引擎
    • Unity
    • UE
  • DCC
    • Houdini
    • PhotoShop
    • Blender
  • 优化
  • 工具
  • 杂谈
    • 游戏
  • 友链
  • 关于
    • About Me
    • 网站公告
    • 维护记录
    • QA
糯米
日语学习中
68
文章
15
评论
44
喜欢