卡通光照
课本上介绍的卡通着色器有使用坡度图(ramp map)和直接程序截断两种方法。因为前者不太方便在编辑器中根据实际模型调整,所以个人比较偏向后一种方法。本文中的光照也是使用后者进行渲染。
float3 N = normalize(法线方向);
float3 L = normalize(光照方向);
float NdotL = dot(N, L);
NdotL = max(0.1, floor(NdotL * 层数参数)/层数参数);
最终光照 = _lightColor0.rgb * NdotL * 衰减因子;
这样就可以得到分层的龙:
乘上 Albedo 后就可以了 下面是光照部分(形参不要写反了!)
half4 LightingMyLighting (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
half NdotL = dot(s.Normal, lightDir);
NdotL = (NdotL + 1) * 0.5;
half cel = floor(NdotL * _LightVal) / _LightVal;
atten = smoothstep(0.2, 0.8, atten);
half3 col = (s.Albedo * _LightColor0.rgb * atten * cel).xyz;
half4 c = half4(col, s.Alpha);
return c;
}
描边 —— 全息描边
全息描边的大致思想是计算出法线和视角较小的点(即视口中模型的边缘部分)进行染色,并且需要急剧变化的边缘。有两个语句都可以达到区分边缘效果:
float NdotV = dot(normalize(normalDir), normalize(viewDir);
NdotV = pow(saturate(1- NdotV), _Val1);
NdotV = step(_Val2, NdotV);
但是由于模型表面法线并非都是平滑连续变化的,这种描边算法不仅会粗细不均,而且使用 NdotV 的算法对正方体一类的模型很不友好。
Shader "Custom/sd_Toon"
{
Properties
{
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Alpha("Tex Alpha",Range(0,1)) = 0.5
_Color("Main Color", Color) = (1,1,1,1)
_LightVal("光照区分度",Range(0.1 , 10)) = 0.5
_OutlineColor("边线颜色", color) = (0,0,0,0)
_TestVal("边缘粗细", Range(0, 1)) = 0.1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf MyLighting
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
float3 viewDir;
float3 worldNormal;
};
half4 _Color;
float _LightVal;
half _Alpha;
fixed _TestVal;
fixed4 _OutlineColor;
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutput o)
{
half3 c;
c = tex2D (_MainTex, IN.uv_MainTex).rgb * _Color;
half NdotV = dot(o.Normal, IN.viewDir);
NdotV = pow(saturate(1- NdotV), 3);
NdotV = step((1-_TestVal), NdotV);
c.rgb = lerp(_Color.rgb, c.rgb, _Alpha);
o.Albedo = lerp(c.rgb, _OutlineColor, NdotV);
o.Alpha = 1;
}
half4 LightingMyLighting (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
half NdotL = dot(s.Normal, lightDir);
NdotL = (NdotL + 1) * 0.5;
half cel = floor(NdotL * _LightVal) / _LightVal;
atten = smoothstep(0.2, 0.8, atten);
half3 col = (s.Albedo * _LightColor0.rgb * atten * cel).xyz;
half4 c = half4(col, s.Alpha);
return c;
}
ENDCG
}
FallBack "Diffuse"
}
描边 —— 顶点膨胀描边
沿着法线
描边是否还有其他方案?回想起2D描边,会发现大部分的处理方案实际上是向外描边,而在shader中可以通过“膨胀”访问到模型外的一部分区域。
整理一下思路:通过顶点沿着【法线】方向膨胀,可以建立比原模型大一点的模型,将此模型上色为描边颜色。通过 Cull Front 剔除面向视点的部分,再将原模型渲染一次盖住中间部分。
float3 dir = mul((float3x3)UNITY_MATRIX_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
o.pos.xy += offset * o.pos.z * _Outline;
可能听起来不错?但是有些模型的模型空间原点不在几何中心,而是在底部或者顶部,导致膨胀出的轮廓不是均匀套在物体周围的。(而我刚好有个这样的模型)
在顶点与法线之间抉择
如何打造同时适配两种情况的着色器呢? 这种情况下,可以通过在顶点方向和法线方向之间插值,根据具体的模型作相应手动调整。 _Factor用于调整:float3 dir = lerp(dirV, dirN, _Factor); 方块采用沿顶点方向膨胀方式,适合有硬边且模型空间中心与几何中心基本重合的模型;鲸采用沿法线方向膨胀方式,适合法线变化比较平缓的模型。 完整代码:
Shader "Lesson/sd_OutlineTest3"
{
Properties
{
_MainTex("Main Tex", 2D) = "white"{}
_Color("Tint of MainTex", Color) = (1,1,1,1)
_Outline ("Outline Range", Range(0, 0.3)) = 0.02
_OutlineColor("Out line Color", Color) = (1,1,1,1)
_Factor("Factor of dirV and dirN", Range(0, 1)) = 1
_LightVal("Layering Val of Light", Range(0.001, 6)) = 0.5
}
SubShader
{
Pass
{
Tags { "LightMode"="Always" }
Cull Front
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed _Outline;
fixed4 _OutlineColor;
fixed _Factor;
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert (appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float3 dirV = normalize(v.vertex.xyz);
float3 dirN = v.normal;
float3 dir = lerp(dirV, dirN, _Factor);
dir = mul((float3x3)UNITY_MATRIX_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
o.pos.xy += offset * o.pos.z * _Outline;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 c = _OutlineColor;
return c;
}
ENDCG
}
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _LightColor0;
sampler2D _MainTex;
fixed4 _Color;
fixed _LightVal;
struct appdata
{
float4 pos:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:TEXCOORD1;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 lightDir : TEXCOORD0;
float3 viewDir:TEXCOORD1;
float3 normal:TEXCOORD2;
float4 mainTex:TEXCOORD3;
};
v2f vert (appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal = v.normal;
o.lightDir = ObjSpaceLightDir(v.vertex);
o.viewDir = ObjSpaceViewDir(v.vertex);
o.mainTex = v.texcoord;
return o;
}
fixed4 frag (v2f i) : COLOR
{
float4 c = 1;
float3 N = normalize(i.normal);
float3 viewDir = normalize(i.viewDir);
fixed NdotL = dot(N, i.lightDir);
NdotL = max(0.1, floor(NdotL * _LightVal)/ _LightVal);
float diff = max(0, dot(N, viewDir));
diff = (diff + 1)/2;
diff = smoothstep(0, 1, diff);
fixed4 col = tex2D(_MainTex, i.mainTex);
c = _LightColor0 * diff * NdotL * col * _Color;
return c;
}
ENDCG
}
}
}
后附: 在一般情况下这种情况是适用的,但总有一些比较极端的模型(如下),并非所有部分都是均匀需要从法线/顶点方向膨胀的。这种情况下可以尝试通过自动插值逐点判断到底是要使用顶点还是法线方向,通过点积确定该点背离几何中心的程度,然后通过lerp函数确定最终dir。但是测试了一下,效果不是很好,于是在这里不作展示。
高光改进
上面展示的程序化的卡通上色采用均分色阶,要想获得足够亮的表面只能调高色彩分阶;但是那样又丧失了卡通分解光照的简洁感。如何人工加一个风格化的高光? 突然想到,荒野之息的卡通着色方式十分廉价,但是又能取得比较好的效果。简单分析了一下可以得出以下信息:
- 拉线风格高光 —— 各向异性高光叠加应用
- 两层颜色 —— 卡通光照分层
- 光色细边 ——全息描边
好耶!三个都学过诶! 第一点:需要调整(或者直接更换)各向异性应用的贴图。因为已经不需要密度那么大的拉丝效果了,游戏中更像是素描高光的表现方式。在这里我随手拿了一张电视波纹的贴图(下图)。 避免过于突兀的高光颜色,直接采用场景中 _LightColor0 的颜色对高光线进行上色。
第二点:需要控制分出的层数保持2层(或者3层),直接观察材质下部的视窗来调整会比较明显。调整数值,使得阴影部分占的区域比较小。
第三点:因为模型边缘的轮廓主要是内描边,所以用到了全息描边的方式。将粗细度调得细一些;描边颜色同样为 _LightColor0 的颜色。
组合拼装一下,可以得到以下效果: (…好像害行?) 完整shader存档:
Shader "Lesson/sd_Toon2"
{
Properties
{
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Alpha("Tex Alpha",Range(0,1)) = 0.5
_Color("Main Color", Color) = (1,1,1,1)
_LightVal("光照区分度",Range(0.1 , 10)) = 0.5
_OutlineColor("边线颜色", color) = (0,0,0,0)
_Thickness("边缘粗细", Range(0, 1)) = 0.1
_SpecTex("高光贴图", 2D) = "white"{}
_SpecPower("高光密度" , Range(0, 0.7)) = 0.2
_SpecPower2("高光范围" , Range(0.4, 1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf MyLighting
#pragma target 3.0
sampler2D _MainTex;
sampler2D _SpecTex;
struct Input
{
float2 uv_MainTex;
float2 uv_SpecTex;
float3 viewDir;
float3 worldNormal;
};
half4 _Color;
float _LightVal;
half _Alpha;
fixed _Thickness;
fixed4 _OutlineColor;
fixed _SpecPower;
fixed _SpecPower2;
struct surfOutput
{
fixed3 Albedo;
fixed3 Normal;
fixed3 Emission;
fixed3 SpecDirection;
fixed Alpha;
};
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout surfOutput o)
{
half3 c;
c = tex2D (_MainTex, IN.uv_MainTex).rgb * _Color;
o.SpecDirection = UnpackNormal(tex2D(_SpecTex, IN.uv_SpecTex));
half NdotV = dot(o.Normal, IN.viewDir);
NdotV = pow(saturate(1- NdotV), 5);
NdotV = step((1-_Thickness), NdotV);
o.Albedo = lerp(c.rgb, _OutlineColor * _LightColor0.rgb, NdotV);
o.Alpha = 1;
}
half4 LightingMyLighting (surfOutput s, half3 lightDir, half3 viewDir, half atten)
{
fixed3 halfVector = normalize(lightDir + viewDir);
halfVector = max(0, dot(s.Normal, halfVector));
halfVector = pow(halfVector, _SpecPower2 * 17);
fixed HdotS = dot(normalize(s.SpecDirection + s.Normal), halfVector);
HdotS = step(_SpecPower, (1 - HdotS));
float spec = normalize(HdotS);
spec = saturate(spec* 0.2);
half NdotL = dot(s.Normal, lightDir);
NdotL = (NdotL + 1) * 0.5;
half cel =max(0.2, floor(NdotL * _LightVal) / _LightVal);
atten = max(0.2, smoothstep(0.3, 0.7, atten));
half3 col = saturate(((1 + spec) * s.Albedo.rgb* _LightColor0.rgb * cel) * atten);
half4 c = half4(col, s.Alpha);
return c;
}
ENDCG
}
FallBack "Diffuse"
}
参数:
后附:由于描边与光照角度并没有关系,所以在旋转的时候并不会变化,背光的情况下有穿帮风险(如下)。以后有时间的时候再改进叭。
|