最近驾考C1通过已拿到驾照,而且每天玩GTA5,好久没管博客了,今天有时间来一篇卡通渲染。
卡通头发渲染也是一个有意思的地方,头发上就像有一圈白条,如下:
这个白条还有个学名叫“天使环(angel ring)”,当然棋魂动漫里面是一个“带锯齿的天使环”。
我们可以稍微简单形象化一下,好比一个sphere(头顶)上有一圈白条,如下:
这种光照其实起源于之前聊过的各向异性光照,比如kajiyakay和marschner模型就很类似,当然我们需要修改得更加卡通化。
hair-rendering模型核心就是高光颜色使用BT(副切换)代替N(法线)去计算,这也好理解,如果使用N(法线)与H(半角向量),如下:
这里我具象的把线框图画出来辅助学习,如果我们使用dot(n,half),那么观察如下(黑色为viewdir,白色为lightdir,黄色为normaldir,粉色为halfdir):
(左)right viewport (右)front viewport
如果有闲工夫可以仔细观察每个顶点:黄线(normaldir)和粉线(halfdir)夹角越小点积越大光强权重越大,则形成白斑(specular分量)。
当然用这种方法是无法形成“天使环”的,而上面提到的模型中使用T(切线)或BT(副切线)则效果如下:
黄色为Normal,蓝色为Tangent,天蓝色则为叉积(左手定则)计算出的BTangent,可以看得出我们先不管hair光照公式如何,光是BTangent副切线就“长”得跟头发一样。所以我感觉我们使用BT参与光照计算,有机会可以得到一个横向垂直BT的“光带”(称为“天使环”),我们先来分别验证三种计算方式:
1.BTangent dot LightDir
Shader "CartoonHair/BTangentDotLightDirShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_DiffuseFactor("Diffuse Factor",Color) = (1,1,1,1)
_SpecularFactor("Specular Factor",Color) = (1,1,1,1)
_SpecularGloss("Specular Gloss",Range(0,50)) = 1
_LightFactor("Light Factor",Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float3 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldTangent : TEXCOORD2;
float3 worldBTangent : TEXCOORD3;
float3 worldP2S : TEXCOORD4;
float3 worldP2V : TEXCOORD5;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _DiffuseFactor;
float4 _SpecularFactor;
float _SpecularGloss;
float4 _LightFactor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldTangent = mul(UNITY_MATRIX_M,v.tangent);
o.worldBTangent = cross(o.worldNormal,o.worldTangent);
o.worldP2V = WorldSpaceViewDir(v.vertex);
o.worldP2S = WorldSpaceLightDir(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float3 worldtangent = normalize(i.worldTangent);
float3 worldbtangent = normalize(i.worldBTangent);
float3 worldp2s = normalize(i.worldP2S);
float3 worldp2v = normalize(i.worldP2V);
float3 worldnorm = normalize(i.worldNormal);
float worldhalf = normalize(worldp2v+worldp2s);
float ndots = max(0,dot(worldnorm,worldp2s));
float sdotbt = max(0,1-abs(dot(worldp2s,worldbtangent)));
float3 light = _LightColor0.rgb*_LightFactor;
float3 diffuse = _LightColor0.rgb*ndots*_DiffuseFactor;
float3 specular = _LightColor0.rgb*pow(sdotbt,_SpecularGloss)*_SpecularFactor;
col*=fixed4(light+diffuse+specular,1);
return col;
}
ENDCG
}
}
}
效果如下:
脑海里思考一下就能想象到1-dot(btangent,lightdir)可以形成“光环带”,“光环带”的大小和位置随太阳光朝向而变化。
2.BTangent dot ViewDir
float vdotbt = max(0,1-abs(dot(worldp2v,worldbtangent)));
float3 specular = _LightColor0.rgb*pow(vdotbt,_SpecularGloss)*_SpecularFactor;
如图:
因为我们用的viewdir,所以光环的大小和位置就只随maincamera的坐标y轴而变化了。
3.BTangent dot HalfDir
float hdotbt = max(0,1-abs(dot(worldhalf,worldbtangent)));
float3 specular = _LightColor0.rgb*pow(hdotbt,_SpecularGloss)*_SpecularFactor;
如图:
三种感觉还是dot(btangent,lightdir)效果好一点。
PS:为了追求计算效率,我们要因地制宜,比如这里的btangent是normal和tangent通过cross计算得到的,如果我们一开始在建模工具(3dmax或blender)中将tangent翻转到btangent(也就是沿着发根到发梢方向),就免去了cross计算。当然如果我们通过贴图采样得到btangent,那就更好了,我们甚至可以通过btangent贴图制作各种定制化的”光环“效果。
下面实现一下官方推荐的kajiyakay光照:
Shader "CartoonHair/KajiyaKayHairShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_DiffuseFactor("Diffuse Factor",Color) = (1,1,1,1)
_LightFactor("Light Factor",Color) = (1,1,1,1)
_SpecularGloss("Specular Gloss",Range(0,100)) = 1
_SpecularFactor("Specular Factor",Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float3 tangent : TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldTangent : TEXCOORD2;
float3 worldBTangent : TEXCOORD3;
float3 worldP2S : TEXCOORD4;
float3 worldP2V : TEXCOORD5;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _DiffuseFactor;
float4 _LightFactor;
float _SpecularGloss;
float4 _SpecularFactor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldTangent = mul(UNITY_MATRIX_M,v.tangent);
o.worldBTangent = cross(o.worldNormal,o.worldTangent);
o.worldP2S = WorldSpaceLightDir(v.vertex);
o.worldP2V = WorldSpaceViewDir(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float3 worldnormal = normalize(i.worldNormal);
float3 worldtangent = normalize(i.worldTangent);
float3 worldbtangent = normalize(i.worldBTangent);
float3 worldp2s = normalize(i.worldP2S);
float3 worldp2v = normalize(i.worldP2V);
float3 worldhalf = normalize(worldp2s+worldp2v);
float ndotl = dot(worldnormal,worldp2s);
float3 light = _LightColor0.rgb*_LightFactor;
float3 diffuse = _LightColor0.rgb*ndotl*_DiffuseFactor;
float3 specular = _LightColor0.rgb*pow(sqrt(1-pow(dot(worldbtangent,worldhalf),2)),_SpecularGloss)*_SpecularFactor;
col*=fixed4(light+diffuse+specular,1);
return col;
}
ENDCG
}
}
}
specular分量计算公式按照文档中推荐的来,效果如下:
我个人觉得官方推荐的计算公式看着比较舒服。接着我们改的稍微卡通化一点:
float specularpower = smoothstep(0.6,1,pow(sqrt(1-pow(dot(worldbtangent,worldhalf),2)),_SpecularGloss));
float3 specular = _LightColor0.rgb*specularpower*_SpecularFactor;
顺便用ps制作一张卡通的cartoon-hair-opaque贴图:
再看看效果:
是不是有点感觉了?当然更加先进的hair光照算法也是不少的,我们有兴趣google上到处逛逛资料和论文就能找到。
ok,我继续玩GTA5,估计通关了再写博客。