找回密码
 立即注册
查看: 537|回复: 0

[笔记] 《Unity Shader入门精要》笔记(七)

[复制链接]
发表于 2021-12-26 12:51 | 显示全部楼层 |阅读模式
本文为《Unity Shader入门精要》第七章内容《基础纹理》上半部分的笔记。
本文相关代码,详见:

原书代码,详见原作者github:
<hr/>纹理映射技术:把一张图“黏”在模型表面,逐纹素(texel,为了和像素进行区别)地控制模型的颜色。
美术建模时,通常会在建模软件中利用纹理展开技术把纹理映射坐标存储在每个顶点上。
纹理映射坐标:定义了该顶点在纹理中对应的2D坐标。通常使用二维变量(u, v)来表示,u是横向坐标,v是纵向坐标,因此纹理映射坐标也被称为UV坐标
无论纹理大小多大,顶点UV坐标的范围通常都被归一化为[0, 1]范围内。
1. 单张纹理

使用一张纹理代替物体的漫反射颜色。
1.1 实践

1.1.1 准备工作

完成以下准备工作:

  • 新建名为Scene_7_1的场景,并去掉天空盒子;
  • 新建名为SingleTextureMat的材质;
  • 新建名为Chapter7-SingleTexture的Shader,并赋给上一步创建的材质;
  • 在场景中新建一个胶囊体,并把第二步的材质赋给它;
  • 保存场景;
详细操作详见《<Unity Shader入门精要>笔记(四)》里的案例常用操作说明。
1.1.2 编写Shader代码

打开Chapter7-SingleTexture,删除里面所有代码,编写如下代码。新的代码逻辑已用注释说明,同时之前写过的部分代码,为了方便回顾,也添加了相应的注释:
// 为Shader命名
Shader "Unity Shaders Book/Chapter 7/Single Texture"
{
    Properties
    {
        // 叠加的颜色,默认为白色
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        // 纹理,类型为2D,没有纹理时,默认用白色覆盖物体的表面
        _MainTex("Main Tex", 2D) = "white" {}
        // 高光颜色,默认为白色
        _Specular ("Specular", Color) = (1, 1, 1, 1)
        // 光泽度,影响高光反射区域的大小
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }

    SubShader
    {
        Pass
        {
            // 指明当前Pass的光照模式
            Tags { "LightMode" = "ForwardBase"}

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            // 为了使用光照相关的内置变量(如:_LightColor0 光照颜色)
            #include "Lighting.cginc"

            /* 定义属性变量 */
            fixed4 _Color;
            sampler2D _MainTex;
            // 与_MainTex配套的纹理缩放(scale)和平移(translation),在材质面板的纹理属性中可以调节
            // 命名规范为:纹理变量名 + "_ST"
            // _MainTex_ST.xy 存储缩放值
            // _MainTex_ST.zw 存储偏移值
            float4 _MainTex_ST;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                // 存储模型的第一组纹理坐标,可以理解为_MainTex对应的原始纹理坐标
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                // 存储纹理坐标的UV值,可在片元着色器中使用该坐标进行纹理采样
                float2 uv : TEXCOORD2;
            };

            v2f vert(a2v v)
            {
                v2f o;

                // 将顶点坐标由模型空间转到裁剪空间
                o.pos = UnityObjectToClipPos(v.vertex);
                // 将法线由模型空间转到世界空间
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                // 将顶点坐标由模型空间转到世界空间
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                // 通过缩放和平移后的纹理UV值
                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                // 也可以调用内置宏TRANSFORM_TEX,得到缩放和平移后的纹理UV值,与上面的计算逻辑是一致的
                // 内置宏的定义:
                // #define TRANSFORM_TEX(tex, name) (tex.xy * name##_ST.xy + name##_ST.zw)
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                // 调用内置函数UnityWorldSpaceLightDir,
                // 得到当前坐标点在世界空间下的光照方向,并进行归一化
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                // 通过内置函数tex2D,根据当前坐标点的UV值,对纹理进行采样拿到纹理颜色
                // 并和颜色属性的乘积得到反射率albedo
                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                // 使用内置变量UNITY_LIGHTMODEL_AMBIENT,
                // 得到环境光的颜色,并和反射率相乘得到环境光部分
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
               
                // 使用内置变量_LightColor0得到光照颜色,乘以反射率,得到光照部分,
                // 再根据兰伯特定律漫反射公式得到漫反射部分
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

                // 使用内置函数UnityWorldSpaceViewDir得到当前坐标点的视角方向,并进行归一化
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                // 基于Blinn-Phong光照模型,得到中间矢量
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                // 基于Blinn-Phong光照模型的公式,得到高光反射部分
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                // 环境光 + 漫反射 + 高光反射,得到最终的颜色值
                return fixed4(ambient + diffuse + specular, 1.0);
            }

            ENDCG
        }
    }

    // 用系统内置的高光shader作为兜底
    Fallback "Specular"
}
保存Shader代码。
上述代码材质的缩放和偏移对应材质面板的如下内容:



1.1.3 配置材质参数

往Unity中添加一张图片(图片可从参考Git仓库中找,名字为Brick_Diffuse.JPG),然后将图片拖到材质面板的MainTex属性处:



可得到如下效果:



1.2 纹理的属性

选中之前导入的图片,在Inspector窗口可以看到纹理的相关属性:



1.2.1 Alpha Source

纹理的类型默认为Default,Alpha Source属性对应的下拉框选中From Gray Scale,透明通道的值将会由每个像素的灰度值生成。(第8章会讲透明效果)



1.2.2 Wrap Mode

Wrap Mode属性对应的下拉框有Repeat和Clamp等属性值:



纹理坐标的范围是[0, 1]。
对于Repeat,如果纹理坐标不在[0, 1]范围,那么它只会取小数部分进行采样,如:1.2当做0.2处理,-2.3当做0.3处理。(上一节的最终效果就是使用了Repeat属性值)



对于Clamp,如果纹理坐标大于1,则取1;如果纹理坐标小于0,则取0。



需要注意的是:
模型的纹理若想要Wrap Mode生效,需要在Shader中处理缩放和偏移的逻辑,也就是:
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// 或
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
1.2.3 Filter Mode

Filter Mode属性决定了当纹理由于变换而产生拉伸时,采用哪种滤波模式。Filter Mode支持3种模式:Point, Bilinear以及Trilinear。它们得到的图片滤波效果依次提升,但性能消耗也依次增大,纹理滤波会影响放大或缩小纹理时得到的图片质量。
下图是将一张小图放到Plane上放大后,在Point、Bilinear、Trilinear三种效果下的对比:



Point模式:使用最近邻滤波,在放大缩小时,采样像素数目通常只有一个,所以图像呈现像素风格的效果;
Bilinear模式:使用线性滤波,对每个目标像素,会找到4个邻近像素,对其线性插值混合后得到最终像素,所以图像看起来被模糊了;
Trilinear模式:原理和Bilinear一样,只是Trilinear会在多级渐远纹理(下一节讲)之间进行混合,如果一张纹理没有使用多级渐远纹理技术,Trilinear得到的结果和Bilinear就是一样的。
1.2.4 多级渐远纹理

纹理缩放需要处理抗锯齿问题,一种常用的方法是使用多级渐远纹理技术(mipmapping)。它将原纹理用滤波处理提前生成不同尺寸的纹理,在实时运行时,直接使用对应尺寸的纹理,速度更快,但缺点是需要额外的空间(约33%)来存储生成的这些纹理,是一种空间换时间的思路。
Unity中可在纹理面板中展开Advenced,勾选Generate Mip Maps一项来开启多级渐远纹理技术。



1.2.5 纹理大小

不同的平台发布游戏,考虑目标平台的纹理尺寸和质量问题,Unity允许不同的平台进行不同的设置。



如果导入的纹理超过了Max Size的设置值,Unity就会把纹理缩放到这个分辨率。
一般导入的纹理可以是非正方形的,但是长宽需要是2的幂,例如:2、4、8、16等,如果使用了非2的幂(Non Power of Two, NPOT)大小的纹理,那么这些纹理往往会占用更多的内存空间,GPU读取这些纹理的速度也会下降,所以尽量使用2的幂大小的纹理。



Format决定了Unity内部使用哪些格式来存储该纹理。使用的纹理格式精度越高,占用的内存越大,得到的效果也越好。可以在纹理面板下方的预览中看到该纹理需要占用的内存空间及相关信息。



2. 凹凸映射

凹凸映射:使用一张纹理来修改模型表面的法线,在物体平整的表面模拟出凹凸的质感。
有两种方法进行凹凸映射:高度纹理法线纹理
2.1 高度纹理

使用一张高度纹理来模拟表面位移,然后得到一个修改后的法线值,这种方法被称为高度映射。
高度图中存储的是强度值,用于表示模型表面局部的海拔高度。颜色越浅的位置,其表面越向外凸起,否则反之。
优点:直观;
缺点:计算复杂,实时计算时不能直接得到表面法线,需要由像素的灰度值计算得到,比较耗性能。



2.2 法线纹理

法线纹理中存储的是表面的法线方向。由于法线方向各分量范围是[-1, 1],而像素分量范围是[0, 1],因此需要做一个映射:


所以需要对法线纹理采样后做一次反映射:


法线纹理根据法线存储的空间分为:模型空间的法线纹理和切线空间的法线纹理。
对于模型的每个顶点,都有一个属于自己的切线空间,空间的原点就是该顶点本身,z轴是顶点的法线方向,x轴是顶点的切线方向,y轴由法线和切线叉积得到,也称副切线



关于切线空间的更多介绍,可参考:https://www.zhihu.com/question/23706933/answer/161968056
下图是两种空间的法线纹理的对比:


图片来源:http://www.surlybird.com/tutorials/TangentSpace/
因为模型空间的法线纹理中的所有法线都是基于同一个模型空间的,是向各个方向的,所以映射后的颜色值是五彩缤纷的;
切线空间下的法线方向都是基于当前顶点自己的切线空间,固定朝向z轴——(0, 0, 1),映射后的RGB值为(0.5, 0.5, 1)浅蓝色,所以法线纹理大片都是浅蓝色,法线纹理存储量每个点在各自的切线空间中的法线扰动方向。
模型空间存储法线的优点:

  • 计算更少,生成简单;(切线空间下,模型切线一般和UV方向相同,想要得到比较好的法线映射需要纹理映射也是连续的)
  • 可以提供平滑的边界;(因为是同一个坐标系下,边界处可以通过差值得到的法线平滑变化;而切线空间需要依赖纹理坐标方向得到,边缘处或尖锐部分会造成更多可见的缝合迹象)
切线空间存储法线的优点:

  • 自由度更高,不依赖于模型网格的形状,可移植到其他模型;
  • 可进行UV动画,因为它基于模型的纹理,而UV动画过程中模型的纹理和法线纹理是同步变换的;
  • 可重用,比如:一个砖块,6个面可以共用同一张法线纹理;
  • 可压缩,因为切线空间下的法线纹理中法线的z方向总是正方向,因此可以存储XY,而Z方向可以通过XY方向叉乘得到。
由以上的优点分析,可见切线空间的灵活性和可重用性让它很多情况下都优于模型空间下的法线纹理,因此大多数情况下会使用切线空间下的纹理坐标,原书也是使用基于切线空间下的法线纹理。
2.3 实践

切线空间下的法线纹理处理光照有两种策略:

  • 在切线空间下计算光照,将光照方向、视角方向变换到切线空间下;
  • 在世界空间下计算光照,将切线空间下的法线变换到世界空间下,再进行光照计算;
从效率上讲,前一种更优,因为前者可在顶点着色器上完成对光照方向和视角方向的变换,而后者因为光照计算在采样获得法线方向之后,所以光照方向和视角方向的变换过程必须在片元着色器上实现;
从通用性上将,后一种更优,因为很多情况下需要借助世界空间中的法线方向进行如环境映射等计算;
2.3.1 在切线空间下计算

1)准备工作

完成如下准备工作:

  • 新建名为Scene_7_2_3的场景,并去掉天空盒子;
  • 新建名为NormalMapTangentSpaceMat的材质;
  • 新建名为Chapter7-NormalMapTangentSpace的Shader,并赋给上一步创建的材质;
  • 在场景中新建一个胶囊体,并将第二步创建的材质赋给它;
  • 保存场景;
2)编写Shader代码

打开Chapter7-NormalMapTangentSpace,编写如下代码,新的代码逻辑已用注释说明:
Shader "Unity Shaders Book/Chapter 7/Normal Map In Tangent Space"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" {}
        // 法线纹理,"bump"表示默认使用Unity内置的法线纹理
        _BumpMap ("Normal Map", 2D) = "bump" {}
        // 控制凹凸程度,当它为0时,表示该法线纹理不会对光照产生任何影响
        _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类型(不同于法线的float3)
                // 使用tangent.w分量来决定切线空间的第三个坐标轴——副切线的方向性
                float4 tangent : TANGENT;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                // UV使用float4类型,xy存储主纹理坐标,zw存储法线纹理坐标(出于减少差值寄存器的使用数量的目的)
                float4 uv : TEXCOORD0;
                // 变换到切线空间的光照方向
                float3 lightDir : TEXCOORD1;
                // 变换到切线空间的视角方向
                float3 viewDir : TEXCOORD2;
            };


            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                // 存储主纹理的uv值
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                // 存储法线纹理的uv值
                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                /*
                // 计算副切线,使用w分量确定副切线的方向性(与法线、切线垂直的有两个方向)
                float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
                // 构建一个从模型空间向切线空间转换的矩阵,按照切线(x轴)、副切线(y轴)、法线(z轴)排列即可得到
                float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
                */
                // 使用内置的宏(在UnityCG.cginc),即可得到变换的矩阵,省去了上面的计算过程,
                // 然后直接调用内置rotation变量就是这个矩阵
                TANGENT_SPACE_ROTATION;

                // 将光照方向由模型空间转到切线空间
                // 使用内置函数ObjSpaceLightDir,得到模型空间下的光照方向
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                // 将视角方向由模型空间转到切线空间
                // 使用内置函数ObjSpaceViewDir,得到模型空间下的视角方向
                o.viewDir = mul(rotation, 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;
                // 如果纹理没有被标记为“Normal map”,则手动反映射得到法线方向
                // tangentNormal.xy = (packedNormal.xy * 2 - 1);
                // 如果纹理被标记为“Normal map”,Unity就会根据不同的平台来选择不同的压缩方法,需要调用UnpackNormal来进行反映射,
                // 如果这时再手动计算反映射就会出错,因为_BumpMap的rgb分量不再是切线空间下的法线方向xyz值了
                tangentNormal = UnpackNormal(packedNormal);
                tangentNormal.xy *= _BumpScale;
                // 因为最终计算是得到归一化的法线,所以利用三维勾股定理得到z分量
                // 因为使用的是切线空间下的法线纹理,所以可以保证z分量为正
                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
        }
    }

    // 用内置的Specular Shader兜底
    Fallback "Specular"
}
保存Shader代码。
3)配置材质参数

找一张纹理图及其对应的法线纹理(可从参考Git仓库里的资源找:Brick_Diffuse.JPG和Brick_Normal.JPG),分别配置到材质面板Main Tex和Normal Map上:




并配置不同的Bump Scale属性值,可以得到不同的纹理凹凸效果:





2.3.2 在世界空间下计算

1)准备工作

完成如下准备工作:

  • 新建名为NormalMapWorldSpaceMat的材质;
  • 新建名为Chapter7-NormalMapWorldSpace的Shader,并赋给上一步创建的材质;
  • 在原来的场景中新建一个胶囊体,并将第一步创建的材质赋给它;
  • 保存场景;
2)编写Shader代码

打开Chapter7-NormalMapWorldSpace,复制粘贴上一节Chapter7-NormalMapTangentSpace的代码,并修改成世界空间下的计算逻辑,修改部分已用注释说明:
// 修改Shader命名
Shader "Unity Shaders Book/Chapter 7/Normal Map In World 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;
                // 插值寄存器最多只能存储float4大小的变量
                // 因此将切线空间到世界空间的变换矩阵的每一行拆分到对应的float4变量中
                // 矩阵是3X3大小,float4的w分量用于分别存储世界空间下的顶点位置的x、y、z分量
                float4 TtoW0 : TEXCOORD1;
                float4 TtoW1 : TEXCOORD2;
                float4 TtoW2 : TEXCOORD3;
            };


            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(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;

                // 将顶点坐标由模型空间转到世界空间
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                // 将法线方向由模型空间转到世界空间
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                // 将切线方向由模型空间转到世界空间
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                // 由切线和法线的叉积得到副切线
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

                /* 计算切线空间到世界空间的矩阵,并存储到TtoWX变量中 */
                // 将切线、副切线、法线按列拜访,
                // 得到从切线空间到世界空间的变换矩阵
                // w分量用来存储顶点在世界空间下的坐标
                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // 解析w分量,得到世界空间下的当前坐标
                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
                // 使用内置函数计算世界空间下的光照方向
                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                // 使用内置函数计算世界空间下的视角方向
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                // 采样并反映射得到法线信息
                fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
                bump.xy *= _BumpScale;
                // 根据三维勾股定理得到z方向的值
                bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
                // 将法线由切线空间转到世界空间
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));

                fixed3 halfDir = normalize(lightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
        }
    }

    // 用内置的Specular Shader兜底
    Fallback "Specular"
}
保存Shader代码。
3)配置材质参数

配置与上一节相同的纹理,可得到相同的效果:



2.4 Unity中的法线纹理类型

1)Texture Type

前面代码中提到:
当法线纹理被标记Normal map,可使用Unity的内置函数UnpackNormal来得到正确的法线方向。
在法线纹理添加到Unity后,在Inspector窗口的Texture Type下拉框选择Normal map即可:



当法线纹理被标记Normal map,Unity会根据不同平台对纹理进行压缩(例如使用DXT5nm格式),再通过UnpackNormal函数来针对不同的压缩格式对法线纹理进行正确的采样。
UnityCG.cginc里UnpackNormal函数的内部实现:
inline fixed3 UnpackNormalDXT5nm(fixed4 packednormal)
{
    fixed3 normal;
    normal.xy = packnormal.wy * 2 - 1;
    normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
    return normal;
}

inline fixed3 UnpackNormal(fixed4 packednormal)
{
    #if defined(UNITY_NO_DXT5nm)
        return packednormal.xyz * 2 - 1;
    #else
            return UnpackNormalDXT5nm(packednormal);
    #endif
}
在DXT5nm格式的法线纹理中,纹理的a通道(即w分量)对应了法线的x分量,g通道对应了法线的y分量,而纹理的r和b通道则会被舍弃,法线的z分量可以由xy分量推导而得。
2)Create from Grayscale

勾选Create from Grayscale,用于从高度图中生成法线纹理。



高度图本身记录的是相对高度,是一张灰度图,白色表示相对更高,黑色表示相对更低。将高度图导入Unity后,除了将其纹理类型设置为Normal map外,还需勾选Create from Grayscale,这样就可以把它和切线空间下的法线纹理同等对待了。
3)Bumpiness和Filtering

勾选Create from Grayscale后,会多出Bumpiness和Filtering两个选项。



Bumpiness用于控制凹凸程度;
Filtering决定了使用什么方式计算凹凸程度,选择Smooth,法线纹理会比较平滑,选择Sharp,会使用Sobel滤波(一种边缘检测时使用的滤波器)来生成法线。


写在最后

本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-22 02:05 , Processed in 0.067532 second(s), 24 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表