unity 3d物体描边效果_从零开始的卡通渲染描边篇
一直對(duì)卡通渲染非常感興趣,前后翻找了不少的文檔,做了一些工作。前段時(shí)間《從零開始》的手游上線了,試著渲染了一下的其中模型,覺得效果很不錯(cuò)。打算寫一個(gè)專欄記錄其中的渲染技術(shù)。在后面的篇章中也想展示一下各個(gè)項(xiàng)目中卡通渲染技術(shù)的變遷,以及討論未來(lái)的一些發(fā)展方向。
卡通渲染屬于非真實(shí)感渲染(Non-photorealistic rendering,簡(jiǎn)稱NPR)。對(duì)應(yīng)的還有真實(shí)感渲染(Photorealistic rendering)。后者旨在渲染真實(shí)感的畫面,而前者則追求更加有藝術(shù)感的畫面效果,例如手繪風(fēng)格的畫面。
NPR也有各種各樣的類型。比如像油畫,鉛筆畫,水墨畫風(fēng)格的畫面。這里主要探討像日本動(dòng)畫那樣的卡通渲染風(fēng)格,目前一般稱之為Cel Shading。卡通渲染在日本那邊很早就在主機(jī)游戲上使用,經(jīng)過了很多嘗試和變遷,最終在《GUILTY GEAR Xrd》系列游戲達(dá)到了非常不錯(cuò)的水準(zhǔn)。國(guó)內(nèi)的廠商在吸收了日本同行的經(jīng)驗(yàn)以后,也制作了非常好的作品,并且在此之上創(chuàng)新,提出了更多的解決方案。
說(shuō)了很多,現(xiàn)在回歸正題。描邊是卡通渲染的一個(gè)非常重要的主題。目前比較流行的描邊方法有兩種,一個(gè)是通過兩次繪制,一次繪制角色,一次繪制描邊。還有一種是基于后處理的描邊。基于后處理的描邊相對(duì)不容易定制,比較適用于對(duì)復(fù)雜場(chǎng)景進(jìn)行描邊。這里講述通過2次繪制來(lái)繪制描邊的方法。在《GUILTY GEAR Xrd》中稱其為Back Facing法。
Back facing描邊法
基本思路是通過兩次繪制,第一次繪制角色,第二次繪制描邊。繪制描邊的時(shí)候,在頂點(diǎn)著色器將頂點(diǎn)沿著法線方向位移一段距離,使得模型輪廓放大,渲染作為描邊。同時(shí)描邊繪制時(shí)使用cull front。這樣描邊和角色重疊的部分會(huì)因?yàn)椴荒芡ㄟ^深度檢測(cè)而cull掉,保證描邊不會(huì)遮擋角色。兩次繪制顛倒順序也是可以的,不過后繪制描邊,可以通過深度檢測(cè)過濾掉很多描邊繪制的像素,效率會(huì)更好。這里先實(shí)現(xiàn)最簡(jiǎn)單的方法,然后逐步進(jìn)行優(yōu)化。
Shader "Unlit/Ouline"{
Properties
{
_OutlineWidth ("Outline Width", Range(0.01, 1)) = 0.24
_OutLineColor ("OutLine Color", Color) = (0.5,0.5,0.5,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
pass
{
Tags {"LightMode"="ForwardBase"}
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 vert(appdata_base v): SV_POSITION
{
return UnityObjectToClipPos(v.vertex);
}
half4 frag() : SV_TARGET
{
return half4(1,1,1,1);
}
ENDCG
}
Pass
{
Tags {"LightMode"="ForwardBase"}
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
half _OutlineWidth;
half4 _OutLineColor;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float4 vertColor : COLOR;
float4 tangent : TANGENT;
};
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert (a2v v)
{
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.pos = UnityObjectToClipPos(float4(v.vertex.xyz + v.normal * _OutlineWidth * 0.1 ,1));//頂點(diǎn)沿著法線方向外擴(kuò)
return o;
}
half4 frag(v2f i) : SV_TARGET
{
return _OutLineColor;
}
ENDCG
}
}
}
現(xiàn)在我們用Unity預(yù)設(shè)的球體進(jìn)行渲染
看起來(lái)描邊效果正常修正攝像機(jī)距離問題
現(xiàn)在我們將攝像機(jī)拉近,發(fā)現(xiàn)攝像機(jī)拉近后,描邊變得很粗
攝像機(jī)拉近后,描邊顯得很粗這是因?yàn)槊柽叺膶挾痊F(xiàn)在是相對(duì)世界空間不變的,這相機(jī)拉近后,顯示就會(huì)變粗。我們期望無(wú)論攝像機(jī)拉近拉遠(yuǎn),描邊的粗細(xì)都能不變。要解決這個(gè)問題,可以通過將法線外擴(kuò)的大小調(diào)整為使用NDC空間的距離進(jìn)行外擴(kuò)。這里參考這篇文章對(duì)代碼進(jìn)行一些修改。
v2f o;UNITY_INITIALIZE_OUTPUT(v2f, o);
float4 pos = UnityObjectToClipPos(v.vertex);
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz);
float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w;//將法線變換到NDC空間
pos.xy += 0.01 * _OutlineWidth * ndcNormal.xy;
o.pos = pos;
return o;描邊的兩邊比較粗,上下比較細(xì),寬度不統(tǒng)一
結(jié)果似乎有些問題,描邊的兩邊粗,上下細(xì)。這是因?yàn)镹DC空間的xy是范圍是[0,1]。但是我這里的窗口分辨率是16:9,所以直接用NDC空間的距離外擴(kuò),不能適配寬屏窗口。所以需要根據(jù)窗口的寬高比再進(jìn)行修正。這里再對(duì)描邊進(jìn)行修改
v2f o;UNITY_INITIALIZE_OUTPUT(v2f, o);
float4 pos = UnityObjectToClipPos(v.vertex);
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz);
float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w;//將法線變換到NDC空間
float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));//將近裁剪面右上角位置的頂點(diǎn)變換到觀察空間
float aspect = abs(nearUpperRight.y / nearUpperRight.x);//求得屏幕寬高比
ndcNormal.x *= aspect;
pos.xy += 0.01 * _OutlineWidth * ndcNormal.xy;
o.pos = pos;
return o;
描邊的寬度顯示正確了攝像機(jī)拉遠(yuǎn)以后,顯示的描邊寬度也保持不變
現(xiàn)在描邊可以正確顯示,而且無(wú)論攝像機(jī)的遠(yuǎn)近,描邊的粗細(xì)可以保持不變了。
修正不光滑物體斷邊問題
之前我們渲染了unity的預(yù)制球體,現(xiàn)在我們換成預(yù)制的立方體試一下。
描邊的四角都斷開了嗯…四個(gè)角的描邊都斷開了。這方案不行,Pass,放棄,(摔)。改用后處理描邊吧。
咳…因?yàn)檫@個(gè)模型每個(gè)面的頂點(diǎn)的法線都垂直于這個(gè)平面。所以描邊的外擴(kuò)也是垂直于平面,當(dāng)模型有轉(zhuǎn)角的情況下,描邊就會(huì)像這樣裂開。Back facing的描邊方法會(huì)有這樣的問題。困擾了我一段時(shí)間,后來(lái)看到一個(gè)叫Toony Colors Pro的Unity插件,有了比較好的解決方法。
要解決這個(gè)問題,需要對(duì)模型外擴(kuò)使用的法線數(shù)據(jù)進(jìn)行修改。這里需要將鄰接面的頂點(diǎn)法線數(shù)據(jù),進(jìn)行平均計(jì)算,計(jì)算出新的法線寫入模型切線數(shù)據(jù)中。然后使用這個(gè)切線數(shù)據(jù)進(jìn)行法線外擴(kuò)。至于為什么要寫到切線數(shù)據(jù)里,這是因?yàn)橹挥蟹ň€和切線數(shù)據(jù)會(huì)隨著骨骼動(dòng)畫而改變,如果角色使用了骨骼動(dòng)畫,就需要寫入切線數(shù)據(jù)。如果沒有使用骨骼動(dòng)畫的需求,將數(shù)據(jù)寫入頂點(diǎn)色中也是可以的。這里我寫了一個(gè)編輯器工具,完成對(duì)mesh數(shù)據(jù)的添加。
public class PlugTangentTools{
[MenuItem("Tools/模型平均法線寫入切線數(shù)據(jù)")]
public static void WirteAverageNormalToTangentToos()
{
MeshFilter[] meshFilters = Selection.activeGameObject.GetComponentsInChildren();
foreach (var meshFilter in meshFilters)
{
Mesh mesh = meshFilter.sharedMesh;
WirteAverageNormalToTangent(mesh);
}
SkinnedMeshRenderer[] skinMeshRenders = Selection.activeGameObject.GetComponentsInChildren();
foreach (var skinMeshRender in skinMeshRenders)
{
Mesh mesh = skinMeshRender.sharedMesh;
WirteAverageNormalToTangent(mesh);
}
}
private static void WirteAverageNormalToTangent(Mesh mesh)
{
var averageNormalHash = new Dictionary();
for (var j = 0; j < mesh.vertexCount; j++)
{
if (!averageNormalHash.ContainsKey(mesh.vertices[j]))
{
averageNormalHash.Add(mesh.vertices[j], mesh.normals[j]);
}
else
{
averageNormalHash[mesh.vertices[j]] =
(averageNormalHash[mesh.vertices[j]] + mesh.normals[j]).normalized;
}
}
var averageNormals = new Vector3[mesh.vertexCount];
for (var j = 0; j < mesh.vertexCount; j++)
{
averageNormals[j] = averageNormalHash[mesh.vertices[j]];
}
var tangents = new Vector4[mesh.vertexCount];
for (var j = 0; j < mesh.vertexCount; j++)
{
tangents[j] = new Vector4(averageNormals[j].x, averageNormals[j].y, averageNormals[j].z, 0);
}
mesh.tangents = tangents;
}
}
同時(shí)描邊的方法里,改為使用切線數(shù)據(jù)作為外擴(kuò)數(shù)據(jù)。
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.tangent.xyz);立方體的描邊也顯示正確了現(xiàn)在這個(gè)立方體的模型也可以正確的描邊了。不過這個(gè)方法只是臨時(shí)修改了mesh數(shù)據(jù),如果要保存下來(lái)的話。一個(gè)可行的方案是使用FBX的SDK來(lái)編寫工具,將額外的切線數(shù)據(jù)寫入模型里。我們?cè)囍脙煞N方式對(duì)角色進(jìn)行描邊來(lái)對(duì)比表現(xiàn)。
使用原始法線數(shù)據(jù)使用平均法線數(shù)據(jù)對(duì)比可以看到,使用新的法線數(shù)據(jù)進(jìn)行描邊,模型描邊斷邊的問題少了很多。
然后再添加一點(diǎn)細(xì)節(jié)
嗯,有那味了,有關(guān)光照部分放在下一篇講頂點(diǎn)色的使用
能多放入一些數(shù)據(jù),就能增加更多的效果。關(guān)于模型頂點(diǎn)色當(dāng)然也不能浪費(fèi)。在《GUILTY GEAR Xrd》中使用模型頂點(diǎn)顏色的四個(gè)通道,對(duì)模型描邊的粗細(xì)、顯隱、相機(jī)距離縮放等進(jìn)行了精細(xì)的控制。當(dāng)然頂點(diǎn)數(shù)據(jù)還可以用來(lái)做很多其他的事情,這取決于想要實(shí)現(xiàn)的效果,和美術(shù)制作的難度。在本篇中,我們使用頂點(diǎn)色控制描邊的粗細(xì)和顏色。對(duì)代碼進(jìn)行一些修改。
v2f vert (a2v v){
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f, o);
float4 pos = UnityObjectToClipPos(v.vertex);
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.tangent.xyz);
float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w;//將法線變換到NDC空間
float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));//將近裁剪面右上角的位置的頂點(diǎn)變換到觀察空間
float aspect = abs(nearUpperRight.y / nearUpperRight.x);//求得屏幕寬高比
ndcNormal.x *= aspect;
pos.xy += 0.01 * _OutlineWidth * ndcNormal.xy * v.vertColor.a;//頂點(diǎn)色a通道控制粗細(xì)
o.pos = pos;
o.vertColor = v.vertColor.rgb;
return o;
}
fixed4 frag(v2f i) : SV_TARGET
{
return fixed4(_OutLineColor * i.vertColor, 0);//頂點(diǎn)色rgb通道控制描邊顏色
}
最終的描邊效果 E·M·T
總結(jié)
在本節(jié)實(shí)現(xiàn)了一個(gè)不管攝像機(jī)距離,可以保持寬度不變的Back Facing描邊方法。優(yōu)化了Back Facing描邊在不光滑物體出現(xiàn)的破邊問題。實(shí)現(xiàn)了通過頂點(diǎn)色數(shù)據(jù)對(duì)描邊進(jìn)行調(diào)整的方法。在下一個(gè)章節(jié)中,將會(huì)討論一些用于卡通渲染的光照計(jì)算的實(shí)現(xiàn)方法。
聲明:發(fā)布此文是出于傳遞更多知識(shí)以供交流學(xué)習(xí)之目的。若有來(lái)源標(biāo)注錯(cuò)誤或侵犯了您的合法權(quán)益,請(qǐng)作者持權(quán)屬證明與我們聯(lián)系,我們將及時(shí)更正、刪除,謝謝。
作者:2173
來(lái)源:https://zhuanlan.zhihu.com/p/109101851
More:【微信公眾號(hào)】?u3dnotes
總結(jié)
以上是生活随笔為你收集整理的unity 3d物体描边效果_从零开始的卡通渲染描边篇的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 这科技感绝了!马英九体验华为鸿蒙智能座舱
- 下一篇: 日媒:后锂离子电池时代竞争 中国碾压式领