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



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

法线贴图原理,切线空间的概念理解

转自http://www.cnblogs.com/tekkaman/p/3992352.html 法线贴图原理 【法线贴图原理】   如果法线处于世界坐标中的(world space...

理解Unity法线贴图的切线空间存储方式

最近再学习一些unity shader的一些东西,看到法线贴图那里突然不是很理解,经过查找资料,现在也是恍然大悟,也是深深敬佩发明切线空间法线贴图那个大神! 话不多说,直接说一下,关于法线贴...
  • nijiayy
  • nijiayy
  • 2017年03月31日 15:54
  • 1198

空间曲线的切线、主法线、副法线

Unity Shader:向量在世界空间与切线空间中变换的相关数学原理详解

1,将向量由世界空间变换至切线空间 2,将向量由切线空间变换至世界空间1,将向量由世界空间变换至切线空间假设在世界空间中有一个球体,还有一个光源,光源的方向接近于正对球体点p的法线。 图1在点...

Unity Shader 学习笔记 (七) 根据切线和法线方向设置模型颜色shader

Unity Shader 学习笔记 (七) 根据切线和法线方向设置模型颜色shader 法线方向颜色Shader Shader代码 Shader "Custom/faxianColor" { Sub...

Shader:法线转世界空间

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

unity shader (5)--实现半兰伯特模型

摘自冯乐乐的《unity shader 入门精要》 这个跟前面的比,只是多了一行代码:Shader "Custom/HalfLambertLevelMat" { Properties {...

【Unity Shaders】法线纹理(Normal Mapping)的实现细节

写在前面 写这篇的目的是为了总结我长期以来的混乱。虽然题目是“法线纹理的实现细节”,但其实我想讲的是如何在shader中编程正确使用法线进行光照计算。这里面最让人头大的就是各种矩阵运算和坐标系之间...

【Unity3D Shader编程】之十 深入理解Unity5中的Standard Shader(二)&屏幕油画特效的实现

本系列文章由@浅墨_毛星云 出品,转载请注明出处。   文章链接: http://blog.csdn.net/poem_qianmo/article/details/49556461 作者:毛星云(浅...
  • zhmxy555
  • zhmxy555
  • 2015年11月08日 17:40
  • 20840

基于物理着色(PBS)及Unity中的实现

============================= 首先是一些基本概念   立体角 是一个物体对于一个特定观察点在三维空间中的角度(观测到的大小),记作Ω, 单位为球面度(sr),即三维弧度,...
  • liumazi
  • liumazi
  • 2017年06月08日 17:37
  • 1332
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Unity3D 法线转换与切线空间总结
举报原因:
原因补充:

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