Unity Shader 卡通渲染效果

Unity Shader系列文章:Unity Shader目錄-初級篇

Unity Shader系列文章:Unity Shader目錄-中級篇

效果:
游戲《大神》(英文名:Okami)的游戲截圖
卡通風格的渲染效果
左圖:未對高光區(qū)域進行抗鋸齒處理扰法。右圖:使用fwidth函數(shù)對高光區(qū)域進行抗鋸齒處理
原理:

1煤篙、渲染輪廓線剩岳;
2芍躏、添加高光;

繪制模型輪廓線的方法有5種類型:

  • 基于觀察角度和表面法線的輪廓線渲染糊饱。這種方法使用視角方向和表面法線的點乘結果來得到輪廓線的信息。這種方法簡單快速,可以在 Pass 中就得到渲染結果祟偷,但局限性很大,很多模型渲染出來的描邊效果都不盡如人意打厘。
  • 過程式幾何輪廓線渲染修肠。這種方法的核心是使用兩個 Pass 渲染。第一個Pass渲染背面的面片户盯,并使用某些技術讓它的輪廓可見嵌施;第二個 Pass 再正常渲染正面的面片。這種方法的優(yōu)點在于快速有效莽鸭,并且適用于絕大多數(shù)表面平滑的模型吗伤,但它的缺點是不適合類似于立方體這樣平整的模型。
  • 基于圖像處理的輪廓線渲染硫眨。使用卷積核進行邊界檢測足淆。這種方法的優(yōu)點在于可以適用于任何種類的模型。但它也有自身的局限所在礁阁,一些深度和法線變化很小的輪廓無法被檢測出來巧号,例如桌子上的紙張。
  • 基于輪廓邊檢測的輪廓線渲染姥闭。上面提到的各種方法丹鸿,一個最大的問題是,無法控制輪廓
    線的風格渲染棚品。對于一些情況靠欢,我們希望可以渲染出獨特風格的輪廓線,例如水墨風格等铜跑。為此门怪,我們希望可以檢測出精確的輪廓邊,然后直接渲染它們锅纺。檢測一條邊是否是輪廓邊的公式很簡單掷空,我們只需要檢查和這條邊相鄰的兩個三角面片是否滿足以下條件:
    (n_0·v>0)≠(n_1·v> 0)
    其中,n_0n_1分別表示兩個相鄰三角面片的法向伞广, 是從視角到該邊上任意頂點的方向拣帽。上述公式的本質在于檢查兩個相鄰的三角面片是否一個朝正面疼电、一個朝背面嚼锄。我們可以在幾何著色器(Geometry hader 的幫助下實現(xiàn)上面的檢測過程。當然蔽豺,這種方法也有缺點区丑,除了實現(xiàn)相對復雜外,它還會有動畫連貫性的問題。也就是說沧侥,由于是逐幀單獨提取輪廓可霎,所以在幀與幀之間會出現(xiàn)跳躍性。
  • 最后一個種類就是混合了上述的幾種渲染方法宴杀。例如癣朗,首先找到精確的輪廓邊,把模型和輪廓邊渲染到紋理中旺罢,再使用圖像處理的方法識別出輪廓線旷余,并在圖像空間下進行風格化渲染。

在本篇中扁达,將會在 Unity 中使用過程式幾何輪廓線渲染的方法來對模型進行輪廓描邊正卧。將使用兩個 Pass 渲染模型:在第一個Pass中,我們會使用輪廓線顏色渲染整個背面的面片跪解,在視角空間下把模型頂點沿著法線方向向外擴張一段距離炉旷,以此來讓背部輪廓線可見。

shader代碼:

// 卡通渲染效果
Shader "Custom/ToonShading"
{
    Properties
    {
        _Color ("ColorTint", Color) = (1, 1, 1, 1)
        _MainTex ("Texture", 2D) = "white" { }
        _Ramp ("Ramp Texture", 2D) = "white" { }// 控制漫反射色調的漸變紋理
        _Outline ("Outline", Range(0, 1)) = 0.1 // 控制輪廓線寬度
        _OutlineColor ("OutlineColor", Color) = (0, 0, 0, 1) // 輪廓線顏色
        _Specular ("Specular", Color) = (1, 1, 1, 1) // 高光反射顏色
        _SpecularScale ("SpecularScale", Range(0, 0.1)) = 0.01 // 控制計算高光反射區(qū)域大小
    }

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

        // 定義渲染輪廓線需要的 Pass
        Pass
        {
            // 使用 NAME 命令為該 Pas 定義了名稱叉讥,這樣別的Shader可以通過此名字調用此Pass
            // 因為描邊在非真實感渲染中是非常常見的效果 為該Pass義名稱可以讓我們在后面的使用中不需要再重復編寫此 Pass
            // 而只需要調用它的名字即可窘行,如: UsePass "Cusstom/ToonShading/OUTLINE"
            NAME "OUTLINE"

            // 剔除正面,只渲染背面
            Cull Front

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            float _Outline;
            fixed4 _OutlineColor;

            // 應用傳遞給定點著色器的數(shù)據(jù)
            struct a2v
            {
                float4 vertex: POSITION; // 語義:模型空間下的頂點坐標
                float4 normal: NORMAL; // 語義:模型空間下的法線
            };

            // 頂點著色器傳遞給片元著色器的數(shù)據(jù)
            struct v2f
            {
                float4 pos: SV_POSITION; // 語義:裁剪空間下的頂點坐標
            };

            // 頂點著色器函數(shù)
            v2f vert(a2v v)
            {
                v2f o;

                // 將頂點坐標從模型空間變換到觀察空間
                float4 pos = mul(UNITY_MATRIX_MV, v.vertex);
                // 將法線從模型空間變換到觀察空間
                float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
                // 是為了盡可能避免背面擴張后的頂點擋住正面的面片
                normal.z = 0.5;
                // 將頂點沿法線方向上擴張
                pos = pos + float4(normalize(normal), 0) * _Outline;
                // 將頂點坐標從觀察空間變換到裁剪空間
                o.pos = mul(UNITY_MATRIX_P, pos);

                return o;
            }

            // 片元著色器函數(shù)
            fixed4 frag(v2f i): SV_TARGET
            {
                // 用輪廓線顏色渲染整個背面
                return fixed4(_OutlineColor.rgb, 1);
            }

            ENDCG

        }

        // Base Pass 計算平行光图仓、環(huán)境光
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            
            Cull Back
            
            CGPROGRAM

            // 編譯指令抽高,保證在pass中得到Pass中得到正確的光照變量
            #pragma multi_compile_fwdbase

            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            #include "UnityShaderVariables.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _Ramp;
            fixed4 _Specular;
            fixed _SpecularScale;
            
            // 應用傳遞給定點著色器的數(shù)據(jù)
            struct a2v
            {
                float4 vertex: POSITION; // 語義: 頂點坐標
                float3 normal: NORMAL; // 語義: 法線
                float4 texcoord: TEXCOORD0; // 語義: 紋理坐標
                float4 tangent: TANGENT; // 語義: 切線
            };
            
            // 頂點著色器傳遞給片元著色器的數(shù)據(jù)
            struct v2f
            {
                float4 pos: SV_POSITION; // 語義: 裁剪空間的頂點坐標
                float2 uv: TEXCOORD0;
                float3 worldNormal: TEXCOORD1;
                float3 worldPos: TEXCOORD2;
                SHADOW_COORDS(3) // 內置宏:聲明一個用于對陰影紋理采樣的坐標 (這個宏參數(shù)需要是下一個可用的插值寄存器的索引值,這里是3)
            };

            // 頂點著色器
            v2f vert(a2v v)
            {
                v2f o;

                // 將頂點坐標從模型空間變換到裁剪空間
                // 等價于o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.pos = UnityObjectToClipPos(v.vertex);

                // 計算紋理坐標(縮放和平移)
                // 等價于o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

                // 將法線從模型空間變換到世界空間
                // 等價于o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                // 將頂點坐標從模型空間變換到世界空間
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                // 內置宏:用于計算聲明的陰影紋理坐標
                TRANSFER_SHADOW(o);
                
                return o;
            }

            // 片元著色器
            fixed4 frag(v2f i): SV_TARGET
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                // 世界空間光向量
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                // 世界空間觀察向量
                fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                // 世界半角向量透绩,用于計算高光反射
                fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);
                
                fixed4 c = tex2D(_MainTex, i.uv);
                // 計算材質反射率
                fixed3 albedo = c.rgb * _Color.rgb;

                // 環(huán)境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                // 計算陰影值和光照衰減
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

                // 計算半蘭伯特漫反射系數(shù)
                fixed diff = dot(worldNormal, worldLightDir);
                // 和陰影值相乘得到最終的漫反射系數(shù)
                diff = (diff * 0.5 + 0.5) * atten;
                
                // 使用漫反射系數(shù)對漸變紋理_Ramp進行采樣翘骂,并將結果和材質的反射率、光照顏色相乘 作為最后的漫反射光照
                fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;

                fixed spec = dot(worldNormal, worldHalfDir);
                // 高光區(qū)域的邊緣不是平滑漸變的帚豪,而是由0突變到1碳竟,對其進行抗鋸齒處理,可以在邊界處很小的一塊區(qū)域內進行平滑處理
                // 使用smoothstep函數(shù)狸臣,w個很小的值莹桅,當spec-threshold小于-w 時,返回0烛亦;大于w時诈泼,返回1;否則在0-1之間進行插值煤禽。
                // 這樣的效果是铐达,我們可以在[-w, w] 區(qū)間內,即高光區(qū)域的邊界處檬果,得到一個從0到1平滑變化的 spec 值瓮孙,從而實現(xiàn)抗鋸齒的目的
                // 盡管我可以把w設為一個很小的定值唐断,不過更好的是選擇使用鄰域像素之間的近似導數(shù)值,這可以通過 fwidth 函數(shù)來得到
                // 使用step(0.0001, _SpecularScale)杭抠,這是為了在 SpecularScaJe 可以完全消除高光反射的光照
                fixed w = fwidth(spec) * 2.0;
                fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale);

                return fixed4(ambient + diffuse + specular, 1);
            }


            ENDCG

        }
    }

    // 設置Fallback脸甘,產生正確的陰影投射效果
    Fallback "Diffuse"
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市偏灿,隨后出現(xiàn)的幾起案子丹诀,更是在濱河造成了極大的恐慌,老刑警劉巖翁垂,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忿墅,死亡現(xiàn)場離奇詭異,居然都是意外死亡沮峡,警方通過查閱死者的電腦和手機疚脐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來邢疙,“玉大人棍弄,你說我怎么就攤上這事∨庇危” “怎么了呼畸?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長颁虐。 經常有香客問我蛮原,道長,這世上最難降的妖魔是什么另绩? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任儒陨,我火速辦了婚禮,結果婚禮上笋籽,老公的妹妹穿的比我還像新娘蹦漠。我一直安慰自己,他們只是感情好车海,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布笛园。 她就那樣靜靜地躺著,像睡著了一般侍芝。 火紅的嫁衣襯著肌膚如雪研铆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天州叠,我揣著相機與錄音棵红,去河邊找鬼。 笑死留量,一個胖子當著我的面吹牛窄赋,可吹牛的內容都是我干的哟冬。 我是一名探鬼主播楼熄,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼忆绰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了可岂?” 一聲冷哼從身側響起错敢,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缕粹,沒想到半個月后稚茅,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡平斩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年亚享,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绘面。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡欺税,死狀恐怖,靈堂內的尸體忽然破棺而出揭璃,到底是詐尸還是另有隱情晚凿,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布瘦馍,位于F島的核電站歼秽,受9級特大地震影響,放射性物質發(fā)生泄漏情组。R本人自食惡果不足惜燥筷,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望院崇。 院中可真熱鬧荆责,春花似錦、人聲如沸亚脆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽濒持。三九已至键耕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間柑营,已是汗流浹背屈雄。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留官套,地道東北人酒奶。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓蚁孔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親惋嚎。 傳聞我的和親對象是個殘疾皇子杠氢,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內容

  • 其實這是現(xiàn)在一些游戲 很喜歡的渲染方式,最后出來的效果還是很不錯的 非真實感渲染 (Non-Photorealis...
    李偌閑閱讀 1,902評論 0 2
  • 卡通渲染 卡通渲染也叫做Toon-Shading或Cel-Shading另伍,屬于非真實感渲染(Non-Photore...
    ayasechihaya閱讀 1,330評論 0 1
  • 非真實感渲染 卡通風格的渲染 原理 要實現(xiàn)卡通渲染有很多方法鼻百,其中之一就是使用基于色調的著色技術(tone-bas...
    BacteriumFox閱讀 740評論 0 0
  • 一.概念 渲染目標紋理:GPU允許把整個三圍場景渲染到一個中間緩沖中多重渲染目標:GPU允許把場景同時渲染到多個渲...
    無職轉生者閱讀 1,308評論 0 1
  • 本文同時發(fā)布在我的個人博客上:https://dragon_boy.gitee.io 卡通風格渲染 渲染輪廓線 基...
    Dragon_boy閱讀 1,152評論 0 2