写在前面
这部分其实算是一个补充,因为早在【Unity Shader】纹理实践3.0:切线空间下使用法线纹理就已经介绍了切线空间下如何进行法线纹理计算的过程,当时由于我的判断问题,我一直认为既然切线空间比世界空间优越很多,那只掌握法线空间下的就够了,实在是我的失误。
刚好学到后面遇上了需要用GrabPass实现玻璃效果,Cubemap和法线纹理需要同时被应用,这里就必须使用世界空间计算了,因此紧急补充一篇如何在世界空间下使用法线纹理的博客。
1 效果及代码
1.1 效果
这跟之前的切线空间下计算的效果其实是没有任何区别的。
1.2 Shader完整代码
Shader "Unity Shaders Book/Chapter 7/WorldSpace_NormalMap"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" {}
_BumpMap ("Bump Map", 2D) = "bump" {}
_BumpScale ("Bump Scale", range(0.1, 1.0)) = 1.0
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT; //获得顶点储存的切线方向,注意这里是float4而非float3
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0; //需要存两组uv,所以是float4
//需要有插值寄存器储存变换矩阵:
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//xy是缩放,zw是偏移
//o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
//o.uv.zw = v.texcoord.zw * _BumpMap_ST.xy + _BumpMap_ST.zw;
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
//计算世界空间下的参数们:
//顶点:
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal = UnityObjectToWorldNormal(v.normal).xyz;
float3 worldTangent = UnityObjectToWorldNormal(v.tangent).xyz;
float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
//计算切线空间 -> 世界空间的矩阵,只需要3x3
//按列摆放
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag(v2f i) :SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
//计算光照需要参数:
fixed3 worldlightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 worldviewDir = normalize(UnityWorldSpaceViewDir(worldPos));
//对纹理采样+解码,得到法线方向
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
//进行正常的光照计算
fixed halfLambert = dot(bump, worldlightDir) * 0.5 + 0.5;
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
fixed3 diffuse = _LightColor0.rgb * halfLambert;
fixed3 halfDir = normalize(worldlightDir + worldviewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, bump)), _Gloss);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT .rgb;
return fixed4((ambient + specular + diffuse) * albedo, 1.0);
}
ENDCG
}
}
FallBack OFF
}
2 一些重点
首先要想实现在世界空间下计算光照模型,需要在片元着色器中先对法线纹理采样,得到切线空间下的法线矢量后,通过变换矩阵将它从切线空间变换到世界空间。
2.1 插值寄存器的大小限制
这一点在我之前的博客中一直都没提到过,但这次实践的很明显,就是在计算从切线空间到世界空间的变换矩阵这里:
//需要有插值寄存器储存变换矩阵:
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
插值寄存器的大小最多只能储存float4的变量,Shader中如果面临着储存矩阵,就可以像上面一样给它拆分成多个变量储存。
这里还充分利用了插值寄存器的大小,把最后的w分量储存进世界空间的顶点位置。
2.2 矩阵分量的排序
//计算切线空间 -> 世界空间的矩阵,只需要3x3
//按列摆放
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, o.worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, o.worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, o.worldNormal.z, worldPos.z);
这里要分清楚T、B、N三个分量是按列排序的,切线空间下是按行排序的,具体为什么可以参考一下【Unity Shader】纹理实践3.0:切线空间下使用法线纹理
3 与切线空间对比
3.1 效率上切线空间完胜
首先,二者的表现效果无任何差别。但在效率上,切线空间完胜世界空间。这是为什么?两种方法都尝试的话很容易发现:
- 世界空间下的矩阵变换计算是在片元着色器中完成的,这涉及到了大量的计算
- 切线空间由于不需要对法线纹理进行改动,直接保留切线空间的法线矢量就行,因此在顶点着色器中提前计算好切线空间下的light、view方向等,在片元着色器中直接采样法线纹理后,进行光照计算就行
3.2 有Cubemap须选择世界空间
前面的博客我已经尝试过了Cubemap的使用,需要用世界空间下的反射方向去采样我们的Cubemap,所以我们如果在使用法线纹理的同时需要使用Cubemap,这时候只能在世界空间下进行计算。