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

news2024/11/17 19:30:34

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.知道切线空间中法向量在坐标轴上的分量xyz(分量是一个标量),同时xyz分量也是切线坐标系转换到世界的心坐标系的分量。因为两个坐标系都一样。
6.然后将分向量相加,就得到世界坐标系的法向量

直接上代码:

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
        }
    }
}

切线空间工具

为了解释切线空间,先写一个工具脚本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 update
  void Start()
  {
  }

  // Update is called once per frame
  void 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.知道切线空间中法向量在坐标轴上的分量xyz(分量是一个标量),同时xyz分量也是切线坐标系转换到世界的心坐标系的分量。因为两个坐标系都一样。
6.然后将分向量相加,就得到世界坐标系的法向量
因此:

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.x
                half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
                half3 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 = [ Tangent  Bitangent Normal ] \mathbf{T} = \begin{bmatrix} \text{Tangent }&\text{Bitangent} & \text{Normal} \end{bmatrix} T=[Tangent BitangentNormal]
由上面向量拆分的世界坐标系的分向量:
T = [ Tangent.x Bitangent.x Normal.x Tangent.y Bitangent.y Normal.y Tangent.z Bitangent.z Normal.z ] \mathbf{T} = \begin{bmatrix} \text{Tangent.x} & \text{Bitangent.x} & \text{Normal.x} \\ \text{Tangent.y} & \text{Bitangent.y} & \text{Normal.y} \\ \text{Tangent.z} & \text{Bitangent.z} & \text{Normal.z} \end{bmatrix} T= Tangent.xTangent.yTangent.zBitangent.xBitangent.yBitangent.zNormal.xNormal.yNormal.z
Normal world = T ⋅ N tangent \text{Normal}_{\text{world}} = \mathbf{T}\cdot \mathbf{N_\text{tangent}} Normalworld=TNtangent
Normal world = T = [ Tangent.x Bitangent.x Normal.x Tangent.y Bitangent.y Normal.y Tangent.z Bitangent.z Normal.z ] ⋅ [ x y z ] \text{Normal}_{\text{world}} = \mathbf{T} = \begin{bmatrix} \text{Tangent.x} & \text{Bitangent.x} & \text{Normal.x} \\ \text{Tangent.y} & \text{Bitangent.y} & \text{Normal.y} \\ \text{Tangent.z} & \text{Bitangent.z} & \text{Normal.z} \end{bmatrix}\cdot\begin{bmatrix} x \\ y\\ z\end{bmatrix} Normalworld=T= Tangent.xTangent.yTangent.zBitangent.xBitangent.yBitangent.zNormal.xNormal.yNormal.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
worldNormal.x = Tangent.x ⋅ tnormal.x + Bitangent.x ⋅ tnormal.y + Normal.x ⋅ tnormal.z \text{worldNormal.x} = \text{Tangent.x} \cdot \text{tnormal.x} + \text{Bitangent.x} \cdot \text{tnormal.y} + \text{Normal.x} \cdot \text{tnormal.z} worldNormal.x=Tangent.xtnormal.x+Bitangent.xtnormal.y+Normal.xtnormal.z
worldNormal.y = Tangent.y ⋅ tnormal.x + Bitangent.y ⋅ tnormal.y + Normal.x ⋅ tnormal.z \text{worldNormal.y} = \text{Tangent.y} \cdot \text{tnormal.x} + \text{Bitangent.y} \cdot \text{tnormal.y} + \text{Normal.x} \cdot \text{tnormal.z} worldNormal.y=Tangent.ytnormal.x+Bitangent.ytnormal.y+Normal.xtnormal.z
worldNormal.z = Tangent.x ⋅ tnormal.x + Bitangent.x ⋅ tnormal.y + Normal.x ⋅ tnormal.z \text{worldNormal.z} = \text{Tangent.x} \cdot \text{tnormal.x} + \text{Bitangent.x} \cdot \text{tnormal.y} + \text{Normal.x} \cdot \text{tnormal.z} worldNormal.z=Tangent.xtnormal.x+Bitangent.xtnormal.y+Normal.xtnormal.z

每一行的几何意义

第一行计算:

worldNormal.x = Tangent.x ⋅ tnormal.x + Bitangent.x ⋅ tnormal.y + Normal.x ⋅ tnormal.z \text{worldNormal.x} = \text{Tangent.x} \cdot \text{tnormal.x} + \text{Bitangent.x} \cdot \text{tnormal.y} + \text{Normal.x} \cdot \text{tnormal.z} worldNormal.x=Tangent.xtnormal.x+Bitangent.xtnormal.y+Normal.xtnormal.z
这里 worldNormal.x \text{worldNormal.x} worldNormal.x是切线空间法线 tnormal \text{tnormal} tnormal 在世界坐标系 X 轴方向上的长度分量。
Tangent.x ⋅ tnormal.x \text{Tangent.x} \cdot \text{tnormal.x} Tangent.xtnormal.x:表示切线方向(在世界 X 轴上的分量)与切线空间法线在切线方向上的分量的乘积。
Bitangent.x ⋅ tnormal.y \text{Bitangent.x} \cdot \text{tnormal.y} Bitangent.xtnormal.y:表示双切线方向(在世界 X 轴上的分量)与切线空间法线在双切线方向上的分量的乘积。
Normal.x ⋅ tnormal.z \text{Normal.x} \cdot \text{tnormal.z} Normal.xtnormal.z:表示法线方向(在世界 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.coloradmin.cn/o/2133313.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

How to see if openAI (node js) createModeration response “flagged“ is true

题意&#xff1a;如何查看 OpenAI (Node.js) createModeration 响应中的 "flagged" 是否为 true 问题背景&#xff1a; Using the OpenAI createModeration feature, I am trying to see if the string gets flagged or not. 使用 OpenAI 的 createModeration 功能…

医学数据分析实训 项目一 医学数据采集

项目一 医学数据采集 一、实践目的 了解医学数据的特点&#xff1b;熟悉常见的医学公共数据库的使用方法&#xff1b;掌握获取医学数据的方法&#xff1b; 二、实践平台 操作系统&#xff1a;Windows10 及以上Python 版本&#xff1a;3.8.x 及以上PyCharm 或 Anoconda 集成…

Ubuntu 安装最新 Google Chrome 浏览器

谷歌浏览器使用简单并且用户友好&#xff0c;使用它浏览互联网愉悦至极。许多用户喜欢 Chrome&#xff0c;因为它加载网页又快又流畅。Chrome 提供强大的安全功能&#xff0c;帮助用户保持在线安全。Google Chrome 官方提供了一个 Debian 软件包存储库&#xff0c;基于 Debian …

Llama Factory :百种以上语言模型的统一高效微调框架

人工智能咨询培训老师叶梓 转载标明出处 大模型适应到特定下游任务时&#xff0c;传统的全参数微调方法成本高昂&#xff0c;因此&#xff0c;研究者们一直在探索更高效的微调技术。由北京航空航天大学和北京大学的研究团队提出了一个名为Llama Factory的统一框架&#xff0c;…

【免费刷题】实验室安全第一知识题库分享

道路千万条&#xff0c;实验安全第一条。 嘿&#xff0c;实验室的小伙伴们&#xff01;是不是还在为实验室安全考试而烦恼&#xff1f;别担心&#xff0c;今天就让我来分享一些实用的题库&#xff0c;帮助你轻松应对考试&#xff0c;同时也更好地保护自己和实验室的安全。 一、…

前端开发第三节课

20-目录文件夹和根目录 4.8图像标签和路径&#xff08;重点&#xff09; 2.路径&#xff08;前期铺垫知识&#xff09; &#xff08;1&#xff09;目录文件夹和目录 实际工作中&#xff0c;我们的文件不能随便乱放&#xff0c;否则用起来很难快速地找到他们&#xff0c;因此…

LeetCode 热题 100 回顾13

干货分享&#xff0c;感谢您的阅读&#xff01;原文见&#xff1a;LeetCode 热题 100 回顾_力code热题100-CSDN博客 一、哈希部分 1.两数之和 &#xff08;简单&#xff09; 题目描述 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标…

基于SpringBoot+Vue的个性化旅游推荐系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的…

free -h 查看内存free空间不足

free空间不足 大部分被buff/cache占用 解决办法一: 手动释放缓存 释放页缓存 sudo sync; sudo sysctl -w vm.drop_caches1 释放目录项和inode缓存 sudo sync; sudo sysctl -w vm.drop_caches2 释放所有缓存&#xff08;页缓存、目录项和inode缓存&#xff09; sudo sync…

【隐私计算】安全多方计算MPC中的高效三角函数计算

隐私计算中的非线性函数很难算&#xff0c;或者开销很大。三角函数更是如此&#xff0c;Squirrel文章中提出了一种高效的计算方式&#xff0c;感觉可以作为一种通用优化方案。 参考文献&#xff1a;Squirrel: A Scalable Secure Two-Party Computation Framework for Training…

几何概率模型

一、几何概率模型 ① 样本空间的样本点为无限个 ② 每个样本点发生的可能性是均等的 ③ P(A)事件A的几何度量值/样本空间的几何度量值 说明&#xff1a;如果样本空间的样本点为有限个&#xff0c;则为古典概型 通过2个例子&#xff0c;来感受下两者的区别 ① 例&#xff1…

大语言模型之ICL(上下文学习) - In-Context Learning Creates Task Vectors

本文译自 《In-Context Learning Creates Task Vectors》 —— 论文中的作者也在用LLaMA模型&#xff0c;笔者自我感觉拉近和世界顶级人才的距离&#xff0c;哈哈内容较长&#xff0c;如想看结论直接看 摘要、介绍与结论几个章节即可&#xff0c;看细节请看目录索引。经验风险最…

ZoneTree: 高性能ACID兼容的.NET有序键值数据库

推荐一个专门针对键值存储的开源数据库。 01 项目简介 ZoneTree基于.Net开发的开源键值数据库。它以其持久化存储、高性能处理、事务性操作和ACID合规性而著称。ZoneTree能够以内存数据库的形式运行&#xff0c;也可以在本地或云存储上进行数据持久化&#xff0c;提供了灵活性…

人工智能物联网:一项综述

这篇论文的标题是《Artificial Intelligence of Things: A Survey》&#xff0c;作者是 Shakhrul Iman Siam 等人&#xff0c;来自不同的大学和研究机构。论文提供了对人工智能物联网&#xff08;AIoT&#xff09;研究的系统性和全面性回顾。以下是论文的主要内容概述&#xff…

【H2O2|全栈】关于CSS(4)CSS基础(四)

目录 CSS基础知识 前言 准备工作 精灵图 概念 属性 案例 浮动 基础属性 清除浮动 案例 预告和回顾 后话 CSS基础知识 前言 本系列博客将分享层叠样式表&#xff08;CSS&#xff09;有关的知识点。 接下来的几期内容相对比较少&#xff0c;主要是对前面的内容进…

服务网关Gateway快速入门

1.引入 网关可以把它理解成坐高铁时的安检&#xff0c;他可以对用户做身份验证&#xff0c;哪些人能通过&#xff0c;哪些人不能通过&#xff0c;都由他决定&#xff0c;如果没有安检&#xff0c;那么高铁的安全性将受到打击&#xff0c;一个微服务没有网关&#xff0c;那么接口…

大数据新视界 --大数据大厂之HBase深度探寻:大规模数据存储与查询的卓越方案

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

webpack原理简述

1.1 核心概念 JavaScript 的 模块打包工具 (module bundler)。通过分析模块之间的依赖&#xff0c;最终将所有模块打包成一份或者多份代码包 (bundler)&#xff0c;供 HTML 直接引用。实质上&#xff0c;Webpack 仅仅提供了 打包功能 和一套 文件处理机制&#xff0c;然后通过生…

Mini-Omni 语言模型在流式传输中边思考边听说应用

引入简介 Mini-Omni 是一个开源的多模态大语言模型,能够在思考的同时进行听觉和语言交流。它具有实时端到端语音输入和流媒体音频输出的对话能力。 语言模型的最新进展取得了显著突破。GPT-4o 作为一个新的里程碑,实现了与人类的实时对话,展示了接近人类的自然流畅度。为了…

69、Python番外篇:从编程范式看如何学习一门编程语言的精髓

引言 在之前的文章中&#xff0c;我们曾聊过如何学习一门编程语言&#xff0c;当时是从程序的构成的角度来分析、展开的&#xff0c;主要提及了数据的表达 数据的处理&#xff0c;也就是数据结构 算法的内容。这个角度对应到所有编程语言&#xff0c;基本都是适用的。但是&a…