简易二次元角色渲染风格(二)

Last modified date

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种:

  1. 按照颜色分多个材质球,每个材质球对应一种颜色,并且单独制定暗部颜色。
  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"
}

Share

This site is protected by wp-copyrightpro.com