C for Graphic:卡通光照

       最近出了个游戏很火,叫原神,我特意下载玩了几天,可以说除了内容不好玩之外(这也是传统内购网游的通病,和一次性付费就能获取小说电影般体验的3A大作不一样,网游必须通过不断的重复性内容延长运营时间、还得通过等级阶段装备吸引充值,所以在我这种全平台玩家的感受上就很繁琐枯燥乏味)其他全都达到了卡通游戏画面标杆,当然其吸金能力是顶呱呱的。

       因为近四年来职位是客户端主程而不是TA,所以我只是写一些项目中需要的功能性或玩法性shader。不过我最喜欢的还是卡通风格的游戏,当然我不是美术艺术方面院校毕业的,所以只能浅显的分析实现一下市面上现有的卡通渲染风格。

       先上游戏截图:

     

       可以看得出来整个画面趋于暖色调,同时用了bloom后期特效,角色外观光照不像传统的光照模型一样光照颜色(强度)均匀渐变,而是类似阶梯性的步长变化,且游戏中角色随着角度调整会看到高亮光斑。那么我们可以想到以下两点(如果在数学上稍微敏感一些):

        1.光照计算的diffuse需要使用step这类函数进行“阶梯化”

        2.光照计算中的specular为了可以达到“光斑”的效果,也需要使用step函数做“高光区域拘束”

        好,那我们来写一下shader看下效果:

Shader "Cartoon/CartoonLightShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _LightFactor("Light Factor",Color) = (1,1,1,1)
        _DiffuseFactor("Diffuse Factor",Color) = (1,1,1,1)
        _SpecularFactor("Specular Factor",Color) = (1,1,1,1)
        _SpecularGloss("Specular Gloss",Range(0,100)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 worldNormal : TEXCOORD1;
                float3 worldP2S : TEXCOORD2;
                float3 worldP2V : TEXCOORD3;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float4 _LightFactor;
            float4 _DiffuseFactor;
            float4 _SpecularFactor;
            float _SpecularGloss;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal.xyz);
                float3 worldvertex = mul(UNITY_MATRIX_M,v.vertex).xyz;
                o.worldP2S = UnityWorldSpaceLightDir(worldvertex);
                o.worldP2V = UnityWorldSpaceViewDir(worldvertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                float3 worldp2s = normalize(i.worldP2S);
                float3 worldp2v = normalize(i.worldP2V);
                float3 worldnorm = normalize(i.worldNormal);

                float ndotl = dot(worldnorm,worldp2s);
                float3 halfdir = normalize(worldp2s+worldp2v);
                float ndoth = dot(worldnorm,halfdir);

                float4 light = _LightColor0*_LightFactor;

                float diffusepower = smoothstep(0,0.05,ndotl);
                float4 diffuse = _LightColor0*diffusepower*_DiffuseFactor;

                float specularpower = smoothstep(0.0005,0.001,pow(ndoth,_SpecularGloss));
                float4 specular = _LightColor0*specularpower*_SpecularFactor;

                col *= (light + diffuse + specular);

                return col;
            }
            ENDCG
        }
    }
}

          效果如下:

          光照计算和传统光照模式一样,唯一不同点就是使用smoothstep函数(好像以前讲过这个函数),而这个函数就是处理卡通光照的关键,先上解释:

          smoothstep详解

          smoothstep称为平滑插值,在 minmax 之间进行插值,在限制处进行平滑。此函数采用与 Lerp 相似的方式在 minmax 之间进行插值。 但是,插值会从起点逐渐加速,然后朝着终点减慢。 这可用于创建表现十分自然的动画、淡化和其他过渡。

float smoothstep(float edge0, float edge1, float x)  
vec2 smoothstep(vec2 edge0, vec2 edge1, vec2 x)  
vec3 smoothstep(vec3 edge0, vec3 edge1, vec3 x)  
vec4 smoothstep(vec4 edge0, vec4 edge1, vec4 x)

vec2 smoothstep(float edge0, float edge1, vec2 x)  
vec3 smoothstep(float edge0, float edge1, vec3 x)  
vec4 smoothstep(float edge0, float edge1, vec4 x)

float t;
t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);

            随便用unity自带的smoothstep函数做了个形象的演示:

void Update()
{
        transform.localPosition = new Vector3(Time.time, Mathf.SmoothStep(0, 5, Time.time * speed), 0);
}

           效果如下:

            所以我们用smoothstep(min,max,val)就可以把diffuse和specular处理成卡通渲染的样子。

            写到这里的时候我突然发现游戏中有时候角色边缘还有一点高亮,就跟弧面玻璃边缘的高亮一样, 有点类似边缘高亮,又不太一样,先实现一下:

         使用视口向量和法向量点积作为参数,但是这样会造成材质球“所有”边缘都带有高亮,还得光源向量和法向量点积作为参数“剔除”背部边缘的高亮。我这写的点积计算大家都可以脑海中模拟出来吧?如果不能够通过思考得到形象感觉的同学,务必用纸和笔画出模型表面、顶点法向量、光源向量、视口向量、半角向量等参数辅助理解,如图:

       

Shader "Cartoon/CartoonLightShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _LightFactor("Light Factor",Color) = (1,1,1,1)
        _DiffuseFactor("Diffuse Factor",Color) = (1,1,1,1)
        _SpecularFactor("Specular Factor",Color) = (1,1,1,1)
        _SpecularGloss("Specular Gloss",Range(0,100)) = 20
        _EdgeColor("Edge Color",Color) = (1,1,1,1)
        _EdgePower("Edge Power",Range(0,1)) = 0.1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 worldNormal : TEXCOORD1;
                float3 worldP2S : TEXCOORD2;
                float3 worldP2V : TEXCOORD3;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float4 _LightFactor;
            float4 _DiffuseFactor;
            float4 _SpecularFactor;
            float _SpecularGloss;
            float4 _EdgeColor;
            float _EdgePower;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal.xyz);
                float3 worldvertex = mul(UNITY_MATRIX_M,v.vertex).xyz;
                o.worldP2S = UnityWorldSpaceLightDir(worldvertex);
                o.worldP2V = UnityWorldSpaceViewDir(worldvertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                float3 worldp2s = normalize(i.worldP2S);
                float3 worldp2v = normalize(i.worldP2V);
                float3 worldnorm = normalize(i.worldNormal);

                float ndotl = dot(worldnorm,worldp2s);
                float3 halfdir = normalize(worldp2s+worldp2v);
                float ndoth = dot(worldnorm,halfdir);

                float4 light = _LightColor0*_LightFactor;

                float diffusepower = smoothstep(0,0.05,ndotl);
                float4 diffuse = _LightColor0*diffusepower*_DiffuseFactor;

                float specularpower = smoothstep(0.0005,0.001,pow(ndoth,_SpecularGloss));
                float4 specular = _LightColor0*specularpower*_SpecularFactor;

                //边缘光计算
                float edgedot = 1-dot(worldp2v,worldnorm);
                float edgepower = smoothstep(_EdgePower-0.01,_EdgePower+0.01,edgedot*ndotl);
                float4 edge = _EdgeColor*edgepower;

                col *= (light + diffuse + specular + edge);
                return col;
            }
            ENDCG
        }
    }
}

         当然,我依旧用了smoothstep“拘束”边缘广的光照效果,达到specular卡通光斑一样,效果如下:

            这样就算是分析完成了简单的卡通渲染的光照计算,那么我找一个角色模型去看看效果:

             还有看得出卡通风格的效果。当然卡通渲染不止皮肤渲染这么一个,还有毛发、眼球等核心渲染,以后有时间我们继续。

猜你喜欢

转载自blog.csdn.net/yinhun2012/article/details/109061689