切线空间:unity中shader切线空间,切线矩阵,TBN矩阵 ,法线贴图深度剖析

unity中shader切线空间
看了网上各种解释,各种推理。直接脑袋大。感觉复杂的高大上。当深入了解后,才发是各种扯淡。

一切从模型法向量开始

在shader中,大部分的光照计算都是与法向量有关。通过法向量和其他向量能计算出模型在光线照射下的明暗变化。
所以我们从模型法线开始
现在模型上看一下法线的样子,法线实际上是一个向量,基于切线空间的。shader是无法显示向量的,但我们可以转换成颜色:
在unity里新建一个shader。命名为Light(可以根据自己喜好来),代码如下

Shader "Custom/Light"
{SubShader{Pass{CGPROGRAM//声明顶点着色器入口#pragma vertex vert//声明片元着色器入口#pragma fragment frag// 包含 UnityObjectToWorldNormal helper 函数的 include 文件#include "UnityCG.cginc"struct v2f {// 我们将输出世界空间法线作为常规 ("texcoord") 插值器之一half3 worldNormal : TEXCOORD0;float4 pos : SV_POSITION;};// 顶点着色器:将对象空间法线也作为输入v2f vert (float4 vertex : POSITION, float3 normal : NORMAL){v2f o;o.pos = UnityObjectToClipPos(vertex);// UnityCG.cginc 文件包含将法线从对象变换到// 世界空间的函数,请使用该函数o.worldNormal = UnityObjectToWorldNormal(normal);return o;}fixed4 frag (v2f i) : SV_Target{fixed4 c = 0;// 法线是具有 xyz 分量的 3D 矢量;处于 -1..1// 范围。要将其显示为颜色,请将此范围设置为 0..1// 并放入红色、绿色、蓝色分量c.rgb = i.worldNormal*0.5+0.5;return c;}ENDCG}}
}

然后新建一个材质球Light并使用这个shader,在场景中创建一个胶囊体并将材质球赋给它,直接拖到上面就行
显示效果
在这里插入图片描述

凹凸图

实际模型法线是通过凹凸图计算出来的,因为凹凸图每个像素可以代表一个模型的某点的包括顶点的法向量,使模型更细腻。
先附上凹凸图
在这里插入图片描述
代码如下

Shader "Custom/Light"
{Properties{// 材质上的法线贴图纹理,// 默认为虚拟的 "平面表面" 法线贴图_BumpMap("Normal Map", 2D) = "bump" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{float3 worldPos : TEXCOORD0;// 通过变换。将基于模型空间的切线空间坐标轴变换到世界空间的坐标轴half3 wTangent : TEXCOORD1;  half3 wBitangent: TEXCOORD2; half3 wNormal: TEXCOORD3; // 法线贴图的纹理坐标float4 uv : TEXCOORD4;float4 pos : SV_POSITION;};// 来自着色器属性的法线贴图纹理sampler2D _BumpMap;float4 _BumpMap_ST;// 顶点着色器现在还需要每顶点切线矢量。// 在 Unity 中,切线为 4D 矢量,其中使用 .w 分量// 指示双切线矢量的方向。// 我们还需要纹理坐标。v2f vert(float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;half3 wNormal = UnityObjectToWorldNormal(normal);half3 wTangent = UnityObjectToWorldDir(tangent.xyz);// 从法线和切线的交叉积计算双切线half tangentSign = tangent.w * unity_WorldTransformParams.w;half3 wBitangent = cross(wNormal, wTangent) * tangentSign;//基于模型空间的切线空间坐标系坐标轴(单位向量)在世界坐标系的表示o.wTangent=  wTangent ;o.wBitangent=  wBitangent ;o.wNormal= wNormal ;o.uv.xy = TRANSFORM_TEX( uv,_BumpMap);return o;}fixed4 frag(v2f i) : SV_Target{fixed4 c = 0;// 对法线贴图进行采样,并根据 Unity 编码进行解码half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));// 将切线空间法线xyz(切向空间个个坐标轴分量(是个标量))分别乘上世界空间下切线空间转换的世界新坐标系下的坐标轴half3 worldNormal=normalize(i.wTangent*tnormal.x+i.wBitangent*tnormal.y+i.wNormal*tnormal.z);c.rgb = worldNormal * 0.5 + 0.5;return c;}ENDCG}}
}

将凹凸图给材质球
效果如下:

在这里插入图片描述

与上图不使用法线贴图相比。模型颜色大基调是不变,模型上方都是绿色,中间由橘黄到粉色再到蓝色转变。只不过法线纹理在这基础上添加了更多细节变化


shader的第一座大山

切线空间

现在引入切线空间的定义

  1. 切线空间的定义
    切线空间是一个局部坐标系统,在模型的每个顶点上定义,模型的顶点为切线空间坐标系的原点。它由以下三个基向量组成:

切线向量 (Tangent):沿着纹理坐标的 U 方向(即纹理横向)的方向。它定义了模型表面在纹理方向上的伸展。
双切线向量(又叫副切线向量) (Bitangent or Binormal):沿着纹理坐标的 V 方向(即纹理纵向)的方向。它与切线向量一起定义了表面上的局部坐标系。
法线向量 (Normal):垂直于表面的方向。
这三个向量一起构成了切线空间坐标轴,并且坐标轴都相互垂直
这些向量形成了一个右手坐标系,切线、法线和副切线向量的关系可以通过一个 3x3 矩阵表示。

说明:(非常重要,后面文章都是围绕它)

1.切线空间的法向量是模型顶点的法向量,原点为模型的顶点,因为法向量和顶点(切线空间坐标原点)是基于模型坐标系,
2.所以,切线空间也是基于模型空间定义的三个坐标轴。
3.所以,三个坐标轴变换到世界是相当于模型空间的三个向量变换到世界坐标的向量,然后就会构成一个和切线空间坐标系完全一样的新空间坐标系,只不过一个是基于模型空间的,一个是基于世界空间的新坐标系。
4.知道模型顶点变换到世界上的点的方法,就能知道三个坐标轴变换到世界坐标轴的方法。而法线和切线做模型顶点信息数据里里已经有了。
5.非常重要的一个概念: 在模型中,切线空间的法线,切线已经有了。然后通过正交得到双切线。我们要做的只是把这切线空间坐标系(模型空间里的单位向量法线、切线、双切线)从模型空间切换到世界空间
6.知道切线空间中法向量在坐标轴上的分量xyz(分量是一个标量),同时xyz分量也是切线坐标系转换到世界的心坐标系的分量。因为两个坐标系都一样。
7.然后将分向量相加,就得到世界坐标系的法向量

直接上代码:

Shader "Custom/Light"
{Properties{// 材质上的法线贴图纹理,// 默认为虚拟的 "平面表面" 法线贴图_BumpMap("Normal Map", 2D) = "bump" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{float3 worldPos : TEXCOORD0;// 通过变换。将基于模型空间的切线空间坐标轴变换到世界空间的坐标轴half3 wTangent : TEXCOORD1;  half3 wBitangent: TEXCOORD2; half3 wNormal: TEXCOORD3; // 法线贴图的纹理坐标float4 uv : TEXCOORD4;float4 pos : SV_POSITION;};// 来自着色器属性的法线贴图纹理sampler2D _BumpMap;float4 _BumpMap_ST;// 顶点着色器现在还需要每顶点切线矢量。// 在 Unity 中,切线为 4D 矢量,其中使用 .w 分量// 指示双切线矢量的方向。// 我们还需要纹理坐标。v2f vert(float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;half3 wNormal = UnityObjectToWorldNormal(normal);half3 wTangent = UnityObjectToWorldDir(tangent.xyz);// 从法线和切线的交叉积计算双切线half tangentSign = tangent.w * unity_WorldTransformParams.w;half3 wBitangent = cross(wNormal, wTangent) * tangentSign;//基于模型空间的切线空间坐标系坐标轴(单位向量)在世界坐标系的表示,然后分向量相加得到法向量o.wTangent=  wTangent ;o.wBitangent=  wBitangent ;o.wNormal= wNormal ;o.uv.xy = TRANSFORM_TEX( uv,_BumpMap);return o;}fixed4 frag(v2f i) : SV_Target{fixed4 c = 0;// 对法线贴图进行采样,并根据 Unity 编码进行解码half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));// 将切线空间法线xyz(切向空间个个坐标轴分量(是个标量))分别乘上世界空间下切线空间转换的世界新坐标系下的坐标轴half3 worldNormal=normalize(i.wTangent*tnormal.x+i.wBitangent*tnormal.y+i.wNormal*tnormal.z);c.rgb = worldNormal * 0.5 + 0.5;return c;}ENDCG}}
}

切线空间工具

前面已经阐述l非常重要的一个概念: 在模型中,切线空间的法线,切线已经有了。然后通过正交得到双切线。我们要做的只是把这切线空间坐标系(模型空间里的单位向量法线、切线、双切线)从模型空间切换到世界空间
为了解释切线空间,先写一个工具脚本TangentSpaceDraw显示切线空间。
新建一个脚本TangentSpaceDraw,写入下面代码,将脚本挂载到模型上。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Serialization;public class TangentSpaceDraw : MonoBehaviour
{public bool isShowLine = true;private Mesh mesh;[Range(0.001f, 0.1f)] public float lineLenght = 0.05f;public Color lineNormalPlane = new Color(0.1f, 0.5f, 0.0f, 0.5f);public Color lineNormalColor = Color.green;public Color lineTangentColor = Color.red;public Color lineBTangentColor = Color.blue;// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){}private void OnDrawGizmos(){if (!isShowLine){return;}if (!mesh){mesh = GetComponent<MeshFilter>().mesh;}for (int i = 0; i < mesh.vertices.Length; i++){Vector3 normal = transform.TransformDirection(mesh.normals[i]);Vector4 tangent = transform.TransformDirection(mesh.tangents[i]);Vector3 pos = transform.TransformPoint(mesh.vertices[i]);Handles.color = lineNormalPlane;Handles.DrawSolidDisc(pos,transform.TransformDirection(mesh.normals[i]), lineLenght * 0.6f); //画个圆,假设这是目标对象Handles.color = lineNormalColor;//绘制切线空间法线坐标轴Handles.ArrowHandleCap(0,transform.TransformPoint(mesh.vertices[i]),transform.rotation * Quaternion.LookRotation(normal),lineLenght,EventType.Repaint);Handles.color = lineTangentColor;//绘制切线空间 切线坐标轴Handles.ArrowHandleCap(0,pos,transform.rotation * Quaternion.LookRotation(tangent),lineLenght,EventType.Repaint);Handles.color = lineBTangentColor;Vector3 btangent = Vector3.Cross(normal, tangent).normalized;btangent *= Mathf.Sign(tangent.w);//绘制切线空间 副切线坐标轴Handles.ArrowHandleCap(0,pos,transform.rotation * Quaternion.LookRotation(btangent),lineLenght,EventType.Repaint);}}
}

通过这个脚本将会绘制切线空间如下
在这里插入图片描述

这是你会看到模型顶点的所有切线空间坐标系,放大后
在这里插入图片描述
核心代码

首先获取模型顶点的法线
Vector3 normal = transform.TransformDirection(mesh.normals[i]);获取模型顶点的切线Vector4 tangent = transform.TransformDirection(mesh.tangents[i]);模型顶点坐标就是切线空间的坐标原点Vector3 pos = transform.TransformPoint(mesh.vertices[i]);模型顶点数据里没有副切线,但是三个切线因为是坐标轴相互垂直。  通过法线和切线坐标轴叉乘获取副切线Vector3 btangent = Vector3.Cross(normal, tangent).normalized;计算副切线的方向btangent *= Mathf.Sign(tangent.w);

通过脚本就会发现每个顶点上都有对应的切线空间

切线空间到世界空间的转换矩阵推导

数学上的一致性

列主序与行主序

列主序(Column-major order):
在列主序的表示中,矩阵的每一列代表一个基向量的分量。这是许多图形学库(包括OpenGL和DirectX)以及数学计算中的标准表示方式。在这种表示方式中,矩阵的列向量通常对应于向量的分量。

行主序(Row-major order):
在行主序的表示中,矩阵的每一行代表一个基向量的分量。这种表示方式在一些其他计算环境和数学软件中常见。
在图形学中,特别是涉及到变换矩阵时,列主序是最常见的方式。许多图形API和数学库默认使用列主序,这使得切线空间矩阵 TT 的列向量表示更为自然和一致。

unity中也是用列主序

先回到高中数学

在三维空间中,一个向量 v 通常表示为 ( ( v x , v y , v z ) ) ((v_x, v_y, v_z)) ((vx,vy,vz)),其中 ( v x ) 、 ( v y ) (v_x)、(v_y) (vx)(vy) ( v z ) (v_z) (vz) 是分量,它们是标量。分量分别表示向量在 (x)、(y) 和 (z) 轴方向上的“伸展”程度,但这些分量本身不是向量,而是数值。

为了形成向量,我们需要将这些标量分量与基向量结合。基向量在三维空间中的标准基向量是:

  • i = (1, 0, 0),沿 (x) 轴方向
  • j = (0, 1, 0),沿 (y) 轴方向
  • k = (0, 0, 1),沿 (z) 轴方向

因此,向量 v 可以写作:

v = v x i + v y j + v z k \mathbf{v} = v_x \mathbf{i} + v_y \mathbf{j} + v_z \mathbf{k} v=vxi+vyj+vzk

这里, ( v x ) 、 ( v y ) 、 ( v z ) (v_x)、(v_y)、(v_z) (vx)(vy)(vz) 是分量(标量),而 i 、 j 和 k \mathbf{i}、\mathbf{j} 和\mathbf{k} ijk是基向量。只有将标量分量与基向量结合在一起,才能得到一个完整的向量。

** 总结一下:

  • 分量(标量) ( v x ) 、 ( v y ) 、 ( v z ) (v_x)、(v_y)、(v_z) (vx)(vy)(vz) 是标量,表示向量在各坐标轴方向上的“大小”。
  • 基向量ijk 是单位向量,指向各坐标轴方向。
  • 向量:由分量与基向量组合得到,即
    v = v x i + v y j + v z k \mathbf{v} = v_x \mathbf{i} + v_y \mathbf{j} + v_z \mathbf{k} v=vxi+vyj+vzk

第一步:切线空间定义

在切线空间中,法线向量的表示为:
Normal tangent = [ x y z ] \text{Normal}_{\text{tangent}} = \begin{bmatrix} x \\ y \\ z \end{bmatrix} Normaltangent= xyz

说明

  • 切线空间的法线向量实际上是模型空间中的法线向量,但其坐标系统基于模型的顶点。
  • 切线空间的原点是模型顶点,因此切线空间的坐标轴(即切线、双切线和法线)也是基于模型空间的坐标轴。
  • 具体来说,切线空间的三个基向量(切线、双切线和法线)都是相对于模型坐标系定义的。

为了将切线空间中的法线向量转换到世界空间,我们需要知道模型空间中的基向量(即切线、双切线和法线)在世界空间中的表示。可以通过将模型顶点的坐标从模型空间变换到世界空间的变换方法来推断出切线空间基向量在世界空间中的表示。即,切线空间的三个基向量(切线、双切线和法线)在世界空间中的表示将由以下步骤确定:

  1. 切线向量 T \mathbf{T} T 在模型空间中定义,为 T model \mathbf{T}_{\text{model}} Tmodel
  2. 双切线向量 B \mathbf{B} B 在模型空间中定义,为 B model \mathbf{B}_{\text{model}} Bmodel
  3. 法线向量 N \mathbf{N} N 在模型空间中定义,为 N model \mathbf{N}_{\text{model}} Nmodel

这些向量在世界空间中的表示分别为 T world \mathbf{T}_{\text{world}} Tworld B world \mathbf{B}_{\text{world}} Bworld N world \mathbf{N}_{\text{world}} Nworld。通过将模型空间的向量变换到世界空间的方法,我们可以获得切线空间中基向量的世界空间表示。

第二步:世界空间定义

为了将切线空间的法线向量转换为世界空间的法线向量,我们需要切线、双切线和法线在世界空间中的表示:

  • 切线在世界空间中的表示: T world = [ T x T y T z ] \mathbf{T}_{\text{world}} = \begin{bmatrix} T_x \\ T_y \\ T_z \end{bmatrix} Tworld= TxTyTz
  • 双切线在世界空间中的表示: B world = [ B x B y B z ] \mathbf{B}_{\text{world}} = \begin{bmatrix} B_x \\ B_y \\ B_z \end{bmatrix} Bworld= BxByBz
  • 法线在世界空间中的表示: N world = [ N x N y N z ] \mathbf{N}_{\text{world}} = \begin{bmatrix} N_x \\ N_y \\ N_z \end{bmatrix} Nworld= NxNyNz
第三步:展开法线转换公式

切线空间的法线向量 Normal tangent \text{Normal}_{\text{tangent}} Normaltangent 可以表示为世界空间的切线、双切线、法线的分向量(世界空间的切线、双切线、法线在第1步以做说明,而且shader由内置函数,在上面代码也能找到)

  				half3 wNormal = UnityObjectToWorldNormal(normal);half3 wTangent = UnityObjectToWorldDir(tangent.xyz);// 从法线和切线的交叉积计算双切线half tangentSign = tangent.w * unity_WorldTransformParams.w;half3 wBitangent = cross(wNormal, wTangent) * tangentSign;

回到数学知识:向量的分解,一个向量都可以分解到坐标轴上的三个分向量,并且一个向量都能表示这个向量在坐标轴的分向量分步骤相加。但要明白的是xyz是坐标轴上三个分向量的长度,是一个标量。

前面说明解释:

前面已说明原理,为了方便,再次说明一下:
1.切线空间的法向量是模型顶点的法向量,原点为模型的顶点,因为法向量和顶点(切线空间坐标原点)是基于模型坐标系,
2.所以,切线空间也是基于模型空间定义的三个坐标轴。
3.所以,三个坐标轴变换到世界是相当于模型空间的三个向量变换到世界坐标的向量,然后就会构成一个和切线空间坐标系完全一样的新空间坐标系,只不过一个是基于模型空间的,一个是基于世界空间的新坐标系。
4.知道模型顶点变换到世界上的点的方法,就能知道三个坐标轴变换到世界坐标轴的方法。而法线和切线做模型顶点信息数据里里已经有了。
5.非常重要的一个概念: 在模型中,切线空间的法线,切线已经有了。然后通过正交得到双切线。我们要做的只是把这切线空间坐标系(模型空间里的单位向量法线、切线、双切线)从模型空间切换到世界空间
6.知道切线空间中法向量在坐标轴上的分量xyz(分量是一个标量),同时xyz分量也是切线坐标系转换到世界的心坐标系的分量。因为两个坐标系都一样。
7.然后将分向量相加,就得到世界坐标系的法向量
因此:

Normal world = T tangent ⋅ x + B tangent ⋅ y + N tangent ⋅ z = T world ⋅ x + B world ⋅ y + N world ⋅ z \text{Normal}_{\text{world}} = \mathbf{T}_{\text{tangent}} \cdot x + \mathbf{B}_{\text{tangent}} \cdot y + \mathbf{N}_{\text{tangent}} \cdot z= \mathbf{T}_{\text{world}} \cdot x + \mathbf{B}_{\text{world}} \cdot y + \mathbf{N}_{\text{world}} \cdot z Normalworld=Ttangentx+Btangenty+Ntangentz=Tworldx+Bworldy+Nworldz
Normal world = T world ⋅ x + B world ⋅ y + N world ⋅ z \text{Normal}_{\text{world}} = \mathbf{T}_{\text{world}} \cdot x + \mathbf{B}_{\text{world}} \cdot y + \mathbf{N}_{\text{world}} \cdot z Normalworld=Tworldx+Bworldy+Nworldz

展开得到:
Normal world = [ T x ⋅ x + B x ⋅ y + N x ⋅ z T y ⋅ x + B y ⋅ y + N y ⋅ z T z ⋅ x + B z ⋅ y + N z ⋅ z ] \text{Normal}_{\text{world}} = \begin{bmatrix} T_x \cdot x + B_x \cdot y + N_x \cdot z \\ T_y \cdot x + B_y \cdot y + N_y \cdot z \\ T_z \cdot x + B_z \cdot y + N_z \cdot z \end{bmatrix} Normalworld= Txx+Bxy+NxzTyx+Byy+NyzTzx+Bzy+Nzz

第四步:整理为矩阵乘法形式

将矩阵竖着看,你会发现正好切向,双切线,法线的分量与 Normal tangent \text{Normal}_{\text{tangent}} Normaltangent的xyz分别相乘。将上述展开式转化为矩阵乘法的形式,得到:
Normal world = [ T x B x N x T y B y N y T z B z N z ] ⋅ [ x y z ] \text{Normal}_{\text{world}} = \begin{bmatrix} T_x & B_x & N_x \\ T_y & B_y & N_y \\ T_z & B_z & N_z \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ z \end{bmatrix} Normalworld= TxTyTzBxByBzNxNyNz xyz
Normal world = [ T x B x N x T y B y N y T z B z N z ] ⋅ Normal tangent \text{Normal}_{\text{world}} = \begin{bmatrix} T_x & B_x & N_x \\ T_y & B_y & N_y \\ T_z & B_z & N_z \end{bmatrix} \cdot \text{Normal}_{\text{tangent}} Normalworld= TxTyTzBxByBzNxNyNz Normaltangent
其中矩阵 T \mathbf{T} T 是:
T = [ T x B x N x T y B y N y T z B z N z ] \mathbf{T} = \begin{bmatrix} T_x & B_x & N_x \\ T_y & B_y & N_y \\ T_z & B_z & N_z \end{bmatrix} T= TxTyTzBxByBzNxNyNz

总结

通过这些步骤,我们得到了矩阵 T \mathbf{T} T,它将切线空间中的法线向量转换为世界空间中的法线向量。这个矩阵的每一列向量分别是切线、双切线和法线在世界空间中的表示。矩阵 T \mathbf{T} T 实现了从模型空间中的切线空间到世界空间的转换。
得到变换矩阵就可以在shader里实现了

Shader "Custom/Light"
{Properties{// 材质上的法线贴图纹理,// 默认为虚拟的 "平面表面" 法线贴图_BumpMap("Normal Map", 2D) = "bump" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{float3 worldPos : TEXCOORD0;// 这三个矢量将保持一个 3x3 旋转矩阵,// 此矩阵进行从切线到世界空间的转换half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.xhalf3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.yhalf3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z// 法线贴图的纹理坐标float2 uv : TEXCOORD4;float4 pos : SV_POSITION;};// 来自着色器属性的法线贴图纹理sampler2D _BumpMap;float4 _BumpMap_ST;// 顶点着色器现在还需要每顶点切线矢量。// 在 Unity 中,切线为 4D 矢量,其中使用 .w 分量// 指示双切线矢量的方向。// 我们还需要纹理坐标。v2f vert(float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;half3 wNormal = UnityObjectToWorldNormal(normal);half3 wTangent = UnityObjectToWorldDir(tangent.xyz);// 从法线和切线的交叉积计算双切线half tangentSign = tangent.w * unity_WorldTransformParams.w;half3 wBitangent = cross(wNormal, wTangent) * tangentSign;// 输出切线空间矩阵o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);o.uv = TRANSFORM_TEX(uv, _BumpMap);return o;}fixed4 frag(v2f i) : SV_Target{fixed4 c = 0;// 对法线贴图进行采样,并根据 Unity 编码进行解码half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));// 将法线从切线变换到世界空间half3 worldNormal;worldNormal.x = dot(i.tspace0, tnormal);worldNormal.y = dot(i.tspace1, tnormal);worldNormal.z = dot(i.tspace2, tnormal);c.rgb = worldNormal * 0.5 + 0.5;return c;}ENDCG}}
}
 				half3 worldNormal;worldNormal.x = dot(i.tspace0, tnormal);worldNormal.y = dot(i.tspace1, tnormal);worldNormal.z = dot(i.tspace2, tnormal);

这段代码实际就是
Normal world = T ⋅ N tangent \text{Normal}_{\text{world}} = \mathbf{T}\cdot \mathbf{N_\text{tangent}} Normalworld=TNtangent
展开式。
当然你也可以构建T矩阵

				half3x3 T = half3x3(i.tspace0, i.tspace1, i.tspace2);half3 worldNormal;worldNormal = mul(T, tnormal);

效果是一样的
在这里插入图片描述

再反过来看

在切线空间里法向量(从法线贴图读出的原色转换而来)
N tangent = [ x y z ] N_\text{tangent}=\begin{bmatrix} x\\y\\z \end{bmatrix} Ntangent= xyz

矩阵 T \mathbf{T} T定义了切线空间基向量在世界空间中的方向:
T = [ T B N ] \mathbf{T} = \begin{bmatrix} \text{T}&\text{B} & \text{N} \end{bmatrix} T=[TBN]
由上面向量拆分的世界坐标系的分向量:
T = [ T.x B.x N.x T.y B.y N.y T.z B.z N.z ] \mathbf{T} = \begin{bmatrix} \text{T.x} & \text{B.x} & \text{N.x} \\ \text{T.y} & \text{B.y} & \text{N.y} \\ \text{T.z} & \text{B.z} & \text{N.z} \end{bmatrix} T= T.xT.yT.zB.xB.yB.zN.xN.yN.z
Normal world = T ⋅ N tangent \text{Normal}_{\text{world}} = \mathbf{T}\cdot \mathbf{N_\text{tangent}} Normalworld=TNtangent
Normal world = T = [ T.x B.x N.x T.y B.y N.y T.z B.z N.z ] ⋅ [ x y z ] \text{Normal}_{\text{world}} = \mathbf{T} = \begin{bmatrix} \text{T.x} & \text{B.x} & \text{N.x} \\ \text{T.y} & \text{B.y} & \text{N.y} \\ \text{T.z} & \text{B.z} & \text{N.z} \end{bmatrix}\cdot\begin{bmatrix} x \\ y\\ z\end{bmatrix} Normalworld=T= T.xT.yT.zB.xB.yB.zN.xN.yN.z xyz
展开得:
Normal world = [ T x ⋅ x + B x ⋅ y + N x ⋅ z T y ⋅ x + B y ⋅ y + N y ⋅ z T z ⋅ x + B z ⋅ y + N z ⋅ z ] \text{Normal}_{\text{world}} = \begin{bmatrix} T_x \cdot x + B_x \cdot y + N_x \cdot z \\ T_y \cdot x + B_y \cdot y + N_y \cdot z \\ T_z \cdot x + B_z \cdot y + N_z \cdot z \end{bmatrix} Normalworld= Txx+Bxy+NxzTyx+Byy+NyzTzx+Bzy+Nzz

整理分别得:
第一个正好是法线向量在坐标轴(切线空间到世界空间坐标轴)的分向量分步相加得到的法向量
Normal world = [ T ⋅ x + B ⋅ y + N ⋅ z ] \text{Normal}_{\text{world}} = \begin{bmatrix} T \cdot x +B \cdot y+N \cdot z \end{bmatrix} Normalworld=[Tx+By+Nz]
第二个是切线空间法线在世界坐标轴的分量
worldNormal.x = T.x ⋅ x + B.x ⋅ y + N.x ⋅ z worldNormal.y = T.y ⋅ x + B.y ⋅ y + N.x ⋅ z worldNormal.z = T.x ⋅ x + B.x ⋅ y + N.x ⋅ z \text{worldNormal.x} = \text{T.x} \cdot \text{x} + \text{B.x} \cdot \text{y} + \text{N.x} \cdot \text{z}\\ \text{worldNormal.y} = \text{T.y} \cdot \text{x} + \text{B.y} \cdot \text{y} + \text{N.x} \cdot \text{z}\\ \text{worldNormal.z} = \text{T.x} \cdot \text{x} + \text{B.x} \cdot \text{y} + \text{N.x} \cdot \text{z} worldNormal.x=T.xx+B.xy+N.xzworldNormal.y=T.yx+B.yy+N.xzworldNormal.z=T.xx+B.xy+N.xz
每一行的几何意义
第一行计算:

worldNormal.x = T.x ⋅ x + B.x ⋅ y + N.x ⋅ z \text{worldNormal.x} = \text{T.x} \cdot \text{x} + \text{B.x} \cdot \text{y} + \text{N.x} \cdot \text{z} worldNormal.x=T.xx+B.xy+N.xz
这里 worldNormal.x \text{worldNormal.x} worldNormal.x是切线空间法线 tnormal \text{tnormal} tnormal 在世界坐标系 X 轴方向上的长度分量。
T.x ⋅ x \text{T.x} \cdot \text{x} T.xx:表示切线方向(在世界 X 轴上的分量)与切线空间法线在切线方向上的分量的乘积。
B.x ⋅ y \text{B.x} \cdot \text{y} B.xy:表示双切线方向(在世界 X 轴上的分量)与切线空间法线在双切线方向上的分量的乘积。
N.x ⋅ z \text{N.x} \cdot \text{z} N.xz:表示法线方向(在世界 X 轴上的分量)与切线空间法线在法线方向上的分量的乘积。
同理,第二行和第三行分别计算了在世界 Y 轴和 Z 轴方向上的分量。

到这里如果你完全理解矩阵的原理那么后面的shader就一路平川了

通过上面的知识,shader切线空间转换到世界空间坐标还有一种更好的写法。就是上面推理开始的步骤,将切线空间坐标轴转换到世界空间系构成一个世界空间内的新空间坐标系。这个坐标系和切向空间坐标系是一样的。只不过一个是相对于模型,一个相对于世界的。就是上面我么提到的
代码如下

Shader "Custom/Light"
{Properties{// 材质上的法线贴图纹理,// 默认为虚拟的 "平面表面" 法线贴图_BumpMap("Normal Map", 2D) = "bump" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{float3 worldPos : TEXCOORD0;// 通过变换。将基于模型空间的切线空间坐标轴变换到世界空间的坐标轴half3 wTangent : TEXCOORD1;  half3 wBitangent: TEXCOORD2; half3 wNormal: TEXCOORD3; // 法线贴图的纹理坐标float4 uv : TEXCOORD4;float4 pos : SV_POSITION;};// 来自着色器属性的法线贴图纹理sampler2D _BumpMap;float4 _BumpMap_ST;// 顶点着色器现在还需要每顶点切线矢量。// 在 Unity 中,切线为 4D 矢量,其中使用 .w 分量// 指示双切线矢量的方向。// 我们还需要纹理坐标。v2f vert(float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;half3 wNormal = UnityObjectToWorldNormal(normal);half3 wTangent = UnityObjectToWorldDir(tangent.xyz);// 从法线和切线的交叉积计算双切线half tangentSign = tangent.w * unity_WorldTransformParams.w;half3 wBitangent = cross(wNormal, wTangent) * tangentSign;//基于模型空间的切线空间坐标系坐标轴(单位向量)在世界坐标系的表示o.wTangent=  wTangent ;o.wBitangent=  wBitangent ;o.wNormal= wNormal ;o.uv.xy = TRANSFORM_TEX( uv,_BumpMap);return o;}fixed4 frag(v2f i) : SV_Target{fixed4 c = 0;// 对法线贴图进行采样,并根据 Unity 编码进行解码half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));// 将切线空间法线xyz(切向空间个个坐标轴分量(是个标量))分别乘上世界空间下切线空间转换的世界新坐标系下的坐标轴half3 worldNormal=normalize(i.wTangent*tnormal.x+i.wBitangent*tnormal.y+i.wNormal*tnormal.z);c.rgb = worldNormal * 0.5 + 0.5;return c;}ENDCG}}
}

​代码段解释

// 将法线从切线空间转换到世界空间
half3 worldNormal = normalize(i.wTangent * tnormal.x + i.wBitangent * tnormal.y + i.wNormal * tnormal.z);

解释步骤
法线贴图采样:

 
half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));

这行代码从法线贴图中采样出法线 tnormal,它是切线空间中的法线向量。UnpackNormal 函数用于解码法线贴图中的颜色信息到切线空间的法线向量。

法线向量在切线空间的分量:

tnormal.x:法线在切线空间的 x 分量。
tnormal.y:法线在切线空间的 y 分量。
tnormal.z:法线在切线空间的 z 分量。
切线空间基向量:

i.wTangent:切线在世界空间的表示。
i.wBitangent:双切线在世界空间的表示。
i.wNormal:法线在世界空间的表示。
法线转换:

c
复制代码
half3 worldNormal = normalize(i.wTangent * tnormal.x + i.wBitangent * tnormal.y + i.wNormal * tnormal.z);
加权求和:将切线空间中的法线 tnormal 的每个分量分别乘以对应的世界空间基向量(切线、双切线和法线),然后将这些结果相加。这个过程实际上是在进行从切线空间到世界空间的变换。
标准化:通过 normalize 函数将结果向量 worldNormal 标准化,确保其长度为 1。这样做是为了保持法线的单位长度,以便正确地表示表面的法线方向。
几何意义
切线空间法线到世界空间法线的转换:通过将切线空间中的法线向量的分量与世界空间中的切线空间基向量(切线、双切线、法线)结合,可以将法线从切线空间转换到世界空间。这是通过对切线空间中法线分量的加权求和实现的。
直观的几何操作:这种方式简洁而直观地表达了如何将切线空间中的法线向量转化为世界空间中的法线向量,同时保持了法线的方向性和单位长度。

上面这两个shader结果是一样的,只不过一个是使用坐标空间转换,一个是矩阵转换。第二个是第一个基础上推理出来的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/427620.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MyBatis-Plus分页查询、分组查询

目录 准备工作1. 实体类2. Mapper类3. 分页插件4. 数据 分页查询1. 使用条件构造器2. 使用自定义sql 分组查询1. 分组结果类2. 自定义sql3. 测试类 准备工作 1. 实体类 对地址字段address使用字段类型转换器&#xff0c;将List转为字符串数组保存在数据库中 package com.exa…

【CSS Tricks】一种基于AV1视频格式的现代图像格式-AVIF

引言 AV1图像文件格式&#xff08;英语&#xff1a;AV1 Image File Format&#xff0c;简称AVIF&#xff09;是由开放媒体联盟&#xff08;AOM&#xff09;开发&#xff0c;采用AV1视讯编码技术压缩图像的一种图像文件格式&#xff0c;能用来储存一般的图像和动态图像。AVIF和苹…

torch.embedding 报错 IndexError: index out of range in self

文章目录 1. 报错2. 原因3. 解决方法 1. 报错 torch.embedding 报错&#xff1a; IndexError: index out of range in self2. 原因 首先看下正常情况&#xff1a; import torch import torch.nn.functional as Finputs torch.tensor([[1, 2, 4, 5], [4, 3, 2, 9]]) embedd…

【Git原理与使用】版本管理与分支管理(1)

目录 一、基本操作 1、初识Git 2、Git安装[Linux-centos] 3、Git安装[ Linnx-ubuntu] 4、创建git本地仓库 5、配置Git 6、认识工作区、暂存区、版本库 7、添加文件 8、查看历史提交记录 9、查看.git文件目录结构 10、查看版本库对象的内容 11、小结&#xff08;在本地的.git仓库…

JVM常用参数配置

JVM常用参数配置 简单的java命令后面跟上配置参数。 -XX&#xff0c;JVM启动参数的一种类型&#xff0c;属于高级。 &#xff0c;开启的意思 &#xff1a;&#xff0c;设置具体参数 #jvm启动参数不换行 #设置堆内存 -Xmx4g -Xms4g #指定GC算法 -XX:UseG1GC -XX:MaxGCPauseM…

Qt_多元素控件

目录 1、认识多元素控件 2、QListWidget 2.1 使用QListWidget 3、QTableWidget 3.1 使用QListWidget 4、QTreeWidget 4.1 使用QTreeWidget 5、QGroupBox 5.1 使用QGroupBox 6、QTabWidget 6.1 使用QTabWidget 结语 前言&#xff1a; 在Qt中&#xff0c;控件之间…

《深度学习》—— 神经网络模型对手写数字的识别

神经网络模型对手写数字的识别 import torch from torch import nn # 导入神经网络模块 from torch.utils.data import DataLoader # 数据包管理工具&#xff0c;打包数据, from torchvision import datasets # 封装了很多与图像相关的模型&#xff0c;数据集 from torchvi…

分布式事务seata

文章目录 CAP理论BASE 理论seata解决分布式事务seata重要对象XA模式AT模式TCC模式saga模式 CAP理论 CAP理论指出在分布式系统中三个属性不可能同时满足。 Consistency 一致性&#xff1a;在分布式的多个节点&#xff08;副本&#xff09;的数据必须是一样的&#xff08;强一致…

展锐平台的手机camera 系统开发过程

展锐公司有自己的isp 图像处理引擎&#xff0c;从2012 年底就开始在智能手机上部署应用。最初的时候就几个人做一款isp的从hal 到kernel 驱动的完整软件系统&#xff0c;分工不是很明确&#xff0c;基本是谁擅长哪些就搞哪些&#xff0c;除了架构和编码实现之外&#xff0c;另外…

软硬件项目运维方案(Doc原件完整版套用)

1 系统的服务内容 1.1 服务目标 1.2 信息资产统计服务 1.3 网络、安全系统运维服务 1.4 主机、存储系统运维服务 1.5 数据库系统运维服务 1.6 中间件运维服务 2 运维服务流程 3 服务管理制度规范 3.1 服务时间 3.2 行为规范 3.3 现场服务支持规范 3.4 问题记录规范…

【数据结构】排序算法---基数排序

文章目录 1. 定义2. 算法步骤2.1 MSD基数排序2.2 LSD基数排序 3. LSD 基数排序动图演示4. 性质5. 算法分析6. 代码实现C语言PythonJavaCGo 结语 ⚠本节要介绍的不是计数排序 1. 定义 基数排序&#xff08;英语&#xff1a;Radix sort&#xff09;是一种非比较型的排序算法&…

秋招常问的面试题:Cookie和Session的区别

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 《Java代码审…

LeetCode[中等] 3. 无重复字符的最长子串

给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长子串 的长度。 思路&#xff1a;滑动窗口&#xff0c;设置左右指针left与right&#xff0c;maxLength存储长度 利用HashSet性质&#xff0c;存储滑动窗口中的字符 如果没有重复的&#xff0c;那么right继续向…

LeetCode_sql_day28(1767.寻找没有被执行的任务对)

描述&#xff1a;1767.寻找没有被执行的任务对 表&#xff1a;Tasks ------------------------- | Column Name | Type | ------------------------- | task_id | int | | subtasks_count | int | ------------------------- task_id 具有唯一值的列。 ta…

无人机企业合法运营必备运营合格证详解

无人机企业运营合格证&#xff0c;是由国家相关航空管理部门&#xff08;如中国民用航空局或其授权机构&#xff09;颁发的&#xff0c;用于确认无人机企业具备合法、安全、高效运营无人机能力的资质证书。该证书是无人机企业开展商业运营活动的必要条件&#xff0c;标志着企业…

原生+jquery写自动消失的提示框

<!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0"> <title>自动消失消息提示</title> <style>/…

信息安全数学基础(9)素数的算数基本定理

前言 在信息安全数学基础中&#xff0c;素数的算数基本定理&#xff08;也称为唯一分解定理或算术基本定理&#xff09;是一个极其重要的定理&#xff0c;它描述了正整数如何唯一地分解为素数的乘积。这个定理不仅是数论的基础&#xff0c;也是许多密码学算法&#xff08;如RSA…

同为TVT设备主动注册协议接入SVMSPro平台

同为设备主动注册协议接入SVMSPro平台 步骤一&#xff1a;进设备网页或者NVR配置界面&#xff0c;进功能面板&#xff0c;网络&#xff0c;平台接入 接入类型&#xff1a;平台软件&#xff0c;勾选启用主动上报 服务器地址&#xff1a;平台服务IP 端口&#xff1a;12009 ID&…

高级算法设计与分析 学习笔记6 B树

B树定义 一个块里面存了1000个数和1001个指针&#xff0c;指针指向的那个块里面的数据大小介于指针旁边的两个数之间 标准定义&#xff1a; B树上的操作 查找B树 创建B树 分割节点 都是选择正中间的那个&#xff0c;以免一直分裂。 插入数字 在插入的路上就会检查节点需不需要…

RabbitMQ 高级特性——持久化

文章目录 前言持久化交换机持久化队列持久化消息持久化 前言 前面我们学习了 RabbitMQ 的高级特性——消息确认&#xff0c;消息确认可以保证消息传输过程的稳定性&#xff0c;但是在保证了消息传输过程的稳定性之后&#xff0c;还存在着其他的问题&#xff0c;我们都知道消息…