【Unity Shader入门精要 第7章】基础纹理(二)

news2025/1/18 20:26:52

除了用于直接表示物体表面颜色,纹理的另外一种常见用法是用来进行凹凸映射,在不增加顶点的情况下,通过纹理来提供额外的法线信息,从而在视觉上增加表面凹凸细节,丰富渲染表现。

最常用的两种凹凸映射为法线贴图和高度图。

1. 法线纹理

1.1 坐标映射

法线纹理中存储的法线是经过归一化之后的值,单个分量的坐标范围为[-1, 1]。

由于颜色值的坐标范围只有[0, 1],因此要保证能完整记录[-1, 1]的法线,就需要在存储时对法线进行坐标映射:

pixel = (normal + 1) / 2

同样,当从法线纹理中采样得到颜色值后,要得到该颜色值对应的法线信息,需要对颜色值进行一次反映射:

normal = 2 * pixel - 1

在将法线纹理导入工程时,我们可以像前文一样设置它的Texture Type为Default,此时这就是一张普通的纹理,法线的xyz对应颜色的rgb通道,在使用时,通过采样得到的rgb值按照上述反映射计算得到法线。

在这里插入图片描述
更多时候,我们会像上图这样将Texture Type设置为Normal map,此时引擎会根据平台对法线纹理进行不同的压缩,法线的xyz分量对应的有可能不再是纹理的rgb值,因此,在这种情况下,为了能够正确得到法线信息,不能再手动进行反映射计算,而是需要通过UnityGC.gcinc文件提供的 UnpackNormal(fixed4 pixel) 方法来自动进行反映射计算。

1.2 模型空间的法线纹理

模型的每个顶点都有自己对应的法线,所有这些法线都是在该模型的模型空间内,一种简单直接的方式是将修改后的法线直接存储到纹理中,生成的就是模型空间的法线纹理。

这种法线纹理的优点是简单且直观,并且由于存储的法线都处于同一个坐标空间下,因此在纹理坐标的接缝处可以对接缝两侧的法线直接进行插值计算,从而做到法线平滑过渡。

1.3 切线空间的法线纹理

实际更多使用的是切线空间的法线纹理。对于每个顶点,都存在一个独属于该顶点的切线空间,构成切线空间的三个坐标轴分别为:

  • X轴:该顶点处的切线
  • Z轴:该顶点处的法线
  • Y轴:该顶点处的副切线(也可以叫副法线),由切线和法线叉乘获得

在导出法线纹理时,将各点处的最终法线先转换到该点的切线空间下,然后将切线空间下的法线坐标归一化并存储到纹理中,在采样时,通过该点的切线和原始法线可以构建出该点处的切线空间,再通过法线纹理的采样结果,就可以计算处该点处实际的法线,因此可以理解为,切线空间下法线纹理中记录的是每一点处的相对法线(或者叫法线的扰动)。

当某一点处的法线没有修改时,其在切线空间中的矢量应该与原始法线一致,也就是Z轴方向(0,0,1),转换为颜色值为(0.5,0.5,1),显示为淡蓝色。通常,模型中大部分法线都是没有修改的,因此往往切线空间下的法线纹理看起来都是一片蓝色。

通过切线空间来存储法线的好处最主要有两点:

  • 一是由于存储的是相对法线,因此与具体模型解耦,可以方便的复用或者进行平移实现uv动画等
  • 另一点是切线空间下存储的法线Z值都为正,再加上法线本身又是归一化处理之后的,那就可以通过XY的值确定Z的值,因此在存储时可以只通过两个颜色通道存储XY的值,利于压缩(相反模型空间下由于往往既有朝向Z轴正向的面又有朝向Z轴负向的面,因此模型空间下的法线的Z值也可能既有正值也有负值)

2. 使用法线纹理

以下只讨论切线空间下的法线纹理。

2.1 转换矩阵

由于纹理中记录的是切线空间中的法线,那么在进行着色时,无论是将光照等信息切换到切线空间下进行计算还是将采样得到的法线切换到世界空间下计算,首先需要确定模型空间、世界空间和切线空间的转换矩阵。

基于切线空间的定义我们得知,该空间的三条坐标轴在模型空间下的表示即为对应点的切线(TangentM)、副切线(BiTangentM)和原始法线(NormalM),那么根据前文(【Unity Shader入门精要 第4章】数学基础(二))中的内容很容易就可以构建出从切线空间到模型空间的转换矩阵为:
( ∣ ∣ ∣ T a n g e n t   M   B i T a n g e n t   M   N o r m a l   M   ∣ ∣ ∣ ) \left( \begin{matrix} | & | & | \\ Tangent~M~ & BiTangent~M~ & Normal~M~\\ | & | & | \end{matrix} \right) Tangent M BiTangent M Normal M 

从模型空间到切线空间的转换矩阵为:
( — T a n g e n t   M   — — B i T a n g e n t   M   — — N o r m a l   M   — ) \left( \begin{matrix} — & Tangent~M~ & — \\ — & BiTangent~M~ & — \\ — & Normal~M~ & — \end{matrix} \right) Tangent M BiTangent M Normal M 

经过简单的矩阵变换,就可以得到切线空间的XYZ轴在世界空间下的表示分别为TangentW 、BiTangentW 和 NormalW ,从而可以构建出从切线空间到世界空间的转换矩阵为:
( ∣ ∣ ∣ T a n g e n t   W   B i T a n g e n t   W   N o r m a l   W   ∣ ∣ ∣ ) \left( \begin{matrix} | & | & | \\ Tangent~W~ & BiTangent~W~ & Normal~W~\\ | & | & | \end{matrix} \right) Tangent W BiTangent W Normal W 

以及从世界空间到切线空间的转换矩阵为:
( — T a n g e n t   W   — — B i T a n g e n t   W   — — N o r m a l   W   — ) \left( \begin{matrix} — & Tangent~W~ & — \\ — & BiTangent~W~ & — \\ — & Normal~W~ & — \end{matrix} \right) Tangent W BiTangent W Normal W 

2.2 在切线空间下计算

在切线空间下计算光照时,为了节省计算量,我们可以先在顶点着色器中将计算要用到的光源方向和视角方向转换到切线空间,然后在片元着色器中与法线纹理中采样得到的法线直接进行光照计算即可。

  • 创建Chapter_7_NormalMap_TagentSpace_Mat作为测试材质
  • 创建Chapter_7_NormalMap_TagentSpace作为测试shader,并赋给Chapter_7_NormalMap_TagentSpace_Mat材质
  • 场景中创建胶囊体,将Chapter_7_NormalMap_TagentSpace_Mat材质赋给胶囊体
  • 场景中添加一盏平行光,并调整平行光角度

首先在shader中需要添加如下属性:

_BumpTex("BumpTex", 2D) = "bump"{}

该属性用于设置法线纹理,默认值为内置的bump纹理。在面板中将测试用的法线纹理赋给该属性。
同时,为了能够对设置的法线纹理正确采样,还需要声明对应的变量:

sampler2D _BumpTex;
float4 _BumpTex_ST;

为了构建旋转矩阵,在顶点着色器的输入结构中,我们需要定义一个用于接收切线数据的字段,并通过TANGENT语义通知Unity将切线信息填充到该字段。而且,该字段的类型选择为float4,主要时为了在下面构建旋转矩阵时通过其w分量来确定副切线的方向。
构建从模型空间到切线空间的旋转矩阵rotation:

float3 _bitangent = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, _bitangent, v.normal);

实际上,通过UnityCG.cginc内置的宏 TANGENT_SPACE_ROTATION 可以自动为我们生成旋转矩阵rotation(名字是固定的),不需要手动计算。

最后,在片元着色器中对法线纹理采样后,通过UnpackNormal方法将颜色值反映射为法线,然后按照正常的步骤计算光照。

最终Shader如下:

Shader "MyShader/Chapter_7/Chapter_7_NormalMap_TagentSpace"
{
    Properties
    {
        _MainTex("MainTex", 2D) = "white"{}
        _BumpTex("BumpTex", 2D) = "bump"{}
        _Specular("Specular", Color) = (1,1,1,1)
        _Gloss("Gloss", Range(1.0, 256.0)) = 16
    }
    
    SubShader
    {
        Pass
        {
            Tags {"LightMode" = "ForwardBase"}
        
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            
            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float2 uv : TEXCOORD0;
            };
            
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 uv : TEXCOORD0;
                float3 tangentView : TEXCOORD1;
                float3 tangentLight : TEXCOORD2;
            };
            
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpTex;
            float4 _BumpTex_ST;
            fixed4 _Specular;
            half _Gloss;
            
            v2f vert(a2v v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex).xy;
                o.uv.zw = TRANSFORM_TEX(v.uv, _BumpTex).xy;
                
//                float3 _bitangent = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
//                float3x3 rotation = float3x3(v.tangent.xyz, _bitangent, v.normal);
                TANGENT_SPACE_ROTATION;
                
                o.tangentView = mul(rotation, ObjSpaceViewDir(v.vertex));
                o.tangentLight = mul(rotation, ObjSpaceLightDir(v.vertex));
                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target
            {
                
                fixed3 _ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                
                float3 _tangentLight = normalize(i.tangentLight);
                fixed4 _packedNormal = tex2D(_BumpTex, i.uv.zw);
                float3 _tangentNormal = UnpackNormal(_packedNormal);
                _tangentNormal.z = sqrt(1.0 - dot(_tangentNormal.xy, _tangentNormal.xy));
                fixed4 _albode = tex2D(_MainTex, i.uv.xy);
                fixed3 _diffuse = _LightColor0.rgb * _albode.xyz * saturate(dot(_tangentNormal, _tangentLight));
                
                float3 _tangentView = normalize(i.tangentView);
                float3 _h = normalize(_tangentView + _tangentLight);
                fixed3 _specular = _LightColor0.rgb * _Specular.xyz * pow(saturate(dot(_tangentNormal, _h)) , _Gloss);
                
                return fixed4(_ambient + _diffuse + _specular, 1);
            }
            
            ENDCG
        }
    
    }

}

效果如下:
在这里插入图片描述

2.3 在世界空间下计算

从纹理中采样得到的法线为切线空间下的法线,要在世界空间下进行光照计算,需要先将采样得到的法线通过变换矩阵的处理转到世界空间下,因此,我们可以在顶点着色器中通过三个float4依次记录变换矩阵的每一行,从而构建好从切线空间到世界空间的变换矩阵,同时,为了减少占用插值寄存器的数量,_worldPos的XYZ被拆分开来,分别记录到了三个float4的w分量中。

另外,这里为了跟上文中在切线空间下计算的渲染效果做对比,没有像书上一样使用Phong模型进行高光反射,依然使用的是Blinn-Phong模型,否则二者渲染出的结果差异还挺明显的。

最终Shader如下:

Shader "MyShader/Chapter_7/Chapter_7_NormalMap_WorldSpace"
{
    Properties
    {
        _MainTex("MainTex", 2D) = "white"{}
        _BumpTex("BumpTex", 2D) = "bump"{}
        _Specular("Specular", Color) = (1,1,1,1)
        _Gloss("Gloss", Range(1.0, 256.0)) = 16
    }
    
    SubShader
    {
        Pass
        {
            Tags {"LightMode" = "ForwardBase"}
        
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            
            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float2 uv : TEXCOORD0;
            };
            
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 uv : TEXCOORD0;
                float4 tangentToWorldX : TEXCOORD1;
                float4 tangentToWorldY : TEXCOORD2;
                float4 tangentToWorldZ : TEXCOORD3;
            };
            
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpTex;
            float4 _BumpTex_ST;
            fixed4 _Specular;
            half _Gloss;
            
            v2f vert(a2v v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex).xy;
                o.uv.zw = TRANSFORM_TEX(v.uv, _BumpTex).xy;
                
                float3 _worldPos = mul(unity_ObjectToWorld, v.vertex);
                float3 _worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                float3 _worldNormal = UnityObjectToWorldNormal(v.normal);
                float3 _worldBitangent = cross(_worldTangent, _worldNormal) * v.tangent.w;
                
                o.tangentToWorldX = float4(_worldTangent.x, _worldBitangent.x, _worldNormal.x, _worldPos.x);
                o.tangentToWorldY = float4(_worldTangent.y, _worldBitangent.y, _worldNormal.y, _worldPos.y);
                o.tangentToWorldZ = float4(_worldTangent.z, _worldBitangent.z, _worldNormal.z, _worldPos.z);
                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target
            {
                
                fixed3 _ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                
                float3 _worldLight = normalize(_WorldSpaceLightPos0.xyz);
                fixed4 _packedNormal = tex2D(_BumpTex, i.uv.zw);
                float3 _tangentNormal = UnpackNormal(_packedNormal);
                _tangentNormal.z = sqrt(1.0 - dot(_tangentNormal.xy, _tangentNormal.xy));
                float3 _worldNormal = normalize(float3(dot(i.tangentToWorldX.xyz, _tangentNormal), dot(i.tangentToWorldY.xyz, _tangentNormal), dot(i.tangentToWorldZ.xyz, _tangentNormal)));
                fixed4 _albode = tex2D(_MainTex, i.uv.xy);
                fixed3 _diffuse = _LightColor0.rgb * _albode.xyz * saturate(dot(_worldNormal, _worldLight));
                
                float3 _worldPos = float3(i.tangentToWorldX.w, i.tangentToWorldY.w, i.tangentToWorldZ.w);
                float3 _worldView = normalize(_WorldSpaceCameraPos.xyz - _worldPos);
//                float3 _refl = normalize(reflect(-_worldLight, _worldNormal));
//                fixed3 _specular = _LightColor0.rgb * _Specular.xyz * pow(saturate(dot(_worldView, _refl)) , _Gloss);
                float3 _h = normalize(_worldLight + _worldView);
                fixed3 _specular = _LightColor0.rgb * _Specular.xyz * pow(saturate(dot(_worldNormal, _h)) , _Gloss);
                
                return fixed4(_ambient + _diffuse + _specular, 1);
            }
            
            ENDCG
        }
    
    }

}

效果如下:
在这里插入图片描述

3. 高度纹理

相比于记录法线,高度纹理的原理更加简单,就时直接把物体表面的起伏程度记录下来。因为只记录起伏,所以只需要一张黑白的灰度图就可以存储所需数据,其中越白的部分代表凸起程度越高。由于灰度图只能记录[0, 255]的范围,所以一般需要设置一个适当的缩放系数,从而纹理能够满足所需的数据跨度。
在这里插入图片描述
这是我在网上随便找的一张灰度图,通过对这张纹理进行采样,然后乘以缩放系数,既可以得到对应的起伏值,从而进行一些操作(比如生成地形)。

单纯使用灰度图的问题是,我们只能获得每个像素凸起程度的数值(或者高度值),但并不知道这个像素的法线方向,而没有法线信息就无法进行光照等着色计算。

要获得高度纹理中每个像素对应的法线信息,需要像下图一样,将纹理的类型选择为Normal map,并勾选Create from Grayscale(从灰度值生成法线),其下的两个变量Bumpiness影响凹凸程度,Filtering影响生成法线的锐利程度。

此时,Unity会根据当前灰度图自动计算每个像素位置的法线,然后将灰度图转变成一张切线空间的法线纹理,之后我们就可以像使用法线纹理一样使用这张纹理了。
在这里插入图片描述
当我们将这张纹理作为法线纹理对一个球体进行光照计算,于是就得到了一个奇怪的星球:

在这里插入图片描述

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

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

相关文章

【C语言每日题解】用函数来模拟实现strlen()、strcpy()、strcmp()、strcat()

🥰欢迎关注 轻松拿捏C语言系列,来和 小哇 一起进步!✊ 学习了函数后,老师让我们用函数来实现上面这四个字符串函数。 我们首先来了解一下这四个字符串函数: 1.strlen函数 用于获取字符串长度(不包括末尾…

用Rust构造一个人员基本信息的实体类,输出json测试结果

目录 一、需求说明 二、知识点 serde 一、需求说明 需求描述:用Rust写一个人员基本信息的实体类, 包括编号、姓名、描述、性别、备注、照片、简历等,加上3条测试数据,输出为json。 cargo add serde serde { version "1.0.201"…

SVN切换账号

SVN切换账号 有这么一种情况,对于一个新项目,项目紧急的情况下,大家会使用一个svn账号下载代码,开始提前熟悉业务。那么当正式开发的时候,每个人的svn账号也已经下发下来了,这个时候大家就需要切换成自己的…

Jquery+Servlet+JDBC实现登录注册功能

前端部分 HTMLCSS部分 引入JQuery包和JQuery.cookie包,前者封装了DOM操作的一些方法,后者封装了对cookie的操作 我们使用cookie主要是为了做登录后页面的跳转时,能存下来这个用户是谁,这样后面可以对单一用户进行操作&#xff…

嵌入式学习-PWM输出比较

简介 PWM技术 输出比较框图介绍 定时器部分 比较器控制部分 输出控制部分 相关寄存器

使用screen窗口在Autodl服务器训练网络

无法使用鼠标滚轮来查看历史输出的消息 vim ~/.screenrc 按i插入 termcapinfo xterm* ti:te 按esc :wq 保存screen创建的会话中,无法使用鼠标滚轮来查看历史输出的消息 解决方法_screen查看历史输出-CSDN博客无法使用鼠标滚轮查看screen会话历史,解决方…

证件照尺寸标准大小及用途介绍

在日常生活中,证件照是我们不可或缺的一部分,无论是办理身份证、驾驶证,还是参加考试、求职,都需要用到不同尺寸和底色的证件照。本文将为您详细介绍证件照的尺寸标准、用途以及底色的选择。 一、证件照的尺寸标准证件照的尺寸通…

【错题集-编程题】小红的ABC(字符串 + 找规律)

牛客对应题目链接:小红的ABC (nowcoder.com) 一、分析题目 算法思路:由于题目要找的是最短的回文子串,并且只有三个字母:a、b、c,因此最短的回文子串的长度要么是 2,要么是 3。因此,我们仅需枚举…

如何重启Windows系统上的Mysql服务

很久很久之前,我想把我的一台windows笔记本当比服务使用,当时还没有搞wsl, 试了vmware在局域网内总是断连。然后就直接在 windows系统上装了一个 windows版本的Mysql81。 有一天需要修改最大连接数, 费了挺大的劲终于找到了配置文件 my.ini…

“碳”索不止

2023年,欧盟宣布将在未来实施CBAM(碳边境调节机制),也称为碳关税,是一种针对进口货物的碳排放定价政策工具。该机制要求在欧盟境内的进口商对其进口产品支付相应的碳排放费用,以弥补其生产过程中产生的碳排…

K8S认证 | CKA题库 + 答案 | 查看Pod CPU资源使用量

2、查看集群中运行Pod CPU资源使用量 您必须在以下Cluster/Node上完成此考题: Cluster Master node Worker node k8s …

6.概率论

概率论在深度学习中也有着广泛的应用。由于深度学习模型通常包含大量的参数和复杂的结构,因此在实际应用中很难直接找到最优解。此时,我们可以利用概率论中的相关概念和方法,如贝叶斯推断、最大似然估计等,来估计模型的参数或评估…

【环境监测与分析】-环境检测过程案例

一.方案背景 空气质量检测,是指对空气质量的好坏进行检测。空气质量的好坏反映了空气中污染物浓度的高低。空气污染是一个复杂的现象,在特定时间和地点空气污染物浓度受到许多因素影响。来自固定和流动污染源的人为污染物排放大小是影响空气质量的最主要…

win11安装SQL Server 2012 企业版

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 系列文章目录前言一、硬件要求二、软件安装问题参考&#…

国外站群服务器如何提高seo排名

很多seo网站优化会选择国外站群服务器,那么国外站群服务器如何提高seo排名,具体方式有哪些?Rak部落小编为您整理发布国外站群服务器如何提高seo排名。 国外站群服务器可以通过以下几种方式提高网站在搜索引擎中的排名: - **内容分发与加速**…

持续拥抱鲲鹏生态 星辰天合深化信创存储能力

近日,鲲鹏昇腾开发者大会与广电五舟“2024 智算中国行”北京站陆续召开,星辰天合持续拥抱鲲鹏生态,以重要的合作伙伴身份受邀参加,并在鲲鹏昇腾开发者大会上荣获“甄选解决方案最佳应用软件奖”,在广电五舟“2024 智算…

USB2.0协议解读

一、说明 本文以Universal Serial Bus Specification Revision 2.0 April 27, 2000内容为准。 USB2.0支持三种速率,High speed(480Mb/s500ppm)、Full speed(12Mb/s2500ppm)以及Low speed(1.5Mb/s1.5%&…

JUnit5测试用例

1.用Test注解表示为测试方法 2.使用DisplayName定义别名 3.使用Assertions类的断言方法 使用断言,可以判断方法的实际执行结果和预期结果是否一致 assertEqualsassertTureassertNotNullassertAllassertThrows 下图是预期与实际不同时报错图 4.使用BeforeEach注解&…

分析人工智能在智慧银行服务中的实际应用以及面临的挑战

一、引言 近年来,人工智能(AI)技术快速发展,其在金融领域,特别是智慧银行服务中的应用日益广泛。人工智能以其独特的数据处理能力、预测分析能力以及自动化决策能力,极大地提升了智慧银行的服务效率、降低了运营成本,并优化了客户体验。然而,人工智能在智慧银行服务中…

RocketMQ-Dashboard 控制台使用详解

1 安装部署 具体部署启动请参考:RocketMQ从安装、压测到运维一站式文档_rocketmq benchmark压测-CSDN博客 RocketMq的dashboard,有运维页面,驾驶舱,集群页面,主题页面,消费者页面,生产者页面&…