简易二次元角色渲染风格(一)
弱化光影,无描边,有轻微立体感的清爽二次元渲染方式。
首先看看最终效果(角色模型动画来自于网络):
- 按住鼠标左键可旋转角色
- 点击左上角按钮可切换视角和角色
日系二次元的渲染风格以自发光为主,特别是皮肤要白皙干净。模型贴图部分不要带强烈的阴影,衣服只要画出固有色和褶皱部分。皮肤需要根据立体结构有一点明暗变化,特别是脖子和衣服遮盖的暗部。
先看看纯自发光的效果:
自发光效果已经看上去不错了。7分靠美术功底,3分靠shader添彩。
纯自发光缺乏立体感,我要加点类似diffuse的明暗变化,但是有没必要做实时光。于是我用一个cubemap实现这个效果。
- cubemap一直朝向camera视角。
- 贴图白色部分略偏右上角,可以造成光从右上角照过来的感觉,立体感更好点。
- 贴图的黑白色当做lerp的系数“t”,另有一个cubecolor与固有色混合成更深一点的固有色“B”,与固有色“A”做计算。
日系风格不能脏,所以尽量少用黑色。日系水彩配色可以研究研究,从红黄暖色到蓝紫冷色,一定要干净通透。
现在立体感有了,还缺一点出彩的东西就是侧逆光。人像摄影的万能邪术侧逆光,啥玩意儿都能照出花儿来。
- rim根据主平行光的角度变化。
- rim宽度由一张横向黑白贴图控制。
脸部的侧逆光在某些角度不好看,我再加一张mask贴图来控制是否显示侧逆光。调整过程要反复测试,保证各个角度都尽量好看。
下图右边没有mask的,眼睛,鼻子,额头等地方的白色逆光很难看。左边加了mask就可以自由控制侧逆光位置。
这样大效果基本就ok了,我没有做的很细,主要是推销一下皮肤和衣服的shader做法。
这个demo里我还加了一些Post-processing的效果。
- Bloom+HDR有曝光效果,配合shader里的Rim侧逆光,其本身就可以把颜色设高。
- 后期矫色:把颜色调暖黄了一点,其实原本颜色也蛮好看的。
另外角色加了Dynamic Bone动态骨骼系统,让衣服和头发飘的自然点。
角色脚下的投影是RenderTexture做的,并且在shader里模糊了一下。
最后贴上shader代码:
Shader "WalkingFat/Toon1/Char02"
{
Properties
{
_MainColor ("Main Color", Color) = (1.0, 1.0, 1.0, 1) // vertex color
_MainTex ("Texture", 2D) = "white" {} // main texture
_ToonCubeColor ("Toon Cube Color", Color) = (1.0, 1.0, 1.0, 1)
_ToonCube ("Toon Cube", Cube) = "" {} // cube map for toon light
_RimColor("Rim Color", Color) = (0.5,0.5,0.5,1)
_RimIntensity("Rim Intensity", Range(0.0, 100)) = 5.0
_RimLightSampler("RimMask", 2D) = "white"{}
_RimMask ("Rim Mask", 2D) = "white" {} // main texture
}
SubShader
{
Tags
{
"RenderType"="Opaque"
"Queue" = "Geometry"
}
LOD 100
Pass
{
Name "FORWARD"
Tags
{
"LightMode" = "ForwardBase"
}
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float3 cubenormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
float3 eyeDir : TEXCOORD3;
float3 lightDir : TEXCOORD4;
UNITY_FOG_COORDS(5) // for fog
};
sampler2D _MainTex;
float4 _MainColor;
samplerCUBE _ToonCube;
float4 _ToonCubeColor;
float4 _MainTex_ST;
float4 _RimColor;
float _RimIntensity;
sampler2D _RimLightSampler;
sampler2D _RimMask;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.cubenormal = mul (UNITY_MATRIX_MV, float4(v.normal,0)); // get cube normal data
// cubenormal = mul (UNITY_MATRIX_MV, float4(v.normal,0));
o.normal = mul(float4(v.normal,0), unity_WorldToObject).xyz; // get vertex normal in world space
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // get vertex pos in world space
o.eyeDir.xyz = normalize( _WorldSpaceCameraPos.xyz - o.worldPos.xyz ).xyz;
o.lightDir = WorldSpaceLightDir( v.vertex );
UNITY_TRANSFER_FOG(o,o.vertex); // for fog
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv) * _MainColor; // get main texture color and blend with mainColor
fixed4 cubeCol = texCUBE(_ToonCube, i.cubenormal); // get cube texture color
fixed4 cubeAddCol = col * _ToonCubeColor;
fixed4 finalCubeCol = lerp (cubeAddCol, col, cubeCol.r);
// 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 ) + 1.5 ) );
falloffU = saturate( rimlightDot * falloffU );
falloffU = tex2D( _RimLightSampler, float2( falloffU, 0.25f ) ).r;
float3 rimCol = falloffU * col * _RimColor * _RimIntensity;
fixed4 rimMaskCol = tex2D(_RimMask, i.uv);
rimCol *= rimMaskCol.r;
fixed4 finalCol = fixed4(finalCubeCol + rimCol, col.a);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, finalCol);
return finalCol;
}
ENDCG
}
}
Fallback "Unlit/Texture"
}