效果展示:
原模型:
簡單分析
卡通渲染又叫非真實渲染(None-Physical Rendering-NPR),一般日漫里的卡通風格有幾個特點:
人物有描邊
有明顯的陰影分界線,沒有太平滑的過渡
以下就根據這兩點來實現卡渲效果;
描邊
法線外擴
實現描邊方式多種,比如卷積區分邊界;
這里使用更簡單的兩個Pass,一個只用純色畫背面,利用法線外擴頂點,根據深度的不同這個純色的背面會被顯示出來,同時又不會遮擋正面;
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 vertColor : TEXCOORD0;
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));
o.vertColor = fixed4(v.vertColor.rgb,1.0);
return o;
}
half4 frag(v2f i) : SV_TARGET
{
return half4(_OutLineColor.rgb * i.vertColor.rgb, 0);
}
ENDCG
}
細節處理(坑)
細節處理前后對比:
-
攝像機遠近邊緣線粗細不同
由於世界坐標系下做外擴,攝像機里物體遠近會影響法線外擴的多少;
解決方案,在NDC坐標系下法線外擴;
//頂點着色器替換以下代碼 float4 pos = UnityObjectToClipPos(v.vertex); //攝像機空間法線 float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz); //將法線變換到NDC空間,投影空間*W分量 float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w; //xy兩方向外擴 pos.xy += 0.01 * _OutlineWidth * ndcNormal.xy * v.vertColor.a; o.pos = pos;
-
上下和左右邊緣線粗細不同
NDC空間是正方形,而視口寬高比是長方體,導致描邊上下和左右的粗細不統一;
解放方案,根據屏幕寬高比縮放法線再外擴;
//將近裁剪面右上角位置的頂點變換到觀察空間 //unity_CameraInvProjection攝像機矩陣逆矩陣,UNITY_NEAR_CLIP_VALUE近截面值,DX:0,OpenGL-1.0;_ProjectionParams.y攝像機近截面 float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y)); //求得屏幕寬高比 float aspect = abs(nearUpperRight.y / nearUpperRight.x); ndcNormal.x *= aspect;
-
頂點重合法線不連續
模型頂點重合時會出現多條法線,在不同的面上法線不同導致描邊不連續;
解決方案,修改模型頂點數據,同頂點多條法線求平均值;
需要和美工協商修改模型數據,這里寫了腳本臨時修改模型數據;
public class PlugTangentTools { [MenuItem("Tools/模型平均法線寫入切線數據")] public static void WirteAverageNormalToTangentToos() { MeshFilter[] meshFilters = Selection.activeGameObject.GetComponentsInChildren<MeshFilter>(); foreach (var meshFilter in meshFilters) { Mesh mesh = meshFilter.sharedMesh; WirteAverageNormalToTangent(mesh); } SkinnedMeshRenderer[] skinMeshRenders = Selection.activeGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(); foreach (var skinMeshRender in skinMeshRenders) { Mesh mesh = skinMeshRender.sharedMesh; WirteAverageNormalToTangent(mesh); } Debug.Log("重合頂點平均法線寫入成功"); } private static void WirteAverageNormalToTangent(Mesh mesh) { var averageNormalHash = new Dictionary<Vector3, Vector3>(); 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; } }
ps:利用模型頂點的四個通道RGBA——對描邊粗細顯影相機距離縮放進行精細控制,需要美工配合;
着色
減少色階
二分法
將有陰影和沒陰影的地方做明顯的區分;
half4 frag(v2f i) : SV_TARGET
{
half4 col = 1;
half4 mainTex = tex2D(_MainTex, i.uv);
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
half3 worldNormal = normalize(i.worldNormal);
half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//半蘭伯特光照模型
half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
//_ShadowRange區分陰影范圍,_ShadowSmooth控制分界線的柔和程度,求出ramp值(百分比)
half ramp = smoothstep(0, _ShadowSmooth, halfLambert - _ShadowRange);
//根據ramp值插值取樣,將陰影和main顏色混合
half3 diffuse = lerp(_ShadowColor, _MainColor, ramp);
diffuse *= mainTex;
col.rgb = _LightColor0 * diffuse;
return col;
}
Ramp貼圖
使用明顯分界的色階圖來取樣,使陰影有明顯的分界線;
邏輯和二分一樣,只是多加個幾個色階;
//_ShadowRange范圍取樣Ramp貼圖
half ramp = tex2D(_RampTex, float2(saturate(halfLambert - _ShadowRange), 0.5)).r;
高光色階
卡渲高光和陰影一樣,和周圍色塊有明顯的分界線;
half3 specular = 0;
half3 halfDir = normalize(worldLightDir + viewDir);
half NdotH = max(0, dot(worldNormal, halfDir));
//_SpecularGloss控制高光光澤度
half SpecularSize = pow(NdotH, _SpecularGloss);
//_SpecularRange高光范圍,_SpecularMulti強度,在范圍內顯示高光有明顯分界
if (SpecularSize >= 1 - _SpecularRange)
{
specular = _SpecularMulti * _SpecularColor;
}
ilmTexture貼圖
《GUILTY GEAR Xrd》中使用的方法,又叫Threshold貼圖;
貼圖的R通道控制漫反射的陰影閾值,G通道控制高光強度,B通道控制高光范圍;
需要和美工配合,沒貼圖就不測了;
總之萬物皆可用貼圖來傳遞信息,rgba代表什么意思可以自行做各種trick;
half4 frag (v2f i) : SV_Target
{
half4 col = 0;
half4 mainTex = tex2D (_MainTex, i.uv);
//取樣ilmTexture
half4 ilmTex = tex2D (_IlmTex, i.uv);
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
half3 worldNormal = normalize(i.worldNormal);
half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//漫反射+陰影
half3 diffuse = 0;
half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
//g通道控制高光強度
half threshold = (halfLambert + ilmTex.r) * 0.5;
half ramp = saturate(_ShadowRange - threshold);
ramp = smoothstep(0, _ShadowSmooth, ramp);
diffuse = lerp(_MainColor, _ShadowColor, ramp);
diffuse *= mainTex.rgb;
half3 specular = 0;
half3 halfDir = normalize(worldLightDir + viewDir);
half NdotH = max(0, dot(worldNormal, halfDir));
half SpecularSize = pow(NdotH, _SpecularGloss);
//b通道控制高光遮罩
half specularMask = ilmTex.b;
if (SpecularSize >= 1 - specularMask * _SpecularRange)
{
//g控制高光強度
specular = _SpecularMulti * (ilmTex.g) * _SpecularColor;
}
col.rgb = (diffuse + specular) * _LightColor0.rgb;
return col;
}
【翻譯】西川善司「實驗做出的游戲圖形」「GUILTY GEAR Xrd -SIGN-」中實現的「純卡通動畫的實時3D圖形」的秘密,前篇(1)
【翻譯】西川善司「實驗做出的游戲圖形」「GUILTY GEAR Xrd -SIGN-」中實現的「純卡通動畫的實時3D圖形」的秘密,前篇(2)
【翻譯】西川善司的「實驗做出的游戲圖形」「GUILTY GEAR Xrd -SIGN-」中實現的「純卡通動畫的實時3D圖形」的秘密,后篇
邊緣泛光
三渲二加點邊緣泛光會增加立體感,讓畫質更真實;效果如下;
__RimMin、_RimMax控制邊緣泛光范圍;
smoothstep使過渡平緩;再乘以RimColor,alpha控制強度;
half f = 1.0 - saturate(dot(viewDir, worldNormal));
half rim = smoothstep(_RimMin, _RimMax, f);
rim = smoothstep(0, _RimSmooth, rim);
half3 rimColor = rim * _RimColor.rgb * _RimColor.a;
col.rgb = (diffuse + specular + rimColor) * _LightColor0.rgb;
mask遮罩圖
用一張貼圖來修正邊緣泛光的效果;
邊緣光的計算使用的是法線點乘視線。在物體的法線和視線垂直的時候,邊緣光會很強。在球體上不會有問題,但是在一些有平面的物體,當平面和視線接近垂直的時候,會導致整個平面都有邊緣光。這會讓一些不該有邊緣光的地方出現邊緣光。
屏幕后效
post-processing官方組件中有bloom效果;
原理:提取圖像中較亮區域,存儲在紋理中,使用高斯模糊模擬光線擴散效果,將該紋理和原圖像混合;過程比較復雜,后面寫屏幕后期效果再分析吧;
完整Shader:
Shader "Unlit/CelRenderFull"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_IlmTex ("IlmTex", 2D) = "white" {}
[Space(20)]
_MainColor("Main Color", Color) = (1,1,1)
_ShadowColor ("Shadow Color", Color) = (0.7, 0.7, 0.7)
_ShadowSmooth("Shadow Smooth", Range(0, 0.03)) = 0.002
_ShadowRange ("Shadow Range", Range(0, 1)) = 0.6
[Space(20)]
_SpecularColor("Specular Color", Color) = (1,1,1)
_SpecularRange ("Specular Range", Range(0, 1)) = 0.9
_SpecularMulti ("Specular Multi", Range(0, 1)) = 0.4
_SpecularGloss("Sprecular Gloss", Range(0.001, 8)) = 4
[Space(20)]
_OutlineWidth ("Outline Width", Range(0, 1)) = 0.24
_OutLineColor ("OutLine Color", Color) = (0.5,0.5,0.5,1)
[Space(20)]
_RimMin ("imMin",float) = 1.0
_RimMax ("RimMax",float) = 2.0
_RimSmooth("RimSmooth",Range(0.0,1))=0.5
_RimColor("RimColor",Color) = (1,1,1,1)
}
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _IlmTex;
float4 _IlmTex_ST;
half3 _MainColor;
half3 _ShadowColor;
half _ShadowSmooth;
half _ShadowRange;
half3 _SpecularColor;
half _SpecularRange;
half _SpecularMulti;
half _SpecularGloss;
half _RimMin;
half _RimMax;
half _RimSmooth;
fixed4 _RimColor;
struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o = (v2f)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
half4 frag (v2f i) : SV_Target
{
half4 col = 0;
half4 mainTex = tex2D (_MainTex, i.uv);
half4 ilmTex = tex2D (_IlmTex, i.uv);
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
half3 worldNormal = normalize(i.worldNormal);
half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
half3 diffuse = 0;
half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
half threshold = (halfLambert + ilmTex.g) * 0.5;
half ramp = saturate(_ShadowRange - threshold);
ramp = smoothstep(0, _ShadowSmooth, ramp);
diffuse = lerp(_MainColor, _ShadowColor, ramp);
diffuse *= mainTex.rgb;
half3 specular = 0;
half3 halfDir = normalize(worldLightDir + viewDir);
half NdotH = max(0, dot(worldNormal, halfDir));
half SpecularSize = pow(NdotH, _SpecularGloss);
half specularMask = ilmTex.b;
if (SpecularSize >= 1 - specularMask * _SpecularRange)
{
specular = _SpecularMulti * (ilmTex.r) * _SpecularColor;
}
half f = 1.0 - saturate(dot(viewDir, worldNormal));
half rim = smoothstep(_RimMin, _RimMax, f);
rim = smoothstep(0, _RimSmooth, rim);
half3 rimColor = rim * _RimColor.rgb * _RimColor.a;
col.rgb = (diffuse + specular + rimColor) * _LightColor0.rgb;
return col;
}
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 vertColor : TEXCOORD0;
float4 pos : SV_POSITION;
};
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;
float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));
float aspect = abs(nearUpperRight.y / nearUpperRight.x);
ndcNormal.x *= aspect;
pos.xy += 0.01 * _OutlineWidth * ndcNormal.xy * v.vertColor.a;
o.pos = pos;
o.vertColor = fixed4(v.vertColor.rgb,1.0);
return o;
}
half4 frag(v2f i) : SV_TARGET
{
return half4(_OutLineColor.rgb * i.vertColor.rgb, 0);
}
ENDCG
}
}
FallBack Off
}