使用的是Unity内置管线,后面有时间再学习:LWRP(URP),还有HDRP,学习任务有点多,一步一步来
本来想弄个资源来学习:后处理实现:深度+法线描边的
但现在有个适合的资源,还是先处理一下简单的卡通渲染效果吧
而且弄好后,后面做其他的效果也有个比较好的模型来做实验
刀的法线是有问题的,可能是建模的同学法线没处理好
子模型、Mask纹理都还不够细分,否则某些部位的光影可以控制得很完美
下面一步步来显示
可以看到纹理中部分的边缘信息也话上去了,如:白色丝绸的边缘,有还超短裙上的黑边条纹。
这些一般不是高级超模的几何体模型,都会画在纹理上。
// 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"
}
加上顶点按法线方向挤出后的背面绘制来描边的效果。
比之前没有描边的好很多,最明显的是,大腿之间的相同颜色的线条、头发与天空盒的线条
上面使用的描边方式比较简单:
具体还有很多种描边,这里只简单介绍这种
// 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"
}
首先是阴影
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"
}
当然了,这个光影效果,不是我们卡通渲染需要的。
卡通渲染的光影过渡是比较硬的,我们可以使用一张1D的纹理过渡图来处理阴影的过渡
该纹理尺寸一般只要:256x1就够了
但是我为了纹理方便查看,我就使用了256x16
下面使用GIMP来绘制(我的游戏本上没有安装PS,因为PS都要收费,破解的又不想安装在游戏本中,因为一般破解程序都有木马,所以我就使用了免费、开源的GIMP,但是肯定没有PS好用,T^T)
我用填充工具随便填个渐变色图
其实这些可以使用额外的纹理来mask或是系数控制
在做光影时,发现模型制作不是很规范
(胸部部分竟然做到了衣服的子模型里)
所以导致胸部的部分光影不对
硬是要解决就是用mask texture来处理,但没必要了,以后再找找看有没更简单的模型,方便测试的
只有ambient+diffuse的光影
思路:
当然这只是其一一种方式
也可以使用:
然后:
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"
}
然后再改了改描边,不需要透视
不然近距离镜头时,描边会变粗,如下图
下面是无透视的描边
看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;
}
最后我将头发颜色调整为红色,风格也挺搭的
调整材质的MainColor参数为红色即可,效果如下:
下面是我们正常高光
但是太平滑了,我们需要硬边过渡
因为需要硬边过渡,我就简单粗暴的添加一个SpecularThreshold来过滤掉一些比较小的高光值
效果如下:
最终高光在没个子模型的材质参数再调整一下
没添加高光前的对比
添加一个自转,不是镜头转了,方便看光影
描边小一些,头发黄色,角度换一下,那把刀好帅
头发那些高光不太理想,一般需要手绘纹理的光影mask来处理就会好很多。或是头发高模法线图也可以
还有头发、大腿、两部的高光过渡太平滑了,我们将其参数调整一下,效果会更好
在此基础上,如果对头发、衣服黄金色纹理,纽扣,铠甲金属,如果再细分一下纹理分通道来控制高光系数纹理图的话,可以制作得非常好的效果,但没有资源。
// 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"
}
一般卡通渲染也是不需要边缘光的,下面我们就下一丢丢的边缘光好了,不多
合成后
// 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"
}
[Toggle] _RimShowAtBackToLight ("RimShowAtBackToLight", Float) = 0 // 边缘光是否被光是才显示
...
rimFactor = lerp(rimFactor, rimFactor * max(0, dot(-lightDir, viewDir)), _RimShowAtBackToLight); // 视线越背光,边缘光应该越亮
// 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"
}
TestNPR_ToonShading_卡通渲染_ambient_diffuse_specular_rim
收集了一些资料,后面进一步了解卡通渲染再去看看,上面的是之前理解的很少一部分内容总结写出来的。