简易二次元角色渲染风格(二)
Cel shader – 实色光影的卡通渲染方式,和通过矫正法线的方式来调整光影位置。
Demo演示:
实色光影的做法:
受光面 = dot (normalDirection, lightDirection)
光影系数 float _UnlitThreshold
如果受光面大于光影系数,则显示为亮面;反之则显示暗面。
参考Shader片段:
fixed4 frag(v2f i) : COLOR {
......
// light and shadow
float3 normalDirection = normalize(i.normal);
float3 lightDirection;
float3 fragmentColor;
lightDirection = normalize(_WorldSpaceLightPos0).xyz;
fragmentColor = _LightColor0.rgb * _UnlitColor.rgb * _Tint.rgb;
if (max(0.0, dot(normalDirection, lightDirection)) >= _UnlitThreshold) {
fragmentColor = _LightColor0.rgb * _Tint.rgb; // lit fragment color
}
......
}
效果如下:
配上贴图颜色,加上一点点侧逆光Rim:
这种渲染方式,要求贴图尽量是简洁的单色,这样配上Cel shader会显得简洁明快。
这种渲染方式下,暗部阴影颜色最好是固有色变暗一点,所以每个部分的暗部颜色都需要手动指定。方法有2种:
- 按照颜色分多个材质球,每个材质球对应一种颜色,并且单独制定暗部颜色。
- 用一张贴图对应各种固有色的暗部颜色。
现在脸部的阴影问题来了,完全写实的脸部阴影不太好看,二次元美少女的脸部阴影是怎么好看怎么画的,不完全按照真实光影。但是3D引擎里不可能那么自由的控制脸部阴影,我们能做的就是尽量让大部分情况下保证脸部阴影美观。
这里就要用到法线修正,让脸部阴影尽量贴近脸部边缘轮廓,而且明暗交界简洁一点。这在各种3D软件里都可以做到。我这儿就直接在maya里做了。
首先创建一个Cel shader贴在模型上,用Attribution Transfer把一个椭圆的normal信息映射到脸部mesh。下图左边是修改前的,阴影比较散乱,右边是修改后的,会干净简洁很多。
然后回到unity里,对比看看效果。如下图:左边是修改过法线的,明暗交界很平滑,右边是解锁法线的,在某些角度看上去阴影很散乱。
我只是简单的用一个椭圆映射法线,效果一般。最好的方式是复制一个脸部mesh,然后把mesh调整到想要的光影状态。最后用写脚本把修改过的法线信息复制到之前的脸部mesh上。
最终效果看项目实际需要,有很多游戏是不太考究这些脸部细节的。
shader代码参考:
Shader "WalkingFat/Toon1/Char03"
{
Properties
{
_Tint ("Tint", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_UnlitColor ("Shadow Color", Color) = (0.5,0.5,0.5,1)
_UnlitThreshold ("Shadow Range", Range(0,1)) = 0.1
_RimColor("Rim Color", Color) = (0.5,0.5,0.5,1)
_RimIntensity("Rim Intensity", Range(0.0, 100)) = 5.0
_RimLightSampler("Rim Sampler", 2D) = "white"{}
}
SubShader {
Tags { "Queue" = "Geometry" "RenderType" = "Opaque"}
LOD 100
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// shadow
#pragma multi_compile_fwdbase
#include "AutoLight.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _LightColor0;
float4 _Tint;
float4 _UnlitColor;
float _UnlitThreshold;
float4 _RimColor;
float _RimIntensity;
sampler2D _RimLightSampler;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD2;
float3 eyeDir: TEXCOORD3;
float3 lightDir: TEXCOORD4;
LIGHTING_COORDS(5,6)
};
v2f vert(appdata v) {
v2f o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.pos = UnityObjectToClipPos (v.vertex);
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
o.normal = normalize(mul(float4(v.normal, 0.0), unity_WorldToObject).xyz);
o.eyeDir.xyz = normalize( _WorldSpaceCameraPos.xyz - o.posWorld.xyz ).xyz;
o.lightDir = WorldSpaceLightDir( v.vertex );
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
fixed4 frag(v2f i) : COLOR {
fixed4 col = tex2D(_MainTex, i.uv) * _Tint;
// light and shadow
float3 normalDirection = normalize(i.normal);
float3 lightDirection;
float3 fragmentColor;
float attenuation = LIGHT_ATTENUATION(i);
lightDirection = normalize(_WorldSpaceLightPos0).xyz;
fragmentColor = _LightColor0.rgb * _UnlitColor.rgb * _Tint.rgb;
if (attenuation * max(0.0, dot(normalDirection, lightDirection)) >= _UnlitThreshold) {
fragmentColor = _LightColor0.rgb * _Tint.rgb; // lit fragment color
}
// Rimlight
float normalDotEye = dot( i.normal, i.eyeDir.xyz );
float falloffU = clamp( 1.0 - abs( normalDotEye ), 0.02, 0.98 );
float rimlightDot = saturate( 0.5 * ( dot( i.normal, i.lightDir + float3(-1,0,0) ) + 1.5 ) );
falloffU = saturate( rimlightDot * falloffU );
falloffU = tex2D( _RimLightSampler, float2( falloffU, 0.25f ) ).r;
float3 rimCol = falloffU * col * _RimColor * _RimIntensity;
return float4(col * fragmentColor + rimCol, 1.0);
}
ENDCG
}
}
Fallback "VertexLit"
}