本篇为大家总结了Unity3D中的法线转换与切线空间知识。在Shader编程中经常会使用一些矩阵变换函数接口,其实它就是把固定流水线中的矩阵变换转移到了可编程流水线或者说GPU中,先看下面的函数语句:
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
在这里先给大家介绍一下,为何使用此函数,模型的顶点法线是位于模型空间下的,因此我们首先需要把法线转换到世界空间中。计算方式可以使用顶点变换矩阵的逆转置矩阵对法线进行相同的变换,因此我们首先得到模型空间到世界空间的变换矩阵的逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法。由于法线是一个三维矢量,因此我们只需要截取_World2Object的前三行前三列即可。下面给大家展示变换Shader代码。
由于光源方向、视角方向大多都是在世界空间下定义的,所以问题就是如何把它们从世界空间变换到切线空间下。我们可以先得到世界空间中切线空间的三个坐标轴的方向表示,然后把它们按列摆放,就可以得到从切线空间到世界空间的变换矩阵,那么再对这个矩阵求逆就可以得到从世界空间到切线空间的变换:
///
/// Note that the code below can handle both uniform and non-uniform scales
///
// Construct a matrix that transforms a point/vector from tangent space to world space
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
float4x4 tangentToWorld = float4x4(worldTangent.x, worldBinormal.x, worldNormal.x, 0.0,
worldTangent.y, worldBinormal.y, worldNormal.y, 0.0,
worldTangent.z, worldBinormal.z, worldNormal.z, 0.0,
0.0, 0.0, 0.0, 1.0);
// The matrix that transforms from world space to tangent space is inverse of tangentToWorld
float3x3 worldToTangent = inverse(tangentToWorld);
// Transform the light and view dir from world space to tangent space
o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));
由于Unity不支持Cg的inverse函数,所以还需要自己定义一个inverse函数,这种做法明显比较麻烦。实际上,在Unity 4.x版本及其之前的版本中,内置的shader一直是原来书上那种不严谨的转换方法,这是因为Unity 5之前,如果我们对一个模型A进行了非统一缩放,Unity内部会重新在内存中创建一个新的模型B,模型B的大小和缩放后的A是一样的,但是它的缩放系数是统一缩放。换句话说,在Unity 5以前,实际上我们在Shader中根本不需要考虑模型的非统一缩放问题,因为在Shader阶段非统一缩放根本就不存在了。但从Unity 5以后,我们就需要考虑非统一缩放的问题了。
Shader "Unity Shaders/Normal Map In Tangent Space" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_BumpScale ("Bump Scale", Float) = 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"
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 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDir: TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
// Unity doesn't support the 'inverse' function in native shader
// so we write one by our own
// Note: this function is just a demonstration, not too confident on the math or the speed
// Reference: http://answers.unity3d.com/questions/218333/shader-inversefloat4x4-function.html
float4x4 inverse(float4x4 input) {
#define minor(a,b,c) determinant(float3x3(input.a, input.b, input.c))
float4x4 cofactors = float4x4(
minor(\_22\_23\_24, \_32\_33\_34, \_42\_43_44),
-minor(\_21\_23\_24, \_31\_33\_34, \_41\_43_44),
minor(\_21\_22\_24, \_31\_32\_34, \_41\_42_44),
-minor(\_21\_22\_23, \_31\_32\_33, \_41\_42_43),
-minor(\_12\_13\_14, \_32\_33\_34, \_42\_43_44),
minor(\_11\_13\_14, \_31\_33\_34, \_41\_43_44),
-minor(\_11\_12\_14, \_31\_32\_34, \_41\_42_44),
minor(\_11\_12\_13, \_31\_32\_33, \_41\_42_43),
minor(\_12\_13\_14, \_22\_23\_24, \_42\_43_44),
-minor(\_11\_13\_14, \_21\_23\_24, \_41\_43_44),
minor(\_11\_12\_14, \_21\_22\_24, \_41\_42_44),
-minor(\_11\_12\_13, \_21\_22\_23, \_41\_42_43),
-minor(\_12\_13\_14, \_22\_23\_24, \_32\_33_34),
minor(\_11\_13\_14, \_21\_23\_24, \_31\_33_34),
-minor(\_11\_12\_14, \_21\_22\_24, \_31\_32_34),
minor(\_11\_12\_13, \_21\_22\_23, \_31\_32_33)
);
#undef minor
return transpose(cofactors) / determinant(input);
}
v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY\_MATRIX\_MVP, v.vertex);
o.uv.xy = v.texcoord.xy * \_MainTex\_ST.xy \_MainTex\_ST.zw;
o.uv.zw = v.texcoord.xy * \_BumpMap\_ST.xy \_BumpMap\_ST.zw;
///
/// Note that the code below can handle both uniform and non-uniform scales
///
// Construct a matrix that transforms a point/vector from tangent space to world space
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
/*
float4x4 tangentToWorld = float4x4(worldTangent.x, worldBinormal.x, worldNormal.x, 0.0,
worldTangent.y, worldBinormal.y, worldNormal.y, 0.0,
worldTangent.z, worldBinormal.z, worldNormal.z, 0.0,
0.0, 0.0, 0.0, 1.0);
// The matrix that transforms from world space to tangent space is inverse of tangentToWorld
float3x3 worldToTangent = inverse(tangentToWorld);
*/
//wToT = the inverse of tToW = the transpose of tToW as long as tToW is an orthogonal matrix.
float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
// Transform the light and view dir from world space to tangent space
o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));
///
/// Note that the code below can only handle uniform scales, not including non-uniform scales
///
// Compute the binormal
// float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
// // Construct a matrix which transform vectors from object space to tangent space
// float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
// Or just use the built-in macro
// TANGENT\_SPACE\_ROTATION;
//
// // Transform the light direction from object space to tangent space
// o.lightDir = mul(rotation, normalize(ObjSpaceLightDir(v.vertex))).xyz;
// // Transform the view direction from object space to tangent space
// o.viewDir = mul(rotation, normalize(ObjSpaceViewDir(v.vertex))).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);
// Get the texel in the normal map
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentNormal;
// If the texture is not marked as "Normal map"
// tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
// tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// Or mark the texture as "Normal map", and use the built-in funciton
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(\_MainTex, i.uv).rgb * \_Color.rgb;
fixed3 ambient = UNITY\_LIGHTMODEL\_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir tangentViewDir);
fixed3 specular = \_LightColor0.rgb * \_Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
return fixed4(ambient diffuse specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
再给大家介绍一种方法:我们想要把法线空切线空间变换到世界空间,即如果我们想要把向量从空间A变换到空间B,则需要得到空间A的三个基向量在空间B下的表示,并把这三个基向量依次按列摆放,再与需要进行变换的列向量相乘即可。因此,我们需要得到切线空间的三个基向量在世界空间下的表示,并把它们按列摆放。切线空间下的三个基向量分别是TBN(切线、副切线和法线),我们已知这三个向量在模型空间下的表示,即模型自带的TBN的值。而它们在世界空间下的表示就可以通过把它们从模型空间变换到世界空间即可。切线T的变换直接用UnityObjectToWorldDir(v.tangent.xyz)变换即可,而法线N的变换就需要考虑非统一缩放的影响,如果我们仍然使用UnityObjectToWorldDir(v. normal.xyz)来直接变换法线就会出现变换后的法线不再于三角面垂直的情况,所以由此构建出来的基向量空间也会是有问题的,导致变换后的法线方向也是错误的,你可以参考下图中的第一行的情况。正确的做法是,在变换法线N时使用UnityObjectToWorldNormal(v.normal)来进行变换,即使用逆转置矩阵去将模型法线N从模型空间变换到世界空间,由此我们就可以得到正确的变换,参考下图第二行的情况。
大家可自行查看UnityCG.cginc文件,它里面封装了很多关于矩阵变换的接口函数如下所示:
来源:oschina
链接:https://my.oschina.net/u/918889/blog/1842582