Unity Shader - Simple Toon Shading - 简单卡通渲染

使用的是Unity内置管线,后面有时间再学习:LWRP(URP),还有HDRP,学习任务有点多,一步一步来

本来想弄个资源来学习:后处理实现:深度+法线描边的
但现在有个适合的资源,还是先处理一下简单的卡通渲染效果吧
而且弄好后,后面做其他的效果也有个比较好的模型来做实验

最终效果 - Final Effect

刀的法线是有问题的,可能是建模的同学法线没处理好
子模型、Mask纹理都还不够细分,否则某些部位的光影可以控制得很完美
Unity Shader - Simple Toon Shading - 简单卡通渲染_第1张图片
下面一步步来显示

无光照,只有纹理与主色调

Unity Shader - Simple Toon Shading - 简单卡通渲染_第2张图片
可以看到纹理中部分的边缘信息也话上去了,如:白色丝绸的边缘,有还超短裙上的黑边条纹。
这些一般不是高级超模的几何体模型,都会画在纹理上。

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = tex2D(_MainTex, i.uv);
                col.rgb *= _MainColor;
                return col;
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

加描边 - Outline

Unity Shader - Simple Toon Shading - 简单卡通渲染_第3张图片
加上顶点按法线方向挤出后的背面绘制来描边的效果。
比之前没有描边的好很多,最明显的是,大腿之间的相同颜色的线条、头发与天空盒的线条

GIF


上面使用的描边方式比较简单:

  • 两个pass
  • 第一个绘制本体
  • 第二个将顶点想法线方向挤出,再绘制本体的背面

具体还有很多种描边,这里只简单介绍这种

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = tex2D(_MainTex, i.uv);
                col.rgb *= _MainColor;
                return col;
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                return UnityObjectToClipPos(vertex + normal * _OutLineWidth);
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

添加光影 - RecieveShadow

首先是阴影

自身接收阴影

Unity Shader - Simple Toon Shading - 简单卡通渲染_第4张图片

Shader

shader中添加了阴影的注释

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };
            struct v2f {
                // 变量名必须为pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                //float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 有采样光源空间深度图,将光源空间下的坐标与深度图比较是否在于深度图
                // 大于返回光影数据值作为系数衰减,否则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * UNITY_LIGHTMODEL_AMBIENT.a;

                atten = atten * 0.5 + 0.5;
                // diffuse
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor * halfLambert * atten;
                return fixed4(ambient + diffuse, 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                return UnityObjectToClipPos(vertex + normal * _OutLineWidth);
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

调整阴影 - Adjusting Shadow Params

当然了,这个光影效果,不是我们卡通渲染需要的。
卡通渲染的光影过渡是比较硬的,我们可以使用一张1D的纹理过渡图来处理阴影的过渡
该纹理尺寸一般只要:256x1就够了
但是我为了纹理方便查看,我就使用了256x16

下面使用GIMP来绘制(我的游戏本上没有安装PS,因为PS都要收费,破解的又不想安装在游戏本中,因为一般破解程序都有木马,所以我就使用了免费、开源的GIMP,但是肯定没有PS好用,T^T)

我用填充工具随便填个渐变色图

  • 皮肤的在这里插入图片描述
  • 非皮肤在这里插入图片描述
    Unity Shader - Simple Toon Shading - 简单卡通渲染_第5张图片
    效果不是很理想
    特别在头发,脸部的光影

其实这些可以使用额外的纹理来mask或是系数控制

在做光影时,发现模型制作不是很规范
(胸部部分竟然做到了衣服的子模型里)
所以导致胸部的部分光影不对
硬是要解决就是用mask texture来处理,但没必要了,以后再找找看有没更简单的模型,方便测试的
Unity Shader - Simple Toon Shading - 简单卡通渲染_第6张图片

Shader

只有ambient+diffuse的光影
思路:

  • 使用diffuse的LdotN系数来控制对GradientTex阴影梯度纹理采样
  • GradientTex纹理主要是控制阴影梯度的,可给外部提供灵活的控制方式

当然这只是其一一种方式
也可以使用:

  • ShadowColor 阴影颜色
  • GradientGrayTex 阴影亮度剔除

然后:

fixed g = tex2D(GradientGrayTex, LdotN).r;
// g *= atten // 阴影衰减
// g *= specular // 高光
fixed3 shadow = ShadowColor * g;
fixed3 combined = ambient + diffuse + specular;

combined = lerp(combined, shadow, _ShadowItensity);

下面shader没有高光的阴影,一般卡通渲染的高光比较少,或是没有高光

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        [NoScaleOffset] _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
        [NoScaleOffset] _GradientTex ("GradientTex", 2D) = "white" {}       // 用于阴影梯度采样纹理,暂时就叫这个名词吧
        _GradientIntensity ("GradientIntensity", Range(0,1)) = 1            // 阴影梯度采样纹理强度
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };
            struct v2f {
                // 变量为必须要pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            fixed4 _MainColor;
            sampler2D _GradientTex;
            fixed _GradientIntensity;
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                //float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 有采样光源空间深度图,将光源空间下的坐标与深度图比较是否在于深度图
                // 大于返回光影数据值作为系数衰减,否则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);

                // ambient
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * UNITY_LIGHTMODEL_AMBIENT.a;

                atten = atten * 0.5 + 0.5;
                // diffuse
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;                                  // 使用半lambert,背光不用太黑
                fixed lightShadowCoef = halfLambert * atten;                            // 乘上光影系数,应用上自身阴影
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor;
                fixed3 gradient = tex2D(_GradientTex, float2(lightShadowCoef, 0)).rgb;  // 使用光影系数采样梯度纹理
                diffuse = lerp(diffuse, diffuse * gradient, _GradientIntensity);        // 阴影强弱插值
                return fixed4(ambient + diffuse, 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

无透视法线挤出描边

然后再改了改描边,不需要透视
不然近距离镜头时,描边会变粗,如下图
Unity Shader - Simple Toon Shading - 简单卡通渲染_第7张图片
下面是无透视的描边
Unity Shader - Simple Toon Shading - 简单卡通渲染_第8张图片

Shader

看vs即可

            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }

整体运行效果

Unity Shader - Simple Toon Shading - 简单卡通渲染_第9张图片

最后我将头发颜色调整为红色,风格也挺搭的

调整材质的MainColor参数为红色即可,效果如下:

高光 - Specular

下面是我们正常高光
但是太平滑了,我们需要硬边过渡
Unity Shader - Simple Toon Shading - 简单卡通渲染_第10张图片
因为需要硬边过渡,我就简单粗暴的添加一个SpecularThreshold来过滤掉一些比较小的高光值
效果如下:
Unity Shader - Simple Toon Shading - 简单卡通渲染_第11张图片
最终高光在没个子模型的材质参数再调整一下
Unity Shader - Simple Toon Shading - 简单卡通渲染_第12张图片
没添加高光前的对比
Unity Shader - Simple Toon Shading - 简单卡通渲染_第13张图片
添加一个自转,不是镜头转了,方便看光影

描边小一些,头发黄色,角度换一下,那把刀好帅
Unity Shader - Simple Toon Shading - 简单卡通渲染_第14张图片
头发那些高光不太理想,一般需要手绘纹理的光影mask来处理就会好很多。或是头发高模法线图也可以

还有头发、大腿、两部的高光过渡太平滑了,我们将其参数调整一下,效果会更好
Unity Shader - Simple Toon Shading - 简单卡通渲染_第15张图片

在此基础上,如果对头发、衣服黄金色纹理,纽扣,铠甲金属,如果再细分一下纹理分通道来控制高光系数纹理图的话,可以制作得非常好的效果,但没有资源。

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        [NoScaleOffset] _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
        [NoScaleOffset] _GradientTex ("GradientTex", 2D) = "white" {}       // 用于阴影梯度采样纹理,暂时就叫这个名词吧
        _GradientIntensity ("GradientIntensity", Range(0,1)) = 1            // 阴影梯度采样纹理强度
        _SpecularPower ("SpecularPower", Range(1,100)) = 80                 // 高光平滑度
        _SpecularIntensity ("SpecularItensity", Range(0,1)) = 1             // 高光强度
        _SpecularThreshold ("SpecularThreshold", Range(0,1)) = 0.3          // 高光阈值
        _SpecularBrightness ("SpecularBrightness", Range(0,1)) = 0.1        // 高光添加的亮度量
        _SpecularValue ("SpecularValue", Range(0,1)) = 1                    // 高光值
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };
            struct v2f {
                // 变量名必须为pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                half3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                UNITY_LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            fixed4 _MainColor;
            sampler2D _GradientTex;
            fixed _GradientIntensity;
            fixed _SpecularPower;
            fixed _SpecularIntensity;
            fixed _SpecularThreshold;
            fixed _SpecularBrightness;
            fixed _SpecularValue;
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将世界空间光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将世界空间阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                half3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 采样光源空间深度图,将光源空间下的坐标与深度图比较
                // 大于深度图的返回光影数据值作为系数衰减,小于深度图的则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
                atten = atten * 0.5 + 0.5;

                // ambient
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;                                      // 使用半lambert,背光不用太黑
                fixed lightShadowCoef = halfLambert * atten;                                // 乘上光影系数,应用上自身阴影
                
                // diffuse
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor;
                fixed3 dGradient = tex2D(_GradientTex, float2(lightShadowCoef, 0)).rgb;     // 使用光影系数采样梯度纹理
                //return fixed4(dGradient,1);
                diffuse = lerp(diffuse,  diffuse * dGradient, _GradientIntensity);          // 漫反射光影强弱插值

                // specular
                half3 hDir = normalize(viewDir + lightDir);
                fixed HdotN = max(0, dot(hDir, i.worldNormal));
                fixed specular = pow(HdotN, _SpecularPower) * _SpecularIntensity;
                specular *= atten;                                                          // 阴影衰减对高光有些影响
                specular = step(_SpecularThreshold, specular) * _SpecularValue;             // 大于阈值的才有效
                //return specular;

                // 高光这儿,我们是源diffuse的高光部分叠加,还有高光亮度叠加
                return fixed4(ambient + diffuse + diffuse * specular + specular * _SpecularBrightness, 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

边缘光 - Rim

一般卡通渲染也是不需要边缘光的,下面我们就下一丢丢的边缘光好了,不多
Unity Shader - Simple Toon Shading - 简单卡通渲染_第16张图片
合成后
Unity Shader - Simple Toon Shading - 简单卡通渲染_第17张图片

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        [NoScaleOffset] _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
        [NoScaleOffset] _GradientTex ("GradientTex", 2D) = "white" {}       // 用于阴影梯度采样纹理,暂时就叫这个名词吧
        _GradientIntensity ("GradientIntensity", Range(0,1)) = 1            // 阴影梯度采样纹理强度
        _SpecularPower ("SpecularPower", Range(1,100)) = 80                 // 高光平滑度
        _SpecularIntensity ("SpecularItensity", Range(0,1)) = 1             // 高光强度
        _SpecularThreshold ("SpecularThreshold", Range(0,1)) = 0.3          // 高光阈值
        _SpecularBrightness ("SpecularBrightness", Range(0,1)) = 0.1        // 高光添加的亮度量
        _SpecularValue ("SpecularValue", Range(0,1)) = 1                    // 高光值
        _RimIntensity ("RimIntensity", Range(0,5)) = 1                      // 边缘光
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };
            struct v2f {
                // 变量名必须为pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                half3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                UNITY_LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            fixed4 _MainColor;
            sampler2D _GradientTex;
            fixed _GradientIntensity;
            fixed _SpecularPower;
            fixed _SpecularIntensity;
            fixed _SpecularThreshold;
            fixed _SpecularBrightness;
            fixed _SpecularValue;
            fixed _RimIntensity;
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将世界空间光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将世界空间阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                half3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 采样光源空间深度图,将光源空间下的坐标与深度图比较
                // 大于深度图的返回光影数据值作为系数衰减,小于深度图的则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
                atten = atten * 0.5 + 0.5;

                // ambient
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;                                      // 使用半lambert,背光不用太黑
                fixed lightShadowCoef = halfLambert * atten;                                // 乘上光影系数,应用上自身阴影
                
                // diffuse
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor;
                fixed3 dGradient = tex2D(_GradientTex, float2(lightShadowCoef, 0)).rgb;     // 使用光影系数采样梯度纹理
                //return fixed4(dGradient,1);
                diffuse = lerp(diffuse,  diffuse * dGradient, _GradientIntensity);          // 漫反射光影强弱插值

                // specular
                half3 hDir = normalize(viewDir + lightDir);
                fixed HdotN = max(0, dot(hDir, i.worldNormal));
                fixed specular = pow(HdotN, _SpecularPower) * _SpecularIntensity;
                specular *= atten;                                                          // 阴影衰减对高光有些影响
                specular = step(_SpecularThreshold, specular) * _SpecularValue;             // 大于阈值的才有效
                //return specular;

                // rim
                fixed rimFactor = (1 - dot(viewDir, i.worldNormal)) * _RimIntensity;        // 边缘光
                rimFactor *= atten;                                                         // 应用上光影衰减系数
                rimFactor = step(_SpecularThreshold, rimFactor) * _SpecularValue;           // 阈值使用高光的
                //return rimFactor;

                specular = max(specular, rimFactor);                                        // 这里简单处理:高光与边缘光哪个亮取哪个

                // 高光这儿,我们是源diffuse的高光部分叠加,还有高光亮度叠加
                return fixed4(
                    ambient + 
                    diffuse + 
                    diffuse * specular + specular * _SpecularBrightness
                    , 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

控制边缘光在背光时才显示 - Rim Show At Back To the Lighting

	[Toggle] _RimShowAtBackToLight ("RimShowAtBackToLight", Float) = 0  // 边缘光是否被光是才显示
...
	rimFactor = lerp(rimFactor, rimFactor * max(0, dot(-lightDir, viewDir)), _RimShowAtBackToLight);   // 视线越背光,边缘光应该越亮


Shader Properties中可调整
在这里插入图片描述

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        [NoScaleOffset] _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
        [NoScaleOffset] _GradientTex ("GradientTex", 2D) = "white" {}       // 用于阴影梯度采样纹理,暂时就叫这个名词吧
        _GradientIntensity ("GradientIntensity", Range(0,1)) = 1            // 阴影梯度采样纹理强度
        _SpecularPower ("SpecularPower", Range(1,100)) = 80                 // 高光平滑度
        _SpecularIntensity ("SpecularItensity", Range(0,1)) = 1             // 高光强度
        _SpecularThreshold ("SpecularThreshold", Range(0,1)) = 0.3          // 高光阈值
        _SpecularBrightness ("SpecularBrightness", Range(0,1)) = 0.1        // 高光添加的亮度量
        _SpecularValue ("SpecularValue", Range(0,1)) = 1                    // 高光值
        _RimIntensity ("RimIntensity", Range(0,5)) = 1                      // 边缘光
        [Toggle] _RimShowAtBackToLight ("RimShowAtBackToLight", Float) = 0  // 边缘光是否被光是才显示
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };
            struct v2f {
                // 变量名必须为pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                half3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                UNITY_LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            fixed4 _MainColor;
            sampler2D _GradientTex;
            fixed _GradientIntensity;
            fixed _SpecularPower;
            fixed _SpecularIntensity;
            fixed _SpecularThreshold;
            fixed _SpecularBrightness;
            fixed _SpecularValue;
            fixed _RimIntensity;
            fixed _RimShowAtBackToLight; 
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将世界空间光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将世界空间阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                half3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 采样光源空间深度图,将光源空间下的坐标与深度图比较
                // 大于深度图的返回光影数据值作为系数衰减,小于深度图的则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
                atten = atten * 0.5 + 0.5;

                // ambient
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;                                      // 使用半lambert,背光不用太黑
                fixed lightShadowCoef = halfLambert * atten;                                // 乘上光影系数,应用上自身阴影
                
                // diffuse
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor;
                fixed3 dGradient = tex2D(_GradientTex, float2(lightShadowCoef, 0)).rgb;     // 使用光影系数采样梯度纹理
                //return fixed4(dGradient,1);
                diffuse = lerp(diffuse,  diffuse * dGradient, _GradientIntensity);          // 漫反射光影强弱插值

                // specular
                half3 hDir = normalize(viewDir + lightDir);
                fixed HdotN = max(0, dot(hDir, i.worldNormal));
                fixed specular = pow(HdotN, _SpecularPower) * _SpecularIntensity;
                specular *= atten;                                                          // 阴影衰减对高光有些影响
                specular = step(_SpecularThreshold, specular) * _SpecularValue;             // 大于阈值的才有效
                //return specular;

                // rim
                fixed rimFactor = (1 - dot(viewDir, i.worldNormal)) * _RimIntensity;        // 边缘光
                rimFactor = lerp(rimFactor, rimFactor * max(0, dot(-lightDir, viewDir)), _RimShowAtBackToLight);   // 视线越背光,边缘光应该越亮
                rimFactor *= atten;                                                         // 应用上光影衰减系数
                rimFactor = step(_SpecularThreshold, rimFactor) * _SpecularValue;           // 阈值使用高光的
                //return rimFactor;

                specular = max(specular, rimFactor);                                        // 这里简单处理:高光与边缘光哪个亮取哪个

                // 高光这儿,我们是源diffuse的高光部分叠加,还有高光亮度叠加
                return fixed4(
                    ambient + 
                    diffuse + 
                    diffuse * specular + specular * _SpecularBrightness
                    , 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

Project

TestNPR_ToonShading_卡通渲染_ambient_diffuse_specular_rim

References

收集了一些资料,后面进一步了解卡通渲染再去看看,上面的是之前理解的很少一部分内容总结写出来的。

  • 【NPR】卡通渲染
  • 卡通渲染及其相关技术总结
  • 风格化角色渲染实践

你可能感兴趣的:(unity,unity-shader)