法线纹理贴图计算(切线空间世界空间)

效率:

在切线空间中计算,效率更高,因为可以在顶点着色器中就完成对光照、视角方向的矩
阵变换,计算量相对较小。( 矩阵变换在顶点着色器中计算)

在世界空间中计算,效率较低,由于需要对法线贴图进行采样,所以变换过程必须在片
元着色器中实现,我们需要在片元着色器中对法线进行矩阵变换。( 矩阵变换在片元着色器中计算)

全局效果

在切线空间中计算,对全局效果的表现可能会不够准确,在处理一些列如镜面反射、环境映射效果时表现效果可能不够准确

在世界空间中计算,对全局效果的表现更准确,可以更容易的应用于全局效果的计算

在选择使用哪种计算方式时主要考虑,若没有全局效果要求,我们优先使用在切线空间下进行光照计算,因为它效率较高;反之,我们选择在世界空间下计算

1、在切线空间下计算法线贴图

在切线空间下进行光照计算,需要把光照方向、视角方向变换到切线空间下参与计算
关键点:计算模型空间到切线空间的变换矩阵,变换矩阵为子空间(切线空间)到父空间(模型空间)的逆矩阵。

由于我们主要用变换矩阵来进行矢量的变换而非点的变换,因此可以变为3x3矩阵,而x、y、z轴分别为切线空间中顶点的切线、副切线、法线。已知切线、法线(从模型数据中可以获取),副切线为切线、法线的叉乘结果,而3个轴为相互垂直的单位向量,因此可以推出变换矩阵是正交矩阵,其逆矩阵就是其转置矩阵,即是模型空间到切线空间的变换矩阵。

用到的内置函数:
得到模型空间光的方向:ObjSpaceLightDir(模型空间顶点坐标)
得到模型空间视角方向:ObjSpaceViewDir(模型空间顶点坐标)
得到光方向和视角方向相对于模型空间的数据表达后,再与模型空间到切线空间的变换矩阵进行运算,即可将他们转换到切线空间下参与后续计算

  • 属性相关:漫反射颜色,单张纹理,法线纹理,凹凸程度,高光反射颜色,光泽度
  • 结构体相关

    顶点着色器中传入:可以使用 UnityCG.cginc 中的 appdata_full,其中包含了我们需要的顶点、法线、切线、纹理坐标相关数据

    片元着色器中传入:自定义一个结构体,其中包含 裁剪空间下坐标、uv坐标、光的方向、视角的方向

  • 顶点着色器回调函数中
  1. 顶点坐标模型转裁剪
  2. 单张纹理和法线纹理 UV坐标缩放偏移计算
  3. 副切线计算:用模型空间中的法线和切线进行叉乘 再乘以切线中的w(确定副切线方向)
  4. 构建模型空间到切线空间的变换矩阵
  5. 将光照方向和视角方向转换到模型空间(利用ObjSpaceLightDir和ObjSpaceViewDir内置函数)
  6. 将光照方向和视角方向转换到切线空间(利用变换矩阵进行乘法运算)
  • 片元着色器回调函数中
  1. 取出法线贴图中的法线信息(利用纹理采样函数tex2D)
  2. 利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
  3. 用得到的切线空间的法线数据 乘以 BumpScale 来控制凹凸程度
  4. 得到单张纹理颜色和漫反射颜色的叠加颜色
  5. 用切线空间下的 光方向、视角方向、法线方向 进行Blinn Phong光照模型计算
Shader "ShaderProj/2/NormalTex_TagentSpace"
{Properties{_MainColor("_MainColor", Color) = (1,1,1,1)_MainTex("_MainTex", 2D) = ""{}_BumpMap("_BumpMap", 2D) = ""{}_BumpScale("_BumpScale", Range(0, 1)) = 1_SpecularColor("_SpecularColor", Color) = (1,1,1,1)_SpecularNum("_SpecularNum", Range(0, 20)) = 18}SubShader{Tags { "LightMode"="ForwardBase" }LOD 100Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"fixed4 _MainColor;sampler2D _MainTex;float4 _MainTex_ST;sampler2D _BumpMap;float4 _BumpMap_ST;float _BumpScale;fixed4 _SpecularColor;float _SpecularNum;struct v2f{float4 pos:SV_POSITION;float4 uv:TEXCOORD0;float3 lightDir:TEXCOORD1;float3 viewDir:TEXCOORD2;};v2f vert (appdata_full v){v2f data;data.pos = UnityObjectToClipPos(v.vertex);data.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);data.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);// 副切线, w 是方向float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w; float3x3 transMatrix = float3x3(v.tangent.xyz,binormal,v.normal);data.lightDir = mul(transMatrix, ObjSpaceLightDir(v.vertex));data.viewDir = mul(transMatrix, ObjSpaceViewDir(v.vertex));return data;}fixed4 frag (v2f i) : SV_Target{// 取出法线纹理贴图当中的数据float4 packedNormal = tex2D(_BumpMap, i.uv.zw);// 将我们取出来的法线数据进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据float3 tangentNormal = UnpackNormal(packedNormal);// 乘以凹凸程度的系数tangentNormal *= _BumpScale;fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb;fixed3 lambertColor = _LightColor0 * albedo * max(0, dot(tangentNormal, normalize(i.lightDir)));float3 halfAngle = normalize(i.viewDir + i.lightDir);fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfAngle)), _SpecularNum);fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;return fixed4(color, 1);}ENDCG}}
}

2、在世界空间下计算法线贴图

世界空间下进行光照计算,需要把法线方向变换到世界空间下参与计算
关键点:计算切线空间到世界空间的变换矩阵,变换矩阵为子空间到父空间的变换
由于我们主要用变换矩阵来进行矢量的变换而非点的变换,因此可以变为3x3矩阵
而x、y、z轴分别为切线空间中顶点的切线、副切线、法线,我们只需要得到3个轴相对于世界空间的向量表达,即可得到该变换矩阵

法线从模型空间到世界空间:UnityObjectToWorldNormal(模型空间法线数据)
切线从模型空间到世界空间: UnityObjectToWorldDir(模型空间切线数据)
世界空间的副切线:用上面计算的结果叉乘即可
由这三个向量组成最终的切线空间到世界的空间的变换矩阵即可

Shader "ShaderProj/2/NormalTex_WorldSpace"
{Properties{_MainTex ("Texture", 2D) = "white" {}_MainColor ("MainColor", Color) = (1,1,1,1)_BumpMap ("BumpMap", 2D) = ""{}_BumpScale ("BumpScale", Range(0, 1)) = 1_SpecularColor ("SpecularColor", Color) = (1,1,1,1)_SpecularNum ("SpcecularNum", Range(0, 20)) = 18}SubShader{Tags { "LightMode"="ForwardBase" }LOD 100Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"sampler2D _MainTex;float4 _MainTex_ST;sampler2D _BumpMap;float4 _BumpMap_ST;float _BumpScale;fixed3 _MainColor;fixed3 _SpecularColor;float _SpecularNum;struct v2f{float4 pos:SV_POSITION;float4 uv:TEXCOORD0;float3 worldPos:TEXCOORD1;float3x3 transMat:TEXCOORD2;};v2f vert (appdata_full v){v2f data;data.pos = UnityObjectToClipPos(v.vertex);data.worldPos = mul(unity_ObjectToWorld, v.vertex);data.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);data.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);float3 worldNormal = UnityObjectToWorldNormal(v.normal);float3 worldTagent = UnityObjectToWorldDir(v.tangent);// 注意不要搞反叉乘的顺序!!!float3 worldBinormal = cross(normalize(worldTagent),  normalize(worldNormal)) * v.tangent.w;// 切线空间到世界空间的转换矩阵data.transMat = float3x3(worldTagent.x, worldBinormal.x, worldNormal.x,worldTagent.y, worldBinormal.y, worldNormal.y,worldTagent.z, worldBinormal.z, worldNormal.z);return data;}fixed4 frag (v2f i) : SV_Target{fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));float4 packedNormal = tex2D(_BumpMap, i.uv.zw);float3 tagentNormal = UnpackNormal(packedNormal);tagentNormal *= _BumpScale;float3 worldNormal = mul(i.transMat, tagentNormal);fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor;fixed3 lambertColor = _LightColor0 * albedo * max(0, dot(worldNormal, lightDir));float3 halfAngle = normalize(lightDir + viewDir);fixed3 specularColor = _LightColor0 * _SpecularColor * pow(max(0, dot(worldNormal, halfAngle)), _SpecularNum);fixed3 color = UNITY_LIGHTMODEL_AMBIENT * albedo + lambertColor + specularColor;return fixed4(color, 1);}ENDCG}}
}

但现在的实现其实有一个异常,当参数 _BumpScale 趋近于 0 时,理论上只应该改变材质表面的凹凸程度,但实际上同时会影响光照导致变暗(因为法线变成了 0),这是不符合常理的,

为了让凹凸系数不影响光的效果,有一种专门的算法

  1. 只让法线中的xy乘以凹凸系数,tangentNormal.xy *= _BumpScale;
  2. 保证法线为单位向量(让法线不会为0,而是趋近于顶点法线),即:
x² +y² +z² = 1
z² = 1 - (x² +y²)
z = sqrt(1 - (x² +y²))
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

通过这样的计算,当凹凸系数在0~1之间变化时,会保证法线为单位向量,这样就不会影响光照表现了,相关代码修改为:

float3 tagentNormal = UnpackNormal(packedNormal);
tagentNormal.xy *= _BumpScale;
tagentNormal.z = sqrt(1.0 - saturate(dot(tagentNormal.xy, tagentNormal.xy)));

这种算法并不是来自真实的物理规律,只是为了“看起来正常”

但是计算中还有可以优化的地方:

我们目前在v2f结构体中世界坐标顶点位置和变换矩阵使用了float3 和 float3x3 的两个变量来存储,但是在很多世界空间下计算 法线贴图的Shader中,往往会使用3个 float4 类型的变量来存储它们

这样做的目的是因为,这种写法在很多情况下可以提高性能,因为它更好地与GPU的硬件架构匹配float4 类型的寄存器是非常高效的,因为现代GPU通常会以 4 分量的向量为基本单位进行并行计算,float3x3 矩阵相对来说需要更多的寄存器和指令来表示和计算

Shader "ShaderProj/2/NormalTex_Optimize"
{Properties{_MainTex ("Texture", 2D) = "white" {}_MainColor ("MainColor", Color) = (1,1,1,1)_BumpMap ("BumpMap", 2D) = ""{}_BumpScale ("BumpScale", Range(0, 1)) = 1_SpecularColor ("SpecularColor", Color) = (1,1,1,1)_SpecularNum ("SpcecularNum", Range(0, 20)) = 18}SubShader{Tags { "LightMode"="ForwardBase" }LOD 100Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"sampler2D _MainTex;float4 _MainTex_ST;sampler2D _BumpMap;float4 _BumpMap_ST;float _BumpScale;fixed3 _MainColor;fixed3 _SpecularColor;float _SpecularNum;struct v2f{float4 pos:SV_POSITION;float4 uv:TEXCOORD0;// 切线空间到世界空间变换矩阵的3行float4 TtoW0:TEXCOORD1;     float4 TtoW1:TEXCOORD2;float4 TtoW2:TEXCOORD3;};v2f vert (appdata_full v){v2f data;data.pos = UnityObjectToClipPos(v.vertex);data.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);data.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);float3 worldPos = mul(unity_ObjectToWorld, v.vertex);float3 worldNormal = UnityObjectToWorldNormal(v.normal);float3 worldTagent = UnityObjectToWorldDir(v.tangent);// 注意不要搞反叉乘的顺序!!!float3 worldBinormal = cross(normalize(worldTagent),  normalize(worldNormal)) * v.tangent.w;// 切线空间到世界空间的转换矩阵data.TtoW0 = float4(worldTagent.x, worldBinormal.x, worldNormal.x, worldPos.x);data.TtoW1 = float4(worldTagent.y, worldBinormal.y, worldNormal.y, worldPos.y);data.TtoW2 = float4(worldTagent.z, worldBinormal.z, worldNormal.z, worldPos.z);return data;}fixed4 frag (v2f i) : SV_Target{float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));float4 packedNormal = tex2D(_BumpMap, i.uv.zw);float3 tagentNormal = UnpackNormal(packedNormal);tagentNormal.xy *= _BumpScale;tagentNormal.z = sqrt(1.0 - saturate(dot(tagentNormal.xy, tagentNormal.xy)));// 1.声明一个 3x3 的变换矩阵//float3x3 transMat = float3x3(i.TtoW0.xyz, i.TtoW1.xyz, i.TtoW2.xyz);//float3 worldNormal = mul(transMat, tagentNormal);// 2.直接矩阵乘法float3 worldNormal = float3(dot(i.TtoW0.xyz, tagentNormal), dot(i.TtoW1.xyz, tagentNormal), dot(i.TtoW2.xyz, tagentNormal));fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor;fixed3 lambertColor = _LightColor0 * albedo * max(0, dot(worldNormal, lightDir));float3 halfAngle = normalize(lightDir + viewDir);fixed3 specularColor = _LightColor0 * _SpecularColor * pow(max(0, dot(worldNormal, halfAngle)), _SpecularNum);fixed3 color = UNITY_LIGHTMODEL_AMBIENT * albedo + lambertColor + specularColor;return fixed4(color, 1);}ENDCG}}
}

3、其他

  • 模型空间下的切线数据

模型数据中的切线数据为float4类型的,其中的w表示副切线的方向
用法线和切线叉乘得到的副切线方向可能有两个,用切线数据中的w与之相乘确定副切线方向

  • Unity当中的法线纹理类型

当我们把纹理类型设置为Normal map(法线贴图)时,我们可以使用Unity提供的内置函数
UnpackNormal来得到正确的法线方向。该函数内部不仅可以进行法线分量 = 像素分量 * 2 – 1
的逆运算,还会进行解压运算(Unity会根据不同平台对法线纹理进行压缩)

  • 法线纹理属性法线纹理属性命名一般为_BumpMap (凸块贴图),还会声明一个名为_BumpScale (凸块缩放) 的float属性,它主要用于控制凹凸程度,为0时,表示没有法线效果,法线的影响会被消除;为1时,表示使用法线贴图中的原始法线信息,没有缩放
  • 如果使用的凹凸纹理不是法线纹理,而是高度纹理,我们需要进行如下设置

图片类型设置为Normal map(法线贴图),勾选 Create from Gryscale(从灰度创建)
这样我们就可以把高度纹理当成切线空间下的法线纹理处理了,多出的Bumpiness(颠簸值)控制凹凸程度,Filtering(过滤模式)决定计算凹凸程度的算法:
Sharp:滤波生成法线
Smooth:平滑的生成法线

  • 光照的计算方式

计算光照方式的两种方式:

  1. 模拟定向光源:直接得到_WorldSpaceLightPos0光照位置 为光照方向,表示光线是平行的,而不是从特定点发射,一般模拟太阳光效果 采用这种方式
  2. 模拟点光源:用光照位置【_WorldSpaceLightPos0 减去顶点坐标】,表示光线是从特定点发射的,并朝着顶点方向一般定点光源采用这种方式

两种方式实现结果其实是有一定差异的,比如,当用第二种方式去计算平行光时,如果只是改变光源的方向,但不改变光源的坐标,那么最终得到的光照效果是不变的。

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

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

相关文章

mybatis druid postgresql statement超时原理原理

yaml设置超时 mybatis-plus:mapper-locations: classpath:/mapper/*.xml # MyBatis Mapper XML文件的位置type-aliases-package: com.company.mi.entity # 实体类所在的包configuration:default-statement-timeout: 10 configuration 设置超时 BaseStatementHandler设置超时 …

Thread 类的基本用法

目录 什么是线程? 编写多线程程序 线程创建的方式 继承 Thread 类,重写 run 方法 实现 Runnable 接口,重写 run 方法 匿名内部类创建 Thread 子类 匿名内部类创建 Runnable 子类对象 lambda表达式 Thread 类和常用方法 Thread 的常…

node.js part1

Node.js Node.js 是一个跨平台JavaScript 运行环境,使开发者可以搭建服务器端的 JavaScript 应用程序。作用:使用Node.js编写服务器端程序 编写数据接口,提供网页资源浏览功能等等 前端工程化:为后续学习Vue和React等框架做铺垫. …

基于CDIO概念的人工智能物联网系统开发与实施的人才培养研究

目录 1. 引言(Introduction) 2. AIoT技术及其培训特点(The Characteristics of AIOT and Its Training) 3. 基于CDIO概念的AIoT课程改革(CDIO Concept-based Reform of AIOT Course) 4. AIoT课程内容安…

Idea里配置Maven版本

一、安装Maven 1. 官网下载maven地址: Maven – Download Apache Maven Binary是可执行版本,已经编译好可以直接使用。 Source是源代码版本,需要自己编译成可执行软件才可使用。tar.gz和zip两种压缩格式,其实这两个压缩文件里面包含的内容是…

Verilog刷题笔记50

题目: Given the following state machine with 1 input and 2 outputs: 解题: module top_module(input in,input [9:0] state,output [9:0] next_state,output out1,output out2);assign next_state[0]~in&(state[0]|state[1]|state[2]|state[3]…

Java方法01:什么是方法

本节视频链接:Java方法01:什么是方法?_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV12J41137hu?p45&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 Java中的‌方法‌是一段执行特定任务的代码片段,‌它是程序的基本构…

C#中的S7协议

S7协议-S7COMM S7COMM 进行写 CTOP->PDU type已知枚举值 0X0E连接请求0x0d连接确认0x08断开请求0x0c断开确认0x05拒绝访问0x01加急数据0x02加急数据确认0x04用户数据0x07TPDU错误0x0f数据传输 S7Header->ROSCTR已知枚举值 0X01JOB REQUEST。主站发送请求0x02Ack。从站…

Android MediaRecorder 视频录制及报错解决

目录 一、start failed: -19 二、使用MediaRecorder录制视频 2.1 申请权限 2.2 布局文件 2.3 MediaRecordActivity 2.4 运行结果 三、拓展 3.1 录制视频模糊(解决) 3.2 阿里云OSS上传文件 3.2.1 权限(刚需) 3.2.2 安装SDK 3.2.3 使用 相关链接 一、start failed…

基于spring boot的小型诊疗预约平台的设计与开发

TOC springboot262基于spring boot的小型诊疗预约平台的设计与开发 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大,随着当前时代的信息化,科学化发展,让社会各行业领域都争相使用新的信息技术,对行业内的各种相关数据进…

C++之模版初阶

目录 前言 1.泛型编程 2.函数模版 2.1函数模版概念 2.2函数模版格式 2.3函数模版的原理 2.4函数模版的实例化 2.5模版参数的匹配原则 3.类模版 3.1类模版的定义格式 3.2类模版的实例化 结束语 前言 前面我们学习了C的类与对象和内存管理,接下来我们继续学习…

【等保测评】Mysql测评中使用的命令汇总

一、身份鉴别 a) 应对登录的用户进行身份标识和鉴别,身份标识具有唯一性,身份鉴别信息具有复杂度要求并定期更换; mysql -uroot -p 查看登录是否需要输入口令鉴别用户身份 select user,host from mysql.user 查看是否存在相同账户…

苹果手机怎么清理重复照片的解决方案

随着智能手机摄像头技术的飞速发展,我们越来越依赖iPhone来记录生活中的点点滴滴。不可避免地,这也导致了大量重复照片的产生,这些重复照片不仅占用了宝贵的存储空间,还使得照片库显得混乱无序。本文将介绍苹果手机怎么清理重复照…

【项目实战】C++视频共享点播系统

目录 一、项目介绍 1.1 对视频共享点播系统的认识 1.2服务端程序负责功能 1.3 服务端功能模块划分 1.4 项目界面演示 1.5预备知识 二.环境搭建 2.1 安装 Jsoncpp 库 2.1.1 使用jsoncpp 2.2 引入httplib库 2.2.1 安装Git(如果你的系统尚未安装Git&#xf…

【算法】弗洛伊德(Floyd)算法求最短路径

目录 1.弗洛伊德(Floyd)算法介绍 2.弗洛伊德算法图解分析 2.1思路: 2.2图和矩阵的准备 2.3弗洛伊德算法的步骤: 2.4疑问 3.弗洛伊德算法的代码实现 3.1创建图并显示距离表与前驱表 3.2完整代码 1.弗洛伊德(Flo…

qt笔记之qml中的TextEdit、TextInput、TextArea、TextField的区别

qt笔记之qml中的TextEdit、TextInput、TextArea、TextField的区别 code review! 文章目录 qt笔记之qml中的TextEdit、TextInput、TextArea、TextField的区别一.对比二.C环境中类似功能的控件 一.对比 TextEdit、TextInput、TextArea和TextField都是用于文本输入的组件&#…

硅谷物理服务器有哪些关键优势和特点

硅谷的物理服务器设施全球知名,为各类企业提供了卓越的IT基础设施支持。下面将逐一探讨硅谷物理服务器的关键优势和特点,rak小编为您整理发布硅谷物理服务器有哪些关键优势和特点。 1. 卓越的性能 高性能计算能力:硅谷的物理服务器采用最新一…

内网渗透之icmp隧道传输

原理 # 为什么要建立隧道 在实际的网络中,通常会通过各种边界设备软/硬件防火墙、入侵检测系统来检查对外连接的情况,如果发现异常,会对通信进行阻断。 ​ # 什么是隧道 就是一种绕过端口屏蔽的方式,防火墙两端的数据包通过防火墙…

算法刷题记录 八十五【图论的广度优先搜索理论基础】

前言 图论章节第2篇。 第1篇:记录 八十二【图论理论基础及深度优先搜索算法】; 本文:记录 八十五【图论的广度优先搜索理论基础】 一、广度优先搜索理论基础 广度优先搜索理论基础 参考链接 1.1 知识点框架 1.2 模拟广度搜索的过程 在有向…

YOLT论文精读

引言 很早之前,在本校老师的带领下接触到了目标检测领域。在卫星遥感图像方面有一篇经典的论文《You Only Look Twice: Rapid Multi-Scale Object Detection In Satellite Imagery》。科研小白一开始反复看了几遍也没弄懂,决定写博客来加深自己的理解。…