目录

​编辑

前言

什么是unity 向量 法线?

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

什么是切线空间?

内容

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

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

自定义一个 inverse 函数

额外的方法

参考引用


前言

什么是unity 向量 法线?

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

// ddx ddy 计算法线
Shader "lcl/ddxddy/CalculateNormal"
{Properties{_MainTex ("Texture", 2D) = "white" {}}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{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中模型会提供一个副切线的方向的数据)。

法线贴图就是记录这个顶点在它的切线下的法线的向量(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.htmlfloat4x4 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 minorreturn 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 spaceo.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 从模型空间变换到世界空间,由此我们就可以得到正确的变换,参考下图第二行的情况。

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

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

并额外提炼总结而成

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

参考引用

shader inverse(float4x4) function - Unity Answers

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

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

Unity3D 法线转换切线空间相关推荐

  1. Unity3D 法线转换与切线空间总结

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

  2. Shader学习(9)法线和切线空间的定义和变换演示

    不如醉里风吹尽,又是一天未学习.西山日下雨足稀,又是一周未学习.垂死病中惊坐起,又是一月未学习 最近迷上了骑砍,游戏一玩啥都不管,做什么事都意兴阑珊. 一.法线的概念 三维模型是用一个一个的点构成的, ...

  3. 切线空间、法线贴图、TBN矩阵

    目录 1 法线贴图 1.1 为什么需要? 1.2 怎么做法线映射? 2 切线空间 2.1 为什么需要切线空间? 2.2 切线空间是什么? 2.3 TBN矩阵 2.4 TBN矩阵计算 3 光照计算是在` ...

  4. OpenGL核心技术之切线空间

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者;已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D实战核心技术详解 ...

  5. 处理顶点——通过切线空间的凹凸映射添加逐像素细节

    问题 虽然前一个教程中具有不变法线的平面物体工作良好,但如果对一个曲面或有转角的表面进行凹凸映射仍会遇到麻烦. 主要问题是包含在凹凸映射中的偏离法线是在切线空间中的,这意味着它与默认法线有联系. 为了 ...

  6. UE4 无需切线空间应用凹凸贴图

    Unreal Engine 4.9 照亮环境 凹凸贴图(Bump mapping) 最早由一名图形程序员发明(1978 James Blinn),它通过调整后的着色计算 来创建凹凸表面的假象,无需增加 ...

  7. UnityShader切线空间学习

    此处连接凹凸感文章现在需要应用在立体的形状上 1.可视化切线空间 /// <summary> /// 可视化切线空间 /// </summary> public class T ...

  8. OpenGL 法线贴图 切线空间 整理

    1. What`s Bump Mapping? Bump Mapping通过改变几何体表面各点的法线,使本来是平的东西看起来有凹凸的效果,是一种欺骗眼睛的技术:). 我们知道,如果几何体表面有高低不平 ...

  9. Unity Shader - 切线空间的法线贴图应用(T2W W2T)

    法线贴图 法线贴图(或是法线纹理)其实就是一张图片中的RGB通道分别存储着法线方向的纹理(有些为了数据压缩将X,Y存储在RG通道,Z是通过1-dot(xy,xy)来近似计算). 它的由来是因为高模运行 ...

  10. 冯乐乐 unity_Unity常用矩阵运算的推导补遗——切线空间

    在上一篇文章中,我写了一些关于Unity中各个坐标空间及其转换矩阵是如何得到的,说实在的,我是那种"记忆需要依靠外部装置存储"类.如同<攻壳机动队>的电子脑一样的人,每 ...

最新文章

  1. SELECT-OPTIONS对象
  2. 文件分享平台php源码,简易社会化用户文件分享系统 v1.0
  3. BZOJ 2662: [BeiJing wc2012]冻结(最短路)
  4. 论论资排辈和有能者居之
  5. 【疼逊】致广大QQ用户的一封信
  6. boost::filesystem目录相关的测试程序
  7. 这款电脑升降桌美到我了
  8. Vue 教程第四篇—— Vue 实例化时基本属性
  9. Web前端期末大作业--响应式健身会所网页设计(HTML+CSS+JS)实现
  10. JS学习笔记5-JavaScript 变量
  11. java kill 线程_java – 如何在等待中杀死正在运行的线程?
  12. Python中ndarray数组切片问题a[-n -x:-y]
  13. 032-OpenCV模板匹配单个对象、多个对象
  14. 手游服务器架设用什么系统,架设手游用什么云服务器
  15. Cisco AP-Regulatory Domain
  16. hive 转拼音udf_自定义UDF函数:将汉字转换成拼音
  17. 【阿里云】域名解析 Tomcat绑定域名
  18. Mojave下修复搜狗输入法斗图和标点配对不生效的问题?
  19. LXC的网络结构和端口映射
  20. 身份证拍照识别软件SDK

热门文章

  1. 可复用的显示隐藏按钮组件
  2. 无线键鼠接收器配对怎么就那么难?简直就是浪费
  3. Js根据域名判断显示不同内容
  4. 超声波清洗机对人体有辐射,有伤害吗?
  5. Datawhale组队学习周报(第026周)
  6. 4200万元新年大单!广西公安厅2个大数据智能化建设项目公开招标
  7. 【数据库】关系型数据库中实体间的关系
  8. ZZULIOJ:1035: 分段函数求值
  9. 怎样写Robots文件?
  10. endless walk