本來(lái)呢,我是不打算寫卡通渲染相關(guān)的東西的鹃祖。雖然我挺喜歡玩女神異聞錄5這種卡通風(fēng)格的游戲,但是從技術(shù)路線來(lái)講普舆,我更希望走寫實(shí)路線的渲染恬口,例如最令我震撼的神秘海域4的畫(huà)面。不過(guò)由于我現(xiàn)在待的公司搞了大半年的卡通風(fēng)格的游戲沼侣,(項(xiàng)目中道崩殂祖能,成為每個(gè)開(kāi)發(fā)心中永遠(yuǎn)的痛。蛾洛。养铸。)所以我還是寫一下相關(guān)的東西,萬(wàn)一以后還要搞轧膘,我可以有個(gè)入口知道要從哪里開(kāi)始弄钞螟。好了,廢話說(shuō)到這兒谎碍,開(kāi)始正篇筛圆。
由于卡通渲染是旨在還原美術(shù)人員手繪的感覺(jué),所以它的漫反射呈現(xiàn)色塊的感覺(jué)椿浓,而不是漸變太援。然后我們利用光的方向和法線方向做點(diǎn)乘后得到的結(jié)果作為范圍劃定的依據(jù)闽晦,分了四層,像這樣:
fixed diff = dot(worldNormal,normalize(worldLight));
diff = diff * 0.5 + 0.5;
fixed w = fwidth(diff) * 2.0;
if (diff < _DiffuseSeg.x + w){
diff = lerp(_DiffuseSeg.x,_DiffuseSeg.y,smoothstep(_DiffuseSeg.x - w,_DiffuseSeg.x + w,diff));
}else if (diff < _DiffuseSeg.y + w){
diff = lerp(_DiffuseSeg.y,_DiffuseSeg.z,smoothstep(_DiffuseSeg.y - w,_DiffuseSeg.y + w,diff));
}else if (diff < _DiffuseSeg.z + w){
diff = lerp(_DiffuseSeg.z,_DiffuseSeg.w,smoothstep(_DiffuseSeg.z - w,_DiffuseSeg.z + w,diff));
}else{
diff = _DiffuseSeg.w;
}
其中_DiffuseSeg
為(0.1提岔,0.3仙蛉,0.6,1.0)碱蒙,這個(gè)可以根據(jù)需求自己調(diào)整荠瘪,不必拘泥。為什么要寫的那么復(fù)雜而不是直接給 diff
設(shè)值呢赛惩,因?yàn)橹苯淤x值顏色的分界線會(huì)有明顯的抗鋸齒哀墓,所以用fwidth
函數(shù)先求出鄰域內(nèi)的梯度值w,再在邊界處+-w
進(jìn)行漸變混合來(lái)消除鋸齒感喷兼。
高光區(qū)域也類似篮绰,我們先得到高光項(xiàng),然后判斷范圍季惯,超過(guò)這個(gè)范圍就是1吠各,否則就是0,沒(méi)有高光勉抓。為了抗鋸齒贾漏,我們也得做類似之前做過(guò)的事。
fixed spec = saturate(dot(worldNormal,halfVector));
spec = pow(spec,_Gloss);
w = fwidth(spec);
if (spec < _SpecularSeg + w){
spec = lerp(0,1,smoothstep(_SpecularSeg - w,_SpecularSeg + w,spec));
}else{
spec = 1;
}
后來(lái)我們覺(jué)得在shader里寫了一大堆ifelse
效率不高藕筋,所以換了個(gè)實(shí)現(xiàn)方式纵散,這種方式是這篇論文A Non-Photorealistic Lighting Model for Automatic Technical Illustration中提出的一個(gè)公式。
其中Kcool和Kwarm分別由公式2得到隐圾。
其中 Kd是漫反射顏色困食, Kblue = (0,0,b),b[0,1],Kwarm = (y,y,0),y[0,1]翎承,alpha和beta都是用戶可調(diào)節(jié)的參數(shù)。
fixed4 k_blue = fixed4(0,0,_Blue,1);
fixed4 k_yellow = fixed4(_Yellow,_Yellow,0,1);
fixed4 k_cool = k_blue + _Alpha * kd;
fixed4 k_warm = k_yellow + _Beta * kd;
fixed temp = dot(normalize(worldLight),worldNormal);
fixed4 diffuse = (1 + temp)/2 * k_cool + (1 - (1+temp/2)) * k_warm;
diffuse *= _DiffuseCol * _LightColor0 * atten;
最后根據(jù)這篇論文Stylized highlights for cartoon rendering and animation還可以對(duì)高亮區(qū)域進(jìn)行風(fēng)格化符匾,其主要思想就是對(duì)Blinn-Phong模型中的半角向量進(jìn)行修改操作叨咖,實(shí)現(xiàn)高亮區(qū)域的縮放、旋轉(zhuǎn)啊胶、平移甸各、分塊和方塊化。(注意這里的順序不要弄錯(cuò)了Q嫫骸Hで恪!)
代碼如下:
//縮放
halfVector -= _ScaleX * halfVector.x * float3(1,0,0);
halfVector = normalize(halfVector);
halfVector -= _ScaleY * halfVector.y * float3(0,1,0);
halfVector = normalize(halfVector);
//旋轉(zhuǎn)
float xR = _RotationX * DegreeToRadian;
float3x3 rotX = float3x3(1, 0, 0,
0, cos(xR), sin(xR),
0, -sin(xR), cos(xR));
float yR = _RotationY * DegreeToRadian;
float3x3 rotY = float3x3(cos(yR), 0, -sin(yR),
0, 1, 0,
sin(yR), 0, cos(yR));
float zR = _RotationZ * DegreeToRadian;
float3x3 rotZ = float3x3(cos(zR), sin(zR), 0,
-sin(zR), cos(zR), 0,
0, 0, 1);
halfVector = mul(rotZ,mul(rotY,mul(rotZ,halfVector)));
//平移
halfVector += float3(_TranslationX,_TranslationY,0);
halfVector = normalize(halfVector);
//分塊
fixed signX = 1;
if (halfVector.x < 0){
signX = -1;
}
fixed signY = 1;
if (halfVector.y < 0){
signY = -1;
}
halfVector -= _SplitX * signX * float3(1,0,0) - _SplitY * signY * float3(0,1,0);
halfVector = normalize(halfVector);
//方塊化
float sqrThetaX = acos(halfVector.x);
float sqrThetaY = acos(halfVector.y);
fixed sqrnormX = sin(pow(2 * sqrThetaX, _SquareN));
fixed sqrnormY = sin(pow(2 * sqrThetaY, _SquareN));
fixed minority = min(sqrnormX,sqrnormY);
halfVector -= _SquareScale * (minority * halfVector.x * fixed3(1, 0, 0) + minority * halfVector.y * fixed3(0, 1, 0));
halfVector = normalize(halfVector);
這里我就不展開(kāi)講了某饰,我對(duì)此興趣不是很大>_<
另外我照著公式寫儒恋,也沒(méi)有采用馮樂(lè)樂(lè)前輩的方法同時(shí)用兩個(gè)角度去改變半角向量善绎,結(jié)果調(diào)來(lái)調(diào)去沒(méi)調(diào)出四個(gè)方塊的高亮,奇怪了诫尽。禀酱。。
最后牧嫉,卡通渲染里必不可少的效果剂跟,黑色描邊,經(jīng)過(guò)項(xiàng)目中的實(shí)踐酣藻,過(guò)程式幾何輪廓渲染的方法效果比較令人滿意(雖然它對(duì)cube這種東西沒(méi)有辦法)曹洽。它的思想是先弄個(gè)pass渲染背面,讓頂點(diǎn)沿著扁平化(這里的扁平化其實(shí)就是讓法線向量的z
統(tǒng)一成一個(gè)值辽剧,我這里取0.01送淆,網(wǎng)上也有取-0.05或者其他什么值的)過(guò)后的法線方向擴(kuò)張,使得背部可見(jiàn)抖仅,再把這部分渲染成輪廓線的顏色即可坊夫。然后第二個(gè)pass里就做正常的卡通渲染做的事。
代碼如下:
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
normal.z = 0.01;
float2 offset = TransformViewToProjection(normal.xy);
float height = o.vertex.z / unity_CameraProjection._m11;//加入這個(gè)參數(shù)可讓物體描邊在離攝像頭遠(yuǎn)的時(shí)候不至于太細(xì)撤卢,近的時(shí)候不至于太粗
float scale = sqrt(height / _OutlineScale);
o.vertex.xy += offset * scale * _Outline;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _OutlineCol;
}
這里有個(gè)技巧环凿,為了讓描邊在鏡頭拉遠(yuǎn)時(shí)不至于看不見(jiàn),拉近時(shí)不至于過(guò)粗放吩,加入了height
這個(gè)變量智听,并且進(jìn)行開(kāi)方操作使得它的變化平緩一些,_OutlineScale
來(lái)控制平緩的度渡紫。
最后放上實(shí)現(xiàn)的效果到推,模型都是從馮樂(lè)樂(lè)前輩的NPR Labs那兒弄來(lái)的。
PS. 如果想深入學(xué)習(xí)卡通渲染的化惕澎,unity chan是個(gè)很好的入手項(xiàng)目莉测,unity官方商店有一代,最近他們?nèi)毡緐nity官方又在GitHub上弄了個(gè)二代唧喉,這是地址
參考
【NPR】卡通渲染