一轮整理:2021年6月16日17:19:23

二轮整理:2021年10月28日20:24:37

作者:聪头

Ch5.开始Unity Shader学习之旅

渲染纹理:

  • OpenGL:左下角(0,0)
  • DirectX:左上角(0,0)

5.1 一个最简单的顶点/片元着色器

基本结构

《Shader入门精要》初级篇笔记 - 图1

ShaderLab和Cg变量匹配关系!!!

《Shader入门精要》初级篇笔记 - 图2

也可以在Cg变量前加uniform

  • uniform关键词是Cg中修饰变量和参数的一种修饰词,仅仅用于提供一些关于该变量初始值是如何指定和存储的相关信息(这和其他图像编程接口的uniform不太一样,可省略)

5.2 内置文件和变量

内置包含文件后缀:.cginc

《Shader入门精要》初级篇笔记 - 图3

  • CGIncludes文件夹
    • 包含了所有的内置包含文件
  • DefaultResources文件夹
    • 包含了一些内置组件或功能所需要的Unity Shader,例如一些GUI元素使用的Shader
  • DefaultResourcesExtra文件夹
    • 包含了所有Unity中内置的Unity Shader
  • Editor文件夹
    • 包含了一个脚本文件,用于定义Unity5引入的Standard Shader所用的材质面板

常用包含文件

《Shader入门精要》初级篇笔记 - 图4

常用顶点输入结构体!!!

《Shader入门精要》初级篇笔记 - 图5

  1. //例如
  2. struct appdata_full {
  3. float4 vertex : POSITION;
  4. float4 tangent : TANGENT;
  5. float3 normal : NORMAL;
  6. float4 texcoord : TEXCOORD0;
  7. float4 texcoord1 : TEXCOORD1;
  8. float4 texcoord2 : TEXCOORD2;
  9. float4 texcoord3 : TEXCOORD3;
  10. fixed4 color : COLOR;
  11. UNITY_VERTEX_INPUT_INSTANCE_ID
  12. };

常用帮助函数

《Shader入门精要》初级篇笔记 - 图6

帮助函数整理详见:6.2 Unity内置函数!!!

5.3 语义!!!

语义实际上就是一个赋给Shader输入和输出的字符串,这个字符串表达了这个参数的含义。通俗来讲,就是让Shader知道从哪里读取数据,并把数据输出到哪里

应用->顶点

《Shader入门精要》初级篇笔记 - 图7

顶点->片元

《Shader入门精要》初级篇笔记 - 图8

片元输出

《Shader入门精要》初级篇笔记 - 图9

5.4 课后答疑

渲染平台差异

当使用多张渲染图像开启抗锯齿后,需要手动处理翻转问题(单张屏幕图像并开启抗锯齿,Unity也会帮我们处理)

  1. #if UNITY_UV_STARTS_AT_TOP
  2. if (_MainTex_TexelSize.y < 0)
  3. uv.y = 1 - uv.y;
  4. #endif

精度选择

float

高精度类型,32位,通常用于世界坐标下的位置,纹理UV,或涉及复杂函数的标量计算,如三角函数、幂运算等。

half

中精度类型,16位,数值范围为[-60000,+60000],通常用于本地坐标下的位置、方向向量、HDR颜色等。

fixed

低精度类型,11位,数值范围为[-2,+2],通常用于常规的颜色与贴图,以及低精度间的一些运算变量等。

在PC平台不管你Shader中写的是half还是fixed,统统都会被当作float来处理。half与fixed仅在一些移动设备上有效。
比较常用的一个规则是,除了位置和坐标用float以外,其余的全部用half。主要原因也是因为大部分的现代GPU只支持32位与16位,也就是说只支持float和half,不支持fixed。

Ch6.Unity中的基础光照

6.1 标准光照模型

环境光

用于描述其他所有的间接光照

《Shader入门精要》初级篇笔记 - 图10

自发光

这个部分描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量

《Shader入门精要》初级篇笔记 - 图11

漫反射

用于描述当光源照射到模型表面时,该表面会向每个方向散射多少辐射量

《Shader入门精要》初级篇笔记 - 图12

Half Lambert

  • 使暗部不那么暗

《Shader入门精要》初级篇笔记 - 图13

高光反射

用于描述当光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量

Phong

《Shader入门精要》初级篇笔记 - 图14

反射向量推导:https://blog.csdn.net/yinhun2012/article/details/79466517

Blinn

《Shader入门精要》初级篇笔记 - 图15

着色

  • 逐顶点:Gouraud着色
  • 逐像素:Phong着色

局限性:无法表现菲涅尔反射;Blinn-Phong模型是各向同性(isotropic)的,当我们固定视角方向和光源方向旋转这个表面时,反射不会发生任何改变。但有些表面时具有各向异性反射性质的,如拉丝金属,毛发等

6.2 Unity内置函数!!!

书P137

观察者

《Shader入门精要》初级篇笔记 - 图16

光源

  • 以下仅可用于前向渲染,而且结果没有被归一化!

《Shader入门精要》初级篇笔记 - 图17

其他

《Shader入门精要》初级篇笔记 - 图18

内置函数得到的方向往往是没有归一化的,我们需要使用normalize函数对结果归一化

实战6-1:基本光照模型

  1. // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
  2. // Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
  3. Shader "Learning/Sampler6_4"
  4. {
  5. Properties {
  6. _Diffuse("Diffuse", Color) = (1,1,1,1)
  7. _Specular("Specular", Color) = (1,1,1,1)
  8. _Gloss("Gloss", Range(8.0, 256.0)) = 20
  9. }
  10. SubShader {
  11. Pass {
  12. Tags { "LightMode" = "ForwardBase" }
  13. CGPROGRAM
  14. #pragma vertex vert
  15. #pragma fragment frag
  16. #include "Lighting.cginc"
  17. fixed4 _Diffuse;
  18. fixed4 _Specular;
  19. float _Gloss;
  20. struct v2f {
  21. fixed4 pos : SV_POSITION;
  22. fixed3 worldNormal : TEXCOORD0;
  23. fixed3 worldPos : TEXCOORD1;
  24. };
  25. v2f vert(appdata_base v)
  26. {
  27. v2f o;
  28. o.pos = UnityObjectToClipPos(v.vertex);
  29. o.worldNormal = mul(v.normal, unity_WorldToObject);//法线乘逆转置矩阵,从模型->世界
  30. o.worldPos = mul(unity_ObjectToWorld, v.vertex);
  31. return o;
  32. }
  33. fixed4 frag(v2f i) : SV_Target
  34. {
  35. //漫反射
  36. fixed3 worldNormal = normalize(i.worldNormal);
  37. fixed3 lightDir = normalize(_WorldSpaceLightPos0);
  38. fixed3 diffuse = _Diffuse * _LightColor0 * (dot(worldNormal, lightDir) * 0.5 + 0.5);
  39. //环境光
  40. fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
  41. ////高光-Phone
  42. //fixed3 reflectDir = reflect(-_WorldSpaceLightPos0, worldNormal);
  43. //fixed3 viewDir =normalize( _WorldSpaceCameraPos - i.worldPos);
  44. //fixed3 specular = _LightColor0 * _Specular * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
  45. //高光-Blinn
  46. fixed3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
  47. fixed3 halfDir = normalize(viewDir + lightDir);
  48. fixed3 specular = _LightColor0 * _Specular * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
  49. return fixed4(diffuse + ambient + specular, 1.0);
  50. }
  51. ENDCG
  52. }
  53. }
  54. }

《Shader入门精要》初级篇笔记 - 图19

Ch7.基础纹理

7.1 单张纹理

shader参数解析

  • _MainTex_ST:在Unity中需要使用纹理名_ST的方式声明某个纹理的属性。其中ST是缩放(scale)是平移(translation)的缩写
    • xy:存储缩放值
    • zw:存储偏移值

Inspector参数解析

  • Alpha from Grayscale:勾选后,透明通道的值将会由每个像素的灰度值生成
  • Wrap Mode:决定了纹理坐标超过[0,1]范围后将会如何被平铺
    • Repeat:纹理坐标超过1,整数部分被舍弃,而直接使用小数部分采样
    • Clamp:纹理坐标超过1,那么将会截取到1,如果小于0,将会截取到0
  • Filter Mode:它决定了当纹理由于变换而产生拉伸时将会采用哪种滤波模式。说人话就是一张64x64的纹理放大至512x512时,需要对纹理进行处理。以下三种,效果和性能耗费逐步增大
    • Point:最近邻滤波。在放大或缩小时,采样的像素数目通常只有一个,因此图像看起来会有一种像素风格的效果
    • Bilinear:线性滤波,找4个邻近像素插值混合
    • Trilinear:在Bilinear基础上,对多级渐远纹理之间进行混合
  • Generate Mip Maps:多级渐远纹理技术。将原纹理提前用滤波处理来得到很多更小的图像,像金字塔一般。每层是对上一层图像降采样的结果。根据摄像机远近,选择相应纹理进行采样,确保结果的合理性

7.2 凹凸映射

法线纹理的存储

  • 模型空间:对于模型顶点自带的法线,它们是定义在模型空间中的,因此将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理。(不常用)
  • 切线空间:对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线方向(n),x轴是顶点的切线方向(t),而y轴可由法线和切线叉积而得,也被称为副切线(bitangent)或副法线

《Shader入门精要》初级篇笔记 - 图20

切线空间满足右手坐标系

《Shader入门精要》初级篇笔记 - 图21

切线空间法线纹理呈现蓝紫色的原因:

  • 切线纹理就是存储了每个点在各自切线空间中的法线扰动方向。如果一个点的法线方向不变,那么就在它的切线空间中,新的法线方向就是z轴方向,即值为(0,0,1),经过映射得到像素颜色为(0.5,0.5,1),就是蓝紫色

两种法线纹理的比较

《Shader入门精要》初级篇笔记 - 图22

《Shader入门精要》初级篇笔记 - 图23

两种光照计算的比较

最常用的就是在切线空间世界空间进行光照计算

效率上:

  • 切线空间优于世界空间,因为前者可以在顶点着色器中就完成对光照方向和视角方向的变换。而第二种方法由于要先对法线纹理进行采样,所以变换过程必须在片元着色器中实现,这意味着我们需要额外进行一次矩阵操作,将法线从切线空变换到世界空间

通用性上:

  • 世界空间由于切线空间。因为我们有时需要在世界空间下进行一些计算,例如使用Cubemap进行环境映射时,我们需要使用世界空间下的反射方向对Cubemap进行采样

采样法线核心代码

  • 未设置成Normal Map,则手动采样
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw); //利用uv采样法线贴图 [0~1]
fixed3 tangentNormal;
tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;//xy映射到[-1,1]*缩放值
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
  • 设置成Normal Map,自动采样
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentNormal;
tangentNormal = UnpackNormal(packedNormal); //自动映射[-1,1]
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

设置成NormalMap好处:

  • 可以让Unity根据不同平台对纹理进行压缩,再通过UnpackNormal函数来针对不同的压缩格式对法线进行正确的采样。压缩原理:只有两个通道必不可少,第三个通道可以推导出来(法线是单位向量,并且切线空间下的法线方向的z分量始终为正)

利用高度图生成法线:白色相对更高,黑色相对更低

  • 勾选Create From Grayscale
  • Bumpiness:控制凹凸程度
  • Filtering:
    • Smooth:使得生成后的法线纹理会比较平滑
    • Sharp:使用Sobel滤波来生成法线。实现很简单,只需要在一个3x3的滤波器中计算x和y方向上的导数,然后从中得到法线即可

求逆矩阵的核心代码!!!

矩阵元素下划线访问方式

float3x3 test = float3x3(0,0,0,
                         0.5,0.5,1,
                         0,0,0);
float3 finalColor = test._21_22_23;
return half4(finalColor, 1);

《Shader入门精要》初级篇笔记 - 图24

求逆函数

//求4x4矩阵的逆矩阵:核心思想 伴随矩阵/行列式
// Unity doesn't support the 'inverse' function in native shader
// so we write one by our own
// Note: this function is just a demonstration, not too confident on the math or the speed
// Reference: http://answers.unity3d.com/questions/218333/shader-inversefloat4x4-function.html
float4x4 inverse(float4x4 input) {
  //余子式的概念:
  //在n阶行列式中,去掉元素aij所在的第i行、第j列,由剩下元素按原来位置与顺序组成的n-1阶行列式
  //minor就是这余子式的,float3x3是按行优先,所以顺序保持一致
  //define语法可以参考《C Primer Plus》P525 16.3 在define中使用参数
  #define minor(a,b,c) determinant(float3x3(input.a, input.b, input.c))
  //cofactors由代数余子式组成
  float4x4 cofactors = float4x4(
    //_xx_xx_xx经验证是cg中访问矩阵元素的方式
    //语法:float3 row = matrix._xx_xx_xx;
    minor(_22_23_24, _32_33_34, _42_43_44), 
    -minor(_21_23_24, _31_33_34, _41_43_44),
    minor(_21_22_24, _31_32_34, _41_42_44),
    -minor(_21_22_23, _31_32_33, _41_42_43),

    -minor(_12_13_14, _32_33_34, _42_43_44),
    minor(_11_13_14, _31_33_34, _41_43_44),
    -minor(_11_12_14, _31_32_34, _41_42_44),
    minor(_11_12_13, _31_32_33, _41_42_43),

    minor(_12_13_14, _22_23_24, _42_43_44),
    -minor(_11_13_14, _21_23_24, _41_43_44),
    minor(_11_12_14, _21_22_24, _41_42_44),
    -minor(_11_12_13, _21_22_23, _41_42_43),

    -minor(_12_13_14, _22_23_24, _32_33_34),
    minor(_11_13_14, _21_23_24, _31_33_34),
    -minor(_11_12_14, _21_22_24, _31_32_34),
    minor(_11_12_13, _21_22_23, _31_32_33)
  );
  #undef minor
  //代数余子式组成的矩阵转置后就是伴随矩阵
  return transpose(cofactors) / determinant(input); //伴随矩阵 / 行列式
}

实战7-1:凹凸映射—切线空间

模型空间—>切线空间变换矩阵的推导

《Shader入门精要》初级篇笔记 - 图25

Shader "Unity Shaders Book/Chapter 7/CT_NormalMapInTangentSpace"
{
  Properties
  {
    _Color ("Color", Color) = (1,1,1,1)
      _MainTex ("Texture", 2D) = "white" {}
    _BumpTex ("NormalMap", 2D) = "bump" {}
    _BumpScale ("BumpScale", Float) = 1.0
      _SpecularColor ("SpecularColor", Color) = (1,1,1,1)
      _Gloss ("Gloss", Range(1, 256)) = 20
  }
  SubShader
  {
    Pass
    {
      Tags { "LightMode"="ForwardBase" }
      CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #include "UnityCG.cginc"
        #include "Lighting.cginc"

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

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

      half4 _Color;
      sampler2D _MainTex;
      float4 _MainTex_ST;
      sampler2D _BumpTex;
      float4 _BumpTex_ST;
      half _BumpScale;
      half4 _SpecularColor;
      half _Gloss;

      //求4x4矩阵的逆矩阵:核心思想 伴随矩阵/行列式
      // Unity doesn't support the 'inverse' function in native shader
      // so we write one by our own
      // Note: this function is just a demonstration, not too confident on the math or the speed
      // Reference: http://answers.unity3d.com/questions/218333/shader-inversefloat4x4-function.html
      float4x4 inverse(float4x4 input) {
        //余子式的概念:
        //在n阶行列式中,去掉元素aij所在的第i行、第j列,由剩下元素按原来位置与顺序组成的n-1阶行列式
        //minor就是这余子式的,float3x3是按行优先,所以顺序保持一致
        //define语法可以参考《C Primer Plus》P525 16.3 在define中使用参数
        #define minor(a,b,c) determinant(float3x3(input.a, input.b, input.c))
        //cofactors由代数余子式组成
        float4x4 cofactors = float4x4(
          //_xx_xx_xx经验证是cg中访问矩阵元素的方式
          //语法:float3 row = matrix._xx_xx_xx;
          minor(_22_23_24, _32_33_34, _42_43_44), 
          -minor(_21_23_24, _31_33_34, _41_43_44),
          minor(_21_22_24, _31_32_34, _41_42_44),
          -minor(_21_22_23, _31_32_33, _41_42_43),

          -minor(_12_13_14, _32_33_34, _42_43_44),
          minor(_11_13_14, _31_33_34, _41_43_44),
          -minor(_11_12_14, _31_32_34, _41_42_44),
          minor(_11_12_13, _31_32_33, _41_42_43),

          minor(_12_13_14, _22_23_24, _42_43_44),
          -minor(_11_13_14, _21_23_24, _41_43_44),
          minor(_11_12_14, _21_22_24, _41_42_44),
          -minor(_11_12_13, _21_22_23, _41_42_43),

          -minor(_12_13_14, _22_23_24, _32_33_34),
          minor(_11_13_14, _21_23_24, _31_33_34),
          -minor(_11_12_14, _21_22_24, _31_32_34),
          minor(_11_12_13, _21_22_23, _31_32_33)
        );
        #undef minor
        //代数余子式组成的矩阵转置后就是伴随矩阵
        return transpose(cofactors) / determinant(input); //伴随矩阵 / 行列式
      }

      v2f vert (appdata v)
      {
        v2f o;
        //求副切线
        float3 binormal = cross(v.normal.xyz, v.tangent.xyz) * v.tangent.w;

        //模型->切线变换矩阵
        //法1:按列展开切线空间的基并求转置,推导见笔记
        //float3x3 objToTangent = float3x3(v.tangent.xyz, binormal.xyz, v.normal.xyz);
        //法2:使用逆矩阵求
        float4x4 TtoObj = float4x4(
          v.tangent.x, binormal.x, v.normal.x, 0,
          v.tangent.y, binormal.y, v.normal.y, 0,
          v.tangent.z, binormal.z, v.normal.z, 0,
          0,0,0,1);
        float4x4 ObjtoT = inverse(TtoObj);

        //将光源、观察方向从:世界 -> 模型 -> 切线空间
        //法1:Unity内置函数
        o.lightDir = mul(ObjtoT, float4(ObjSpaceLightDir(v.vertex), 0)).xyz;
        o.viewDir = mul(ObjtoT, float4(ObjSpaceViewDir(v.vertex), 0)).xyz;
        //法2:手动求解
        //若光源为平行光, _WorldSpaceLightPos0存的是方向
        //o.lightDir = mul(objToTangent, mul(unity_WorldToObject, _WorldSpaceLightPos0).xyz);
        //o.viewDir = mul(ObjToT, mul(unity_WorldToObject, _WorldSpaceCameraPos) - float4(0,0,0,1)).xyz;

        //纹理采样
        //法1:Unity内置函数
        o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
        o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpTex);
        //法2:手动求解
        //o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
        //o.uv.zw = v.texcoord.xy * _BumpTex_ST.xy + _BumpTex_ST.zw;

        o.pos = UnityObjectToClipPos(v.vertex);
        return o;
      }

      half4 frag (v2f i) : SV_Target
      {
        float3 lightDir = normalize(i.lightDir);
        float3 viewDir = normalize(i.viewDir);

        //从NormalMap中获取切线空间法线
        float4 packedNormal = tex2D(_BumpTex, i.uv.zw); //采样法线贴图
        float3 normalDir = UnpackNormal(packedNormal); //解析采样结果
        normalDir.xy *= _BumpScale;
        normalDir.z = sqrt(1 - saturate(dot(normalDir.xy, normalDir.xy)));//点乘记: x^2 + y^2

        //漫反射
        float3 albedo = tex2D(_MainTex, i.uv.xy) * _Color;
        float NdotL = saturate(dot(normalDir, lightDir));
        half3 diffuseColor = NdotL * albedo * _LightColor0.xyz;

        //高光反射
        float3 halfDir = normalize(lightDir + viewDir);
        float NdotH = saturate(dot(halfDir, normalDir));
        half3 specularColor = _SpecularColor.xyz * _LightColor0.xyz * pow(NdotH, _Gloss);

        //环境光
        half3 ambientColor = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

        half3 col = diffuseColor + specularColor + ambientColor;
        return half4(col, 1);
      }
      ENDCG
    }
  }

  Fallback "Specular"
}

《Shader入门精要》初级篇笔记 - 图26

《Shader入门精要》初级篇笔记 - 图27

实战7-2:凹凸映射—世界空间

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

Shader "Unity Shaders Book/Chapter 7/CT_NormalMapInWorldSpace"
{
  Properties
  {
    _Color ("Color", Color) = (1,1,1,1)
      _MainTex ("Texture", 2D) = "white" {}
    _BumpTex ("NormalMap", 2D) = "bump" {}
    _BumpScale ("BumpScale", Float) = 1.0
      _SpecularColor ("SpecularColor", Color) = (1,1,1,1)
      _Gloss ("Gloss", Range(1, 256)) = 20
  }
  SubShader
  {
    Pass
    {
      Tags { "LightMode"="ForwardBase" }
      CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #include "UnityCG.cginc"
        #include "Lighting.cginc"

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

      struct v2f
      {
        float4 pos : SV_POSITION;
        float4 uv : TEXCOORD0;
        float4 TtoW1 : TEXCOORD1;
        float4 TtoW2 : TEXCOORD2;
        float4 TtoW3 : TEXCOORD3;
      };

      half4 _Color;
      sampler2D _MainTex;
      float4 _MainTex_ST;
      sampler2D _BumpTex;
      float4 _BumpTex_ST;
      half _BumpScale;
      half4 _SpecularColor;
      half _Gloss;

      v2f vert (appdata v)
      {
        v2f o;
        //将模型空间->世界空间
        float3 worldTangent = normalize(mul(unity_ObjectToWorld, v.tangent).xyz);
        //float3 worldNormal = UnityObjectToWorldNormal(v.normal);
        float3 worldNormal = normalize(mul(float4(v.normal, 0), unity_WorldToObject).xyz);
        float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

        //切线空间 -> 世界空间变换矩阵
        o.TtoW1.xyz = worldTangent;
        o.TtoW2.xyz = worldBinormal;
        o.TtoW3.xyz = worldNormal;

        //顶点世界空间坐标
        float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
        o.TtoW1.w = worldPos.x;
        o.TtoW2.w = worldPos.y;
        o.TtoW3.w = worldPos.z;

        //纹理采样
        o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
        o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpTex);

        o.pos = UnityObjectToClipPos(v.vertex);
        return o;
      }

      half4 frag (v2f i) : SV_Target
      {
        float3 worldPos = float3(i.TtoW1.w, i.TtoW2.w, i.TtoW3.w);
        float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); //若为平行光,就是光源方向
        float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - worldPos);

        //从NormalMap中获取切线空间法线
        float4 packedNormal = tex2D(_BumpTex, i.uv.zw); //采样法线贴图
        float3 normalDir = UnpackNormal(packedNormal); //解析采样结果
        normalDir.xy *= _BumpScale;
        normalDir.z = sqrt(1 - saturate(dot(normalDir.xy, normalDir.xy)));//点乘记: x^2 + y^2

        //将法线从切线空间变换到世界空间(这里是正交矩阵,逆转置矩阵等于其本身)
        float3x3 TtoW = float3x3(i.TtoW1.x, i.TtoW2.x, i.TtoW3.x,
                                 i.TtoW1.y, i.TtoW2.y, i.TtoW3.y,
                                 i.TtoW1.z, i.TtoW2.z, i.TtoW3.z);
        normalDir = normalize(mul(TtoW, normalDir));

        //漫反射
        float3 albedo = tex2D(_MainTex, i.uv.xy) * _Color;
        float NdotL = saturate(dot(normalDir, lightDir));
        half3 diffuseColor = NdotL * albedo * _LightColor0.xyz;

        //高光反射
        float3 halfDir = normalize(lightDir + viewDir);
        float NdotH = saturate(dot(halfDir, normalDir));
        half3 specularColor = _SpecularColor.xyz * _LightColor0.xyz * pow(NdotH, _Gloss);

        //环境光
        half3 ambientColor = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

        half3 col = diffuseColor + specularColor + ambientColor;
        return half4(col, 1);
      }
      ENDCG
    }
  }

  Fallback "Specular"
}

《Shader入门精要》初级篇笔记 - 图28

实战7-3:渐变纹理(核心)

  • 使用Half-Lambert采样渐变纹理
float halfLambert = 0.5 * dot(lightDir, worldNormal) + 0.5; //[-1,1] -> [0,1],去采样
fixed3 diffuse = _Color * _LightColor0 * tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb;

《Shader入门精要》初级篇笔记 - 图29

渐变纹理的WrapMode设为Clamp,防止因为精度问题,导致出现脏点

Ch8.透明效果

8.1 透明度测试

透明度不满足条件就会被舍弃。代码详见P165

//参数x:裁剪时使用的标量或矢量条件,类型为float4/float3/float2/float1/float
//描述:如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色
void clip(x)

被舍弃的片元不会进行任何处理,也不会对颜色缓冲产生任何影响(说明在片元着色器就已经完成舍弃操作了);否则,就会按照普通不透明物体的处理方式来处理它,即进行深度测试和深度写入

故透明度测试不需要关闭深度写入

8.2 透明度混合

需要关闭深度写入,会与颜色缓冲区的颜色混合

排序!!!

①先渲染所有不透明物体,并开启它们的深度测试和深度写入

②把半透明物体按它们距离摄像机的远近进行排序,然后从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入

《Shader入门精要》初级篇笔记 - 图30

Unity在内部使用一系列整数索引来表示每个渲染队列,且索引号越小表示越早被渲染

双Pass渲染

解决重叠问题

Pass
{
    ZWrite On
    ColorMask 0
}

解决正反面渲染顺序问题

  • 先Cull Front正常渲染,再Cull Back正常渲染

混合命令

  • 源颜色:用S表示,指的是由片元着色器产生的颜色值
  • 目标颜色:用D表示,指的是从颜色缓冲区中读取到的颜色值

《Shader入门精要》初级篇笔记 - 图31

混合因子 !!!

本质:就是设置四个因子,即源和目标各分量的权重。源rgb因子,源a因子;目标rgb因子,目标a,得到输出O

《Shader入门精要》初级篇笔记 - 图32

《Shader入门精要》初级篇笔记 - 图33

《Shader入门精要》初级篇笔记 - 图34

混合操作!!!

本质:本质就是设置源和目标的混合关系

《Shader入门精要》初级篇笔记 - 图35

《Shader入门精要》初级篇笔记 - 图36

  • 混合操作命令通常是与混合因子一起工作的。但需要注意的是,当使用Min和Max混合操作时,混合因子实际上是不起任何作用的,它们仅仅会判断原始的源颜色和目的颜色之间的比较结果

常见混合类型!!!

《Shader入门精要》初级篇笔记 - 图37

《Shader入门精要》初级篇笔记 - 图38

实战8-1:透明度混合之双Pass渲染

如果模型自身存在重叠,采用双Pass渲染;如果模型本身没有重叠,可以使用单Pass渲染

  • 原理:
    • 第一个Pass:开启深度写入,但不输出颜色。它的目的仅仅是为了把该模型的深度值写入深度缓冲中
    • 第二个Pass:进行正常的透明度混合,由于上一个Pass已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染
Shader "Unity Shaders Book/Chapter 8/CT_AlphaBlendingWithZWrite" {
  Properties {
    _Color ("Color Tint", Color) = (1, 1, 1, 1)
      _MainTex ("Main Tex", 2D) = "white" {}
    _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
  }
  SubShader {
    Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}

    // Extra pass that renders to depth buffer only
    Pass {
      ZWrite On
      ColorMask 0 //设置颜色通道的写掩码(write mask),0表示不写
    }

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

      ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        CGPROGRAM

        #pragma vertex vert
        #pragma fragment frag

        #include "Lighting.cginc"

        fixed4 _Color;
      sampler2D _MainTex;
      float4 _MainTex_ST;
      fixed _AlphaScale;

      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, _MainTex);

        return o;
      }

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

        fixed4 texColor = tex2D(_MainTex, i.uv);

        fixed3 albedo = texColor.rgb * _Color.rgb;

        fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

        fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

        return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
      }

      ENDCG
    }
  } 
  FallBack "Transparent/VertexLit"
}

《Shader入门精要》初级篇笔记 - 图39《Shader入门精要》初级篇笔记 - 图40

左图为单Pass渲染,右图为双Pass渲染

标签解释:通常透明度混合或测试都应该在SubShader中设置这3个标签

  • Queue:混合渲染队列为Transparent,测试为AlphaTest
  • RenderType:可以让Unity把这个shader归入到提前定义的组(混合就是Transparent,测试就是TransparentCutout),以指明该Shader是一个使用了透明度混合的shader。该标签常用于着色器替换功能
  • IngnoreProjector:设置为true,意味着这个Shader不会受到投影器(Projectors)的影响

补充:双面渲染的透明效果

采用双Pass渲染

  • 第一个Pass:只渲染背面
  • 第二个Pass:只渲染正面

Unity会顺序执行SubShader中的各个Pass,因此我们可以保证背面总是在正面之前渲染,从而可以保证正确的深度渲染关系

在Pass里面调用Cull Off | Front | Back相应指令进行透明度混合