NPR——卡通渲染(一)

NPR——卡通渲染

本文的目的是系統的探討遊戲中的卡通渲染技術,以期深刻掌握卡通渲染中所用技術原理。卡通渲染是一種非真實感的圖形渲染(NPR)技術(所謂真實感圖形渲染是指計算機模擬真實自然的圖形技術,最重要的是 Light & Shadow Rendering,達到真實的光影表現)。[侑虎科技——卡通渲染技術總結] 我們常見的卡通風格可大體分爲美式和日式的,美式風格整體光照、陰影着色更貼近真實效果,而日式卡通往往與真實自然效果差別巨大。從早期的 Cel-Shading [Wiki] 到 ToneBasedShading,技術在不斷的深入,並且實際應用越來越多,如今的二次元遊戲畫面很多以卡通渲染技術爲基礎。

1.1 輪廓線

漫畫風格的卡通形象一般都會有明確的輪廓線,美式卡通如迪士尼的許多電影動畫則不然。本文主要討論漫畫風格的卡通渲染所採用的技術。

1.1.1 基於 2D 圖像的邊緣檢測算法

圖像的邊緣可以指灰度不連續,或者亮度、深度、表面法線、表面反射係數等圖像像素“值”不連續的地方。具體使用圖像灰度或是圖像亮度檢測圖像邊緣可根據需要選擇。

Sobel 算子 [3]

Sobel 邊緣檢測算法的基本原理是利用兩組 3 X 3 的橫向和縱向卷積模板,求取圖像 XY 的方向的亮度差分近似值。可以通過設定閾值 Thresold 來判定圖像邊緣,公式:

G=G(x)2+G(y)2.

但是問了節省計算消耗,通常我們使用近視表達式:

G=|G(x)+G(y)|.

G 大於某個閾值 Thresold 時,我們便認爲點 P(x,y) 已經到達了圖像邊緣,且我們使用以下公式表示圖像邊緣的方向:

Θ=arctan(G(y)G(x)).
Canny 算子 [4]

1.1.2 幾何描邊法

幾何描邊法的基本原理是渲染兩次物體,第一次渲染剔除物體的正面,在模型座標系下,根據頂點位置向量和法向量的內積(正\負)計算頂點位置伸縮方向,這種方法是網文介紹崩壞3渲染時所使用的,(最簡便的方法是直接使用法向量 N 乘以描邊大小 OutlineSize ,加上頂點位置,得到膨脹後的頂點位置,此類方法都稱爲 Shell-Method,另一類方法不改變頂點位置,而是通過改變 Z 值,將背面整體前移,該方法稱爲 Z-Bias Method)。這是比較傳統的幾何描邊法。

Geometry Outline 是使用比較多的描邊法,實現簡單,描邊寬度具備較好的可控性,有的描邊實現,可能會基於幾何描邊法做一些擴展,比如控制描邊顏色,模糊描邊等等。

1.1.3 基於視角的描邊法

基於視角的描邊法基本原理是根據表面法線與視線的點積判斷“邊緣程度”,我們知道點積的幾何意義在於判定兩個向量的相似程度,也就是說當點積值越趨近於 -1 時,它們的相似度越低,因此,在 Shader 實現中,我們可以設置一個閾值 (Thresold),當 Dot(Normal,ViewDir)<Thresold ,我們判定該頂點或者像素(可以選擇 Vertex Shader 或者 Pixel Shader)是圖形或者圖像邊緣。

該方法對於描邊的寬度可控性不強,但往往可以獲得更好的卡通表現效果。

1.2 卡通着色

卡通風格一般可以大致分爲日式和美式的 [2], 日式卡通風格凸出大範圍的純色色塊,光影邊界明顯,“非真實感”明顯;而美式卡通色彩比較豐富,光影表現更真實自然。下文將探討實現這兩種風格的着色技術。

1.2.1 Cel-Shading [5]

引文 [5] 中 Cel-Shading 的前兩個實現步驟(Outline、Basic Texture)不再詳述,我們着重關注其第三步——Shading,也就是着色。Cel-Shading 的基本原理是降低色階 [2],計算方法如下:

Lambert=dot(normals,lightDir).

Lambert=(Lambert+1.0)0.5.

OutColor=tex2D(RampMapfixed2(Lambert,Lambert)).rgblightColor.rgbmainTex.rgb.

上文公式表示計算 Lambert 光照模型,將其點積值 [1,1] 映射至 [0,1] ,以此作爲 UV 座標採樣梯度貼圖,將得到的顏色與光照顏色以及模型主紋理顏色相乘,得到最後的 Fragment 輸出顏色。Unity ShaderLab 相關代碼如下:

        Pass
        {
            NAME "CELSHADING"

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;
            uniform float4 _MainTex_ST;

            uniform sampler2D _RampMap;
            uniform float3 _DiffuseColor;
            uniform float _DiffuseScale;
            uniform float _NormalOffset;

            uniform float3 _SpecularColor;
            uniform float _Shininess;
            uniform float _SpecularMult;
            uniform float _SpecThresold;

            struct v2f
            {
                float4 Position : POSITION0;
                float2 UV : TEXCOORD0;
                float3 WorldPos : TEXCOORD1;
                float3 WorldNormal : TEXCOORD2;
            };

            fixed4 celShading(v2f i);

            v2f vert(appdata_base i)
            {
                v2f o;
                o.Position = UnityObjectToClipPos(i.vertex);
                o.WorldPos = mul(unity_ObjectToWorld, i.vertex);
                o.WorldNormal = UnityObjectToWorldNormal(i.normal);

                o.UV = TRANSFORM_TEX(i.texcoord, _MainTex);
                return o;
            }

            fixed4 frag(v2f i) : COLOR
            {
                fixed4 outColor;
                outColor = celShading(i);
                return outColor;
            }

            fixed4 celShading(v2f i)
            {
                fixed4 mainColor = tex2D(_MainTex, i.UV);
                if (mainColor.a <= 0.01f)
                {
                    discard;
                }

                fixed3 worldViewDir = UnityWorldSpaceViewDir(i.WorldPos);
                fixed3 worldLightDir = /*UnityWorldSpaceLightDir(i.WorldPos);*/normalize(_WorldSpaceLightPos0.xyz);
                fixed3 halfDir = normalize(worldViewDir + worldLightDir);

                fixed3 normal = i.WorldNormal;
                normal.xy *= _NormalOffset;
                normal = normalize(normal);
                fixed nlDot = dot(normal, worldLightDir);
                nlDot = nlDot * 0.5f + 0.5f;

                fixed ramp = tex2D(_RampMap, fixed2(nlDot * 0.95f, nlDot * 0.95f)).r;
                fixed3 diffuse = mainColor.rgb * _DiffuseColor * ramp * _DiffuseScale;

                fixed nhDot = dot(normal, halfDir);
                nhDot = saturate(nhDot);
                fixed spec = pow(nhDot, _Shininess);
                spec = step(_SpecThresold, spec);
                fixed3 specular = spec * _SpecularMult * _SpecularColor;

                fixed4 outColor;
                outColor.a = mainColor.a;
                outColor.rgb = diffuse + specular;

                return outColor;
            }

            ENDCG
        }

高光部分實現的是 Bling-Phong 光照模型,加入了幾個高光可調參數。

1.2.2 Tone Based Shading

Tone Based Shading 的基本原理是根據“明暗程度”選擇冷或暖色調進行着色,具體算法如下:

OutColor=1+dot(normal,lightDir)2kcool+(11+dot(normal,lightDir)2)kwarm.

kcool=kblue+αkd.   kwarm=kyellow+βkd.

其中 kd 表示貼圖自身顏色,kbluekyellow 分別表示“冷”和“暖”基準色調,其他是一些可調參數,(注:該算法表達和引文 [NPR渲染] 的實現有所區別。)基本 Tone Based Shading 基於 Unity ShaderLab 實現如下:

        Pass
        {   
            NAME "TONEBASEDSHADING_FORWARDBASE"

            Tags{ "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            uniform sampler2D _MainTex;
            uniform float4 _MainTex_ST;
            uniform float4 _TintColor;

            uniform float3 _SpecularColor;
            uniform float _Shininess;
            uniform float _SpecularMult;
            uniform float _SpecThresold;


            uniform float3 _KBlue;
            uniform float _Alpha;
            uniform float3 _KYellow;
            uniform float _Beta;

            struct v2f
            {
                float4 pos : POSITION0;
                float2 uv : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
                SHADOW_COORDS(3)
            };

            fixed4 toneBasedShading(v2f i);

            v2f vert(appdata_full i)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(i.vertex);
                o.worldPos = mul(unity_ObjectToWorld, i.vertex);
                o.worldNormal = UnityObjectToWorldNormal(i.normal);
                o.uv = TRANSFORM_TEX(i.texcoord, _MainTex);

                TRANSFER_SHADOW(o);

                return o;
            }

            fixed4 frag(v2f i) : COLOR
            {
                fixed4 outColor;
                outColor = toneBasedShading(i);
                return outColor;
            }

            fixed4 toneBasedShading(v2f i)
            {
                fixed4 mainColor = tex2D(_MainTex, i.uv);
                if(mainColor.a < 0.01f)
                {
                    discard;
                }
                mainColor *= _TintColor;

                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos); //normalize(_WorldSpaceLightPos0.xyz);
                fixed3 worldViewDir = UnityWorldSpaceViewDir(i.worldPos);
                fixed3 halfDir = normalize(worldLightDir + worldViewDir);

                //fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                fixed  nlDot = dot(worldNormal, worldLightDir);
                nlDot = (nlDot + 1.0f) * 0.5f * atten;
                fixed3 kCool = _KBlue + _Alpha * mainColor.rgb;
                fixed3 kWarm = _KYellow + _Beta * mainColor.rgb;

                fixed spec = pow(saturate(dot(worldNormal, halfDir)), _Shininess);
                spec = step(_SpecThresold, spec);
                fixed3 specular = spec * _SpecularMult * _SpecularColor;

                fixed4 outColor;
                outColor.a = mainColor.a;
                //outColor.rgb = ambient;
                outColor.rgb = nlDot * kCool + (1.0f - nlDot) * kWarm;
                outColor.rgb += specular;

                return outColor;
            }

            ENDCG
        }

1.2.3 基於 Tone Based Shading 的日式卡通

引文 [1] 中介紹了遊戲《罪惡裝備》使用的卡通着色算法(西川善司的兩篇文章詳述了《罪惡裝備》製作流程),表示如下:

OutColor=((darkness<thresold)?kcoolksss:kwarm)lightColor.rgbkd.
darkness=dot(normal,lightDir)AO.

它的基本原理是根據明暗度選擇“冷”或者“暖”色調着色,不像基礎 Tone Based Shading “冷”和“暖”色調之間會有插值,區別在於前者“冷”和“暖”色調界限區分明顯,後者“冷”和“暖”色調過度平滑。 [1] 中作者在計算 darkness 時考慮了陰影 Shadow ,本文的實現中則沒有考慮這些,Unity ShaderLab 代碼如下:

Shader "NPR/NPR_JapanStyleShading"
{
    Properties
    {
        [Header(Main Texture Setting)]
        [Space(5)]
        _MainTex("Texture", 2D) = "white" {}
        _TintColor("Tint Color", color) = (0.5, 0.5, 0.5, 1.0)
        [Space(30)]

        [Header(Outline Setting)]
        [Space(5)]
        _OutlineColor("Outline Color", color) = (0.0, 0.0, 0.0, 1.0)
        _OutlineSize("Outline Size", range(0.0, 1.0)) = 0.1
        _ZBias("Z Bias", range(-1.0, 1.0)) = 0.0
        [Space(30)]

        [Header(Specular Setting)]
        [Space(5)]
        _SpecularColor("Specular Color", color) = (1.0, 1.0, 1.0, 1.0)
        _Shininess("Shininess", range(0.0, 20.0)) = 0.1
        _SpecularMult("Multiple Factor", range(0.1, 1.0)) = 1
        _SpecThresold("Thresold", range(0.1, 1.0)) = 0.5
        [Space(30)]


        [Header(Tone Shading Setting)]
        [Space(5)]
        _KCool("Cool", color) = (0.0, 0.0, 1.0, 1.0)
        _KWarm("Warm", color) = (1.0, 1.0, 0.0, 1.0)
        _Darkness("Darkness", range(0.0, 1.0)) = 0.5
        _KSSS("SSS Color", color) = (1.0, 1.0, 1.0, 1.0)
        //[Space(20)]

        //[Header(StylizedHighLight Setting)]

    }
    SubShader
    {
        Tags{
            "RenderType" = "Opaque"
            "Queue" = "Geometry"
        }

        UsePass "NPR/NPR_CelShading/OUTLINE"

        Pass
        {       
            NAME "JAPANSTYLESHADING_FORWARDBASE"

            Tags{ "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            #include "NPR_StylizedHighLight.cginc"

            uniform sampler2D _MainTex;
            uniform float4 _MainTex_ST;
            uniform float4 _TintColor;

            uniform float3 _SpecularColor;
            uniform float _Shininess;
            uniform float _SpecularMult;
            uniform float _SpecThresold;

            uniform float3 _KCool;
            uniform float3 _KWarm;
            uniform fixed _Darkness;
            uniform float3 _KSSS;

            struct v2f
            {
                float4 pos : POSITION0;
                float2 uv : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
                // float3 tanToWorld_1 : TEXCOORD3;
                // float3 tanToWorld_2 : TEXCOORD4;
                // float3 tanToWorld_3 : TEXCOORD5;
                SHADOW_COORDS(6)
            };

            fixed4 japanStyleShading(v2f i);

            v2f vert(appdata_full i)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(i.vertex);
                o.worldPos = mul(unity_ObjectToWorld, i.vertex);
                o.worldNormal = UnityObjectToWorldNormal(i.normal);
                o.uv = TRANSFORM_TEX(i.texcoord, _MainTex);

                TRANSFER_SHADOW(o);

                return o;
            }

            fixed4 frag(v2f i) : COLOR
            {
                fixed4 outColor;
                outColor = japanStyleShading(i);
                return outColor;
            }

            fixed4 japanStyleShading(v2f i)
            {
                fixed4 mainColor = tex2D(_MainTex, i.uv);
                if(mainColor.a < 0.01f)
                {
                    discard;
                }
                mainColor *= _TintColor;

                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos); //normalize(_WorldSpaceLightPos0.xyz);

                //fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

                fixed nlDot = dot(worldNormal, worldLightDir);
                nlDot = (nlDot * 0.5f + 0.5f) * atten;
                fixed darkness = step(_Darkness, nlDot);
                fixed3 cool = (_KCool * _KSSS) * (1.0f - darkness);
                fixed3 warm = _KWarm * darkness;
                fixed3 diffuse = (cool + warm) * _LightColor0.rgb * mainColor.rgb;

                fixed3 worldViewDir = UnityWorldSpaceViewDir(i.worldPos);
                fixed3 halfDir = normalize(worldLightDir + worldViewDir);
                fixed spec = pow(saturate(dot(worldNormal, halfDir)), _Shininess);
                spec = step(_SpecThresold, spec);
                fixed3 specular = spec * _SpecularMult * _SpecularColor;

                fixed4 outColor;
                outColor.a = mainColor.a;
                //outColor.rgb = ambient;
                outColor.rgb = diffuse;
                outColor.rgb += specular;

                return outColor;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

實現效果:

這是基本的實現效果,與《崩壞3》遊戲效果差距很大,後期有關《NPR——卡通渲染》系列文章會在此基礎上進一步延展。

1.2.4 基於 Tone Based Shading 的美式卡通

關於美式卡通的着色風格,許多文章都提到了 Valve 的《軍團要塞2》以及引文 [7],知乎上有”拳四郎” [8] 的具體實現描述。

1.3 風格化

論文 [9] 風格化的高光方法我在試驗中一直沒有調出來,不論是標準的方塊化方法還是引文 [6] 的方法變種。本文實現代碼如下:

fixed3 halfVecScale(fixed3 halfVec)
{
    halfVec -= fixed3(_ScaleX * halfVec.x, 0.0f, 0.0f);
    halfVec = normalize(halfVec);
    halfVec -= fixed3(0.0f, _ScaleY * halfVec.y, 0.0f);
    halfVec = normalize(halfVec);
    return halfVec;
}

fixed3 halfVecRotate(fixed3 halfVec)
{
    return halfVec;
}

fixed3 halfVecTranslation(fixed3 halfVec)
{
    halfVec += fixed3(_Tx, _Ty, 0.0f);
    halfVec = normalize(halfVec);
    return halfVec;
}

fixed3 halfVecSplit(fixed3 halfVec)
{
    halfVec = halfVec - fixed3(_SplitX * sign(halfVec.x), 0.0f, 0.0f) - fixed3(0.0f, _SplitY * sign(halfVec.y), 0.0);
    halfVec = normalize(halfVec);
    return halfVec;
}

fixed3 halfVecSquaring(fixed3 halfVec)
{
    float theta = min(acos(halfVec.x), acos(halfVec.y));
    float sqrnorm = pow(sin(2.0f * theta), _Squaring);
    halfVec = halfVec - _SquaringArea * sqrnorm * (fixed3(halfVec.x, 0.0f, 0.0f) + fixed3(0.0f, halfVec.y, 0.0f));
    halfVec = normalize(halfVec);

    return halfVec;
}

fixed3 stylizedHighLight(fixed3 halfVec)
{
    fixed3 stylizedHalfVec;

    stylizedHalfVec = halfVecScale(halfVec);
    stylizedHalfVec = halfVecRotate(stylizedHalfVec);
    stylizedHalfVec = halfVecTranslation(stylizedHalfVec);
    stylizedHalfVec = halfVecSplit(stylizedHalfVec);
    stylizedHalfVec = halfVecSquaring(stylizedHalfVec);

    return stylizedHalfVec;
}

Reference

[1] 侑虎科技——卡通渲染技術總結
[2] Wiki——卡通渲染
[3] Sobel 邊緣檢測算法
[4] Canny 邊緣檢測算法
[5] Wiki——Cel-Shading
[6] NPR渲染
[7] Jason Mitchell, Moby Francke, Dhabih Eng. Illustrative Rendering in Team Fortress 2
[8] 拳四郎——風格化角色渲染實踐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章