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

Last modified date

弱化光影,无描边,有轻微立体感的清爽二次元渲染方式。

首先看看最终效果(角色模型动画来自于网络):

  • 按住鼠标左键可旋转角色
  • 点击左上角按钮可切换视角和角色

日系二次元的渲染风格以自发光为主,特别是皮肤要白皙干净。模型贴图部分不要带强烈的阴影,衣服只要画出固有色和褶皱部分。皮肤需要根据立体结构有一点明暗变化,特别是脖子和衣服遮盖的暗部。

先看看纯自发光的效果:

自发光效果已经看上去不错了。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"
}