【Shader】 用SurfaceShader实现的NPR渲染

在URP中,SurfaceShader已经不再被支持了,学URP和HLSL去吧,别碰SurfaceShader了。

前言

  没错,又是老生常谈的NRP(非真实)渲染,或者说卡通渲染。最近事情不多,研究了一下Shader方面感兴趣的东西,首先试了一下用RenderTexture实现的实时MatCap,有点意思但找不到什么应用场景,暂时丢到一边了。然后不知道为什么又回到了NPR上面,于是就照着现在公司项目中的渲染效果为参考来编写了。最后的结果感觉完成度还可以,所以拿出来吹逼一下。

Outline 描边

  搞NPR的要点之一无疑就是描边,关于这个,有件事就想提一下。我写Shader都是用SurfaceShader编写。之前做描边碰到的最大的问题就是怎么实现描边,因为那时候看到不止一篇文章说SurfaceShader没有Pass,就非常僵硬。但是,这次我在搜索的时候居然发现原来SurfaceShader是可以加Pass的!所以描边问题自然就引刃而解了,感谢这篇文章。
  做法是传统的Inverted-Hull,代码是从Toony Colors Pro 2插件中抄来的,翻译了一下变成SurfaceShader可以用的代码。支持多种途径(通常、顶点颜色、切线、UV)来控制描边,支持固定宽度,支持通过一些参数微调,效果蛮玄学的,不过总比不能调要好。

不要用Cutout
有时制作头发或者衣服上有镂空之类,会采用Cutout的做法,在Shader里使用clip函数对像素进行剔除。但是想要使用Inverted-Hull来做描边的话,这种做法就不行了,因为描边是跟着mesh走的,剔除掉表面的像素并不会改变mesh的形状,描边就会出现问题。而且面片对这种描边方式本身就不友好,所以不要再用面片做头发了,请做出体积!

自定义光照:二刺螈

  二刺螈不需要渐变!一般的Lambert模型的光照计算公式为dot(normal, lightDir) * atten,最简单的做法——对其round一下就可以使明暗分离为两层了。

  某些情况下,仅仅两层的明暗关系可能不够用,所以使Shader还支持了Ramp贴图来对光照进行映射。可以自己制作不同的Ramp贴图来实现想要的效果。适当采用一些渐变也有着反锯齿的效果(下图中间)。
  其实这也是非常基础的操作,在Unity官方的Surface Shader Custom Lighting Example里就有示例。

五彩斑斓的黑

  暗部如果只是纯黑色就显得很闷了,现在日系插画都有着很漂亮的暗部颜色,所以增加了对暗部进行着色的功能,混色算法采用了PS图层混合模式的“滤色”模式。

Cel贴图

  研究公司项目里的角色渲染时,发现存在一张被广泛的使用的被称为Cel的贴图,用来控制阴影形状,有点法线贴图的意思。尝试反推了该贴图的用法,使用后可以在头发和衣服褶皱等地方看到明显的效果。

边缘光

这个很常见也很简单我就不多说了,总之在NPR中也是蛮必要的一种效果。

Stylized Highlight 风格化高光

  一开始用传统Blinn-Phong模型的高光算法,效果相当恶心,所幸找到了一个好用的轮子——风格化的高光。代码是从这里来的,我翻译了一下,然后添加了一个SpecMask贴图的功能——对于不需要显示高光的区域涂黑即可。
  友情提示:此效果不适合面数很低的模型。

关于反锯齿

  由于有外描边这种细线的存在,不进行反锯齿就很容易满屏幕狗牙,分辨率越低越明显,所以极力推荐采取一定的反锯齿措施。不管是MSAA还是后期处理的TAA或FXAA(在官方的PostProcessing包中就有),都会让画面观感明显变好,顺便再配合一些此类渲染必备的Bloom效果,就可以获得比较满意的画面了。

关于打光

  和一般的实时光照打光方式相同,推荐一个Directional Light即可。此外也会受环境光(Environment Lighting)影响, 可以在Lighting页面里调整。

完整代码

特性大致就是以上这些了,下面是完整的Shader代码。

// ----------一些参考----------
// http://www.ggxrd.com/Motomura_Junya_GuiltyGearXrd.pdf
// ----------------------------
Shader "Gypsum/Cel-Shading" {
    Properties {
        [Header(Culling)]
        [Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull Mode", Float) = 2
        
        [Space(5)]
        [Header(Base Color)]
        _Color ("Tint", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}

        [Header(Cel Shading Parameters)]
        _ShadowColor ("Shadow Color", Color) = (0,0,0,1)
        [Toggle(_ENABLE_RAMP)] _EnableRamp ("Enable Ramp", float) = 0.0
        _RampTex ("Ramp Map", 2D) = "white" {}
        _CelTex ("Cel Map", 2D) = "white" {}
        _CelOffset ("Cel Offset", Range(-1,1)) = 0

        [Space(5)]
        [Header(Rim Light)]
        [HDR] _RimColor ("Rim Color", Color) = (1,1,1,1)
        _RimPower ("Rim Power", Range(1,64)) = 8

        [Space(5)]
        [Header(Outline)]
        [KeywordEnum(REGULAR,VERTEXCOLOR,TANGENT,UV2)] _OutlineNormalMode ("Normal Mode", float) = 0.0
        [Toggle(_OUTLINECONSTWIDTH)] _OutlineConstWidth ("Constant Width", float) = 0.0
        _OutlineColor ("Color", Color) = (0, 0, 0, 1)
        _OutlineWidth ("Width", Range(0,5)) = 1.0
        [Toggle(_OUTLINEZSMOOTH)] _OutlineZSmooth ("Enable Z Correction", float) = 0.0
        _ZSmooth ("Z Correction", Range(-3.0,3.0)) = -0.5
        _Offset1 ("Z Offset", Float) = 0
        // _Offset2 ("Z Offset 2", Float) = 0 //似乎没什么作用所以没有启用

        [Space(5)]
        [Header(Specular)]
        [Toggle(_ENABLE_SPECULAR)] _EnableSpecular ("Enable", float) = 0.0
        [HDR] _SpecularColor ("Color", Color) = (1, 1, 1, 1)
        _SpecularMask ("Mask", 2D) = "white" {}
        _SpecularPower ("Shininess", Range(1, 100)) = 48
        _SpecularSegment ("Segment", Range(0, 1)) = 0.9
    }

    Subshader {
        Tags { "RenderType"="Opaque"}

        CGPROGRAM
        #pragma surface surf Cel addshadow
        #pragma shader_feature _ENABLE_SPECULAR
        #pragma shader_feature _ENABLE_RAMP

        sampler1D _RampTex;
        sampler2D _CelTex;
        sampler2D _MainTex;
        sampler2D _SpecularMask;

        fixed _CelOffset;
        fixed4 _ShadowColor;
        fixed4 _Color;
        fixed4 _RimColor;
        half _RimPower;
        half4 _SpecularColor;
        half _SpecularPower;
        fixed _SpecularSegment;

        // ----------一些颜色混合函数----------
        fixed Greyscale(fixed3 input)
        {
            return (input.r + input.g + input.b) / 3; 
        }
        fixed3 Blend_Multiply(fixed3 color0, fixed3 color1)
        {
            return color0 * color1;
        }
        fixed3 Blend_Overlay(fixed3 color0, fixed3 color1)
        {
            if(Greyscale(color0) <= 0.5)
            {
                return 2 * color0 * color1;
            }
            else
            {
                return 1 - 2 * ((1 - color0) * (1 - color1));
            }
        }
        fixed3 Blend_Screen(fixed3 color0, fixed3 color1)
        {
            return 1 - (1 - color0) * (1 - color1);
        }
        // ------------------------------------

        struct Input {
            float2 uv_MainTex;
            float3 viewDir;
            // fixed3 worldNormal;
            // float3 worldPos;
        };

        // 自定义一个SurfaceOutput
        struct SurfaceOutputCel
        {
            fixed3 Albedo;
            fixed3 Emission;
            float3 Normal;
            fixed Alpha;
            half2 UV; //在Lighting函数中贴图就需要传UV到Output中
            // fixed3 WorldNormal;
            // float3 WorldPos;
        };

        void surf(Input IN, inout SurfaceOutputCel o)
        {
            // Input to Output
            o.UV = IN.uv_MainTex;
            // o.WorldNormal = IN.worldNormal;
            // o.WorldPos = IN.worldPos;
            // Rim light
            fixed rim = dot(o.Normal, IN.viewDir);
            rim = (saturate(pow(1 - rim, _RimPower)));
            fixed3 finalRim = rim * _RimColor.rgb * _RimColor.a;
            o.Emission = finalRim;
            // Base Color
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        
        half4 LightingCel(SurfaceOutputCel s, half3 lightDir, half3 viewDir, half atten)
        {
            // ----------Stylized Highlights----------
            // https://github.com/candycat1992/NPR_Lab
            // ---------------------------------------
#ifdef _ENABLE_SPECULAR
            fixed3 worldNormal = normalize(s.Normal);
            fixed3 worldHalfDir = normalize(viewDir + lightDir);
            fixed spec = max(0, dot(worldNormal, worldHalfDir));
            spec = pow(spec, _SpecularPower);
            fixed w = fwidth(spec);
            if (spec < _SpecularSegment + w) {
                spec = lerp(0, _SpecularSegment, smoothstep(_SpecularSegment - w, _SpecularSegment + w, spec));
            } else {
                spec = _SpecularSegment;
            }
            half3 specular = spec * _SpecularColor.rgb * tex2D(_SpecularMask, s.UV);
#else
            fixed3 specular = 0;
#endif
            // ----------------------------------------
            // ----------Cel-Shading Lighting----------
            // ----------------------------------------
            half NdotL = dot(s.Normal, lightDir);
            half cel = lerp(fixed3(1,1,1), saturate(Greyscale(tex2D(_CelTex, s.UV) + _CelOffset)), dot(lightDir,s.Normal));
#ifdef _ENABLE_RAMP
            cel = tex1D(_RampTex, cel);
            half ramp = tex1D(_RampTex, saturate(atten * NdotL) * 0.5 + 0.5);
            half3 shadow = lerp(fixed3(1,1,1), Blend_Screen(fixed3(1,1,1) * saturate(ramp * cel), _ShadowColor.rgb), _ShadowColor.a);
#else
            half3 shadow = lerp(fixed3(1,1,1), Blend_Screen(fixed3(1,1,1) * saturate(round(NdotL * atten * cel)), _ShadowColor.rgb), _ShadowColor.a);
#endif
            half4 c;
            c.rgb = Blend_Screen(shadow * s.Albedo * _LightColor0, specular);
            c.a = s.Alpha;
            return c;
        }
        ENDCG
        // ----------Outline Pass----------
        // https://www.videopoetics.com/tutorials/pixel-perfect-outline-shaders-unity/#building-the-classic-outline-shader
        // https://assetstore.unity.com/packages/vfx/shaders/toony-colors-pro-2-8105
        // --------------------------------
        Pass {
            Cull Front
            Offset [_Offset1], 0 //[_Offset2]
            
            CGPROGRAM
            #include "UnityCG.cginc"
            #pragma multi_compile _OUTLINENORMALMODE_REGULAR _OUTLINENORMALMODE_VERTEXCOLOR _OUTLINENORMALMODE_TANGENT _OUTLINENORMALMODE_UV2
            #pragma shader_feature _OUTLINECONSTWIDTH
            #pragma shader_feature _OUTLINEZSMOOTH
            #pragma vertex Vertex
            #pragma fragment Fragment

            half _ZSmooth;
            half _OutlineWidth;
            half4 _OutlineColor;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            }; 
            
            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            v2f Vertex(a2v v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);

                //Correct Z artefacts
                #ifdef _OUTLINEZSMOOTH
                    float4 pos = float4(UnityObjectToViewPos(v.vertex), 1.0);
                    
                    #ifdef _OUTLINENORMALMODE_VERTEXCOLOR
                        //Vertex Color for Normals
                        float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, (v.color.xyz*2) - 1);
                    #elif _OUTLINENORMALMODE_TANGENT
                        //Tangent for Normals
                        float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.tangent.xyz);
                    #elif _OUTLINENORMALMODE_UV2
                        //UV2 for Normals
                        float3 normal;
                        //unpack uv2
                        v.uv2.x = v.uv2.x * 255.0/16.0;
                        normal.x = floor(v.uv2.x) / 15.0;
                        normal.y = frac(v.uv2.x) * 16.0 / 15.0;
                        //get z
                        normal.z = v.uv2.y;
                        //transform
                        normal = mul( (float3x3)UNITY_MATRIX_IT_MV, normal*2-1);
                    #else
                        float3 normal = mul( (float3x3)UNITY_MATRIX_IT_MV, v.normal);
                    #endif
                    
                    normal.z = -_ZSmooth;
                    
                    #ifdef _OUTLINECONSTWIDTH
                        //Camera-independent outline size
                        float dist = distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, v.vertex));
                        pos = pos + float4(normalize(normal),0) * _OutlineWidth * 0.01 * dist;
                    #else
                        pos = pos + float4(normalize(normal),0) * _OutlineWidth * 0.01;
                    #endif
                    
                #else

                    #ifdef _OUTLINENORMALMODE_VERTEXCOLOR
                        //Vertex Color for Normals
                        float3 normal = (v.color.xyz*2) - 1;
                    #elif _OUTLINENORMALMODE_TANGENT
                        //Tangent for Normals
                        float3 normal = v.tangent.xyz;
                    #elif _OUTLINENORMALMODE_UV2
                        //UV2 for Normals
                        float3 n;
                        //unpack uv2
                        v.uv2.x = v.uv2.x * 255.0/16.0;
                        n.x = floor(v.uv2.x) / 15.0;
                        n.y = frac(v.uv2.x) * 16.0 / 15.0;
                        //get z
                        n.z = v.uv2.y;
                        //transform
                        n = n*2 - 1;
                        float3 normal = n;
                    #else
                        float3 normal = v.normal;
                    #endif
                    
                    //Camera-independent outline size
                    #ifdef _OUTLINECONSTWIDTH
                        float dist = distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, v.vertex));
                        float4 pos =  float4(UnityObjectToViewPos(v.vertex + float4(normal, 0) * _OutlineWidth * 0.01 * dist), 1.0);
                    #else
                        float4 pos = float4(UnityObjectToViewPos(v.vertex + float4(normal, 0) * _OutlineWidth * 0.01), 1.0);
                    #endif
                #endif
                o.pos = mul(UNITY_MATRIX_P, pos);
                return o;
            }
            
            float4 Fragment (v2f IN) : COLOR
            {
                return _OutlineColor;
            }
            ENDCG
        }
    }
}

结语

  之前第一次看《罪恶装备Xrd》艺术风格讲解的时候,有种惊为天人的感觉,就一直想着自己什么时候也可以试着搞一下这类Shader。其实NPR都是一些很老的技术,好几年前就可以做到了,只是它的难点从来就不是技术。
  PPT里有几个点我认为讲得非常好:

  • 不只是一个Shader就完事,而是要构建整个工作流。(Not just a shader, but a whole workflow)
      他们的人物动画每一帧都是手K,而且不做补间,是为了追求“有限动画”的感觉。并且动画的每一帧都会对光照做针对性调整,还充斥着大量的形变缩放,以营造日式动画类似“金田系”作画的夸张透视效果。最后游戏能呈现出这样几乎没有破绽的2D效果,巨大的美术工作量的功不可没。试图用一个Shader就想让自己的游戏达到完美的风格化渲染效果,无疑是天真的。

  • 让美术决定效果,而不是数学公式。(Let the artist decide, not the math)
      确实有时候就会碰到这种情况——这个公式看起来更正确一点,但是效果很微妙;那种算法看起来很莫名其妙,但是效果很棒。所以该用哪种?可能大部分时候我们只需要表象正确就可以了,毕竟做游戏就少不了Trick,没必要一味的追求“正确”吧。

  以上是一些个人的小小感想。希望本文对你有用,再见。

你可能感兴趣的:(【Shader】 用SurfaceShader实现的NPR渲染)