文章目录

  • 最终效果 - Final Effect
  • 无光照,只有纹理与主色调
    • Shader
  • 加描边 - Outline
    • GIF
    • Shader
  • 添加光影 - RecieveShadow
    • 自身接收阴影
    • Shader
  • 调整阴影 - Adjusting Shadow Params
    • Shader
  • 无透视法线挤出描边
    • Shader
    • 整体运行效果
  • 高光 - Specular
    • Shader
  • 边缘光 - Rim
    • Shader
  • 控制边缘光在背光时才显示 - Rim Show At Back To the Lighting
    • Shader
  • Project
  • References

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

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

最终效果 - Final Effect

刀的法线是有问题的,可能是建模的同学法线没处理好
子模型、Mask纹理都还不够细分,否则某些部位的光影可以控制得很完美

下面一步步来显示

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


可以看到纹理中部分的边缘信息也话上去了,如:白色丝绸的边缘,有还超短裙上的黑边条纹。
这些一般不是高级超模的几何体模型,都会画在纹理上。

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 100Pass { // solidName "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


加上顶点按法线方向挤出后的背面绘制来描边的效果。
比之前没有描边的好很多,最明显的是,大腿之间的相同颜色的线条、头发与天空盒的线条

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 100Pass { // solidName "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 { // outlineName "Outline"Cull FrontCGPROGRAM#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

首先是阴影

自身接收阴影

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 100Pass { // solidName "Solid"Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础passCGPROGRAM#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;// diffusefixed 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 { // outlineName "Outline"Cull FrontCGPROGRAM#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)

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

  • 皮肤的
  • 非皮肤

    效果不是很理想
    特别在头发,脸部的光影

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

在做光影时,发现模型制作不是很规范
(胸部部分竟然做到了衣服的子模型里)
所以导致胸部的部分光影不对
硬是要解决就是用mask texture来处理,但没必要了,以后再找找看有没更简单的模型,方便测试的

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 100Pass { // solidName "Solid"Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础passCGPROGRAM#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);// ambientfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * UNITY_LIGHTMODEL_AMBIENT.a;atten = atten * 0.5 + 0.5;// diffusefixed 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 { // outlineName "Outline"Cull FrontCGPROGRAM#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 spacefloat4 pos = UnityObjectToClipPos(vertex);// to view space normalfixed3 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"
}

无透视法线挤出描边

然后再改了改描边,不需要透视
不然近距离镜头时,描边会变粗,如下图

下面是无透视的描边

Shader

看vs即可

            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {// projection spacefloat4 pos = UnityObjectToClipPos(vertex);// to view space normalfixed3 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;}

整体运行效果


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

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

高光 - Specular

下面是我们正常高光
但是太平滑了,我们需要硬边过渡

因为需要硬边过渡,我就简单粗暴的添加一个SpecularThreshold来过滤掉一些比较小的高光值
效果如下:

最终高光在没个子模型的材质参数再调整一下

没添加高光前的对比

添加一个自转,不是镜头转了,方便看光影

描边小一些,头发黄色,角度换一下,那把刀好帅

头发那些高光不太理想,一般需要手绘纹理的光影mask来处理就会好很多。或是头发高模法线图也可以

还有头发、大腿、两部的高光过渡太平滑了,我们将其参数调整一下,效果会更好


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

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 100Pass { // solidName "Solid"Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础passCGPROGRAM#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;// ambientfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;fixed LdotN = dot(lightDir, i.worldNormal);fixed halfLambert = LdotN * 0.5 + 0.5;                                      // 使用半lambert,背光不用太黑fixed lightShadowCoef = halfLambert * atten;                                // 乘上光影系数,应用上自身阴影// diffusefixed3 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);          // 漫反射光影强弱插值// specularhalf3 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 { // outlineName "Outline"Cull FrontCGPROGRAM#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 spacefloat4 pos = UnityObjectToClipPos(vertex);// to view space normalfixed3 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

一般卡通渲染也是不需要边缘光的,下面我们就下一丢丢的边缘光好了,不多

合成后

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 100Pass { // solidName "Solid"Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础passCGPROGRAM#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;// ambientfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;fixed LdotN = dot(lightDir, i.worldNormal);fixed halfLambert = LdotN * 0.5 + 0.5;                                      // 使用半lambert,背光不用太黑fixed lightShadowCoef = halfLambert * atten;                                // 乘上光影系数,应用上自身阴影// diffusefixed3 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);          // 漫反射光影强弱插值// specularhalf3 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;// rimfixed 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 { // outlineName "Outline"Cull FrontCGPROGRAM#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 spacefloat4 pos = UnityObjectToClipPos(vertex);// to view space normalfixed3 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 100Pass { // solidName "Solid"Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础passCGPROGRAM#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;// ambientfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;fixed LdotN = dot(lightDir, i.worldNormal);fixed halfLambert = LdotN * 0.5 + 0.5;                                      // 使用半lambert,背光不用太黑fixed lightShadowCoef = halfLambert * atten;                                // 乘上光影系数,应用上自身阴影// diffusefixed3 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);          // 漫反射光影强弱插值// specularhalf3 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;// rimfixed 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 { // outlineName "Outline"Cull FrontCGPROGRAM#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 spacefloat4 pos = UnityObjectToClipPos(vertex);// to view space normalfixed3 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 Shader - Simple Toon Shading - 简单卡通渲染相关推荐

  1. Unity Shader 学习笔记(27)渲染轮廓线(描边)方法、卡通风格渲染、素描风格渲染

    Unity Shader 学习笔记(27)渲染轮廓线(描边)方法.卡通风格渲染.素描风格渲染 参考书籍:<Unity Shader 入门精要> 渲染轮廓线(描边) 五种方法: 基于观察角度 ...

  2. Unity Shader - 板砖日志 - 简单的树、草 等植物的 随风飘扬 动画

    文章目录 目的 思路 Script include cginc appled shader csharp 效果 目的 便于后续自己的 CTRL+C,V的面向复制.粘贴编程 思路 非常简单:可以使用 p ...

  3. Unity shader 护盾shield的简单实现

    扰动 直接对uv进行变换就可以了,记得首先把六边形格子地图的Tilling调高点 先预先调成合适大小的六边形,然后repeat铺满整个护盾 1 2 3 4 5 6 // Tiles and offse ...

  4. 2019年6月日记-Unity Shader Graph 菲尼尔简单边缘发光

    ShaderGraph 菲尼尔反射边缘光 创建菲尼尔反射节点,与Color节点相差获得边缘发光的颜色. 控制菲尼尔节点的大小,调出vector1节点来可视化调整. 新建Remap(重映射)节点,将Ti ...

  5. unity 3d物体描边效果_从零开始的卡通渲染描边篇

    序言: 一直对卡通渲染非常感兴趣,前后翻找了不少的文档,做了一些工作.前段时间<从零开始>的手游上线了,试着渲染了一下的其中模型,觉得效果很不错.打算写一个专栏记录其中的渲染技术.在后面的 ...

  6. 二次元卡通渲染——进阶技巧

    前言 随着<原神>游戏的盛行,国内对于二次元游戏这块儿领域越来越看重了.二次元项目中本身基于日本的卡通动漫而来,所以最后的本质都是为了尽量还原2D立绘,而并不像PBR追求物理正确,只要好看 ...

  7. Unity shader入门精要(学习总结)

    (学习总结) 一,基础概念 什么是shader? Unity Shader定义了渲染所需的各种代码(如顶点着色器和片元着色器).属性(如使用哪些纹理等)和指令(渲染和标签设置等),而材质则允许我们调节 ...

  8. 学习Shader Unity Shader 基础

    1.如何充分利用 Unity Shader 来为我们的游戏增光添彩? 材质和 Unity Shader: 在Unity中,我们需要配合使用材质(Material)和 Unity Shader 才能达到 ...

  9. 《Unity Shader入门精要》 第五章 开始学习Unity Shader之旅 笔记

    开始学习Unity Shader之旅 最简单的片元着色器 #pragma vertex vert #pragma fragment frat 告诉Unity那个函数包括顶点着色器的代码,哪个函数包括片 ...

最新文章

  1. python的print格式化输出的format()方法和%两种方法
  2. 【深度学习】上海交大发布 MedMNIST 医学图像分析数据集 新基准
  3. .Net 实用技术收藏!!!
  4. JsonPath的使用
  5. 《Python Cookbook 3rd》笔记(3.5):字节到大整数的打包与解包
  6. 蔡天西:去麻省理工,发现找不到一个能“打”的
  7. 第29月第14天 evpp
  8. vmware通过vCerter Converter Standalone实现不同VC的V2V虚拟机迁移
  9. Android学习笔记---13_文件的操作模式.各个应用之间的文件权限
  10. centos 环境变量_CentOS系统下为用户添加root权限
  11. PyTorch 学习笔记(三):transforms的二十二个方法
  12. 服务器安装固态硬盘的步骤,服务器系统安装中不识别固态硬盘问题
  13. 主成分分析(PCA)算法实现iris数据集降维
  14. SSM框架利用Filter实现页面不登陆拦截,禁止跳过登录页面不登陆强制访问
  15. ppt 制作海报 导出高分辨率图片
  16. 华为m2青春版刷机android6,华为揽阅M2青春版线刷刷机教程_揽阅M2 LTE版救砖rom刷机包...
  17. (赴日流程)家属滞在签证
  18. 割平面算法求解整数规划
  19. 国内镜像加速 Android 源码下载
  20. 软文营销成功案例:如何进行媒体宣发-世媒讯

热门文章

  1. 【转】购买智能手机必须要知道的一些知识(cortex A8/A9/A5/A15 智能手机名称整理)...
  2. 索尼电视android屡次停止,索尼电视应用助手目前暂停使用怎么办 教你解决
  3. codevs 2495 水叮当的舞步
  4. 了解报表开发证书——FCRP
  5. 我的第一个微信小程序(手机记事本)
  6. 校招----纷享销客面经
  7. 麒麟970升级鸿蒙吗,受鸿蒙系统影响,众多华为手机或要说再见,包括麒麟970机型!...
  8. 如何利用电位器控制舵机
  9. 农业生产适宜性评价之环境资源评价算法
  10. python可以应用lbm_格子玻尔兹曼方法(LBM)python程序提速