中山大学 数据科学与计算机学院 软件工程数字媒体 洪鹏圳
前言
在上一篇博客:Illustrative Rendering in Team Fortress 2中我们介绍了Value公司在《军团要塞2》中的卡通着色算法。这篇博客我将用Unity简单实现该论文中角色的卡通渲染。
实现
我们按照论文中的步骤来逐步实现:
Albedo
首先渲染出Figure6(a)的Albedo贴图,即人物模型本身自带的纹理。着色器代码如下:
Shader "NPR_Lab/TeamFotress2" {
Properties {
_Color("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
}
SubShader {
Pass{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
//定义Properties变量
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
//定义顶点着色器输入
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};
//定义顶点着色器输出
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
//顶点着色器
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
//片段着色器
fixed4 frag(v2f i) : SV_Target{
//计算所需的方向向量
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 c = tex2D(_MainTex, i.uv) * _Color;
fixed3 albedo = c.rgb;
return c;
}
ENDCG
}
}
FallBack "Diffuse"
}
效果如下,可以看出纹理中基本都是简约的纯色色块:
Wraped diffuse
首先来回顾下论文里计算非依赖视角的光照部分的公式(1):
可见,在非依赖视角的光照部分,《军团要塞2》中除了考虑了一般的漫反射外,还加入了基于模型法线方向的环境光分量(ambient cube),此外,通常的漫反射分量改为了wrapped diffuse(通过一张漫反射变形贴图)。
其中,Wraped diffuse就是中括号里加号右边那部分。在这里我只考虑一个光源,并让α = 0.5,β = 0.5,γ = 1,使用的漫反射变形贴图如下:
片段着色器代码如下:
//计算wraped diffuse term
half difLight = dot(worldNormal, worldLightDir);//n·l
half halfLambert = pow(0.5 * difLight + 0.5, 1.0);//半兰伯特因子
half3 ramp = tex2D(_RampTex, float2(halfLambert, halfLambert)).rgb;//漫反射变形
half3 difWarping = ramp * 2;//乘2使得模型更加明亮
half3 wrapDiffuseTerm = _LightColor0.rgb * difWarping;
可以看到我在计算漫反射变形函数w()时,按照论文中的做法在对漫反射变形贴图进行纹理采样后乘上了个2,所以最终结果是偏亮了,在调节了一下光源颜色后,Wraped diffuse的效果图如下:
效果和论文中Figure6(b)很接近了~
Ambient cube
Ambient cube即是公式(1)中括号加号左边那个a(n),我的做法是先通过amd的cubemap gen生成一个法线贴图:
然后通过Diffuse Cube map得到Ambient Cube,效果如下:
可见和Figure6(c)很接近。
计算非依赖视角的光照部分
要计算非依赖视角的光照部分,其实就是计算公式(1)。我们只需要把上面的wraped diffuse 和 Ambient cube加起来再乘一个漫反射系数kd即可。片段着色器代码如下:
//计算View Independent Lighting Terms
half3 viewIndependentLightTerms = kd * (ambientCubeTerm + wrapDiffuseTerm);
效果如下:
可以看到手臂上的效果和Figure6(e)中的差不多了,那么接下来我们就来添加依赖视角的光照部分(即Phong镜面高光加上自定义的边缘高光)。
计算依赖视角的光照部分
先回顾一下论文中计算依赖视角的光照部分的公式(2):
可见,在依赖视角的光照部分中,《军团要塞2》除了考虑一般的镜面反射(Pong高光)外,还基于菲涅尔现象实现了类似边缘光的效果。
其中,Specular就是式子中的左半部分,Rim lighting就是式子中的右半部分。
实现公式(2)的片段着色器代码如下:
//***计算视角相关部分***
//计算多重Phong高光部分
half3 r = reflect(worldLightDir, worldNormal);//反射向量
half3 refl = dot(r, worldViewDir);//r·v
half fresnelForSpecular = 1;//fs,这里随便取了个值
half fresnelForRim = pow(1 - dot(worldNormal, worldViewDir), 4);//fr = (1-(n·v))^4
half3 ks = tex2D(_SpecularMask, i.specularMask_uv).rgb;//高光遮罩系数,如果为1应用高光,如果为0不应用
half3 kr = tex2D(_RimMask, i.rimMask_uv).rgb;//边缘遮罩纹理值,用于减弱边缘高光对模型上某些部分的影响
half3 multiplePhongTerms = _LightColor0.rgb * ks * max(fresnelForSpecular * pow(refl, _Specular), fresnelForRim * pow(refl, _Rim));
//计算Rim Lighting部分
half av = float(1);//对环境盒子的取值函数,这里省略直接取1
half3 dedicatedRimLighting = dot(worldNormal, worldUp) * fresnelForRim * kr * av;
half3 viewDependentLightTerms = multiplePhongTerms + dedicatedRimLighting;
//最终输出颜色
fixed3 finalCol = viewIndependentLightTerms + viewDependentLightTerms;
但是因为我没有找到模型对应的高光遮罩和边缘遮罩纹理,所以最后获得的结果效果很差。然后我参考了Shader - Specular Term - Fresnel[2],换了种算法,不使用遮罩并且只使用一个高光菲涅尔因子,同时将Phong高光模型换成Blinn-Phong高光模型加快计算效率。
计算Blinn-Phong高光部分的代码如下:
float3 halfVector = normalize(worldLightDir + worldViewDir);//使用Blinn-Phong
float3 specBase = pow(saturate(dot(halfVector, worldNormal)), _SpecularPower);
float fresnel = 1.0 - dot(worldViewDir, halfVector);
fresnel = pow(fresnel, 5.0);
fresnel += _SpecularFresnel * (1.0 - fresnel);
half3 multiplePhongTerms = specBase * fresnel * _LightColor0.rgb;
得到的结果为:
与Figure6(f)效果相近。
计算Rim lighting部分的代码如下:
half rim = 1.0 - saturate(dot(worldViewDir, worldNormal));
half3 dedicatedRimLighting = _RimColor.rgb * pow(rim, _RimPower);
得到的结果为:
与Figure6(g)效果相近。
将Blinn-Phong高光和Rim lighting相加得到依赖视角的光照部分,片段着色器代码如下:
half3 viewDependentLightTerms = multiplePhongTerms + dedicatedRimLighting;
得到的结果为:
效果和Figure6(h)相近。
最终结果
将依赖视角的光照部分和非依赖视角的光照部分相加得到最终结果:
//最终输出颜色
fixed3 finalCol = viewIndependentLightTerms + viewDependentLightTerms;
return fixed4(finalCol, 1.0);
运行效果为:
可以看到我们的最终结果和Figure6(j)中一样,在明暗交界处有明显的泛红(wrapped diffuse的效果),同时模型边缘有高光效果!
Reference
- [1] Jason Mitchell, Moby Francke, Dhabih Eng. Illustrative Rendering in Team Fortress 2
- [2] Shader - Specular Term - Fresnel
- [3] 【Shader拓展】Illustrative Rendering in Team Fortress 2
- [4] 风格化角色渲染实践
- [5] 卡通渲染及其相关技术
结语
本博客的代码和资源均可在我的github上下载,别忘了点颗Star哟!