1. 程式人生 > >NPR——卡通渲染(一)

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 的基本原理是根據“明暗程度”選擇冷或暖色調進行著色,具體演算法如下: