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

在上一篇文章中,我写了一些关于Unity中各个坐标空间及其转换矩阵是如何得到的,说实在的,我是那种“记忆需要依靠外部装置存储”类、如同《攻壳机动队》的电子脑一样的人,每次遇到问题了再去对着笔记慢慢翻找才是我的风格:

破晓:Unity中常用矩阵的推导​zhuanlan.zhihu.com
629fd5c905b1ffe8a3d4fb40e6077a0c.png

在文章中我漏了一个本该提到的内容,那就是切线空间。

一般在法线贴图中,颜色都是偏蓝的,按照颜色空间和坐标空间的对应来看,z值一般都很大,大部分情况下,都是接近垂直于物体表面的。

如果是不需要法线贴图的情况下还好说,用NORMAL语义可以直接从顶点获得模型空间下的法线,如果要使用法线贴图?那就需要切线空间了。

常识告诉我们,构建一个空间笛卡尔直角坐标系需要三个互相垂直的基向量,而常识又告诉我们,两个向量的叉乘结果垂直于这两个向量构成的平面,故我们可以用两个向量完成切线空间的构建。

一个就是法线(Normal),一个是切线(Tangent),其叉乘结果为副切线(Bitangent)或者副法线(Binormal),这两种叫法我都看到过。以Unity官方的Shader例子举例:

v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
{v2f o;//...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);//...return o;
}fixed4 frag (v2f i) : SV_Target
{// 从法线贴图里采样并解码法线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);//...
}

众所周知,两个向量的叉乘不支持交换律,会因为顺序的不同而具备有两个方向的结果,而很多时候模型可能被缩放过,导致它是镜像的,为了避免该问题,使用这个Vector4向量unity_WorldTransformParams的w值来保证,如果模型被镜像偶数次,则w=1,反之则为-1。

这个矩阵一般被简称TBN矩阵,用于将切线空间向其他坐标空间转换,它需要使用到其他坐标空间下的法线和切线进行计算。

而Unity又在UnityCG.cginc里提供了这样一个宏:

// Declares 3x3 matrix 'rotation', filled with tangent space basis
#define TANGENT_SPACE_ROTATION float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

这里居然直接用模型坐标空间的法线和切线在做叉乘?莫慌,看看其他地方都用它干了啥,哦,视差贴图啊?那没事了……

冯乐乐已经谈到过这件事情:

【常见问题】关于法线转换的问题(以及在切线空间下计算法线纹理的问题) · Issue #45 · candycat1992/Unity_Shaders_Book​github.com
5296e03dd615d00c691740fbcef2d318.png

这涉及到非标准缩放的问题——

这是因为Unity 5之前,如果我们对一个模型A进行了非统一缩放,Unity内部会重新在内存中创建一个新的模型B,模型B的大小和缩放后的A是一样的,但是它的缩放系数是统一缩放。换句话说,在Unity 5以前,实际上我们在Shader中根本不需要考虑模型的非统一缩放问题,因为在Shader阶段非统一缩放根本就不存在了。但从Unity 5以后,我们就需要考虑非统一缩放的问题了。

说了这么多介绍类的东西,还没说正题:那就是为什么左乘TBN矩阵可以把向量转换出去?

我们先把TBN矩阵的运算写一下:

equation?tex=%5Cbegin%7Bbmatrix%7D+t_x+%26+b_x%26+n_x++%5C%5C+t_y+%26+b_y+%26+n_y+%5C%5C+t_z+%26+b_z+%26+n_z+%5Cend%7Bbmatrix%7D%5Cbegin%7Bbmatrix%7D+v_x+%5C%5C+v_y++%5C%5C+v_z++%5Cend%7Bbmatrix%7D%3D+%5Cbegin%7Bbmatrix%7D+t_x+v_x+%2B+b_xv_y%2Bn_xv_z%5C%5C+t_yv_x%2Bb_yv_y%2Bn_yv_z++%5C%5C+t_zv_x%2B+b_zv_y%2Bn_zv_z++%5Cend%7Bbmatrix%7D%5C%5C%5C

这个形式相当于与形如

equation?tex=%5Cbold+x+%3D%28t_x%2C+b_x%2C+n_x%29 的向量做了点积,而
equation?tex=%5Cbold+x 就是世界空间的基向量在切线空间的表示,因为
equation?tex=t_x
equation?tex=%5Cbold+t 在世界空间下对
equation?tex=x 轴基向量的投影:
equation?tex=t_x%3Ddot%28%5Cbold+t%2C+%281%2C0%2C0%29%29%3D%5Cleft%7C+%5Cbold+t+%5Cright%7Ccos%5Ctheta%5C%5C 反过来,也可以认为是
equation?tex=x 轴基向量在切线空间基向量
equation?tex=t 轴上的投影,同理
equation?tex=b_x
equation?tex=n_x+ 也是如此。

equation?tex=%5Cbold+v
equation?tex=%5Cbold+x 做点积,就相当于把自己投影了过去,三个分量就是在世界坐标三个基向量上的投影长度,组合起来的新的向量,就是
equation?tex=%5Cbold+v 在世界坐标空间下的表示。

谈到这个就不得不说我在头发渲染中犯下的问题了:

破晓:使用Kajiya-Kay模型的头发渲染​zhuanlan.zhihu.com
zhihu-card-default.svg

此处做一个勘误。

当时我信誓旦旦地这么写:

一个是副切线的计算问题,不能直接用世界空间下的法线和切线cross,而是应当在模型空间cross好再转换:
o.tangent = mul(unity_ObjectToWorld, v.tangent);
o.normal = mul(unity_ObjectToWorld, v.normal);
o.bitangent = mul(unity_ObjectToWorld, cross(v.normal, v.tangent));
//错误写法:o.bitangent = cross(o.normal, o.tangent);

其实是错误的,因为本文提到过,叉乘结果是有两个方向的,我这边叉乘的顺序恰好得到的是相反的副切线,而当时数学不过关,大脑萎缩,一看副切线结果不同,想当然以为是没转换空间,正确的写法应是:

o.normal = UnityObjectToWorldNormal(v.normal);
o.tangent = UnityObjectToWorldDir(v.tangent.xyz);
o.bitangent = cross(v.normal, v.tangent) * v.tangent.w * unity_WorldTransformParams.w;

其次是使用副切线而不是切线的问题,这个是在Unity里材质预览窗看到的效果,在Preview窗口,切成Plane并输出tangent可以看到是红色的,是水平而非垂直方向的,所以高光偏移时要传入垂直方向上的副切线。


利用叉乘,同样还能从高度图中反推法线图:

fixed4 frag (v2f i) : SV_Target
{float2 uvx0 = i.uv - float2(_MainTex_TexelSize.x, 0) * 0.5;float2 uvx1 = i.uv + float2(_MainTex_TexelSize.x, 0) * 0.5;float2 uvy0 = i.uv - float2(0, _MainTex_TexelSize.y) * 0.5;float2 uvy1 = i.uv + float2(0, _MainTex_TexelSize.y) * 0.5;float3 dx = float3(_MainTex_TexelSize.x,0,(1 - tex2D(_MainTex, uvx1).r) - (1 - tex2D(_MainTex, uvx0).r));float3 dy = float3(0,_MainTex_TexelSize.y,(1 - tex2D(_MainTex, uvy1).r) - (1 - tex2D(_MainTex, uvy0).r));float3 n = cross(dx, dy);n.z *= 10;//这里可以控制法线收束于z轴的程度float3 normal = normalize(n);normal = normal * 0.5 + 0.5;	return float4(normal, 1);
}

我们分别在UV的x和y方向上单独讨论,以x方向为例,我们可以得知在该点处的梯度向量形如

equation?tex=%5Cbold+k+%3D+%28dx%2C+0%2Cf%28x%2B0.5+%2A+dx%29+-+f%28x-0.5%2Adx%29%29+%5C%5C

此处,高度上的插值反映在z轴上,因为切线空间又可以被认为是局部物体表面空间,xy平面被认为是uv,而z轴穿插入物体表面。

使用x和y方向单独计算出来的梯度向量,就能够得到法线方向了。但有一点需要注意的是,计算出来的法线需要手动调整收束程度以适配项目使用。比如想要更平缓的法线图,就需要收束法线到z轴上更多一点。

e885e41bb337b9dc0579cc7dd299c84c.png

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

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

相关文章

mysql 字段类型设计_Mysql字段类型设计相关问题!-阿里云开发者社区

Mysql是以文件存储在我们的系统的硬盘上面,那么(1)当我们读取写入的时候就会有磁盘IO的问题(2)当我们存储的数据是以页单位存储,而且每页的大小是16K,那么我们要尽可能的让我们的一页数据存放的更多。表结构宽度不要太大,也就是列…

mysql保存时乱码了_MySQL保存中文乱码的原因和解决办法

(3)MySQL的字符集设置。这个是重点了,一般都是在这里搞错而出现了mysql乱码。mysql编码设置可以分为三种设置:数据库的编码、表的编码、和字段的编码。a、数据库的编码:在sqlyog工具中操作把,右击数据库点击更改数据库&#xff0c…

mysql字符串语法_MySQL语法模板 函数:字符串

返回字符串的ASCII码ascii(str)返回字符串的二进制码bin(n)字符串的位数bit_length(str)字符串的字符数char(n,... [using charset])字符串的字符数character_length(str)字符串的字符数char_length(str)压缩函数compress(string_to_compress)进制转换conv(n,from_base,to_base…

python中convert函数用法_Python Pandas DataFrame.tz_convert用法及代码示例

Pandas DataFrame是带有标签轴(行和列)的二维大小可变的,可能是异构的表格数据结构。算术运算在行和列标签上对齐。可以将其视为Series对象的dict-like容器。这是 Pandas 的主要数据结构。Pandas DataFrame.tz_convert()用于将tz-aware轴转换为目标时区。用法&#…

ubuntu mysql支持中文_ubuntu (16.04) server 英文原版 添加中文语言支持 消除java 程序、mysql 数据库不能处理中文的错误...

ubuntu 16.04对于腾讯云主机要添加源,因为腾讯云主机(CVM)的源库中包很少文件/etc/apt/sources.list是一个普通可编辑的文本文件,保存了ubuntu软件更新的源服务器的地址。和sources.list功能一样的是/etc/apt/sources.list.d/*.list(*代表一个文件名&…

python偶数个数_python基础

标识符命名规则开发中,通常约定俗称遵守如下规则:删除变量和垃圾回收机制可以通过del语句删除不在使用的变量a 123 del a #删除了栈,没有删除堆链式赋值xy123 相当于 x123;y123系列解包赋值系列数据赋值给对应相同…

keyshot怎么让物体发光_户外发光字的防水措施怎么做,不亮了怎么修。

户外发光字的防水措施与维修发光字大多是安装在户外的,因此会受到热晒、雨淋、寒冻等因素的影响,这些因素都会加速LED发光字的老化,从而使LED发光字进水老化。那么,该怎么防止这种情况呢?一、在每个广告字的笔画的最下方&#xf…

linux mysql 确认命令_LINUX启动/重启/停上MYSQL的命令(详解)

如何启动/停止/重启MySQL一、启动方式1、使用 service 启动:service mysqld start2、使用 mysqld 脚本启动:/etc/inint.d/mysqld start3、使用 safe_mysqld 启动:safe_mysqld&二、停止1、使用 service 启动:service mysqld st…

mysql删除员工_数据库删除职工信息

《数据库技术原理与应用教程第2版》——1.4数据管理本节书摘来自华章出版社《数据库技术原理与应用教程第2版》一书中的第1章,第1.4节,作者 徐洁磐 操凤萍  ,更多章节内容可以访问云栖社区“华章计算机”公众号查看。1.4数据管理人类社会有着…

mysql和ORTHOMCL_Orthomcl的详细使用

参考了众多文章并结合实际操作后的感想。Orthomcl需要的linux详细配置,简单叙述。系统:unixBLASTDatabase:oracle 或 mysql内存4G,硬盘100Gperl: DBI module 和 DBD-mysqlMCL程序详细操作过程及需要注意的点:1.数据库安…

手撕包菜 mysql_手撕包菜搭建

概述最近做了两件事,一件事就是买了块1t硬盘,第二件事就是买了个百度云会员,无奈找不到资源下载,那就没办法了,搭建一个磁力链接搜索引擎来爬去链接,然后去找资源。说道磁力链接搜索引擎,最好的…

word流程图怎么使箭头对齐_word 流程图 怎么把箭头对整齐啊?

word里画流程图把箭bai头画直du,可通过shift键实现画出zhi垂直或水平的直箭头。方法步dao骤如下:回1、打开需要答操作的WORD文档,选中需要画出箭头的流程图框图,点解绘图工具中的“排列对齐”,然后选中“左右居中”对齐。2、在插入形状中找到并点击箭头,使用箭头形状工具…

java 多线程同步_详解Java多线程编程中的线程同步方法

1、多线程的同步:1.1、同步机制:在多线程中,可能有多个线程试图访问一个有限的资源,必须预防这种情况的发生。所以引入了同步机制:在线程使用一个资源时为其加锁,这样其他的线程便不能访问那个资源了&#…

java商品管理系统_【Java Web】简易商品信息管理系统——首个Web项目

正文之前在学习了一段时间的Java Web的内容之后,当然需要有个项目来练练手,我相信大多数人的首选项目都是信息管理系统吧,所以我选择了商品信息管理系统目前项目源码已全部上传至GitHub,欢迎大家来fork —— 商品信息管理系统正文…

java 向上舍入_介绍Java的大数类(BigDecimal)和八种舍入模式

1.BigDecimal简介BigDecimal 由任意精度的整数非标度值 和32 位的整数标度 (scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负scale 次幂。因此,BigDecimal表示的数值是(unscaledValue 10-s…

java循环的内部改变循环变量的值_java在for循环中怎样修改参数值?

// 待认证QueryFilter queryFilter0 new QueryFilter();queryFilter0.addCommand(new Command("busStatus", Op.EQ, "0"));int taxStatus0 taxInfoDao.getCount(queryFilter0);int businessStatus0 businessInfoDao.getCount(queryFilter0);int socialS…

java redis rpush_Redis Rpush 命令

Redis Rpush 命令Redis Rpush 命令用于将一个或多个值插入到列表的尾部(最右边)。如果列表不存在,一个空列表会被创建并执行 RPUSH 操作。当列表存在但不是列表类型时,返回一个错误。注意:在 Redis 2.4 版本以前的 RPUSH 命令,都只…

java jstat 命令_java高分局之jstat命令使用(转)

转自:http://blog.csdn.net/h_025/article/details/52813817java高分局之jstat命令使用jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]注意:使用的jdk版本是…

java判_java中判空

一、概述java中判等似乎很简单,用来判断对象引用(内存地址)是否相同,equals用来判断值是否相同。你可以试用String对象轻松区分这一点。那么在null判等(也就是判空操作)时呢?可以通过如下代码明白两个判等的比较:String name nul…

java启动应用_java 学习:在java中启动其他应用,由jenkins想到的

在jenkins的实践中遇到了一个问题:我的项目依赖其他第三方应用的地方比较多,而且会占用多个端口,如何处理端口和启动/关闭第三方应用成了难题。初级解决方案:在服务端上面写一堆bat文件,,,&…