【UnityShader入门精要学习笔记】第七章 基础纹理

在这里插入图片描述
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:

  • 书本中句子照抄 + 个人批注
  • 项目源码
  • 一堆新手会犯的错误
  • 潜在的太监断更,有始无终

我的GitHub仓库

总之适用于同样开始学习Shader的同学们进行有取舍的参考。



基础纹理

纹理的最初目的就是使用一张图片来控制模型的外观,使用纹理映射(texture mapping)技术 可以把图形纹理映射到模型的表面。用纹理上的像素来逐纹素(texel) 控制模型的颜色。

在美术人员建模的时候,通常会在建模软件中利用纹理展开技术把纹理映射坐标(texture-mapping coordinates) 存储在每个顶点上,而纹理映射坐标定义了该顶点在纹理中对应的2D坐标(因为纹理图是2D图形),通常这些坐标使用一个二位变量(u,v)来表示,其中u是横向坐标,v是纵向坐标。因此,纹理映射坐标也被称为UV坐标。

纹理一般是正方形的,其边长可能不定。但是UV坐标的范围一定都会被归一化(标准化)为 [ 0 , 1 ] [0,1] [0,1]的范围内。然而纹理采样时使用的纹理坐标不一定是在 [ 0 , 1 ] [0,1] [0,1]的范围内。例如在平铺模式中,纹理坐标超出了 [ 0 , 1 ] [0,1] [0,1]之后,可以通过取模运算来用UV坐标填充:
在这里插入图片描述

在之前的学习中,我们也不止一次说过OpenGL和DirectX的坐标系差异问题。不过在Unity中,不同平台的纹理坐标系已经处理好了,默认原点位于左下角:

在这里插入图片描述
接下来我们将学习如何对纹理进行采样实现视觉效果,并介绍纹理的属性等基本概念。


单张纹理

我们通常会用一张纹理来代替物体的漫反射颜色,让我们看看如何用纹理来作为模型的颜色:

同样创建材质,模型,Shader,去除天空盒。我们来编写这个Shader:

Shader "Unlit/SingleTexture"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex("MainTex",2D) = "white" {}
        _Specular ("Specular",Color) = (1,1,1,1)
        _Gloss("Gloss", Range(8.0,255)) = 20
    }
    
    SubShader
    {
        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            // 额外声明的变量_MainTex_ST,用于存储纹理的属性
            // 这些属性包括纹理的平铺属性和缩放属性,(2个二维向量)
            float4 _MainTex_ST;
            fixed4 _Specular;
            float _Gloss;

                        
            ENDCG
        }
    }
}

在上述代码中,我们除了声明了必要的属性之外,还声明了一个额外的变量_MainTex_ST,用于存储纹理的属性。这个float4存储了纹理的平铺属性和缩放属性(2个二维向量)。
在这里插入图片描述

            struct a2v
            {
                float4 vertex : POSITION;
                float3 nromal :NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

上述代码中,我们使用TEXCOORD0语义定义了结构体a2vv2f。需要注意的是,在顶点着色器中,TEXCOORD语义赋值的是纹理坐标。而在片元着色器中,TEXCOORD语义则代表了我们自定义的一个变量。

因此我们在v2f中自定义了worldNormal,worldPos,uv变量,以便接收顶点着色器传递的数值结果。

最后附上完整的代码:

Shader "Unlit/SingleTexture"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex("MainTex",2D) = "white" {}
        _Specular ("Specular",Color) = (1,1,1,1)
        _Gloss("Gloss", Range(8.0,255)) = 20
    }
    
    SubShader
    {
        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            // 额外声明的变量_MainTex_ST,用于存储纹理的属性
            // 这些属性包括纹理的平铺属性和缩放属性,(2个二维向量)
            float4 _MainTex_ST;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 nromal :NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.nromal);
                o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;

                // UV信息= UV坐标 * 平铺 + 偏移
                // o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target{
                fixed3 worldNormalDir = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                // 使用材质颜色作为漫反射颜色
                fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormalDir,worldLightDir));

                // 使用blinn-Phong模型
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormalDir,halfDir)),_Gloss);
                fixed3 color = specular + diffuse + ambient;
                return  fixed4(color,1.0);
            }
            
            ENDCG
        }
    }
    Fallback "Specular"
}

在这里插入图片描述

结果效果很奇葩,不过和我们上述代码是相符的。最后的颜色值是(漫反射)纹理颜色 + 高光反射 + 环境光。

之所以需要用纹理颜色代替漫反射,其实也好理解,物体的表面纹理就是漫反射的结果,是直射光被反射的结果。纹理贴图已经决定了物体的表面颜色,因此就是替代了漫反射的结果。


纹理的属性

纹理映射的实现看起来很简单,但是实际实现远比我们想象的复杂。
在这里插入图片描述
一张纹理图的导入会有不同的属性配置:第一个属性是纹理类型,包括了默认纹理,法线贴图,精灵图,光照贴图等等。我们需要为导入的纹理选择正确的类型,这样才能方便我们在引擎中的使用——Unity可以为Shader传递正确的纹理,并在一些状况下对纹理进行优化。

在这里插入图片描述

在下方我们还能选择纹理的透明通道类型,一般我们直接使用图像的透明通道,有时也会使用灰度值表示透明度。

在这里插入图片描述
纹理的Wrap Mode决定了纹理的平铺模式,这个属性决定了纹理坐标在超过[0,1]的取值范围后将会被如何平铺。WrapMode需要注意的一般就是Repeat和Clamp模式,Repeat模式下若纹理坐标超过了1,则会被取模运算,最终纹理贴图会不断重复。而Clamp模式下,若纹理坐标大于1则被截取到1,小于0则截取到0。

在这里插入图片描述
在这里插入图片描述

(上图展示了在不同模式下,Tilling属性为(3,3)时的效果)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

另一个重要属性是Filter Mode属性。它决定了纹理由于变换而产生拉伸时将会使用哪种滤波模式。FilterMode支持三种模式:Point ,Lilinear以及Triliner。三种模式的滤波效果依次提升,需要消耗的性能也依次增大。纹理滤波会影响放大或缩小纹理时得到的图片质量。

这三种模式的区别在于:

  • Point使用了最近邻滤波(nearest neighbor) 。在放大或缩小时,由于采样像素数只有一个,因此图像通常看起来是像素风的。
  • Bilinear滤波使用了线性滤波 ,对每个像素会寻找相邻的4个像素进行线性插值混合得到最终像素值,因此图像看起来被模糊了。
  • Trilinear滤波几乎与Bilinear滤波一样,只是Trilinear滤波还会与多级渐远纹理进行混合 , 如果没有使用多级渐远纹理,那么Trilinear滤波等同于Bilinear滤波。

一张图片放大,那么一个像素会被放大为多个像素,反之缩小则多个像素被压缩成一个像素。纹理缩小是更复杂的,因为我们往往需要处理抗锯齿的问题。多级渐远纹理(mip mapping) 是一个解决方法。原理其实很简单,就是提前用滤波处理原纹理,就能得到缩小时的图像了。处理多次就获得了多级渐远纹理。这样在图像缩小的时候,就不用实时计算,可以直接得到结果像素。又是一个典型的空间换时间策略。
在这里插入图片描述
我们在贴图属性面板下选择Advanced类型,然后勾选Generate Mip Maps,就能生成多级渐远纹理了。

通过右图可以看出,使用Trilinea + 多级渐远纹理渲染效果是最好的。

最后要考虑纹理的最大尺寸和纹理模式。如果导入的纹理大小超过了MaxTextureSize中的设置值,那么Unity将会把该纹理缩放为这个最大分辨率。理想状况下,导入的纹理可以是非正方形的,但长宽应当是2的整幂次,否则会造成多余的计算(非2的幂次大小纹理被称为NPOT

在这里插入图片描述
最后是导入压缩面板,这个面板决定了纹理的尺寸和存储格式,选择的精度越大,占用空间越多,得到的纹理精度越高,效果越好。

在这里插入图片描述
通过Block我们能看到纹理的内存占用,对于一些无需高精度存储的纹理,尽量使用正确的压缩格式。


凹凸映射

纹理的另一种常见的应用就是凹凸映射(bump mapping) 。凹凸纹理的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节,这种方法不会真的改变模型的顶点位置,只是通过渲染让模型看起来好像凹凸不平。

实现凹凸映射的方法主要有两种,一种是使用一张高度纹理(height map) ** 来模拟表面位移(displacement)** ,然后得到一个修改后的法线值,这种方法也被称为高度映射(height mapping) 。另一种方法则是使用一张法线纹理(normal map) 来直接存储表面法线,这种方法被称为法线映射 (normal mapping) 。(尽管我们常常将凹凸映射和法线映射当作相同的技术,但是还是有些许不同的)

高度纹理

在这里插入图片描述

使用高度图来实现凹凸映射,在高度图中存储的是强度值(intensity),代表了模型表面局部的海拔高度。因此,颜色越浅,代表了该模型的表面月向外凸起,而颜色越深则代表了该位置越向里凹。

这种方法的好处是纹理非常直观,看得出模型表面的高低。缺点是计算更加复杂,在实时计算时不能直接得到表面法线,而是需要由像素的灰度值计算得到,因此需要消耗更多的性能。

高度图通常会和法线映射一起使用,用于给出表面凹凸的额外信息,也就是说我们常常使用法线映射来修改光照。


法线纹理

法线纹理中存储的就是表面的法线方向,而由于法线方向的分量范围在[-1,1],而像素分量为[0,1],因此需要我们做一个映射,即: p i x e l = n o r m a l + 1 2 pixel = \frac{normal +1}{2} pixel=2normal+1(其实就是半兰伯特时讲到的)

因此我们在Shader中对法线纹理进行纹理采样后,还需要对结果进行依次反映射的过程,以得到原先的法线方向。反应设的过程实际就是使用上面映射函数的逆函数:
n o r m a l = p i x e l ∗ 2 + 1 normal = pixel * 2 +1 normal=pixel2+1

在这里插入图片描述

由于法线贴图是在模型上创建的,因此法线方向当然是相对于模型空间定义的。这种纹理被称为模型空间的法线纹理 。然而在实际中我们往往会采用另一种坐标空间,即模型顶点的切线空间(tangent space) 来存储法线。切线空间是相对于每个顶点而言的,将顶点作为切线空间的原点,z轴方向为法线(n)方向,x轴方向为切线(t)方向,y轴方向可由法线和切线叉乘得到,被称为副切线(bitangent,b)

在平时开发中我们使用的法线纹理通常指的是切线空间下的法线纹理 , 也就是蓝蓝的那张图片。为什么法线纹理看起来是蓝色的?因为相对于每个顶点的切线空间而言,若法线贴图的法线位置和模型本身顶点法线位置相同,则对应的法线坐标值则为(0,0,1),根据颜色值映射关系0.5x + 0.5,则得到的颜色信息为(0.5,0.5,1),也就是浅蓝色。法线纹理的本质其实就是使用rgb颜色值来存储切线空间下的法线坐标。

由于大部分法线在z轴上的方向不变(和模型本身法线方向一样),因此法线贴图看起来总是蓝蓝的。

我们之所以选择切线空间,是因为切线空间来存储法线有一些优点:

  • 自由度更高。模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型。而切线空间使用的是相对法线信息,因此即使应用到不同的网格上也可以获得合理的结果
  • 可以进行UV动画,例如对UV进行凹凸移动,法线信息可以应用到相应顶点上
  • 可以重用法线纹理,例如一个砖块我们只使用一张法线纹理就可以应用到六个面上。
  • 可压缩,由于切线空间的法线纹理中法线方向总是正方向,因此我们可以通过存储XY方向来推导出z方向。

实践

根据法线纹理的性质,由于法线纹理存储的是切线空间下的法线方向,因此我们有两种选择:

  • 一种是用切线空间进行光照计算,因此需要把worldLightDir和viewDir变换到切线空间下
  • 另一种是选择在世界空间下进行光照计算,因此我们需要把采样得到的法线方向变换到世界空间下,再和worldLightDir以及viewDir进行计算得到最终光照,为此我们需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中

从效率上来说第一种方法要优于第二种方法,因为我们可以在顶点着色器中完成对光照方向和视角方向的变换。

第二种方法则需要先对法线纹理进行采样,所以变换过程必须在片元着色器中实现。这意味着我们需要在片元着色器中进行矩阵操作。

从通用性的角度来说,第二种方法更好,有时我们需要在世界空间下完成一些计算,例如使用Cubemap进行环境映射时,我们就可以同时进行法线映射计算。

在切线空间下计算

为了在切线空间下计算,我们需要知道从模型空间到切线空间的变换矩阵。从切线空间到模型空间的变换矩阵是很容易求的,因为从切线空间到模型空间的变换只包括平移和旋转,因此这个变换的逆矩阵就等于它的转置矩阵。因此从模型空间到切线空间的变换矩阵就是从切线空间到模型空间的变换矩阵的转置矩阵。

切线空间到模型空间的变换矩阵,其实就是切线空间的xyz按列向量排序即可。那么模型空间到切线空间的矩阵就是它的转置,也就是xyz按行向量排序。

让我们看看Shader的结构:

    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex("MainTex",2D) = "white"{}
        _BumpMap("NormalMap",2D) = "bump"{}
        _BumpScale("Bump Scale",Float) = 1.0
        _Specular ("Specular", Color) = (1,1,1,1)
        _Gloss("Gloss", Range(8.0,256)) = 20
    }

Properties定义了两个2D类型用于采样纹理图和法线贴图,BumpScale用于调整法线凹凸度
在Pass块定义的属性中,有一个特殊的点需要注意:我们会额外定义两个变量(_MainTex_ST,_BumpMap_ST)用于存储贴图的纹理属性(是自动赋值的)

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;

结构体a2v中,由于我们需要从模型空间获取切线空间,因此我们还需要定义切线和模型UV属性:

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 texcoord : TEXCOORD0;
            };

在v2f中我们定义float4类型的变量uv用于传递纹理贴图和法线贴图属性

struct v2f
{
	float4 pos : SV_POSITION;
	float4 uv : TEXCOORD0;
	float3 lightDir : TEXCOORD1;
	float3 viewDir : TEXCOORD2;
};

那么想要实现计算,那么我们就需要获取在切线空间下的各个光照的方向,这一步我们就在a2v完成(因为是获取模型信息的步骤)。我们需要得到模型空间转切线空间的矩阵(也就是切线空间到模型空间的转置矩阵,而切线空间到模型空间的转换矩阵为xyz列向量依次排列)。

获取了切线空间上的光照信息后,我们将其传递到v2f中,通过纹理采样获取纹理贴图的颜色值和法线贴图的颜色值,对于法线贴图,我们还需要将颜色值逆映射回切线空间的坐标。

最后,我们用得到的切线lightDir,切线viewDir和切线normalDir带入到光照模型中计算光照。

最后我们贴出完整的代码:

Shader "Custom/NormalMapTangentSpaceCopy"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex("MainTex",2D) = "white"{}
        _BumpMap("NormalMap",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"
            #include "UnityCG.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;
            };

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

                // 计算贴图纹理和法线贴图的UV坐标
				o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                // 使用宏直接获取rotation矩阵,也就是模型空间到切线空间的变换矩阵
				TANGENT_SPACE_ROTATION;
                // 获取在模型空间下的方向,再转为切线空间
				o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
				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);

                // tex2D方法获取指定贴图对应像素坐标上的值,一般是颜色值
                fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw);
                fixed3 tangentNormal;

                // 是对法线采样的反映射函数,也就是tangentNormal = 2 * packedNormal.xyz - 1
                // 将法线从切线贴图上的RGB转为切线空间的法线坐标
                tangentNormal = UnpackNormal(packedNormal);
                tangentNormal.xy *= _BumpScale;
                // 归一化方向向量模长为1,即为(tangentNormal.x)^2 + (tangentNormal.y)^2 + (tangentNormal.z)^2 = 1
                // dot(tangentNormal.xy,tangentNormal.xy) = (x,y) · (x,y) = x^2 + y^2
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy)));

                // albedo值 = 纹理贴图像素颜色值 * 纹理面板颜色值
                fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                // 用切线空间下的法线和光照方向,视角方向来计算
                fixed3 diffuse = albedo * _LightColor0.rgb * saturate(dot(tangentNormal,tangentLightDir));
                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
                fixed3 specular = _Specular.rgb * _LightColor0.rgb * pow(saturate(dot(tangentNormal,halfDir)),_Gloss);

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

在这里插入图片描述

可以看到不同BumpScale下的法线凹凸度表现不同。

一个疑问?为什么在代码中tangentNormal.xy *= _BumpScale;明明没有对z轴进行乘法,却改变了法线的凹凸度。切线空间下法线方向不是由z轴确定的吗?

想了许久我才想明白答案,因为法线在z轴上的分量就是通过xy进行计算的。_BumpScale是对法线的非统一缩放。想想一下,法线,切线和副切线共同构成一个三角形,这个三角形满足 x 2 + y 2 + z 2 = 1 x^2 + y^2 + z^2 =1 x2+y2+z2=1的关系,_BumpScale实际上就是我们对这个三角形进行拉伸,随着x边和y边的改变,z边当然也会相应改变,因此在各方向上的分量会发生变化,但始终不变的是z方向一定是朝向正方向的,若法线朝向负方向则会被我们取为0,则表面光照就渲染成黑色的了,如下图所示:
在这里插入图片描述


在世界空间下计算

在世界空间下计算光照,那么我们就应当把法线信息变换到世界空间下。在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给片元着色器。最后在片元着色器中完成光照计算。

难点在于如何获取从切线空间转换到世界空间的变换矩阵,其实也就是先从切线空间到模型空间,再从模型空间到世界空间。而我们知道切线空间到模型空间的变换矩阵其实就是模型空间的法线的xyz列向量组成的,因此只需要将xyz都转换为世界空间下的坐标都得到了切线空间到世界空间的变换矩阵。

Shader "Custom/NormalMapWorldSpaceCopy"
{
    Properties
    {
        _Color("Color Tint",Color)= (1,1,1,1)
        _MainTex("MainTex",2D) = "white" {}
        _BumpMap("NormalMap",2D) = "bump" {}
        _BumpScale("BumpScale",Float) = 1
        _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"
            #include "UnityCG.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 T2W0 : TEXCOORD1;
                float4 T2W1 : TEXCOORD2;
                float4 T2W2 : 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;
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                //float3 worldTagent = mul(unity_ObjectToWorld,v.tangent).xyz;
                float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                //副切线同时垂直于切线和法线,为2个向量的叉乘结果
                //v.tangent.w用法线方向来决定副切线的方向
                float3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;

                //将用到的世界光照向量用矩阵传递给v2f,该矩阵即为切线空间到世界空间的变换矩阵
                o.T2W0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
                o.T2W1 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
                o.T2W2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
                
                return o;
            }

            fixed4 frag(v2f i) :SV_Target{
                float3 worldPos = float3(i.T2W0.w,i.T2W1.w,i.T2W2.w);

                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                // 是对法线采样的反映射函数,也就是tangentNormal = 2 * packedNormal.xyz - 1
                // 将法线从切线贴图上的RGB转为切线空间的法线坐标
                fixed3 Normal = UnpackNormal(tex2D(_BumpMap,i.uv.zw));
                Normal.xy *= _BumpScale;
                Normal.z = sqrt(1.0 - saturate(dot(Normal.xy,Normal.xy)));
                // 矩阵相乘计算出世界坐标下的法线,再标准化
                Normal = normalize(half3(dot(i.T2W0.xyz,Normal),dot(i.T2W1.xyz,Normal),dot(i.T2W2.xyz,Normal)));

                // albedo值 = 纹理贴图像素颜色值 * 纹理面板颜色值
                fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                // 用切线空间下的法线和光照方向,视角方向来计算
                fixed3 diffuse = albedo * _LightColor0.rgb * saturate(dot(Normal,lightDir));
                fixed3 halfDir = normalize(lightDir + viewDir);
                fixed3 specular = _Specular.rgb * _LightColor0.rgb * pow(saturate(dot(Normal,halfDir)),_Gloss);

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

对比视图:

在这里插入图片描述
从上图对比来看,世界坐标和切线坐标下的法线表现并无区别。在Unity5.x中,所有内置的UnityShader都使用了世界空间来进行光照计算。

在这里插入图片描述

在对法线贴图进行设置时,不要忘记将材质类型设置为法线贴图。

在这里插入图片描述
此外若我们使用的是灰度图(高度图)来作为法线贴图,那么就需要勾选Create form Crayscale选项。Bumpiness代表了凹凸程度,而Filtering决定计算凹凸度的滤波方式。


渐变纹理

在这里插入图片描述

使用渐变纹理,我们可以进行一种卡通风格的渲染,因为卡通风格的色调是突变的,我们可以用于模拟卡通中的阴影色块。

Shader "Custom/RampTexture_Shader_Copy"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _RampTex("Ramp Tex",2D) = "white" {}
        _Specular("Specular",Color) = (1,1,1,1)
        _Gloss("Gloss",Range(8.0,255)) = 20
    }
    
    SubShader
    {
        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            fixed4 _Color;
            sampler2D _RampTex;
            float4 _RampTex_ST;
            fixed4 _Specular;
            float _Gloss;
            
            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                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;
                //o.uv = TRANSFORM_TEX(v.texcoord,_RampTex);
                o.uv = v.texcoord.xy * _RampTex_ST.xy + _RampTex_ST.zw;
                return o;
            }

            fixed4 frag(v2f i):SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                //fixed3 worldPos = normalize(i.worldPos);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed haleLambert = 0.5 * dot(worldNormal,worldLightDir) + 0.5;
                // 对漫反射光照应用渐变纹理
                // 使用半兰伯特对漫反射光照根据渐变纹理贴图进行采样颜色值
                // 值域为[0,1],对应纹理贴图最左侧为0,最右侧为1
                // 其实渐变纹理的颜色值只在横轴上变化,因此本质是一维的
                // 只是纹理本身是二维图像,因此我们用一个二维向量进行采样
                fixed3 diffuseColor = tex2D(_RampTex,fixed2(haleLambert,haleLambert)).rgb * _Color.rgb;

                fixed3 diffuse = _LightColor0.rgb * diffuseColor;
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal,halfDir)),_Gloss);
                return  fixed4(diffuse + specular + ambient , 1.0);
            }
            
            ENDCG
        }
    }
    Fallback "Specular"
}

实际上渐变纹理就是将原来的光照值映射为纹理颜色值与环境光照值的乘积,从而实现颜色值的过渡,这种风格往往应用在卡通风格的渲染中。

在上述代码中,我们使用了半兰伯特模型,并根据计算出的坐标值对纹理贴图进行颜色值的采样。而渐变纹理_RampTex虽然是二维图像,但本质上还是一个一维纹理,因为在纵轴方向上颜色值是不变的。在这里插入图片描述

此外,渐变纹理的WrapMode需要改为Clamp模式,防止对纹理进行采样时由于浮点数精度而造成的问题。如上图所示,Repeat模式下存在着瑕疵部分,这是由于半兰伯特模型计算出了1.0001这样的值,在保存为fixed数值时自动舍弃了整数部分,只保留小数部分,因此接近于0。导致渲染异常


遮罩纹理

遮罩纹理(Mask Texture)是什么?简单来说,遮罩是用于控制纹理UV渲染的一种贴图。它可以用于保护某些区域免遭修改。

使用遮罩纹理的流程一般是:通过采样得到遮罩纹理的纹素值,然后使用其中的一个或者几个通道的值来与表面属性进行相乘,这样只需要对应遮罩通道值为0时,即可避免表面属性的影响。

在这里插入图片描述
如上图所示,纯漫反射模型缺少高光,高光反射+漫反射模型的高光表现不符合石砖的物理性质,而漫反射+高光反射+遮罩的石砖既能保留高光,又保证高光效果符合实际

让我们实现一个遮罩纹理渲染:

(实际上过程很简单,就是我们在使用切线空间计算高光反射的基础之上乘以遮罩贴图uv颜色值即可)

Shader "Custom/MaskTexture_Shader_Copy"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex("MainTex",2D) = "white" {}
        _Bump("Normal Map",2D) = "bump" {}
        _BumpScale("BumpScale",Float) = 1.0
        _SpecularMask("Specular Mask",2D) = "white" {}
        _SpecularScale("Specular 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"
            #include "UnityCG.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _Bump;
            float _BumpScale;
            sampler2D _SpecularMask;
            float _SpecularScale;
            fixed4 _Specular;
            float _Gloss;

            // 在切线空间计算法线
            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 texcoord :TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 lightDir : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy  + _MainTex_ST.zw;

                TANGENT_SPACE_ROTATION;
                o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
                o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;

                return o;
            }

            float4 frag(v2f i):SV_Target
            {
                fixed3 tangentLightDir = normalize(i.lightDir);
                fixed3 tangentViewDir = normalize(i.viewDir);

                fixed4 packedNormal = tex2D(_Bump,i.uv.xy);
                fixed3 tangentNormal;

                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 = albedo * _LightColor0.rgb * saturate(dot(tangentNormal,tangentLightDir));
                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);

                // 获取遮罩并乘以倍率
                fixed specularMask = tex2D(_SpecularMask,i.uv).r * _SpecularScale;
                // 将最后的高光乘以遮罩颜色
                fixed3 specular = _Specular.rgb * _LightColor0.rgb * pow(saturate(dot(tangentNormal,halfDir)),_Gloss) * specularMask;

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

在这里插入图片描述

左侧是未使用遮罩贴图的模型,右侧使用了遮罩贴图,可以看出遮罩贴图对高光部分进行了遮罩计算,使得高光的范围不那么大了,更符合石砖的物理性质。并且我们也可以通过对遮罩乘以scale来控制用于相乘的值的大小。

另外,在此案例中,这张遮罩纹理的rgb分量存储的都是同一个值,但是在实际开发中,一张纹理的rgbA的四个通道分量常常存储不同的值用于设置不同的遮罩。

其他纹理遮罩

在现代游戏开发中,RGBA分量通常存储不同属性,其中R通道存储高光反射的强度,G通道存储边缘光照的强度,B通道存储高光反射的指数,A通道存储自发光强度。

因此遮罩纹理不单单保存遮罩值,为了不浪费通道,还会保存一些其他的值。


总结

本章中我们学习了不同纹理贴图的作用以及如何用于计算。使用材质贴图进行表面光照颜色值相乘,使用法线贴图实现凹凸映射,使用渐变纹理实现光照处与阴影处的颜色值过渡,使用遮罩纹理实现遮罩计算。

本章涉及Shader较多,但光照计算万变不离其宗。只需记住两种模板:切线空间法线计算以及世界空间法线计算即可。

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值