Unity3D 法线转换与切线空间总结

原创 2017年08月29日 10:02:06

 在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文件,它里面封装了很多关于矩阵变换的接口函数如下所示:



版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Shader:法线转世界空间

转 http://www.cnblogs.com/qzzlw/archive/2012/10/23/2736277.html 顶点从模型空间转换到世界坐标空间只需要乘以世界转换矩阵即可,...

OpenGL核心技术之切线空间

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等...

Unity喷墨效果Shader实现

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等...
  • jxw167
  • jxw167
  • 2017-07-07 23:00
  • 2823

Unity Shader Example 15 (Gerstner Wave)

Shader "Unlit/GerstnerWave" { Properties { _MainTex ("Texture", 2D) = "black" {} // example 1...

[DirectX11]Gerstner波 实现简单的水面模拟

上一篇文章中,介绍了一个简单数值方法来模拟圆形扩散波的效果,但是这种方法对于自然中像海浪一样的波 就无能为力了。所以,这篇文章介绍用Gerstner波来模拟水面波纹效果。 一、Gers...

Unity Shader: 优化GPU代码--用step()代替if else等条件语句。

普通的卡通着色Shader: 先看一个Shader,卡通着色。 Shader "Unlit/NewToonShading" { Properties { _Shin...

都说“知乎”逼格高,我们来实现“知乎”回答详情页动画效果

2014已经远去,2015年的目标很简单,就是继续熟悉Android的上层API,虽然偶尔会为了某个问题去研究下FrameWork的代码,但是对于我们这种新手来说,只有对上层的API用的熟练了,才能更...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)