NPR——卡通渲染
本文的目的是系統的探討遊戲中的卡通渲染技術,以期深刻掌握卡通渲染中所用技術原理。卡通渲染是一種非真實感的圖形渲染(NPR)技術(所謂真實感圖形渲染是指計算機模擬真實自然的圖形技術,最重要的是 Light & Shadow Rendering,達到真實的光影表現)。[侑虎科技——卡通渲染技術總結] 我們常見的卡通風格可大體分爲美式和日式的,美式風格整體光照、陰影着色更貼近真實效果,而日式卡通往往與真實自然效果差別巨大。從早期的 Cel-Shading [Wiki] 到 ToneBasedShading,技術在不斷的深入,並且實際應用越來越多,如今的二次元遊戲畫面很多以卡通渲染技術爲基礎。
1.1 輪廓線
漫畫風格的卡通形象一般都會有明確的輪廓線,美式卡通如迪士尼的許多電影動畫則不然。本文主要討論漫畫風格的卡通渲染所採用的技術。
1.1.1 基於 2D 圖像的邊緣檢測算法
圖像的邊緣可以指灰度不連續,或者亮度、深度、表面法線、表面反射係數等圖像像素“值”不連續的地方。具體使用圖像灰度或是圖像亮度檢測圖像邊緣可根據需要選擇。
Sobel 算子 [3]
Sobel 邊緣檢測算法的基本原理是利用兩組 3 X 3 的橫向和縱向卷積模板,求取圖像 、 的方向的亮度差分近似值。可以通過設定閾值 來判定圖像邊緣,公式:
但是問了節省計算消耗,通常我們使用近視表達式:
當 大於某個閾值 時,我們便認爲點 已經到達了圖像邊緣,且我們使用以下公式表示圖像邊緣的方向:
Canny 算子 [4]
1.1.2 幾何描邊法
幾何描邊法的基本原理是渲染兩次物體,第一次渲染剔除物體的正面,在模型座標系下,根據頂點位置向量和法向量的內積(正\負)計算頂點位置伸縮方向,這種方法是網文介紹崩壞3渲染時所使用的,(最簡便的方法是直接使用法向量 乘以描邊大小 ,加上頂點位置,得到膨脹後的頂點位置,此類方法都稱爲 Shell-Method,另一類方法不改變頂點位置,而是通過改變 Z 值,將背面整體前移,該方法稱爲 Z-Bias Method)。這是比較傳統的幾何描邊法。
Geometry Outline 是使用比較多的描邊法,實現簡單,描邊寬度具備較好的可控性,有的描邊實現,可能會基於幾何描邊法做一些擴展,比如控制描邊顏色,模糊描邊等等。
1.1.3 基於視角的描邊法
基於視角的描邊法基本原理是根據表面法線與視線的點積判斷“邊緣程度”,我們知道點積的幾何意義在於判定兩個向量的相似程度,也就是說當點積值越趨近於 -1 時,它們的相似度越低,因此,在 Shader 實現中,我們可以設置一個閾值 (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 光照模型,將其點積值 映射至 ,以此作爲 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 的基本原理是根據“明暗程度”選擇冷或暖色調進行着色,具體算法如下:
其中 表示貼圖自身顏色, 和 分別表示“冷”和“暖”基準色調,其他是一些可調參數,(注:該算法表達和引文 [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] 中介紹了遊戲《罪惡裝備》使用的卡通着色算法(西川善司的兩篇文章詳述了《罪惡裝備》製作流程),表示如下:
它的基本原理是根據明暗度選擇“冷”或者“暖”色調着色,不像基礎 Tone Based Shading “冷”和“暖”色調之間會有插值,區別在於前者“冷”和“暖”色調界限區分明顯,後者“冷”和“暖”色調過度平滑。 [1] 中作者在計算 時考慮了陰影 ,本文的實現中則沒有考慮這些,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] 拳四郎——風格化角色渲染實踐