一、纹理

1.纹理的概念

(1)纹理是用来干嘛的

  • 骨:模型骨骼
  • 肉:三角面片(由模型顶点围成模型的轮廓)
  • 皮:纹理图片
  • 纹理的主要作用:使用一张图片控制模型的外观
    • 使用纹理映射技术,将图片和模型联系起来,让模型能呈现出图片中的颜色表现
    • 纹理映射
      • 它用来将图像(纹理)映射到三维模型的表面,从而赋予模型更加真实和细致的外观。
      • 这个过程实际上是将二维图像映射到三维空间的过程

(2)如何进行纹理映射?

  • 利用纹理展开技术把纹理映射坐标存储在每个顶点上。模型表面的顶点都与纹理坐标相关联

  • 纹理坐标通常使用二维坐标系统(称为UV坐标),其中U表示水平轴,V表示垂直轴

    image-20250722142759242

(3)UV坐标对于Shader开发的意义

  • 我们在进行Shader开发时,在顶点着色器回调函数传入的数据中可以获取到模型中的UV坐标的数据。
  • 有了UV坐标,只要有正确的纹理贴图(图片),就可以用顶点的UV坐标从图片中取出映射的颜色用于渲染。

(4)UV坐标的注意事项

  • 纹理坐标(UV坐标)的横轴和纵轴的取值范围是被归一化过的,在[0,1]的范围中
    主要目的是为了适应不同大小的纹理图片(256x256、512x512、1024x1024等等)

  • 只有顶点中记录了UV坐标,因此在数据传递进片元着色器之前,UV坐标会在中间阶段进行插值运算。

    也就是每个片元中得到的UV坐标大部分都是插值计算得结果,因此我们可以得到图片中非顶点位置的颜色

2. 单张纹理

(1)书写单张纹理颜色采样Shader

  1. 第一步 完成Shader文件基本结构,让其不报错
  2. 第二步 纹理属性和CG成员变量声明
    • 关键知识点:
      • CG中映射ShaderLab中的纹理属性,需要有两个成员变量
      • 一个用于映射纹理颜色数据,一个用于映射纹理缩放平移数据
    • ShaderLab中的属性:图片属性(2D)。用于利用UV坐标提取其中颜色
    • CG中用于映射属性的成员变量
      • 1.sampler2D 用于映射纹理图片
      • 2.float4 用于映射纹理图片的缩放和平移。(固定命名方式 纹理名_ST (S代表scale缩放 T代表translation平移))
  3. 第三步 用缩放平移参数参与uv值计算
    • 如何获取模型中携带的uv信息
      • 在顶点着色器中,我们可以利用TEXCOORD语义获取到模型中的纹理坐标信息
      • 它是一个float4类型的
      • xy获取到的是纹理坐标的水平和垂直坐标
      • zw获取到的是纹理携带的一些额外信息,例如深度值等
    • 如何计算??
      • 固定算法:先缩放,后平(偏)移。缩放用乘法,平(偏)用加法
      • 纹理坐标.xy * 纹理名_ ST.xy + 纹理名_ ST.wz
      • 或者直接用内置宏 TRANSFORM_TEX(纹理坐标变量, 纹理变量)
  4. 第四步 在片元着色器中进行纹理颜色采样

(2)重要纹理相关设置回顾

  1. Texture Type(纹理图片类型) 和 Texture Shape(纹理图片类型)

    决定了我们是否能在Shader当中获取正确数据

  2. Wrap Mode(循环模式)

    决定了缩放偏移的表现效果

    • Repeat:在区块中重复纹理
    • Clamp: 拉伸纹理的边缘
    • Mirror:在每个整数边界上镜像纹理以创建重复图案
    • Mirror Once:镜像纹理一次,然后将拉伸边缘纹理
    • Per-axis:单独控制如何在U轴和V轴上包裹纹理
  3. Filter Mode(过滤模式)

    决定了放大缩小纹理时看到的图片质量

    • Point:纹理在靠近时变为块状
    • Bilinear:纹理在靠近时变得模糊
    • Trilinear:与Bilinear类似,但纹理也在不同的Mip级别之间模糊
    • 过滤模式在开启MipMaps根据实际表现选择,可以达到不同的表现效果

(3)单张纹理结合BlinnPhong光照模型

  • 在计算时,有以下的3点注意点

    1. 纹理颜色需要和漫反射材料颜色 进行乘法叠加, 它们两共同影响最终的颜色

      1
      fixed3 col = tex2D(_MainTex, i.uv) * _LambertColor;
    2. 兰伯特光照模型计算时,漫反射材质颜色使用 1 中的叠加颜色计算

      1
      fixed3 lambertColor = LambertColor(i.wNormal) * col ;
    3. 最终使用的环境光叠加时,环境光变量UNITY_LIGHTMODEL_AMBIENT需要和 1 中颜色进行乘法叠加

    ​ 为了避免最终的渲染效果偏灰

    1
    fixed3 color = col * UNITY_LIGHTMODEL_AMBIENT + blinnPhongColor +lambertColor;
    1. 其他的计算步骤同BlinnPhong的逐片元光照实现

(4)实现代码

  • 效果:上为没有光照,下为有光照

    image-20250722212908560
  • 代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    Shader "Unlit/SimpleTexture" {
    Properties {
    //主纹理
    _MainTex ("Texture", 2D) = "white" { }
    //漫反射材质光照颜色
    _LambertColor ("LambertColor", Color) = (1, 1, 1, 1)
    //高光材质光照颜色
    _BlinnPhongColor ("BlinnPhongColor", Color) = (1, 1, 1, 1)
    _HighLightValue ("HighLightValue", Range(0, 20)) = 10
    }
    SubShader {
    Tags { "RenderType" = "Opaque" }
    LOD 100

    Pass {
    Tags { "LightMode" = "ForwardBase" }
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    // make fog work
    #pragma multi_compile_fog

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    struct appdata {
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL;
    };

    struct v2f {
    float2 uv : TEXCOORD0;
    //裁剪空间下顶点
    float4 vertex : SV_POSITION;
    //世界空间下法线
    float3 wNormal : NORMAL;
    //世界空间下顶点
    float3 wPos : TEXCOORD1;
    };
    //映射对应纹理属性的图片颜色数据
    sampler2D _MainTex;
    //映射对应纹理属性的缩放 平移数据
    float4 _MainTex_ST;

    fixed3 _LambertColor;
    fixed3 _BlinnPhongColor;
    float _HighLightValue;

    //漫反射颜色计算
    //参数:世界空间下法线
    fixed3 LambertColor(fixed3 wNormal) {
    float3 lightDir = normalize(_WorldSpaceLightPos0);
    return _LightColor0.rgb * max(0, dot(lightDir, wNormal));
    }

    //高光颜色计算
    //参数1:世界空间下顶点
    //参数2:世界空间下法线
    fixed3 BlinnPhongColor(fixed3 wPos, fixed3 wNormal) {
    //计算标准化视口方向
    float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - wPos.xyz);
    //计算标准化入射光方向
    float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
    //计算半角向量
    float3 halfDir = normalize(viewDir + lightDir);
    return _LightColor0 * _BlinnPhongColor * pow(max(0, dot(wNormal, halfDir)), _HighLightValue);
    }
    v2f vert(appdata v) {
    v2f o;

    //用平移缩放值参与uv计算
    //o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw
    //如果没有进行缩放和平移 那么 这个计算后 值是不会产生变化的
    //因为缩放默认值是1和1 ,平移默认值是0和0
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.wPos = UnityObjectToWorldDir(v.vertex);
    o.wNormal = UnityObjectToWorldNormal(v.normal);
    return o;
    }

    fixed4 frag(v2f i) : SV_Target {
    // sample the texture
    //这传入的uv 是经过插值运算后的 就是每一个片元都有自己的一个uv坐标
    //这样才能精准的在贴图当中取出颜色
    //`纹理颜色`需要和`漫反射材质颜色` 进行`乘法叠加`, 它们两共同影响最终的颜色
    fixed3 col = tex2D(_MainTex, i.uv) * _LambertColor;

    //新知识点:兰伯特光照模型计算时,漫反射材质颜色使用 1 中的叠加颜色计算
    fixed3 lambertColor = LambertColor(i.wNormal) * col ;
    fixed3 blinnPhongColor = BlinnPhongColor(i.wPos, i.wNormal);
    //环境光变量UNITY_LIGHTMODEL_AMBIENT需要和 1 中颜色进行乘法叠加,为了避免最终的渲染效果偏灰

    fixed3 color = col * UNITY_LIGHTMODEL_AMBIENT + blinnPhongColor +lambertColor;
    return fixed4(color.rgb, 1);
    }
    ENDCG
    }
    }
    }

3. 凹凸纹理

(1)凹凸纹理是用来做什么的?

  • 纹理除了可以用来进行颜色映射外,另外一种常见的应用就是进行凹凸映射

  • 凹凸映射的目的:是使用一张纹理来修改模型表面的法线,让我们不需要增加顶点,而让 模型看起来有凹凸效果。

  • 原理:

    • 光照的计算都会利用法线参与计算,决定最终的颜色表现效果。那么在计算“假” 凹凸面时``,使用“真”凹凸面的法线参与计算,呈现出来的效果可以以假乱真

    image-20250722220243014

  • 凹凸纹理最大的作用

    • 就是让模型可以在不添加顶点(不增加面)的情况下 让模型看起来同样充满细节(凹凸感),是一种视觉上的“欺骗”技术。
  • 要进行凹凸映射,目前有两种主流方式: 1.高度纹理贴图 2.法线纹理贴图

(2)高度纹理贴图(了解)

  • 高度纹理贴图一般简称高度图存储了模型表面上每个点的高度信息。通常它使用灰度图像,其中不同灰度值 表示不同高度

  • 较亮区域通常对应较高的点较暗的区域对应较低的点。 它主要用于模拟物体表面的位移

    image-20250722220531814
  • 存储规则:图片中的某一个像素点的RGB值是相同的,都表示高度值,A值一般 情况下为1。高度值范围一般为0~1,0代表最低,1 代表最高

  • 优点:可以通过高度图很明确的知道模型表面的凹凸情况

  • 缺点:无法在Shader中直接得到模型表面点的法线信息, 而是需要通过额外的计算得到,因此会增加性能消耗,所以 我们几乎很少使用它。

  • 我们在使用凹凸纹理时,一般都会使用法线纹理贴图

(3)法线纹理贴图

  • 法线纹理贴图一般简称法线贴图 或 法线纹理 。它存储了模型表面上每个点的法线方向。
  • 存储规则:图片中的RGB值分别存储法线的X、Y、Z分量值,A值可以用于存储其他信息, 比如材质光滑度等。
  • 优点:从法线贴图中取出的数据便是法线信息,可以直接 简单处理后就参与光照计算,性能表现更好
  • 缺点:我们无法直观的看出模型表面的凹凸情况
  • 法线纹理贴图读取分量数据的规则
    • 由于法线XYZ分量范围在[-1,1]之间, 而像素RGB分量范围在[0,1]之间 因此我们需要做一个映射计算
    • 存储图片时像素分量 = (法线分量 + 1) / 2 因此当我们取出像素分量使用时需要进行逆运算
    • 读取数据时法线分量 = 像素分量 * 2 -1
  • 两种法线纹理贴图的存储方式

    1. 基于模型空间的法线纹理

      image-20250722221108502

    2. 基于切线空间的法线纹理

      image-20250722221153759
  • 切线空间下的法线纹理贴图是蓝色的原因

    image-20250722221252728
  • 为什么要使用切线空间下的法线纹理贴图

    image-20250722221335160

(4)法线贴图的计算方式

  • 两种主流计算方式
    1. 切线空间下进行光照计算,需要把光照方向、视角方向变换到切线空间下参与计算
    2. 世界空间下进行光照计算,需要把法线方向变换到世界空间下参与计算
  • 各自的优缺点——效率

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

      ( 矩阵变换在顶点着色器中计算)

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

      ( 矩阵变换在片元着色器中计算)

  • 各自的优缺点——全局效果

    • 在切线空间中计算,对全局效果的表现可能会不够准确

      在处理一些列如镜面反射、环境映射效果时表现效果可能不够准确

    • 在世界空间中计算,对全局效果的表现更准确

      可以更容易的应用于全局效果的计算

  • 若没有全局效果要求,我们优先使用在切线空间下进行光照计算,因为它效率较高

  • 在切线空间下计算

    • 在切线空间下进行光照计算,需要把光照方向、视角方向变换到切线空间下参与计算.

    • 关键点

      • 计算模型空间到切线空间的变换矩阵

        image-20250722233012607
    • 其它用到的核心知识:

      • 得到模型空间光的方向:ObjSpaceLightDir(模型空间顶点坐标)

      • 得到模型空间视角方向:ObjSpaceViewDir(模型空间顶点坐标)

      • 得到光方向和视角方向相对于模型空间的数据表达后,再与模型空间到切线空间的变换矩阵进行运算

        即可将他们转换到切线空间下参与后续计算。

  • 在世界空间下计算

    • 在世界空间下进行光照计算,需要把法线方向变换到世界空间下参与计算

    • 关键点:

      • 计算切线空间到世界空间的变换矩阵

        image-20250722233757581

      image-20250722233856096
  • 关键知识点补充:

      1. 模型空间下的切线数据

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

        image-20250722234038014
      2. Unity当中的法线纹理类型

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

        image-20250722234143332

      3. 法线纹理属性命名一般为_BumpMap(凸块贴图)

        我们还会声明一个名为_BumpScale (凸块缩放) 的float属性 它主要用于控制凹凸程度

        当它为0时,表示没有法线效果,法线的影响会被消除 当它为1时,表示使用法线贴图中的原始法线信息,没有缩放 我们可以根据实际需求调整它的值,来达到视觉上令人满意的效果。

      4. 如果使用的凹凸纹理不是法线纹理,而是高度纹理,我们需要进行如下设置

        图片类型设置为Normal map(法线贴图)

        勾选 Create from Grayscale(从灰度创建)

        这样我们就可以把高度纹理当成切线空间下的法线纹理处理了

        多出的Bumpiness(颠簸值)控制凹凸程度

        Filtering(过滤模式)决定计算凹凸程度的算法

        Sharp:滤波生成法线

        Smooth:平滑的生成法线

        image-20250722234353285

(5)法线贴图编写流程

  • 效果展示:从左到右依次为(在切线空间计算,在世界空间计算,无法线)

    image-20250724151612330

  • 切线空间下进行光照计算

  • 需要把光照方向、视角方向变换到切线空间下参与计算

  • 关键点:

    1. 属性相关

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

      顶点着色器中传入:

      • 可以使用 UnityCG.cginc 中的 appdata_full。其中包含了我们需要的顶点、法线、切线、纹理坐标相关数据

      片元着色器中传入:

      • 自定义一个结构体:其中包含 裁剪空间下坐标、uv坐标、光的方向、视角的方向。
    3. 顶点着色器回调函数中

      • 顶点坐标模型转裁剪

      • 单张纹理和法线纹理 UV坐标缩放偏移计算

      • 副切线计算。用模型空间中的法线和切线进行叉乘 再乘以切线中的w(确定副切线方向)

      • 构建模型空间到切线空间的变换矩阵

        ——  切线  ——
        

        —— 副切线 ——

        —— 法线 ——

      • 将光照方向和视角方向转换到模型空间(利用ObjSpaceLightDir和ObjSpaceViewDir内置函数)

      • 将光照方向和视角方向转换到切线空间(利用变换矩阵进行乘法运算)

    4. 片元着色器回调函数中

      • 取出法线贴图中的法线信息(利用纹理采样函数tex2D)
      • 利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
      • 用得到的切线空间的法线数据 乘以 BumpScale 来控制凹凸程度
      • 得到单张纹理颜色和漫反射颜色的叠加颜色
      • 用切线空间下的 光方向、视角方向、法线方向 进行Blinn Phong光照模型计算。
  • 切线空间下计算代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    Shader "Unlit/NormalTexture_NW" {
    Properties {
    //主贴图
    _MainTex ("Texture", 2D) = "white" { }
    //法线贴图
    _BumpMap ("BumpMap", 2D) = "white" { }
    //凹凸系数
    _BumpScale ("BumpScale", Range(0, 1)) = 1
    //漫反射材质颜色
    _LambertColor ("LambertColor", color) = (1, 1, 1, 1)
    //高光材质颜色
    _BlinnPhongColor ("BlinnPhongColor", Color) = (1, 1, 1, 1)
    //光泽度
    _HighLightColor ("HighLightColor", Range(0, 50)) = 10
    }
    SubShader {
    Tags { "RenderType" = "Opaque" }
    LOD 100

    Pass {
    Tags { "LightMode" = "ForwardBase" }
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    // make fog work


    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    struct appdata {
    //模型顶点
    float4 vertex : POSITION;
    //模型法线
    float3 normal : NORMAL;
    //模型切线
    float4 tangent : TANGENT;
    //主uv
    float2 main_uv : TEXCOORD0;
    //法线uv
    float2 normal_uv : TEXCOORD1;
    };

    struct v2f {
    //可以分开两个uv声明,也可以用一个float4装载
    float2 main_uv : TEXCOORD0;
    float2 normal_uv : TEXCOORD1;
    float4 vertex : SV_POSITION;


    float3 lightDir : TEXCOORD2;
    float3 viewDir : TEXCOORD3;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;

    sampler2D _BumpMap;
    float4 _BumpMap_ST;

    fixed3 _LambertColor;
    fixed3 _BlinnPhongColor;
    float _HighLightColor;

    //凹凸系数
    float _BumpScale;


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


    //UV缩放偏移计算
    o.main_uv = TRANSFORM_TEX(v.main_uv, _MainTex);
    o.normal_uv = TRANSFORM_TEX(v.normal_uv, _BumpMap);
    //副切线计算 用法线和切线叉乘得到的副切线方向可能有两个,用切线数据中的w与之相乘确定副切线方向
    float3 biTangent = cross(normalize(v.normal), normalize(v.tangent)) * v.tangent.w;
    //计算模型空间到切线空间的变换矩阵
    float3x3 rotation = float3x3(v.tangent.xyz, biTangent, v.normal);

    //将光照和视角方向转换到模型空间(内置函数)
    o.lightDir = ObjSpaceLightDir(v.vertex);
    o.viewDir = ObjSpaceViewDir(v.vertex);
    //将光照和视角方向转换到切线空间(利用变换矩阵)
    o.lightDir = mul(rotation, o.lightDir);
    o.viewDir = mul(rotation, o.viewDir);

    return o;
    }


    fixed4 frag(v2f i) : SV_Target {
    //取出法线贴图内的法线信息
    float4 packedNormal = tex2D(_BumpMap, i.normal_uv);
    //将取出的法线进行逆运算并且可能进行解压,最终得到切线空间下的法线
    float3 tangentNormal = UnpackNormal(packedNormal);
    //乘凹凸系数
    tangentNormal.xy *= _BumpScale;
    tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
    //计算blinnphong光照
    //纹理颜色与漫反射材质颜色叠加
    fixed3 mainColor = tex2D(_MainTex, i.main_uv) * _LambertColor;
    //lambert
    fixed3 lambertColor = _LightColor0 * mainColor * max(0, dot(normalize(i.lightDir), tangentNormal));
    //半角向量
    fixed3 halfDir = normalize(normalize(i.lightDir) + normalize(i.viewDir));
    //Blinnphong高光
    fixed3 blinnPhong = _LightColor0 * _BlinnPhongColor * pow(max(0, dot(halfDir, tangentNormal)), _HighLightColor);

    fixed3 color = mainColor * UNITY_LIGHTMODEL_AMBIENT + lambertColor + blinnPhong;


    return fixed4(color.rgb, 1);
    }
    ENDCG
    }
    }
    }
  • 世界空间下进行光照计算:

  • 需要把法线方向变换到世界空间下参与计算

  • 主要思路:在顶点着色器``中计算切线空间到世界空间的变换矩阵,在片元着色器中进行法线采样转换。

  • 关键点:

    image-20250724144925488
  • 世界空间下计算代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    Shader "Unlit/NormalTexture_WW" {
    Properties {
    //主贴图
    _MainTex ("Texture", 2D) = "white" { }
    //法线贴图
    _BumpMap ("BumpMap", 2D) = "white" { }
    //凹凸系数
    _BumpScale ("BumpScale", Range(0, 1)) = 1
    //漫反射材质颜色
    _LambertColor ("LambertColor", color) = (1, 1, 1, 1)
    //高光材质颜色
    _BlinnPhongColor ("BlinnPhongColor", Color) = (1, 1, 1, 1)
    //光泽度
    _HighLightColor ("HighLightColor", Range(0, 50)) = 10
    }
    SubShader {

    LOD 100

    Pass {
    Tags { "LightMode" = "ForwardBase" }

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag


    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    struct appdata {
    //模型空间下顶点
    float4 vertex : POSITION;
    //主uv
    float2 main_uv : TEXCOORD0;
    //主法线uv
    float2 normal_uv : TEXCOORD1;
    //模型空间下法线
    float3 normal : NORMAL;
    //模型空间下切线
    float4 tangent : TANGENT;
    };

    struct v2f {
    float2 main_uv : TEXCOORD0;
    float2 normal_uv : TEXCOORD1;
    //世界坐标顶点
    float3 wPos : TEXCOORD2;
    //裁剪空间顶点
    float4 vertex : SV_POSITION;
    // 变换矩阵
    float3x3 rotation : TEXCOORD3;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;

    sampler2D _BumpMap;
    float4 _BumpMap_ST;

    fixed3 _LambertColor;
    fixed3 _BlinnPhongColor;
    float _HighLightColor;

    //凹凸系数
    float _BumpScale;
    v2f vert(appdata v) {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    //计算uv偏移缩放值
    o.main_uv = TRANSFORM_TEX(v.main_uv, _MainTex);
    o.normal_uv = TRANSFORM_TEX(v.normal_uv, _BumpMap);
    //坐标转换

    o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    float3 wNormal = normalize(UnityObjectToWorldNormal(v.normal)) ;
    float3 tangent = normalize(UnityObjectToWorldDir(v.tangent)) ;
    //计算副切线
    float3 biTangent = cross(normalize(wNormal), normalize(tangent)) * v.tangent.w;
    o.rotation = transpose(float3x3(tangent, biTangent, wNormal));

    return o;
    }

    fixed4 frag(v2f i) : SV_Target {

    //取出法线贴图内的法线信息
    float4 packedNormal = tex2D(_BumpMap, i.normal_uv);
    //将取出的法线进行逆运算并且可能进行解压,最终得到切线空间下的法线 并且转换为世界空间下
    float3 tangentNormal = UnpackNormal(packedNormal) ;
    tangentNormal.xy *= _BumpScale;
    tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
    //把切线空间下的法线转换到世界空间下
    float3 worldNormal = mul(i.rotation, tangentNormal);

    //主纹理采样
    fixed3 col = tex2D(_MainTex, i.main_uv) * _LambertColor;
    //计算漫反射颜色
    fixed3 lambertColor = _LightColor0 * col * max(0, dot(normalize(_WorldSpaceLightPos0), worldNormal));
    //计算半角向量
    float3 halfDir = normalize(normalize(_WorldSpaceLightPos0) + normalize(UnityWorldSpaceViewDir(i.wPos)));
    //计算高光颜色
    fixed3 blinnPhongColor = _LightColor0 * _BlinnPhongColor * pow(max(0, dot(halfDir, worldNormal)), _HighLightColor);
    fixed3 color = UNITY_LIGHTMODEL_AMBIENT * col + lambertColor + blinnPhongColor;


    return fixed4(color.rgb, 1);
    }
    ENDCG
    }
    }
    }
  • 修改凹凸系数的计算方式,让法线系数不影响光照
  1. 只让法线中的xy乘以凹凸系数

    tangentNormal.xy *= _BumpScale;

  2. 保证法线为单位向量(让法线不会为0,而是趋近于顶点法线)

    x² + y² + z² = 1 => z² = 1 - (x² + y²) => z = 根号下(1 - (x² + y²))

    tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

  • 通过这样的计算,当凹凸系数在0~1之间变化时,会保证法线为单位向量。这样就不会影响光照表现了
  • 知识点四 提高性能的写法
  • 我们目前在v2f结构体中,世界坐标顶点位置和变换矩阵使用了float3float3x3 的两个变量来存储
  • 但是在很多世界空间下计算 法线贴图的Shader中,往往会使用3个 float4 类型的变量来存储它们
  • 这种写法在很多情况下可以提高性能,因为它更好地与GPU的硬件架构匹配。
    • float4 类型的寄存器是非常高效的
    • 因为现代GPU通常会以 4 分量的向量为基本单位进行并行计算
  • 通常为:xyz存矩阵,w存世界坐标顶点

4. 渐变纹理

(1)渐变纹理是用来做什么的

  • 渐变纹理就是用于控制漫反射光照结果的``一种存储数据的方式

  • 主要作用:

    • 让游戏中的对象具有插画卡通风格。

    • 渐变纹理的使用可以保证物体的轮廓线相比之前使用的传统漫反射光照更加明显 而且还能提供多种色调的变化,可以让模型更具卡通感

      image-20250724174310203

(2)渐变纹理的基本原理

  • 基本原理:就是在计算漫反射时利用半兰伯特光照模型公式中后半部分得到一个0~1区间的值,将这个值作为uv坐标中的uv值 从渐变纹理中取出颜色 与 公式中前面部分进行颜色叠加,最终得到漫反射光照颜色。
  • 半兰伯特:漫反射光照颜色 = 光源的颜色 * 材质的漫反射颜色 *((标准化后物体表面法线向量 · 标准化后光源方向向量)* 0.5 + 0.5)
  • 也就是说: 决定漫反射明暗的不再是由 0~1这个值决定 而是由渐变纹理中取出的颜色进行叠加达到最终效

(3)渐变纹理代码实现

  • 效果:(左是原模型)

    image-20250724202922776
  • 实现步骤

    image-20250724203116794
  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    Shader "Unlit/RampTexture" {
    Properties {
    //主贴图
    _MainTex ("MainTex", 2D) = "" { }
    //纹理贴图
    _RampTex ("RampTex", 2D) = "" { }
    //漫反射颜色
    _MainColor ("MainColor", Color) = (1, 1, 1, 1)
    //高光颜色
    _SpecularColor ("SpecularColor", Color) = (1, 1, 1, 1)
    //光泽度
    _SpecularNum ("SpecularNum", Range(0, 200)) = 10
    }


    SubShader {
    Tags { "RenderType" = "Opaque" }
    LOD 100

    Pass {
    Tags { "LightMode" = "ForwardBase" }
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    // make fog work


    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    struct appdata {
    float4 vertex : POSITION;
    float2 main_uv : TEXCOORD0;
    float2 ramp_uv : TEXCOORD1;
    float3 normal : NORMAL;
    };

    struct v2f {
    float2 uv : TEXCOORD0;
    float3 wPos : TEXCOORD1;
    float3 wNormal : NORMAL;
    float4 vertex : SV_POSITION;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;
    sampler2D _RampTex;
    float4 _RampTex_ST;

    fixed3 _MainColor;
    fixed3 _SpecularColor;
    float _SpecularNum;
    v2f vert(appdata v) {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv.xy = TRANSFORM_TEX(v.main_uv, _MainTex);

    o.wPos = mul(unity_ObjectToWorld, v.vertex);
    o.wNormal = UnityObjectToWorldNormal(v.normal);

    return o;
    }
    fixed4 frag(v2f i) : SV_Target {
    //计算兰伯特光照后半公式的值
    float halfLambertNum = dot(normalize(i.wNormal), normalize(_WorldSpaceLightPos0)) * 0.5 + 0.5;
    //构建uv 取出渐变纹理对应颜色
    fixed3 rampColor = tex2D(_RampTex, fixed2(halfLambertNum, halfLambertNum));
    //取出主材质颜色
    fixed3 col = tex2D(_MainTex, i.uv) * _MainColor;
    fixed3 diffuseColor = _LightColor0 * col * rampColor;
    //高光反射颜色
    //计算半角向量
    float3 halfDir = normalize(normalize(UnityObjectToViewPos(i.wPos)) + normalize(_WorldSpaceLightPos0));
    fixed3 specularColor = _LightColor0 * _SpecularColor * pow(max(0, dot(halfDir, normalize(i.wNormal))), _SpecularNum);
    fixed3 color = UNITY_LIGHTMODEL_AMBIENT * col + diffuseColor + specularColor;
    return fixed4(color.rgb, 1);
    }
    ENDCG
    }
    }
    }

(4)修改渐变纹理设置 避免黑点出现

  • 避免渐变纹理接缝处有黑点

    • 我们需要将 Wrap Mode(循环模式)切换为 Clamp(拉伸模式)

      image-20250724203227978

  • 出现黑点的原因是:

    • 浮点数计算可能存在误差,会出现超过1的值(1.00001)比如cos

    • 如果使用Repeat(重复模式),会舍弃整数部分,保留小数0.00001

    • 这时对应的颜色会是最左边的值,因此会出现黑色

      image-20250724203417219

(5)综合法线纹理和渐变纹理

  • 效果图:(左无法线,右有法线)

    image-20250724204520956
  • 代码:把法线贴图的改一下就行。

5. 遮罩纹理

(1)遮罩纹理是用来做什么的

  • 遮罩纹理通常用于控制或限制某些效果的显示范围
  • 它允许我们可以保护某些区域,使它们免于某些修改
  • 一般情况下,遮罩纹理也会是一张灰度图,rgba分量都是一样的我们利用它存储的值参与到
    • 光照(指定某些区域受光影响的程度)
    • 透明度(指定某些区域透明的程度)
    • 特效(指定某些区域出现特效)
  • 等等相关的计算中 从而来让指定区域达到我们想要的效果
  • 我们以高光遮罩纹理举例

  • 右图三个胶囊体的对比就是 高光遮罩纹理起到的效果

  • 利用高光遮罩纹理 我们可以控制模型上的各个区域 受到高光影响的强弱

    image-20250724210321898

(2)高光遮罩纹理的基本原理

  1. 从纹理中取出对应的遮罩掩码值(颜色的RGB值都可以使用)
  2. 用该掩码值和遮罩系数(我们自己定义的)相乘得到遮罩值
  3. 用该遮罩值和高光反射计算出来的颜色相乘

(3)高光遮罩纹理的实现

  • 效果:(左)

    image-20250724213747079
  • 代码:

    1
    2
    3
    //计算遮罩系数
    fixed3 mainColor = tex2D(_MainTex, i.main_uv) * _LambertColor;
    fixed3 blinnPhong = _LightColor0 * _BlinnPhongColor * pow(max(0, dot(halfDir, tangentNormal)), _HighLightColor) * maskColor;

二、透明

1. 知识回顾

  1. 回顾 渲染标签— 渲染队列

    确定物体的渲染顺序

  2. 回顾 渲染状态— 深度缓冲

    记录像素的深度值,用于之后进行比较,决定片元(颜色)的去留

    image-20250725112044774

  3. 回顾 渲染状态— 深度测试

    将当前片元深度值和深度缓冲中深度值进行比较,决定片元(颜色)去留

    image-20250725112010657

  4. 回顾 渲染状态— 混合方式

    将通过深度测试的片元颜色颜色缓冲区中的颜色按指定算法进行混合 并更新颜色缓冲区

    image-20250725111943855

2. 渲染顺序的重要性

(1)深度测试和深度写入带来的好处

  • 有了深度测试和深度写入发挥作用 让我们不需要关心不透明物体的渲染顺序

  • 比如:

    • 一个物体A 挡住了物体B 即使底层逻辑中先渲染A,后渲染。我们也不用担心B的颜色会把A覆盖 因为在进行深度测试时,远处的B的深度值无法通过深度测试 因为它的深度会比已经写入深度缓冲中A的深度值大 重合处的片元会被丢弃,颜色自然就不会写入,最终重叠处渲染出来的会是A的颜色。

      image-20250725114108625

  • 我们之前写的所有Shader都没有刻意的去设置 深度测试(默认小于判断)、深度写入(默认开启) 、 混合模式(默认不混合) 、渲染队列(默认几何Geometry队列) 相关内容 是因为对于不透明的物体来说,使用默认设置就能够得到正确的渲染效果 但目前我们将要学习的透明相关知识,就需要对他们进行改变

  • 其中,最最最重要的改变就是:

    处理透明混合时,需要关闭深度写入

(2)透明混合为什么需要关闭深度写入

  • 图形学中模拟出现实世界的半透明效果是通过将 多个颜色进行混合计算呈现出来的

  • 举例:

    对于下图来说,如果前方的A对象不关闭深度写入,那么B被A遮挡的部分会在深度测试时被舍弃。

    那么我们就无法得到被遮挡部分的颜色,也就无法进行颜色的混合计算。

    因此我们必须关闭透明物体的深度写入

    让其不会导致被遮挡物体无法通过深度测试从而无法进行颜色混合

    image-20250725114411943
  • 正确的效果:

    image-20250725114449676

(3)关闭深度写入带来的问题

image-20250725114530872

  • 从这个例子我们看出,想要得到正确的半透明混合效果 需要先渲染不透明物体再渲染透明物体

image-20250725114603751

  • 如果先渲染没有开启深度写入的前面的物体 之后渲染后面的物体就会将前面物体的颜色覆盖 或出现错误前后关系表现

(4)如何解决渲染顺序带来的问题

image-20250725114718387

image-20250725114755807

image-20250725114808171

3. 设置深度写入和渲染队列

(1)Unity Shader中设置深度写入

  • 深度写入默认是开启的

  • 我们需要通过渲染状态中的ZWrite off 指令主动关闭深度写入

  • 般写在Pass中

    image-20250725115640169

(2)Unity Shader中设置渲染队列

image-20250725115719315

  • 渲染队列一般都定义在SubShader语句块中,影响之后的所有Pass渲染通道
  • 使用该Shader(着色器)的物体,就会根据你设置的渲染队列在特定的时间进行渲染
  • 使用渲染队列Queue 时,一般会搭配忽视投影器IgnoreProjector 和渲染类型RenderType 一起使用
    • image-20250725115902527

4. 设置混合命令

(1)混合的基本原理

  • 当我们在进行渲染时,当片元通过了深度测试后,会进入到混合流程中。

  • 在混合流程中:

    • 当前片元的颜色被称为 源颜色

    • 颜色缓冲区中的颜色被称为 目标颜色

  • 混合 就是:

    源颜色和目标颜色用对应的混合算法进行计算后

    输出一个新的颜色更新到颜色缓冲区中

  • 注::这些颜色都是RGBA包含透明通道A

    image-20250725122151309

(2)混合的计算规则—— 混合因子

image-20250725122241161 image-20250725122318808

image-20250725143926354

  • 两种使用方法

    image-20250725144005579

(3)混合的计算规则—— 混合操作

  • 在ShaderLab当中除了可以使用Blend 混合命令来设定混合因子

  • 还提供了一个BlendOp 混合操作命令来设定混合的计算方式

  • 他的基本语法是:

    image-20250725144153174

    image-20250725144203615

(4)常见的混合类型

image-20250725144228993

5. 透明效果实现【透明测试】

  • 在游戏开发中,对象的某些部位完全透明其他部位完全不透明,这种透明需求往往不需要半透明效果。

    相对比较极端,只有看得见和看不见之分 ,比如树叶、草、栅栏等等。

(1)透明测试的基本原理

  • 基本原理:

    • 通过一个阈值来决定哪些像素应该被保留,哪些应该被丢弃
  • 具体实现:

    • 片元携带的颜色信息中的透明度(A值),在 不满足条件时(通常是小于某个阈值),
    • 片元就会被舍弃,被舍弃的片元不会在进行任何处理,不会对颜色缓冲区产生任何影响
    • 满足条件时(通常是大于等于某个阈值),该片元会按照不透明物体的处理方式来处理
  • 阈值判断使用的方法:

    • 利用CG中的内置函数:clip(参数)

    • 该函数有重载,参数类型可以是 float4 float3 float2 float 等等

    • 如果传入的参数任何一个分量是负数就会舍弃当前片元

    • 它的内部实现会用到一个 discard 指令,代表剔除该片元 不再参与渲染

      1
      2
      3
      4
      5
      void clip(float4 x)
      {
      if(any(x < 0))
      discard;
      }

(2)透明测试实现

  1. 我们复制 颜色纹理结合光照模型的Shader(因为我们的测试资源没有法线贴图等数据)
  2. 在属性中加一个阈值_Cutoff,取值范围为0~1,用来设定用来判断的阈值。并在CG中添加属性的映射成员
  3. 将渲染队列设置为AlphaTest,并配合IgnoreProjector和RenderType一起设置
  4. 在片元着色器中获取了颜色贴图颜色后,就进行阈值判断
  • 效果展示:

    • image-20250725150634873
  • 代码:

    1
    2
    3
       Tags { "RenderType" = "TransparentCutout" "Queue" = "AlphaTest" "IgnoreProjector" = "True" }
    fixed4 texColor = tex2D(_MainTex, i.uv);
    clip(texColor.a - _Cutoff);

6. 透明度混合——半透明效果实现

(1) 透明度混合是用来处理哪种需求的?

  • 透明度测试,并不能用于实现半透明效果。一般用来处理镂空效果
  • 透明度混合,主要就是用来实现半透明效果的

(2)透明度混合的基本原理

  • 基本原理:

    • 关闭深度写入,开启混合,让片元颜色和颜色缓冲区中颜色进行混合计算
  • 具体实现:

    • 采用半透明的混合因子进行混合 Blend SrcAlpha OneMinusSrcAlpha

      目标颜色 = SrcAlpha * 源颜色 + (1-SrcAlpha) * 目标颜色

      ​ = 源颜色透明度 * 源颜色 + (1-源颜色透明度) * 目标颜色

    • 声明一个0~1区间的_AlphaScale用于控制对象整体透明度

(3)透明度混合实现

  1. 我们复制 颜色纹理结合光照模型的Shader(因为我们的测试资源没有法线贴图等数据)
  2. 在属性中加一个阈值_AlphaScale,取值范围为0~1,用来设定对象整体透明度。并在CG中添加属性的映射成员
  3. 将渲染队列设置为Transparent,并配合IgnoreProjector和RenderType一起设置
  4. 关闭深度写入Zwrite off,设置混合因子Blend SrcAlpha OneMinusSrcAlpha
  5. 在片元着色器中获取了颜色贴图颜色后,修改最后返回颜色的A值为 纹理.a * _AlphaScale
  • 效果:

    image-20250725154238382
  • 代码:

    1
    2
    3
    4
    5
     Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" }
    //关闭深度写入
    ZWrite Off
    //混合设置
    Blend SrcAlpha OneMinusSrcAlpha

(4)疑问

  • 几何体(渲染队列为几何队列)会先渲染,深度已经写入缓冲区。

    半透明物体(渲染队列为透明队列)会后渲染,并且会从远到近的渲染,即使开启深度写入,理论上来说在前面的半透明物体也能够通过深度测试,和颜色缓冲区中的颜色进行混合,那为什么还需要关闭深度写入呢?

  • 渲染规则是:根据像素从远到近渲染的

  • 如果开启深度写入:

    • 前提:在都是半透明物体中。如果一个是半透明,一个不透明。永远不会发生这种,因为半透明始终后渲染。

    • 在重叠的部分,因为后面正放的物体像素在前,会先渲染并且写入深度缓冲中。

    • 这样的话,斜着的物体重叠部分的像素会因为没法通过深度测试而被舍弃。

    image-20250725161612511

(5)开启深度写入的半透明效果

  • 开启深度写入的半透明效果是用来处理哪种需求的?
    • 本身结构较为复杂的模型,使用之前的透明度混合Shader会由于关闭了深度写入。会产生错误的渲染效果。
    • 虽然我们可以通过拆分模型的方式解决部分问题,但是对于一些结构复杂的模型,拆分模型的方式会增加工作量
    • 因此我们可以采用 开启深度写入的半透明Shader 来优化效果
  • 开启深度写入的半透明效果的基本原理
    • 使用两个Pass渲染通道来处理渲染逻辑
    • 第一个Pass:
      • 开启深度写入,不输出颜色(目的是让该模型各片元的深度值能写入深度缓冲)
    • 第二个Pass:
      • 进行正常的透明度混合(和上节课一样)
    • 这样做的话,当执行第一个Pass时,会执行深度测试,并进行深度写入
    • 如果此时该片元没有通过深度测试会直接丢弃,不会再执行第二个Pass
    • 对于同一个模型中处于屏幕同一位置的片元们,会进行该位置的深度测试再决定渲染哪个片元。
  • 如何做到不输出颜色?
  • 使用 ColorMask 颜色遮罩 渲染状态(命令)
  • 它主要用于控制颜色分量是否写入到颜色缓冲区中
    • ColorMask RGBA 表示写入颜色的RGBA通道
    • ColorMask 0 表示不写入颜色
    • ColorMask RB 表示只写入红色和蓝色通道
  • 注意:
    1. 开启深度写入的半透明效果,模型内部之间不会有任何半透明效果(因为模型内部深度较大的片元会被丢弃掉)
    2. 由于有两个Pass渲染通道,因此它会带来一定的性能开销
  • 实现 开启深度写入的半透明效果

    1. 我们透明度混合的Shader代码
    2. 在SubShader中之前的Pass渲染通道前面加一个Pass渲染通道
    3. 在新加Pass渲染通道中开启深度写入,并且使用 ColorMask 0 颜色遮罩 渲染命令,不输出颜色
    image-20250725164605505

7.【透明效果实现】双面渲染的透明效果

(1)双面渲染的透明效果用来处理哪种需求的?

  • 对于现实世界的半透明物体,我们不仅可以透过它看到其他物体的样子,也可以看到这个物体自己的内部结构
  • 但是我们之前实现的 透明度测试 和 透明度混合 相关Shader,都无法看到模型的内部结构。
  • 而双面渲染的透明效果Shader就是来解决该问题的
  • 让我们不仅可以透过半透明物体看到其他物体的样子还可以看到自己的内部结构

(2)双面渲染的透明效果的基本原理

  • 基本原理:
    • 默认情况下,Unity会自动剔除物体的背面,而只渲染物体的正面
      • 双面渲染的基本原理就是利用我们之前学习过的 Cull 剔除指令来进行指定操作
      • Cull Back 背面剔除
      • Cull Front 正面剔除
      • 不设置的话,默认为背面剔除
    • 对于透明度测试Shader
      • 由于它无需混合,因此我们直接 关闭剔除即可
    • 对于透明度混合Shader
      • 由于它需要进行混合,需要使用两个Pass。一个用于渲染背面,一个用于渲染正面
      • 两个Pass中除了剔除命令不同 其他代码和之前一致

(3)实现 双面渲染的透明效果Shader

  • 透明度混合

    1. 复制 透明度混合相关Shader代码
    2. 复制之前的Pass,变成两个一模一样的Pass
    3. 在第一个Pass中剔除正面 Cull Front,在第二个Pass中剔除背面Cull Back(相当于一个片元先渲染背面再渲染正面)
  • 效果:

    image-20250725170249338