Skip to content

【常见问题】关于法线转换的问题(以及在切线空间下计算法线纹理的问题) #45

@candycat1992

Description

@candycat1992
Owner

有不少读者问我法线转换的问题,例如P128页的代码:

// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)_World2Object);

很多人觉得这句话是个bug,但其实这段代码下面的文字已经详细解释过了:
169c7769-64be-477e-ae8f-adfa84370e0a

由于是入门书籍,因此当时在写这一段的时候自认为写的比较详细,加之在第4章单独开辟了一节(4.7节)作为解释,奈何有若干读者跳过了这一段,作为作者表示很伤心T_T……因此开了一个issue特此说明!

希望大家在看书的时候从前往后看,遇到有一些难以理解的不要急于跳过,尤其是第4章,这一章非常关键,法线这里会有问题的读者我相信肯定没有仔细读过第4章^_^。我在写书的时候会在重要的地方用文字强调很多遍,请大家能够仔细阅读下文字,不要急于看代码和敲代码,这样这本书才能给您带来最大的作用~

最后,感谢大家对本书的支持。

Activity

changed the title [-]【常见问题】关于法线转换的问题[/-] [+]【常见问题】关于法线转换的问题(以及在切线空间下计算法线纹理的问题)[/+] on Nov 22, 2016
candycat1992

candycat1992 commented on Nov 22, 2016

@candycat1992
OwnerAuthor

法线纹理:在切线空间下计算

由于前几天有个读者问了我一个问题,我突然发现P148在切线空间下计算法线光照一节是有问题的。

问题

目前书里面是使用内置宏TANGENT_SPACE_ROTATION来构建一个“从模型空间到切线空间的变换矩阵”:

/// 
/// 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;

上面代码的问题在于,如果模型存在非统一缩放,即缩放尺度各分量值不相等,那么上述方法就会造成错误的法线变换问题。

改正

由于光源方向、视角方向大多都是在世界空间下定义的,所以问题就是如何把它们从世界空间变换到切线空间下。我们可以先得到世界空间中切线空间的三个坐标轴的方向表示,然后把它们按列摆放,就可以得到从切线空间到世界空间的变换矩阵,那么再对这个矩阵求逆就可以得到从世界空间到切线空间的变换:

///
/// 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函数,这可以参考更新后的Chapter7-NormalMapTangentSpace.shader

这种做法明显比较麻烦。实际上,在Unity 4.x版本及其之前的版本中,内置的shader一直是原来书上那种不严谨的转换方法,这是因为Unity 5之前,如果我们对一个模型A进行了非统一缩放,Unity内部会重新在内存中创建一个新的模型B,模型B的大小和缩放后的A是一样的,但是它的缩放系数是统一缩放。换句话说,在Unity 5以前,实际上我们在Shader中根本不需要考虑模型的非统一缩放问题,因为在Shader阶段非统一缩放根本就不存在了。但从Unity 5以后,我们就需要考虑非统一缩放的问题了。

结论

所以结论就是,大家最好还是按Chapter7-NormalMapWorldSpace.shader转换到世界空间算更加方便,这也是Unity 5之后所有内置Shader的做法。当然,如果你可以确保没有非统一缩放那么哪种都可以。

hungry0

hungry0 commented on Feb 10, 2017

@hungry0

乐乐女神的书不仅内容好,售后服务还做的这么好,这么前卫,O(∩_∩)O哈哈~ 我也是遇到了法线变换过程的迷惑,看了你这篇还有http://www.cnblogs.com/mengdd/archive/2011/08/30/2598025.html 这篇以后,就懂了!

candycat1992

candycat1992 commented on Feb 11, 2017

@candycat1992
OwnerAuthor

@hungry0 感谢支持🙂

nothingcat

nothingcat commented on Apr 30, 2017

@nothingcat

唔,我照着实现148页代码时发现到tangent space的转换有问题,过来发现确实已经有反馈过的了,这本书对于入门很有帮助,第四章写的很赞,非常感谢欸嘿嘿。; )

candycat1992

candycat1992 commented on May 2, 2017

@candycat1992
OwnerAuthor

@IronicGoose 有帮助就好~~~感谢支持 :)

xwc2021

xwc2021 commented on May 18, 2017

@xwc2021

回報1個問題
上面的shader在偶數次鏡射時光照是正確的,但奇數次鏡射時binormal就會差1個方向

偶數次鏡射: scale(-1,-1,1) scale(-1,1,-1) scale(1,-1,-1)
奇數次鏡射: scale(-1,1,1) scale(1,-1,1) scale(1,1,-1) scale(-1,-1,-1)

測試後發現Unity的 surface shader有處理這個情況
Unity的Normal mapping sample在下面的連結裡
https://docs.unity3d.com/Manual/SL-SurfaceShaderExamples.html
按[show generate code]出來的結果如下

  float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
  fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
  fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
  fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
  fixed3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;
  o.tSpace0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
  o.tSpace1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
  o.tSpace2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

上面的unity_WorldTransformParams.w應該就是[鏡射因子]
在奇數次鏡射會是(-1),而偶數次鏡射時還是(1)

/////////////////////////////////////////////////////////////////分隔線
另外在Unity裡作了一下實驗
發現即使作了scale.x=-1 Unity[在畫面上顯示的物件x軸]和[transform.right存儲的內容],編輯器裡的3軸顯示都不會反應出鏡射後方向上的變化

但像下面這麼作
Vector3 real_right =transform.lossyScale.x * transform.right;
Vector3 real_up =transform.lossyScale.y * transform.up;
Vector3 real_forward =transform.lossyScale.z * transform.forward;
還是可以得到object真實的軸向

然後檢查
cross(real_right ,real_up)和real_forward 是不是同向,就可以決定[鏡射因子]

但也有可能Unity是直接檢查sign(lossyScale.x * lossyScale.y * lossyScale.z)

GITHUB243884919

GITHUB243884919 commented on May 28, 2017

@GITHUB243884919

hi, candy,遮罩文理Chapter7-MaskTexture也使用了切线空间,用的TANGENT_SPACE_ROTATION。是不是也存在上面说的问题。

candycat1992

candycat1992 commented on May 31, 2017

@candycat1992
OwnerAuthor

@GITHUB243884919 对的,之后如果遇到法线纹理,还是转换到世界空间算更加方便。如果出第二版的话,会把所有的法线纹理转换到世界空间,不会再这么算了。

6 remaining items

xwc2021

xwc2021 commented on Aug 31, 2017

@xwc2021

@laomoi
還有一個可能是你的uv有翻轉過,你是使用自己的模型嗎?
或是你有在Unity裡對模型作負值的縮放嗎?

laomoi

laomoi commented on Aug 31, 2017

@laomoi

@xiangwei71 用的是unity自带的Capsule。 我仔细看了书上配图7.15, bumpScale确实需要为负数才能有正确的凹凸感 = =

xwc2021

xwc2021 commented on Aug 31, 2017

@xwc2021

@laomoi
你是不是用新版的unity,我發現我用unity2017使用內建的stander shader時,normal的方向也不正確也
(內建的stander shader 法向量剛好也反過來了阿阿)

mingingzi

mingingzi commented on Sep 27, 2017

@mingingzi

您好,我不太明白tangent和binormal跟uv之间的关系。您在书中说“切线往往与纹理空间对齐”。我自己搜了一下,看到这句话“tangent代表的是顶点u增大的方向,binormal代表v增大的方向,二者未必是互相垂直关系”,一直想不明白uv增大的方向为什么会不垂直。不垂直的话uv坐标轴是不是也不垂直?uv贴图难道不是方形的了吗?

helpking

helpking commented on Jul 20, 2019

@helpking

乐乐女神的书不仅内容好,售后服务还做的这么好,这么前卫,O(∩_∩)O哈哈~ 我也是遇到了法线变换过程的迷惑,看了你这篇还有http://www.cnblogs.com/mengdd/archive/2011/08/30/2598025.html 这篇以后,就懂了!

还是这个解释移动。书中的公式推到反而让人迷惑更多。

vanxining

vanxining commented on Mar 18, 2020

@vanxining

乐乐女神的书不仅内容好,售后服务还做的这么好,这么前卫,O(∩_∩)O哈哈~ 我也是遇到了法线变换过程的迷惑,看了你这篇还有http://www.cnblogs.com/mengdd/archive/2011/08/30/2598025.html 这篇以后,就懂了!

还是这个解释移动。书中的公式推到反而让人迷惑更多。

那一篇文章啥推导都没有,啥引用都没有,就干巴巴地说什么是什么的逆矩阵之类的话,我觉得更加难以令人明白。

JunC74

JunC74 commented on Jul 23, 2020

@JunC74

仍旧是150页计算binormal方向时的疑问,感觉cross的2个参数需要颠倒一下顺序, 不然从左手坐标系来看,binormal的方向是反过来的。(跟图7.12所示是相反的)
float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
是不是应该改成
float3 binormal = cross( normalize(v.tangent.xyz), normalize(v.normal) ) * v.tangent.w;

切线的tangent.w的值1或-1的意义

Highmiao

Highmiao commented on Oct 20, 2020

@Highmiao

法线纹理:在切线空间下计算

由于前几天有个读者问了我一个问题,我突然发现P148在切线空间下计算法线光照一节是有问题的。

问题

目前书里面是使用内置宏TANGENT_SPACE_ROTATION来构建一个“从模型空间到切线空间的变换矩阵”:

/// 
/// 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;

上面代码的问题在于,如果模型存在非统一缩放,即缩放尺度各分量值不相等,那么上述方法就会造成错误的法线变换问题。

改正

由于光源方向、视角方向大多都是在世界空间下定义的,所以问题就是如何把它们从世界空间变换到切线空间下。我们可以先得到世界空间中切线空间的三个坐标轴的方向表示,然后把它们按列摆放,就可以得到从切线空间到世界空间的变换矩阵,那么再对这个矩阵求逆就可以得到从世界空间到切线空间的变换:

///
/// 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函数,这可以参考更新后的Chapter7-NormalMapTangentSpace.shader

这种做法明显比较麻烦。实际上,在Unity 4.x版本及其之前的版本中,内置的shader一直是原来书上那种不严谨的转换方法,这是因为Unity 5之前,如果我们对一个模型A进行了非统一缩放,Unity内部会重新在内存中创建一个新的模型B,模型B的大小和缩放后的A是一样的,但是它的缩放系数是统一缩放。换句话说,在Unity 5以前,实际上我们在Shader中根本不需要考虑模型的非统一缩放问题,因为在Shader阶段非统一缩放根本就不存在了。但从Unity 5以后,我们就需要考虑非统一缩放的问题了。

结论

所以结论就是,大家最好还是按Chapter7-NormalMapWorldSpace.shader转换到世界空间算更加方便,这也是Unity 5之后所有内置Shader的做法。当然,如果你可以确保没有非统一缩放那么哪种都可以。

您好,我对更改前的方法有点疑惑,想请问一下,更改前的方法之所以不能处理非统一缩放,是因为计算的模型空间到切线空间的矩阵有问题吗,我不明白这个矩阵哪里有问题。倒是ObjSpaceLightDir(v.vertex)这个方法,如果是非统一缩放那么变换到模型空间的光照方向好像发生了变化?

xwc2021

xwc2021 commented on Oct 25, 2021

@xwc2021

您好,我不太明白tangent和binormal跟uv之间的关系。您在书中说“切线往往与纹理空间对齐”。我自己搜了一下,看到这句话“tangent代表的是顶点u增大的方向,binormal代表v增大的方向,二者未必是互相垂直关系”,一直想不明白uv增大的方向为什么会不垂直。不垂直的话uv坐标轴是不是也不垂直?uv贴图难道不是方形的了吗?

如果妳是想知道「tangent和binormal跟uv之间的关系」:這張圖給妳參考
https://photos.app.goo.gl/1Nj8ojgitocw5iSu7
三角形的頂點是A、C、B、對映到的uv座標是a、b、c

uv座標的2個軸是u-axis、v-axis向量
a= (a.u,a.v) 代表
從uv座標的原點 o=(0,0)出發
沿著u-axis軸移動 a.u 長度、再沿著v-axis軸移動a.v長度,會到達點a

o + u-axis✖️a.u + v-axis✖️a.v = a

b和c也是類似的
o + u-axis✖️b.u + v-axis✖️b.v = b
o + u-axis✖️c.u + v-axis✖️c.v = c

這有什麼用呢?
現在換看頂點ACB
想像ACB會在同1個平面上P,然後再想像P上會有1個原點D
一開始我們不知道D的實際位置
但我們可以模仿uv空間
o + u-axis✖️a.u + v-axis✖️a.v = a
寫出
D + T✖️a.u + BN✖️a.v = A
D + T✖️b.u + BN✖️b.v = B
D + T✖️c.u + BN✖️c.v = C

再來只要解上面3個聯立方程式,就能找出tangent了
https://photos.app.goo.gl/1UVE8M3LQBSwupPm8
上面提到的T.w是什麼
那是個跟mirror有關的東西、除了是uv的mirror(因為artist在建模時、對稱的物件可以只用1半的uv)
也能用在物件的縮放上(比如說物件沿著自己的local x軸縮放-1)

如果你想知道更細節可以參考這裡
https://gpnnotes.blogspot.com/search/label/uv%20%26%20tangent

wyryyds

wyryyds commented on Jan 28, 2023

@wyryyds

这正好解决了我的问题,非常有用的帮助

spicy-ice

spicy-ice commented on Jan 28, 2023

@spicy-ice
luluxiu666

luluxiu666 commented on Jun 11, 2023

@luluxiu666

法线纹理:在切线空间下计算

由于前几天有个读者问了我一个问题,我突然发现P148在切线空间下计算法线光照一节是有问题的。

问题

目前书里面是使用内置宏TANGENT_SPACE_ROTATION来构建一个“从模型空间到切线空间的变换矩阵”:

/// 
/// 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;

上面代码的问题在于,如果模型存在非统一缩放,即缩放尺度各分量值不相等,那么上述方法就会造成错误的法线变换问题。

改正

由于光源方向、视角方向大多都是在世界空间下定义的,所以问题就是如何把它们从世界空间变换到切线空间下。我们可以先得到世界空间中切线空间的三个坐标轴的方向表示,然后把它们按列摆放,就可以得到从切线空间到世界空间的变换矩阵,那么再对这个矩阵求逆就可以得到从世界空间到切线空间的变换:

///
/// 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函数,这可以参考更新后的Chapter7-NormalMapTangentSpace.shader
这种做法明显比较麻烦。实际上,在Unity 4.x版本及其之前的版本中,内置的shader一直是原来书上那种不严谨的转换方法,这是因为Unity 5之前,如果我们对一个模型A进行了非统一缩放,Unity内部会重新在内存中创建一个新的模型B,模型B的大小和缩放后的A是一样的,但是它的缩放系数是统一缩放。换句话说,在Unity 5以前,实际上我们在Shader中根本不需要考虑模型的非统一缩放问题,因为在Shader阶段非统一缩放根本就不存在了。但从Unity 5以后,我们就需要考虑非统一缩放的问题了。

结论

所以结论就是,大家最好还是按Chapter7-NormalMapWorldSpace.shader转换到世界空间算更加方便,这也是Unity 5之后所有内置Shader的做法。当然,如果你可以确保没有非统一缩放那么哪种都可以。

您好,我对更改前的方法有点疑惑,想请问一下,更改前的方法之所以不能处理非统一缩放,是因为计算的模型空间到切线空间的矩阵有问题吗,我不明白这个矩阵哪里有问题。倒是ObjSpaceLightDir(v.vertex)这个方法,如果是非统一缩放那么变换到模型空间的光照方向好像发生了变化?

首先,第一个问题,随着物体的缩放(MVP矩阵M矩阵的变化),模型空间内的法向量也会进行同样的缩放,进而导致法向量不与平面垂直,我们如果想让他重新垂直的话可以使用书中变化到世界空间的方法来解决这个问题。原理的话可以参考这个链接:https://zhuanlan.zhihu.com/p/261667233
第二个问题,我感觉是的,原方法相当于直接构建了从模型空间到切线空间的变化,但是因为原模型空间的法线已经是错误的了,所以变化矩阵也是错误的。

spicy-ice

spicy-ice commented on Jun 11, 2023

@spicy-ice
TheZoomFlash

TheZoomFlash commented on Jun 11, 2023

@TheZoomFlash
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @vanxining@laomoi@helpking@candycat1992@JunC74

        Issue actions

          【常见问题】关于法线转换的问题(以及在切线空间下计算法线纹理的问题) · Issue #45 · candycat1992/Unity_Shaders_Book