Unity3D 法线转换&切线空间

简介: Unity3D 法线转换&切线空间

05f4e00868fe4434982839f66f340805.jpeg

前言


什么是unity 向量 法线?


利用偏导数,在Unity里面显示法线:


// ddx ddy 计算法线
Shader "lcl/ddxddy/CalculateNormal"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            #include "UnityCG.cginc"
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldPos : TEXCOORD0;
            };
            sampler2D _MainTex;
            float4 _MainTex_ST;
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {
                float3 normalDir = normalize(cross(ddy(i.worldPos),ddx(i.worldPos)));
                return fixed4(normalDir,1);
            }
            ENDCG
        }
    }
}

什么是切线空间?


切线空间就是,基于模型上的一个顶点建立的坐标空间,它的X轴是这个顶点在模型中的切线分量,Z轴是改点在模型上的法线,Y轴就是这个顶点的副切线,因为坐标空间XYZ三个面都是互相垂直的嘛,所以这个副切线我们是可以求出来的(以为同样是与一个平面垂直的单位向量是有两方向的,在Unity中模型会提供一个副切线的方向的数据)。

image.png

法线贴图就是记录这个顶点在它的切线下的法线的向量(x,y,z)。

这里我们还要说一下就是,法线的分量范围是[-1,1]而像素(颜色)的分量范围是[0,1],所以,利用法线贴图来表示法线的话是需要一个转化的,即 pixel = (normal +1)/2

因为我们的坐标空间都是切线空间下的,所以,法向量基本都是指向正方向的,就算是没有指向正方向也只是偏了一个角度而已,总之不太会出现和正方向的太大的偏差(所以,这也是为什么我们也把这种贴图叫做法线扰动贴图,就是记录它和正方向的一个偏差)。所以呢,我们的法线分量基本都是1附近的,转化成像素也就是(1+1)/2还是1附近,而法线对应的像素通道是RGB中的B,这也就解释了为什么法线贴图基本都是呈现蓝色。

使用切线空间的法线贴图还有一个重要的一点就是,它可以节省空间,相比于,模型空间下的法线贴图,它可以只存储切线分量和副切线分量,然后法线的分量就可以计算出来了。

而根据一致的X,Y就可以算出来Z了:

Z  = sqrt(1 - (x^2 + y^2));

或许我们在看一些Unity Shader 的CG代码的时候,会发现它是这么计算法线Z分量的:

float3 tangentNormal = tex2D(_BumpTex,uv_BumpTex);
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy)));

这个计算和我们上面是一样的,因为dot点积操作就是(x,y)(x,y) = x^2 + y^2;(这里只要不要去想点积的几何意义就好,我们只是单纯的拿它来做个计算)。


所以,改顶点的法向量就可以完全求出来啦。


当然这个是在切线空间下的该顶点的法向量,比如在Unity中需要计算光照什么的,我们还是会需要把这个法向量变换到对一个的坐标空间。

内容


正文部分即为大家总结了:

Unity3D 中的法线转换与切线空间知识


在 Shader 编程中经常会使用一些矩阵变换函数接口,其实它就是把固定流水线中的矩阵变换转移到了可编程流水线或者说 GPU 中,先看下面的函数语句吧:

// 将法线从对象空间变换到世界空间
o.worldNormal = mul(v.normal, (float3x3)_World2Object);

那为何使用此函数呢,下面简单介绍一下:


模型的顶点法线是位于模型空间下的,因此我们首先需要把法线转换到世界空间中。

计算方式可以使用顶点变换矩阵的逆转置矩阵对法线进行相同的变换,因此我们:

1、首先得到模型空间到世界空间的变换矩阵的逆矩阵_World2Object


2、通过调换它在 mul 函数中的位置,得到和转置矩阵相同的矩阵乘法。


由于法线是一个三维矢量,因此我们只需要截取_World2Object 的前三行前三列即可。下面给大家展示变换 Shader 代码:

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

///
/// 请注意,下面的代码可以处理均匀和非均匀比例
///
// 构造一个矩阵,将点/向量从切线空间转换到世界空间
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);
// 从世界空间变换到切线空间的矩阵是tangentToWorld
float3x3 worldToTangent = inverse(tangentToWorld);
// 将灯光和视图方向从世界空间转换为切线空间
o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));

自定义一个 inverse 函数


由于 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不支持本机着色器中的“逆”函数
            //所以我们自己写一个
            //注意:此函数只是一个演示
            //参考: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;
        //
        // 请注意,下面的代码可以处理均匀和非均匀比例
                //
                //构造一个矩阵,将点/向量从切线空间转换到世界空间
        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);
        // 从世界空间转换到切线空间的矩阵是切线世界的逆矩阵
        float3x3 worldToTangent = inverse(tangentToWorld);
        */
        // 只要tToW是正交矩阵,wToT=tToW的倒数=tToW的转置
        float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
        // 将灯光和视图方向从世界空间转换为切线空间
        o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
        o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));
        //
        // 请注意,下面的代码只能处理均匀刻度,不包括非均匀刻度
                //
                //计算二正态
        float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
        // 构造一个矩阵,将向量从对象空间转换为切线空间
        float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
        // 或者只使用内置宏
        TANGENT\_SPACE\_ROTATION;
        // 将灯光方向从对象空间变换到切线空间
        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);
        // 获取法线贴图中的纹理
        fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
        fixed3 tangentNormal;
        // 如果纹理未标记为“法线贴图”
        tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
        tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
        // 或者将纹理标记为“法线贴图”,并使用内置函数
        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 从模型空间变换到世界空间,由此我们就可以得到正确的变换,参考下图第二行的情况。

2fb9fd33be654873aa22328606ab17bd.png

大家可自行查看 UnityCG.cginc 文件,它里面封装了很多关于矩阵变换的接口函数,如下所示:

image.png

注:正文包含部分内容转载自:Unity3D 法线转换与切线空间总结_海洋_的博客-CSDN博客_unity 切线

并额外提炼总结而成

其他的,推荐Unity3D 中空间矩阵变换M和V推导Unity3D 中空间矩阵变换M和V推导 - 知乎

参考引用


shader inverse(float4x4) function - Unity Answers

Unity技术美术TA:Shader篇-学习视频教程-腾讯课堂

Unity3D 法线转换与切线空间总结_海洋_的博客-CSDN博客_unity 切线

目录
相关文章
|
C# 图形学
【Unity】贝塞尔曲线关于点、长度、切线计算在 Unity中的C#实现
原文:【Unity】贝塞尔曲线关于点、长度、切线计算在 Unity中的C#实现 写在前面 最近给项目做了个路径编辑,基本思路是满足几个基本需求: 【额外说明】其实本篇和这个没关系,可以跳过“写在前面”这部分,跨到正文部分 编辑时: ① 随意增减、插入、删除路点,只要路点数量大于1,绘制曲线,曲线必定经过路点。
3214 0
|
17天前
|
图形学
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版3(附带项目源码)
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版3(附带项目源码)
24 2
|
17天前
|
图形学
【制作100个unity游戏之28】花半天时间用unity复刻童年4399经典小游戏《黄金矿工》(附带项目源码)
【制作100个unity游戏之28】花半天时间用unity复刻童年4399经典小游戏《黄金矿工》(附带项目源码)
33 0
|
17天前
|
存储 JSON 关系型数据库
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
30 2
|
17天前
|
图形学
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)(上)
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)
26 2
|
17天前
|
图形学
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版2(附带项目源码)
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版2(附带项目源码)
15 1
|
17天前
|
存储 JSON 图形学
【unity实战】制作unity数据保存和加载系统——小型游戏存储的最优解
【unity实战】制作unity数据保存和加载系统——小型游戏存储的最优解
20 0
|
17天前
|
图形学
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)(下)
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)(下)
22 0
|
17天前
|
存储 JSON 关系型数据库
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版13(完结,附带项目源码)
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版13(完结,附带项目源码)
24 0
|
17天前
|
图形学
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版12(附带项目源码)
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版12(附带项目源码)
19 0