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,绘制曲线,曲线必定经过路点。
3278 0
|
20天前
|
图形学 C#
超实用!深度解析Unity引擎,手把手教你从零开始构建精美的2D平面冒险游戏,涵盖资源导入、角色控制与动画、碰撞检测等核心技巧,打造沉浸式游戏体验完全指南
【8月更文挑战第31天】本文是 Unity 2D 游戏开发的全面指南,手把手教你从零开始构建精美的平面冒险游戏。首先,通过 Unity Hub 创建 2D 项目并导入游戏资源。接着,编写 `PlayerController` 脚本来实现角色移动,并添加动画以增强视觉效果。最后,通过 Collider 2D 组件实现碰撞检测等游戏机制。每一步均展示 Unity 在 2D 游戏开发中的强大功能。
62 6
|
19天前
|
测试技术 C# 图形学
掌握Unity调试与测试的终极指南:从内置调试工具到自动化测试框架,全方位保障游戏品质不踩坑,打造流畅游戏体验的必备技能大揭秘!
【9月更文挑战第1天】在开发游戏时,Unity 引擎让创意变为现实。但软件开发中难免遇到 Bug,若不解决,将严重影响用户体验。调试与测试成为确保游戏质量的最后一道防线。本文介绍如何利用 Unity 的调试工具高效排查问题,并通过 Profiler 分析性能瓶颈。此外,Unity Test Framework 支持自动化测试,提高开发效率。结合单元测试与集成测试,确保游戏逻辑正确无误。对于在线游戏,还需进行压力测试以验证服务器稳定性。总之,调试与测试贯穿游戏开发全流程,确保最终作品既好玩又稳定。
40 4
|
20天前
|
图形学 缓存 算法
掌握这五大绝招,让您的Unity游戏瞬间加载完毕,从此告别漫长等待,大幅提升玩家首次体验的满意度与留存率!
【8月更文挑战第31天】游戏的加载时间是影响玩家初次体验的关键因素,特别是在移动设备上。本文介绍了几种常见的Unity游戏加载优化方法,包括资源的预加载与异步加载、使用AssetBundles管理动态资源、纹理和模型优化、合理利用缓存系统以及脚本优化。通过具体示例代码展示了如何实现异步加载场景,并提出了针对不同资源的优化策略。综合运用这些技术可以显著缩短加载时间,提升玩家满意度。
38 5
|
19天前
|
前端开发 图形学 开发者
【独家揭秘】那些让你的游戏瞬间鲜活起来的Unity UI动画技巧:从零开始打造动态按钮,提升玩家交互体验的绝招大公开!
【9月更文挑战第1天】在游戏开发领域,Unity 是最受欢迎的游戏引擎之一,其强大的跨平台发布能力和丰富的功能集让开发者能够迅速打造出高质量的游戏。优秀的 UI 设计对于游戏至关重要,尤其是在手游市场,出色的 UI 能给玩家留下深刻的第一印象。Unity 的 UGUI 系统提供了一整套解决方案,包括 Canvas、Image 和 Button 等组件,支持添加各种动画效果。
47 3
|
19天前
|
设计模式 存储 人工智能
深度解析Unity游戏开发:从零构建可扩展与可维护的游戏架构,让你的游戏项目在模块化设计、脚本对象运用及状态模式处理中焕发新生,实现高效迭代与团队协作的完美平衡之路
【9月更文挑战第1天】游戏开发中的架构设计是项目成功的关键。良好的架构能提升开发效率并确保项目的长期可维护性和可扩展性。在使用Unity引擎时,合理的架构尤为重要。本文探讨了如何在Unity中实现可扩展且易维护的游戏架构,包括模块化设计、使用脚本对象管理数据、应用设计模式(如状态模式)及采用MVC/MVVM架构模式。通过这些方法,可以显著提高开发效率和游戏质量。例如,模块化设计将游戏拆分为独立模块。
44 3
|
20天前
|
图形学 开发者 存储
超越基础教程:深度拆解Unity地形编辑器的每一个隐藏角落,让你的游戏世界既浩瀚无垠又细节满满——从新手到高手的全面技巧升级秘籍
【8月更文挑战第31天】Unity地形编辑器是游戏开发中的重要工具,可快速创建复杂多变的游戏环境。本文通过比较不同地形编辑技术,详细介绍如何利用其功能构建广阔且精细的游戏世界,并提供具体示例代码,展示从基础地形绘制到植被与纹理添加的全过程。通过学习这些技巧,开发者能显著提升游戏画面质量和玩家体验。
52 3
|
20天前
|
图形学 数据可视化 开发者
超实用Unity Shader Graph教程:从零开始打造令人惊叹的游戏视觉特效,让你的作品瞬间高大上,附带示例代码与详细步骤解析!
【8月更文挑战第31天】Unity Shader Graph 是 Unity 引擎中的强大工具,通过可视化编程帮助开发者轻松创建复杂且炫酷的视觉效果。本文将指导你使用 Shader Graph 实现三种效果:彩虹色渐变着色器、动态光效和水波纹效果。首先确保安装最新版 Unity 并启用 Shader Graph。创建新材质和着色器图谱后,利用节点库中的预定义节点,在编辑区连接节点定义着色器行为。
70 0
|
1月前
|
图形学
小功能⭐️获取Unity游戏物体上,所挂载组件的名称
小功能⭐️获取Unity游戏物体上,所挂载组件的名称
|
20天前
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
31 0