【图形学】《Shader入门精要》基础和初级【1-8】笔记
聪头 游戏开发萌新

基础和初级 1-8

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

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

作者:聪头

本书Unity版本:v5.2.1

Github:https://github.com/candycat1992/Unity_Shaders_Book

本文档为独自整理的笔记,故章节的主题和书本不一致!

—————– 基础篇 —————–

Ch1.欢迎来到Shader的世界

介绍各章节内容,本文档为独自整理的笔记,故章节的主题和书本不一致!

Ch2.渲染流水线

![image-20211027112031079](基础和初级 1-8.assets/image-20211027112031079.png)

2.1 应用阶段

  • 输入:无

  • 执行者:CPU

  • 输出结果:渲染图元(点、线三角面等)

过程:

  1. 把数据加载到显存中:硬盘 –> 内存 –> 显存

    • 内存数据加载到显存后,可以移除。但对于一些数据,如用于碰撞检测的网格数据,则不希望移除
    • 场景数据:摄像机、视锥体、场景中包含模型、光源信息等。粗粒度剔除,把那些不可见的物体剔除出去。
  2. 设置渲染状态:定义场景中网格是怎样被渲染的

    • 简单理解为Unity给Material绑定Shader,给模型绑定Material并设置参数

    • 例如,指定顶点/片元着色器,光源属性,材质等

  3. 调用DrawCall

    • 由CPU发给GPU的命令,指向一个需要被渲染的图元(primitives)列表。给定一个DrawCall,GPU会根据渲染状态(如材质、纹理、着色器等)和所有输入的顶点数据进行计算,最终输出成屏幕上显示的像素。该计算过程,就是GPU流水线

2.2 几何阶段

  • 输入:CPU

  • 执行者:GPU

  • 输出结果:屏幕空间的顶点信息

image

过程:

  1. 顶点着色器:完全可编程控制,必须实现
    • 用于实现顶点的空间变换、顶点着色等
    • 坐标变换:对顶点的坐标(即位置)进行某种变换。必须完成:把顶点从模型空间转换到齐次裁剪空间。
  2. 曲面细分着色器:完全可编程控制,可选
    • 细分图元
  3. 几何着色器:完全可编程控制,可选
    • 用于执行逐图元的着色操作或产生更多图元
  4. 裁剪:可配置,不可编程
    • 裁剪不在相机范围内的顶点,并剔除三角图元的面片
    • ![image-20210616181056609](基础和初级 1-8.assets/image-20210616181056609.png)
  5. 屏幕映射:GPU固定实现
    • 负责把每个图元的坐标转换到屏幕坐标系中

2.3 光栅化阶段

  • 输入:屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等

  • 执行者:GPU

  • 输出结果:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色

过程:

  1. 三角形设置:GPU固定实现

    • 计算光栅化一个三角形网格所需的信息
    • 上一阶段输出的都是三角网格的顶点,即每条边的两个端点。我们需要计算每条边上的像素坐标,得到整个三角网格对像素的覆盖情况。
    • 计算三角网格表示数据的过程就叫做三角形设置
  2. 三角形遍历:GPU固定实现

    • 片元:像素被一个三角网格覆盖,就会生成一个片元(fragment)。一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色(利用上一阶段的三角网格的3个顶点信息对整个覆盖区域的像素进行插值)。这些状态包括(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点颜色,例如法线、纹理坐标等
    • 找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也叫做扫描变换(Scan Conversion)
  3. 片元着色器:完全可编程控制,可选

    • 输入的是上一个阶段对顶点信息插值得到的结果,输出的是一个或多个颜色值。这一阶段可以完成很多重要的渲染技术,如纹理采样

    解释: 上一阶段不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据,用来表述一个三角网格是怎样覆盖每个像素的。一句话说,每个片元只负责存储这样一系列数据,真正会对像素产生影响的是下一阶段,逐片元操作

  4. 逐片元操作:可配置,不可编程

    • 很多重要操作,如修改颜色、深度缓冲、混合等

    • ![image-20210616181608813](基础和初级 1-8.assets/image-20210616181608813.png)

    • 逐片元操作阶段所做的操作。只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区中

image

区别:

1.模板测试和写入一起开和关;深度测试和写入可分别开启

2.模板测试未通过也可更新值;深度测试没通过不可更新值

image

源颜色:片元着色器的颜色值

目标颜色:已经存在于颜色缓冲区中颜色值

补充:

  • 屏幕显示的是颜色缓冲区中的颜色值。GPU会使用双重缓冲的策略,对场景的渲染是在幕后发生的,即在后置缓冲区中。一旦场景已经被渲染到了后置缓冲区中,GPU就会交换前置缓冲区和后置缓冲区中的内容,前置缓冲区就是显示在屏幕上的图像。由此,保证我们看到的图像总是连续的

2.4 课后答疑

DrawCall

性能瓶颈:CPU(而非GPU),每次调用DrawCall,CPU需要向GPU发送很多内容,包括数据、状态和命令等。在这一阶段,CPU需要完成很多工作,例如检查渲染状态等。而GPU的渲染能力很强,因此渲染速度往往快于CPU提交命令的速度。如果DrawCall数量太多,CPU就会把大量时间花费在提交DrawCall上,造成CPU过载

减少DrawCall的办法:

  • 避免使用大量很小的网格,当不可避免地使用很小的网格时,考虑是否可以合并它们
  • 避免使用过多的材质。尽量在不同的网格之间共有同一个材质

什么是Shader

  • GPU流水线上一些可高度编程的阶段,而由着色器编译出来的最终代码是会在GPU上运行的(对于固定管线的渲染来说,着色器有时等同于一些特定的渲染设置)

Ch3.UnityShader基础

3.1 UnityShader概述

  • Unity Material和Shader
image

把材质(Material)想象成组件,把UnityShader想象成源文件。

  • 组件的目的:个性化

  • 源文件的目的:通用性

  • ShaderLab:为Unity开发者提供的高层级的渲染抽象层

3.2 Shader结构:名称

  • 使用”/“控制位置

![img](基础和初级 1-8.assets/_CopyPix_1610467306_2.png)

3.3 Shader结构:Properties

  • 材质和UnityShader的桥梁,通过材质面板传递Shader属性

![img](基础和初级 1-8.assets/_CopyPix_1610467284_1.png)

解释:

  • Name:Shader访问的名字
  • display name:材质面板上的名字
  • Propertype:类型
  • DefaultValue:默认值

Properties属性!!!

三个叹号:需要掌握的知识点,方便日常开发中通过目录快速定位

![img](基础和初级 1-8.assets/_CopyPix_1610467420_3.png)

说明:

  • 2D、Cube、3D的内置纹理默认值包括:white、black、gray、bump

例子

image
  • 为了在Shader中可以访问到这些属性,我们需要在Cg代码片中定义和这些属性类型相匹配的变量。

  • 需要说明的是,即使我们不在Properties语义块中声明这些属性,也可以直接在Cg代码片中定义变量。此时,我们可以通过过脚本向Shader中传递这些属性

3.4 Shader结构:SubShader

  • 每个UnityShader可以包含多个SubShader语义块(至少一个),加载时Unity选择第一个可在目标平台运行的SubShader。如果都不支持,使用Fallback

解释:不同显卡具有不同的能力,写SubShader就是为了在旧显卡使用计算复杂度较低的着色器,在高级显卡使用计算复杂度较高的着色器,以便提供更出色的画面

重要组成

  • SubShader:定义了一系列Pass以及可选的状态([RenderSetup])和标签([Tags])设置
  • Pass:定义了一次完整的渲染流程

语法:

image

状态设置!!!

  • SubShader和Pass语法相同

![img](基础和初级 1-8.assets/_CopyPix_1610468312_6.png)

SubShader专属标签!!!

概念:SubShader标签是一个键值对,它的键和值都是字符串类型,用来告诉Unity渲染引擎:我希望怎样以及何时渲染这个对象

语法:![img](基础和初级 1-8.assets/_CopyPix_1610468434_7.png)

类型:

image

Pass语义块

语法:

![img](基础和初级 1-8.assets/_CopyPix_1610470346_15.png)

Pass名称

  • Name “MyPassName”
  • 通过Pass名称,我们可以使用ShaderLab的UsePass命令来直接使用其他Unity Shader中的Pass
    • 例如:UsePass “MyShader/MYPASSNAME”

Unity内部会把所有Pass名称转换成大写字母

Pass专属标签!!!

概念:Pass标签也是告诉渲染引擎我们希望怎样来渲染物体

语法:![img](基础和初级 1-8.assets/_CopyPix_1610468434_7.png)

类型:

![img](基础和初级 1-8.assets/_CopyPix_1610469466_13.png)

特殊的Pass

  • UsePass:复用其他UnityShader中的Pass
  • GrabPass:抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理

3.5 Shader结构:Fallback

  • 所有SubShader都不能运行,使用该Shader
  • Fallback还会影响阴影的投射,在渲染阴影纹理中,Unity会在每个UnityShader中寻找一个阴影投射的Pass。Fallback使用的内置Shader中包含了这样一个通用的Pass

语法:![img](基础和初级 1-8.assets/_CopyPix_1610469581_14.png)

例子:Fallback "VertexLit"

其他语义:有时间自行完善

  • 扩展编辑界面:CustomEditor
  • 命令分组:Category

3.6 着色器分类

表面着色器

Surface Shader

概念:Unity自己创造的一种着色器代码类型,是对顶点/片元着色器更高层的抽象,本质仍是顶点/片元着色器,定义在SubShader块(而非Pass块)中的CGPROGRAM和ENDCG之间。可以使用Cg/HLSL几乎所有语法。

![image-20211027141818806](基础和初级 1-8.assets/image-20211027141818806.png)

顶点/片元着色器

Vertex/Fragment Shader

概念:本书使用。使用Cg/HLSL语言来编写,更复杂但更灵活。定义在Pass内

3.7 课后答疑

UnityShader != 真正的Shader

image

Ch4.学习Unity所需的数学基础

推荐视频:

4.1 笛卡尔坐标系

二维笛卡尔坐标系:

![image-20210616200043474](基础和初级 1-8.assets/image-20210616200043474.png)

三维笛卡尔坐标系:

  • 基矢量:3个坐标轴
  • 标准正交基:3个坐标轴之间互相垂直,且长度为1
  • 正交基:坐标轴之间互相垂直但长度不为1

判断坐标系:哪只手满足就是哪个坐标系

  • 大拇指:+x
  • 食指:+y
  • 中指:+z

判断旋转正方向:伸出坐标系对应的手,大拇指对准旋转轴正方向,握拳方向就是旋转正方向(左手顺时针,右手逆时针)

4.2 点和矢量

点:n维空间中的一个位置,没有大小、宽度的概念

矢量(向量):n维空间中一种包含了模和方向的有向线段

标量:只有模,没有方向

规定:

![image-20211027144859926](基础和初级 1-8.assets/image-20211027144859926.png)

矢量运算:

  • 矢量和标量乘法/除法:image

  • 矢量的加法和减法:image

  • 矢量的模:![image-20210616200632856](基础和初级 1-8.assets/image-20210616200632856.png)

  • 单位矢量:![image-20210616200645116](基础和初级 1-8.assets/image-20210616200645116.png)

  • 点积

    • 公式1:![img](基础和初级 1-8.assets/_CopyPix_1610596172_1.png)
    • 公式2:![img](基础和初级 1-8.assets/_CopyPix_1610596190_2.png)
  • 叉积

    • 公式:![img](基础和初级 1-8.assets/_CopyPix_1610596306_4.png)
    • 模:![img](基础和初级 1-8.assets/_CopyPix_1610596321_5.png)
    • 方向:确定坐标系,根据左(右)手法则,从a到b握拳,拇指方向为叉积结果方向

4.3 矩阵

定义

  • mxn个标量组成的长方形数组

行向量和列向量:

image

矩阵运算

  • 矩阵和标量相乘:image

  • 矩阵和矩阵相乘:image

    image

矩阵乘法性质

  1. 矩阵乘法不满足交换律:![image-20210616201311692](基础和初级 1-8.assets/image-20210616201311692.png)
  2. 矩阵乘法满足结合律:![image-20210616201304986](基础和初级 1-8.assets/image-20210616201304986.png)

特殊矩阵

1.方块矩阵:简称方阵,行和列数目相等

  • 对角元素:行和列号相等的元素。如m11

  • 对角矩阵:除对角元素外均为0的矩阵。非常适合次方计算。

![image-20210616201636339](基础和初级 1-8.assets/image-20210616201636339.png)

2.单位矩阵:任何矩阵和它相乘的结果还是原来的矩阵

![image-20210616201644840](基础和初级 1-8.assets/image-20210616201644840.png)

3.转置矩阵:实际是对原矩阵的一种运算。行列对调

  • 性质一:![image-20211027152547410](基础和初级 1-8.assets/image-20211027152547410.png)

  • 性质二:![image-20211027152553314](基础和初级 1-8.assets/image-20211027152553314.png)

4.逆矩阵:满足

![image-20210616201827625](基础和初级 1-8.assets/image-20210616201827625.png)

逆矩阵判断:

  1. 必须是方阵
  2. 行列式不为0

逆矩阵性质:

  1. 逆矩阵的逆矩阵是原矩阵本身(设M可逆)

    ![image-20210616202250777](基础和初级 1-8.assets/image-20210616202250777.png)

  2. 单位矩阵的逆矩阵是它本身

    ![image-20210616202301981](基础和初级 1-8.assets/image-20210616202301981.png)

  3. 转置矩阵的逆矩阵是逆矩阵的转置

    ![image-20210616202310929](基础和初级 1-8.assets/image-20210616202310929.png)

  4. 矩阵串接相乘后的逆矩阵等于反向串接各个矩阵的逆矩阵。可推广到多维

    ![image-20210616202317611](基础和初级 1-8.assets/image-20210616202317611.png)

    ![image-20210616202331557](基础和初级 1-8.assets/image-20210616202331557.png)

5.正交矩阵

  • 如果一个方阵M和它的转置矩阵的乘积是单位矩阵的话,我们就说这个矩阵是正交的

![image-20210616202452202](基础和初级 1-8.assets/image-20210616202452202.png)

等价于

![image-20210616202515644](基础和初级 1-8.assets/image-20210616202515644.png)

推论

- c1 -:表示一个三维向量c1的行展开式

image image

4.4 变换

变换:指的是我们把一些数据,如点、方向矢量甚至颜色等,经过某种方式进行转换的过程

线性变换:指可以保留矢量加和标量乘的变换

![image-20210616203541827](基础和初级 1-8.assets/image-20210616203541827.png)

仿射变换:合并线性变换和平移变换。仿射变换可以使用一个4x4的矩阵表示,为此,我们需要把矢量扩展到四维空间下,就是齐次坐标空间

齐次坐标:四维向量

  • 点:w=1
  • 向量:w=0

1.平移矩阵

![img](基础和初级 1-8.assets/_CopyPix_1610607136_6.png)

2.缩放矩阵

![img](基础和初级 1-8.assets/_CopyPix_1610607147_7.png)

3.旋转矩阵(正交矩阵)

无论坐标轴如何变换,两两之间总是互相垂直,且长度为1,满足上述推论。故旋转矩阵是正交矩阵

  • 绕x轴旋转θ

![img](基础和初级 1-8.assets/_CopyPix_1610607178_8.png)

  • 绕y轴旋转θ

![img](基础和初级 1-8.assets/_CopyPix_1610607202_9.png)

  • 绕z轴旋转θ

![img](基础和初级 1-8.assets/_CopyPix_1610607212_10.png)

约定:先缩放,再旋转,最后平移

4.5 坐标空间变换

  • 变谁就按列展开乘谁,详见P69
    • 第一个”谁”:目标坐标空间,即变换到的空间
    • 第二个”谁”:自己的坐标轴在目标空间下的表示

如果矩阵正交,那么只需转置即可得到逆变换

已知子坐标空间C的3个坐标轴在父坐标空间P下的表示Xc,Yc,Zc,以及原点位置Oc。给定一个子坐标空间一点Ac=(a,b,c),确定其在父空间下的位置Ap。

![img](基础和初级 1-8.assets/_CopyPix_1610608729_11.png)

个人心得

记录于:2021.10.27

根据看的角度不同,分为两种变换类型。二者的区别在于前后是否有其他坐标系参与:

  1. 坐标变换:我们坐标系下的点经过一个矩阵变换到我们坐标系下另一个点
    • 如果不好想象变换过程,可以看变换是如何对基坐标产生影响的,应用于所有的点即可
  2. 基变换:它们坐标系下的点,在我们坐标系下的表示
    • 基变换的逆:我们坐标系下的点,在它们坐标系下的表示(这就说明MVP矩阵是如何实现点在不同空间转换的)

举例说明1:以二维旋转变换为例

image

完善:基变换的逆–2022年7月26日22:20:35

举例说明2:基变换的逆的结论的证明

  • 同时解决了一个问题:为什么从前一个空间,到下一个空间的变换,有按列向量展开为矩阵的说法?
  • 这里没有引入平移维度,由[4.5 坐标空间变换](#4.5 坐标空间变换)暂且认为多引入一列Oc即可得到正确结果,时间原因,暂不举例说明
  • 王炸结论:变换到哪个空间,就用那个空间表示自身的基,按列向量展开基坐标成矩阵。这个矩阵就可以实现坐标空间的变换
image

顶点的坐标变换过程

image

难点:投影矩阵推导

参考链接:https://blog.csdn.net/zhanghua1816/article/details/23121735

并非真正从3D投影到2D,而是投影到NDC (Normalized Device Coordinates) 中,但是投影变换不会进行齐次除法,所以结果并非真正意义上的点,而是一个待处理的四维向量(w分量非0非1,而是和z有关)。所以四维向量需经过齐次除法,才算真正成为NDC中的点。

个人小结:

  • 观察空间 - 投影变换 -> 裁剪空间(手动实现)
  • 裁剪空间 - 齐次除法 -> NDC空间(底层实现)
  • NDC空间 - 屏幕映射 -> 屏幕空间(底层实现)

NDC和裁剪空间并非同一空间!

正交投影矩阵

image image

透视投影矩阵

image image image

4.6 法线变换

详见P86

结论:用变换矩阵的逆转置矩阵变换法线即可得到正确的结果

4.7 Shader内置变量!!!

变换矩阵

image

摄像机和屏幕参数

image image

4.8 答疑解惑(了解)

Cg中的矢量和矩阵类型

矢量初始化:

  • float4 a = float4(1.0, 2.0, 3.0, 4.0);

矩阵初始化(行优先填充):

  • float3x3 M = float3x3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0,7.0, 8.0, 9.0);

$$
\begin{matrix}
1 & 2 & 3 \
4 & 5 & 6 \
7 & 8 & 9 \
\end{matrix}
$$

注意:脚本的Matrix4x4(列优先填充)

获取Unity中的屏幕坐标

法1.frag函数直接声明语义

在片元着色器的输入中声明VPOS(HLSL)或WPOS(Cg)语义

  • 设屏幕分辨率400x300,VPOS、WPOS各分量取值范围如下
  • xy值代表在屏幕空间中的像素坐标
    • x∈[0.5,400.5]
    • y∈[0.5,300.5]
    • z∈[0,1],近平面z为0,远平面z为1
    • 透视:w∈[1/Near,1/Far](正交:w=1。这些值是通过投影矩阵变换后的w分量取倒数得到的)

原点在(0.5,0.5),即像素中心对应的是浮点值

视口坐标:屏幕空间除以屏幕分辨率得到的。左下角(0,0),右上角(1,1)

  • 例子.求视口空间坐标

![img](基础和初级 1-8.assets/_CopyPix_1610615065_15.png)

法2.ComputeScreenPos函数

原理:

image

ComputeScreenPos内部实现

image

核心思想:手动模拟屏幕映射,得到未经齐次除法的视口坐标

例子.

image

不能在顶点着色器中先做齐次除法,具体详见P92,但是写得也不是很明白,只能隐约感觉到其中的奥妙

解释如下:

  • 总之,不能在投影空间(齐次裁剪空间)中直接进行插值,因为这并不是一个线性的空间,而插值往往是线性的

—————– 初级篇 —————–

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

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

作者:聪头

Ch5.开始Unity Shader学习之旅

渲染纹理:

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

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

基本结构

image

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

![img](基础和初级 1-8.assets/_CopyPix_1610638213_2.png)

也可以在Cg变量前加uniform

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

5.2 内置文件和变量

内置包含文件后缀:.cginc

image
  • CGIncludes文件夹

    • 包含了所有的内置包含文件
  • DefaultResources文件夹

    • 包含了一些内置组件或功能所需要的Unity Shader,例如一些GUI元素使用的Shader
  • DefaultResourcesExtra文件夹

    • 包含了所有Unity中内置的Unity Shader
  • Editor文件夹

    • 包含了一个脚本文件,用于定义Unity5引入的Standard Shader所用的材质面板

常用包含文件

image

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

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

常用帮助函数

image

帮助函数整理详见:[6.2 Unity内置函数!!!](#6.2 Unity内置函数!!!)

5.3 语义!!!

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

应用->顶点

![img](基础和初级 1-8.assets/_CopyPix_1610639854_6.png)

顶点->片元

![img](基础和初级 1-8.assets/_CopyPix_1610639919_7.png)

片元输出

![img](基础和初级 1-8.assets/_CopyPix_1610639970_8.png)

5.4 课后答疑

渲染平台差异

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

1
2
3
4
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
uv.y = 1 - uv.y;
#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 标准光照模型

环境光

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

![img](基础和初级 1-8.assets/_CopyPix_1611156059_1.png)

自发光

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

![img](基础和初级 1-8.assets/_CopyPix_1611156067_2.png)

漫反射

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

![img](基础和初级 1-8.assets/_CopyPix_1611156086_3.png)

Half Lambert

  • 使暗部不那么暗

![img](基础和初级 1-8.assets/_CopyPix_1611157347_6.png)

高光反射

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

Phong

image

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

Blinn

image

着色

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

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

6.2 Unity内置函数!!!

书P137

观察者

image

光源

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

其他

image

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

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

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
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

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

Shader "Learning/Sampler6_4"
{
Properties {
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 256.0)) = 20
}

SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct v2f {
fixed4 pos : SV_POSITION;
fixed3 worldNormal : TEXCOORD0;
fixed3 worldPos : TEXCOORD1;
};

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

o.worldNormal = mul(v.normal, unity_WorldToObject);//法线乘逆转置矩阵,从模型->世界

o.worldPos = mul(unity_ObjectToWorld, v.vertex);

return o;

}

fixed4 frag(v2f i) : SV_Target
{
//漫反射
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 lightDir = normalize(_WorldSpaceLightPos0);
fixed3 diffuse = _Diffuse * _LightColor0 * (dot(worldNormal, lightDir) * 0.5 + 0.5);
//环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
////高光-Phone
//fixed3 reflectDir = reflect(-_WorldSpaceLightPos0, worldNormal);
//fixed3 viewDir =normalize( _WorldSpaceCameraPos - i.worldPos);
//fixed3 specular = _LightColor0 * _Specular * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
//高光-Blinn
fixed3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
fixed3 halfDir = normalize(viewDir + lightDir);
fixed3 specular = _LightColor0 * _Specular * pow(saturate(dot(halfDir, worldNormal)), _Gloss);

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

image

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)或副法线
image

切线空间满足右手坐标系

image

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

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

两种法线纹理的比较

image image

两种光照计算的比较

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

效率上:

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

通用性上:

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

采样法线核心代码

  • 未设置成Normal Map,则手动采样
1
2
3
4
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,自动采样
1
2
3
4
5
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方向上的导数,然后从中得到法线即可

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

矩阵元素下划线访问方式

1
2
3
4
5
float3x3 test = float3x3(0,0,0,
0.5,0.5,1,
0,0,0);
float3 finalColor = test._21_22_23;
return half4(finalColor, 1);
image

求逆函数

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
//求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:凹凸映射–切线空间

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

image
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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"
}

image image

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

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
119
120
121
// 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"
}

image

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

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

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

Ch8.透明效果

8.1 透明度测试

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

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

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

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

8.2 透明度混合

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

排序!!!

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

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

image

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

双Pass渲染

解决重叠问题

1
2
3
4
5
Pass
{
ZWrite On
ColorMask 0
}

解决正反面渲染顺序问题

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

混合命令

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

混合因子 !!!

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

image image image

混合操作!!!

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

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

常见混合类型!!!

image image

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

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

  • 原理:
    • 第一个Pass:开启深度写入,但不输出颜色。它的目的仅仅是为了把该模型的深度值写入深度缓冲中
    • 第二个Pass:进行正常的透明度混合,由于上一个Pass已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染
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
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"
}

imageimage

左图为单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相应指令进行透明度混合

 评论