【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,没必要一味的追求“正确”吧。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 今天四嫂都六十六了 老陈家四十多口人 都在祝福 四嫂 漂亮善良厚重 西山坡造就了 美丽善良的姑娘 最初的美我们懂得...
    云中漫步无穷尽阅读 262评论 0 4
  • 越是无能的人。 越是喜欢坑害对他好的人。 坑的他周围的人伤心的离开。 最后,他 周围就剩下跟他一样垃圾的人。 越是...
    2cf888c015e6阅读 236评论 0 0
  • 无所事事,电脑卡,又不想开电视,想表达点什么?又怕暴露些什么真的有点像当初 少年不愁滋味,为赋新词强说愁的时节,有...
    章渺渺阅读 143评论 1 0
  • 1.背景:作为一名测试,在没有apk的情况下如何测试移动端的功能呢,拉取Git代码,本地启服务,在自己本机测试 2...
    软件测试笔记阅读 2,156评论 0 1