写在前面

未学习完…
一些我不会的概念去参考了RTR以及其他人的笔记进行了适当补充
优秀的笔记参考:
百人计划作业 (yuque.com)——by 晓痕

一. 基础

1.渲染流水线

见ppt

2.数学基础

见shader入门精要,蛮详细的,但是透视投影矩阵的推导没有细讲

3.纹理介绍

纹理是什么?

纹理宏观来讲就是一张图片,是一种可供着色器读写的结构化存储形式

纹理管线

投影函数:纹理投影,通常是在建模过程中,展uv阶段使用的,也就是获取模型空间某顶点对应在纹理空间下的坐标位置,并将纹理空间下这个坐标存储在模型顶点数据中

通讯函数:对上述的纹理坐标进行平移缩放等扩展操作,更新成为新的纹理坐标

纹理采样:也就是我们在片元着色器中的tex2D函数这种操作
**依赖纹理读取:**只要片元着色器中使用的UV采样坐标不是由顶点着色器传来,而是直接读取运算的,就会发生依赖纹理读取,影响性能

以上图片是纹理管线的示例,图中纹理大小为256x256,即0.32*256 ~= 81

纹理采样设置

Filter Mode是纹理在由于变换所导致的拉伸缩放时所采用的一种调整模式

双线性插值:由最邻近的四个点位的像素值插值得来

mipmap

mipmap可以用来解决纹理缩小时导致的纹理采样的颜色丢失等问题

mipmap的生成原理

原理就是基层纹理为原纹理,下一级纹理为原纹理的1/4分辨率,即原纹理中四个相邻像素插值得到一个像素值,只保留该像素,使得原纹理缩小为原来的1/4,再下一级同理。根据等比数列求和可知,最终mipmap存的纹理集大小仅仅比一张原纹理多了1/3的内存

mipmap具体使用时采样level的选择原理

image-20240930200616916image-20240930200655016

在屏幕空间中选取当前像素点右方和上方的两个相邻像素点,分别查询当前点,右方点,上方点,三个点对应在Texture Space纹理空间下的纹理坐标。计算出当前像素点与右方和上方点在纹理空间下的距离,取二者中的最大值,称为L(这里取最大值然后当作一个正方形)。
得出这个最大距离L以后,对其取log2,得到的结果值称为levelD,这个值就是level的选取值
为什么:因为算出屏幕上两个像素间在纹理空间下的距离的值L,我们取log2就是查询这个距离值是2的几次方。假如L是2^2^,说明屏幕上两个相邻像素间在纹理中是四个纹素距离,而mipmap生成的level为2的纹理刚好就是由原像素四个为一组合并为一个像素的缩小纹理图,也就是说,在level为2的纹理图中,两个相邻纹素的距离就是四个原纹素距离,刚好可以对应!

三线性插值

当我们的levelD是一个小数,说明并没有得到准确对应的那个mipmap纹理图,此时使用三线性插值。也就是下取整的levelD纹理图中使用一次双线性插值,得到一个纹理值,再在上取整的levelD纹理图中进行一次双线性插值。两个纹理值再根据levelD的小数部分进行一次线性插值,得到最终的纹理值。

mipmap的缺陷

各向异性过滤

是一种方法,也就是不使用正方形的方式进行mipmap预处理操作,而是基于纹理图像特点在不同方向有不同精度进行mipmap生成
也就是使用各自比例的矩形进行缩小纹理的预处理

积分图Summed-Area Table

各向异性过滤的一种实现方式

没听懂,先放这里

无约束的各向异性过滤——让内存占用也几乎只是多了1/3

以下摘自RTR,这个方法好像就是ue和unity所采用的各向异性过滤的实现方法

EWA过滤

纹理相关的渲染优化

CPU上的优化——使用纹理图集/数组

使用纹理图集就是把大量的小纹理合成一张大的纹理,这样draw call时只需要提交一次纹理,减少频繁改变纹理导致的draw call增加

GPU上的优化——纹理压缩

常见的纹理

CubeMap 立方体纹理

我们从cubemap的立方体中心为原点,选择一个方向发射射线,接触到的点就是采样得到的纹理值。
**具体采样过程:**采样方向矢量三维中选择绝对值最大的那一维,作为我们要取采样的cubemap立方体对应的那个面上的2D纹理图。如上图所示,z值最大,因此我们在cubemap的六个面中应该选择-Z这个面,也就是后方向面的这张纹理贴图!
接下来,我们让x,y分量除以z的绝对值,此时xy转换到了-1~1范围,因此对这个值+1再除以二,将x,y转换到0-1范围,作为后面这张纹理贴图的uv采样坐标!

Bump Map 凹凸贴图

这里目前没有给出具体介绍,请参考unity shader入门精要7.2节凹凸映射

Displacement Map 位移贴图

使用位移贴图的要求:要求表面的顶点数量要很多,如果三角形面的顶点间距离是大于位移贴图定义的顶点距离时(即实际顶点的频率是低于贴图的采样频率),偏移就失效了。
遇到上述情况,可以使用曲面细分,再进行位移贴图采样

4.PC,手机图形API介绍

基础概念

OpenGL 3.0和2.0 ES的一些改变

骁龙Adreno对应的ES版本型号

二. 光照基础

1.颜色空间

光的发送者——光源

光源是产生光的物体。我们对光的认知是一种波长与能量的分布

光的属性——波长

光的属性——能量分布

光的接收者——人眼

相对亮度感知

在阴暗环境下人眼对亮度变化的感知更加明显
比如,在全黑环境下点亮一盏灯的感受比在有一盏灯时点亮100盏灯的感受更强烈

人眼HDR

人眼在亮部中仍然可以区分出细节上的区别,在暗部也能区分出细节层次。但目前图形学,或者说目前的摄影都做不到,一张照片很难同时拥有高光细节和暗部细节

人眼感光细胞分布

人眼可以把感知色彩的细胞分为两大类,杆状细胞锥状细胞
杆状细胞:感知亮度,只要有5-14个光子打到杆状细胞就会产生神经信号给大脑
锥状细胞:感知色彩

锥状细胞

人眼的本质

人眼的本质就是光源的接收者,作用是接受外部光线输入,输出神经电信号给大脑

人眼接收光的微积分公式

颜色空间的历史

1800年——色彩的猜想

1905年——Munsell艺术家色彩系统

1931年——CIE建立科学色彩系统

通过不断调整RGB光源的值,直到观察者认为下白板和上白板上颜色一致,这时我们就得到一个特定的颜色对应什么RGB的混合。

可见,这个Y坐标代表的亮度,而xy组成的二维空间就是上图的色域马蹄图

色彩空间的定义

1.色域

决定RGB三原色的坐标位置(形成色彩空间三角形的三个顶点和边界)

2.Gamma

gamma是对色彩空间进行采样的一种方式,或者说对色彩空间的一种切分方式,从色域三角形边界向内部白点方向进行切分

拿shader入门精要中伽马矫正部分来理解,也是是一样的道理——人眼对暗部颜色的区分感知更明显,对亮度高的颜色的感知不那么明显,因此如果gamma = 1的线性方式区分颜色值,会有浪费。我们应该多花一些内存来表示较暗的颜色的颜色值,较亮的可以少一些,因此出现了gamma != 1 的非线性颜色空间切分方式,但是这样非线性色彩空间切分方式会导致我们的颜色混合等操作在非线性空间中进行,这样显然会有问题。为了解决非线性空间问题,我们会使用一种变换,把非线性空间颜色转换成到线性空间中,得到一种Linear Space Color,这样再做操作就没问题了,这个步骤一般是unity帮我们做,这也就是伽马矫正
也就是说,gamma切分了色域,使得颜色更合理存储,但是gamma非线性时会带来我们渲染的问题,因此需要进行伽马矫正,得到线性空间让我们进行渲染

3.白点

白点是色域三角形的中心点,也是颜色明度最亮的点

sRGB色彩空间

常用的色彩空间

2.模型与材质

渲染管线与模型基础

与模型相关的渲染管线

模型携带的信息

材质基础

材质是什么

漫反射

镜面反射

折射

粗糙镜面反射

粗糙镜面折射

多层纹理

次表面散射

多层皮肤模型

改变材质表面的方法

模型携带的数据再渲染中的作用

1.顶点动画

2.纹理动画

渲染的过程中改变纹理坐标,使其采样时发生偏移,来达到动画的效果

水面效果的原理如下:
原理一:光照计算——反射和折射得到颜色再进行扰动的效果
利用法线贴图,改变法线方向,进而改变反射和折射的方向

原理二:水面本身贴图进行uv坐标偏移实现水面移动动画效果
改变uv采样点的位置,产生uv纹理动画效果

3.顶点色

eg,顶点色达到一定的值,就做阴影

eg2:使用模型的顶点色作为mask使用

4.插值:重心坐标

重心坐标插值在光栅化阶段进行,这里的x,y可以理解为屏幕坐标
重心坐标不能保证投影后不变,所以应该先在三维情况下找到重心坐标再插值
利用这个公式可以得到当前像素点的顶点颜色、法线、纹理坐标,然后就能通过周围三个顶点进行插值

5.顶点法线,面法线

面法线本质上还是顶点法线,只是存储方式不同

面法线:三个顶点共用一个法线,插值出来结果相同

顶点法线:每个顶点都有一个发现,插值出来也就不同

扩展:NPR渲染中的描边

作业——copy自 苏格拉没有底

1.顶点色的其他作用
  • 补充:

    • 最常用的:作为一种存储的mask使用(这样可以少使用一张图)
      • e.g1:不想让脸上有菲涅尔效应,就把脸涂黑,乘上顶点色
      • e.g2:不想让某些地方有描边,可以如法炮制
      • e.g3:想把其它信息塞入顶点色。
    • 准则:塞入顶点色的信息必须是线性变化的,如果不是,就要做好精度打折的准备。
  • 可用于预先指定照明、阻光和其他视觉效果。

  • 就是把颜色信息存在顶点里,但是在低模的情况下效果很差。

  • 3ds Max 中的所有对象都具有贴图通道,其中保存关于纹理贴图以及顶点颜色、照明和 Alpha 的信息。网格对象同样具有几何体和顶点选择通道。

    • 主通道为顶点颜色,这可以使对象中的每个顶点都有其自己的颜色,并且在顶点之间自动渐变。此着色默认情况下不可见,但您可以通过“对象属性”设置切换其显示。还可以通过“顶点绘制”修改器等各种功能查看和编辑顶点着色。它可用于预先指定照明、阻光和其他视觉效果。贴图通道数据也可由如游戏引擎等外部应用程序使用。

https://www.ddove.com/edu/chapter/8123.html

2.模型光滑组对法线有什么影响

①先搞清楚光滑组是什么

  • 没有真正的光滑面,所有面都是三角形

  • 光滑组的含义:下面图标出了面的亮度,纯属打比方,不是确切数字,两面之间的过渡就是两面亮度和的平均值,光滑组处理面之间的光照信息,提高它们的亮度、饱和度。

    • 如果两个面一个光滑组是1,另一个是2,就不进行计算
    • 如果他们的光滑组都是1,就会进行光照计算,产生光滑效果,影响最终渲染。
  • 自动光滑:所有面的夹角小于45度的进行光滑

  • 光滑组:通过处理面之间的光照信息来达到光滑效果,是用来设置边缘线的光滑显示的。

  • 网格平滑和涡轮平滑:通过增加面,把面分的更细腻来表达曲度

  • 我们平常说的布线合理,拓朴其实是保持两个三角面的一致性(构成一个四边面的俩三角面)

②光滑组对法线的影响
法线

  • 烘焙法线的意义,就是把高模的法线方向,用一张图(RGB)来存储法线信息,存到低模的表面上。贴上法线贴图的低模,就会在视觉上产生凹凸不平、增加细节的渲染效果,从而看起来像高模一样。Normal Mapping 法线贴图本质上就是一种图片,只是这张图片的用途比较特殊而已。
  • 没有光滑组的话,烘出来的法线贴图是一棱一棱的。一般情况下最少也要给一个光滑组
  • 参考链接中的例子:渐变色的法线贴图在substancepainter会出现黑边情况(光滑组的问题)

光滑组(软硬边)和UV对法线的影响

  • 光滑组相连的模型,法线贴图都存在大渐变色,导致模型的法线效果会很奇怪(平面上有发暗发亮的光影)。当你发现你的模型出现这种渐变时,一定是出现了光滑组的问题。
  • 中间的两个模型出现了不同程度的接缝(第三个模型的接缝非常明显,第二个模型则弱一些)。光滑组和uv统一相连或断开,是不会出现明显接缝的,当遇到接缝问题,优先考虑模型的光滑组和uv是否统一。

3.基础语法介绍——HLSL

1.基本数学运算

radians打错了
noise(x)这里的形参是一个二维向量,对应一个点坐标,返回一个噪声值

2.幂指对函数

ret是尾数,exp是指数,frexp是把一个浮点数存为计算机中二进制浮点数形式,分别得到尾数和指数(二进制形式)

3.三角函数和双曲函数

双曲正余弦的几何定义:
如图中,正余弦的几何定义是单位圆定义的。
双曲正余弦就是单位圆上对应的正余弦边延长与双曲线相交点所定义

4.数据范围类

fmod(x,y) = x % y 这里是取模,符号跟被除数x相同
saturate几乎不耗资源,clamp有一定开销(弹幕)

5.类型判断类

6.向量与矩阵类

这里的distance的定义使用了点之间的距离公式计算,但是也可以用于向量计算(至于其是否有意义就不清楚了)

7.光线运算类

lit函数的具体说明如下:使用了Blinn-Phong光照模型

8.1D纹理查找

ddx和ddy解析

ddx,ddy在上图tex2D中的作用是,只有相邻屏幕像素对应到纹理纹素差值大于给定的ddx值时,才进行采样,ddy同理

9.2D纹理查找

10.3D纹理查找

11.CubeMap纹理查找

作业

ddx和ddy的实际用处——苏格拉没有底

4.传统经验光照模型

局部光照模型

局部光源的定义

只关心直接光照部分,即光源发出到物体表面经过反射至摄像头的光线

局部光照可以分为四个部分 漫反射,高光反射,环境光,自发光

漫反射

Lambert余弦定理

高光反射

环境光

自发光

局部光照的整体效果

1.Lambert模型

2.Phong模型

3.Blinn-Phong模型

Phong和Blinn-Phong模型的区别

着色模型

Gouraud Shading——顶点着色

Flat Shading——面着色

Phong Shading——片元着色

逐片元着色

IBL——基于图像的光照

基于图像的光照(Image based lighting, IBL)是一类光照技术的集合。其光源不是如[前一节教程](https://learnopengl-cn.github.io/07 PBR/02 Lighting/)中描述的可分解的直接光源,而是将周围环境整体视为一个大光源。IBL 通常使用(取自现实世界或从3D场景生成的)环境立方体贴图 (Cubemap) ,我们可以将立方体贴图的每个像素视为光源,在渲染方程中直接使用它。这种方式可以有效地捕捉环境的全局光照和氛围,使物体更好地融入其环境。

由于基于图像的光照算法会捕捉部分甚至全部的环境光照,通常认为它是一种更精确的环境光照输入格式,甚至也可以说是一种全局光照的粗略近似。基于此特性,IBL 对 PBR 很有意义,因为当我们将环境光纳入计算之后,物体在物理方面看起来会更加准确。

5.Bump Map的改进

什么是凹凸映射

凹凸映射的常见方法是法线映射,视差映射,浮雕映射

Normal Mapping 法线映射

切线空间到世界空间转换

c:children子空间
p:parent父空间
xc:子空间中x坐标轴在父空间下的表示
yc:子空间中y坐标轴在父空间下的表示
Oc:子空间中坐标原点在父空间下的表示
该矩阵就是子空间->父空间的转换矩阵

对于切线空间来说,切线Tangent是x轴,副切线Binormal是y轴,法线Normal是z轴
所以切线空间到世界空间变换矩阵一般记作TBN矩阵,就是把T,B,N作为x,y,z如上 列组合起来
世界空间到切线空间变换就是TBN的转置,因为TBN是个正交阵

切线空间的好处

法线存在各个空间里都可以

存放在切线空间下的好处:

    • 自由度高。
      • 模型空间下是绝对法线信息(仅可以用在创建它时的那个模型)
      • 而切线空间下的是相对法线信息,是对当前物体法线的扰动。(可以复用)
    • 可进行uv动画。
      • 比如:移动uv坐标来实现凹凸移动效果
    • 可以重用法线纹理。
      • 比如:一个立方体,6个面可以用一张法线贴图
    • 可压缩。
      • 由于切线空间下贴图中法线的Z方向总是正方向(模型空间下可以是负的),那么我们只存XY(切线和副切线)就能推出Z(法线)了,可以少存一个。

Unity中法线贴图的压缩格式

上面写错了,GA存储的是法线的yx分量

  • 关于解码法线贴图时要做一个“*2-1”的操作的解释

      • 法线纹理存的就是表面法线,由于法线分量范围为[-1,1],像素的分量范围为[0,1] 因此我们通常需要做一个映射:pixel=(normal+1)/2,解码时就要做一个反向的操作
    • 关于normal.xy *= scale;的解释
      • 是对法线的扰动效果进行缩放

Parallax Mapping 视差映射

要求是顶点数量较多的模型,效果才会显著

视差映射基本实现

陡峭视差映射

比如我要采样视角看向的A点的实际值,查询高度图发现其高度是约0.75,但是视线达到的点高度为0,因此逐层步进,直到一个点的高度大于等于0.75时停止,该点的uv值就是A的实际值

Relief Mapping 浮雕映射

浮雕是时差的改良版,减少开销,增加精确度。但仍然是一种近似和欺骗眼睛的效果

6.Gamma矫正

什么是Gamma矫正

为什么需要Gamma编码和矫正

所以编码时是一个上凸的曲线,为了给暗部颜色更多空间存储,精度更大!

请见下图,实际上下面那条线才是物理上亮度均匀增加的,但我们的人眼却不这么认为!

韦伯定律:我们对某种事物的感觉的刺激量会随着刺激的增加而减小感知变化的程度

CRT——阴极射线显像管

CRT的特性自动给我们进行了gamma矫正,因此我们使用gamma编码后再用CRT输出就是很合理的色彩,非常巧

中灰值

值得注意的是,在gamma编码和矫正都确定的条件下,我们取的一个中灰值,在人眼观察下也不是一个特定值
请见下二图,A,B区域实际上是同样的颜色和亮度

线性工作流

Unity中选择颜色空间

SubstancePainter导出的贴图

PhotoShop导出的贴图

7.LDR和HDR

动态范围

动态范围 = 最高亮度 / 最低亮度

基本概念

HDR——High Dynamic Range 高动态范围
LDR——Low Dynamic Range 低动态范围

将自然界中很亮的光如太阳光颜色(HDR中的亮度),转换到屏幕能显示的最大亮度值(LDR中的亮度),这个过程叫Tone Mapping

为什么需要HDR

1.为了超过1的亮度的色彩也能有很好的细节表现

2.可以利用HDR超过1的颜色值实现光晕效果(Bloom)!

Unity中的HDR

HDR和Bloom

1.渲染原图

2.渲染超出阈值的高光颜色的图

3.对高光颜色图进行高斯模糊

4.叠加高斯模糊后的高光图和原图得到最终结果

Unity中Bloom的流程

在第一步down sample处计算高光的像素,然后不停的做down sample并存在rt里,到达一定次数后(由参数控制),再一步步up scale回去,在这个过程中会将之前的rt加入,一步步up sample回到原来

HDR和Tone Mapping

上面图片中,底下那张暗图是线性ToneMapping映射,可见效果极差!

LUT——Look Up Table

简单的理解:就是滤镜,通过LUT,你可以将一组RGB值输出为另一组RGB值,从而改变画面的曝光与色彩

和ToneMapping不同,LUT是在LDR之间做变化。 而ToneMapping是对HDR做变换的。

和ToneMapping不同,LUT是在LDR之间做变化。 而ToneMapping是对HDR做变换的。

8.Flow Map的实现

什么是FlowMap

flowmap向量场偏移uv后采样结果如下:

FlowMap的Shader实现

使用UV - time

为什么是相减?
先来看看 uv+time 的情况(u,v) + (time,0) :模型上某个点: 随着time增加,采样到的像素越远
视觉上可以形容为:更远距离的像素偏移向该点,视觉效果和我们直观认识到的运算法则是相反的,所以需要相减

uv偏移并没有改变顶点位置,只是采样到了更远的像素

我们需要更好的运动方向,正确的方法是从flowMap获取流动方向

颜色值范围是[0,1]的,而方向向量的范围都是[-1,1],因此我们就需要使用映射手段(即乘以2,减去1),这样才能从FlowMap获取我们的流向(之前推导mvp矩阵时也用过这个方法)

拿到Time还需要使用frac函数取其小数部分,以达到我们的周期循环效果
但frac()函数有个问题,那就是周期的有断层 人们希望0.99循环 到1的断层去掉 这时候有个聪明人想到了加权平均

用两条有周期有规律变换的线 蓝紫两线的加权不断变换使断层效果消失 x=0.5时蓝线权重1 紫线权重0 x=1时蓝线权重0紫线权重1

代码:

(片元着色器部分)

FlowMap纹理的制作工具和方法

Flowmap painter

Houdini制作FlowMap——(未看)

待填坑…没学过Houdini

9.GPU硬件架构

写得很好的笔记——晓痕

GPU硬件架构笔记 (yuque.com)

GPU是什么

GPU 全称是 Graphics Processing Unit 图形处理单元
GPU是显卡最核心的部件,但我们经常把gpu和显卡混用

GPU架构的关键发展历史

GPU微观物理结构

GPU完整的渲染流程

Early-Z

GPU的优化技术

SIMD

SIMT

SIMT时,相当于每一个Core都是上面SIMD的for循环中的一次循环
SIMD中跑完一遍完整的for循环得到的结果和SIMT中一步指令得到的结果相同

co-issue

GPU资源机制

GPU的内存架构

GPU和CPU类似,也有多级缓存结构:寄存器,L1缓存,L2缓存,GPU显存,系统显存
速度从左到右依次变慢

由此可见,shader直接访问寄存器,L1,L2缓存比较快,但访问纹理,常量缓存和全局内存非常慢,会造成很高的延迟

Shader运行在硬件层面的理解

三. 进阶应用

1.深度测试和模板测试

1.模板测试 Stencil Test

模板测试的初步理解和效果

左图为颜色缓冲区,中间为模板缓冲区,我们人为设置部分像素位置模板值为1
最后我们之渲染模板值为1的像素,其他都为黑色,就得到了右图

一些模板测试可以实现的效果:

作者:MinionsArt
连接:https://www.patreon.com/posts/14832618

模板测试是什么

1.从渲染管线来看,经过片元着色器后的阶段

Pixel Ownership Test:测试哪些像素是有使用权限的。比如unity操作界面中,你的整个编辑器中只有game和scene的窗口部分会渲染游戏画面,因为只有这部分屏幕区域的像素有使用权限

Scissor Test:可以对要渲染的像素部分进行自定义规则的裁剪,比如你设置只渲染左上角

Alpha Test:透明度测试,给定片元透明度加减某参数结构小于0时会被剔除

2.从逻辑上理解
3.从概念上理解

Unity Shader中使用模板测试

ZFail——模板测试通过但是深度测试未通过时执行什么操作

左边 是当前的Ref参考值
右边 是当前模板buffer中存储的值

更新值就是stencilOperation

渲染顺序:先渲染正常场景中的物体,再渲染写入模板值的Mask面,最后渲染模板测试中要部分渲染部分剔除的物体

模板测试总结:

模板测试扩展

模板测试Demo

请见他人笔记
图形 3.1 深度与模板测试 (yuque.com)

2.深度测试 Z Test

深度测试是什么

1.从逻辑上理解
2.从概念上理解

深度测试,就是针对当前对象在屏幕上(准确来讲是framebuffer中)所对应的像素点,将该对象该片元的深度值与当前该片元对应的像素点存储的深度值进行比较,如果通过了,本对象在该像素点才会将颜色写入该像素点的颜色缓冲区,否则不会写入颜色缓冲区

2.从渲染的发展历史理解

深度缓冲区 Z-Buffer

Z Write

Z Test的比较函数

渲染队列——Unity中

简述Early-Z技术

因为Z Test是在片元着色器之后进行的,会有大量浪费!

Early-Z就是Z-Cull 进行初步的遮挡剔除
Z Test就是Z-Check 确保最终遮挡关系正确

Early-Z的具体技术讲解在后续内容

Z-Buffer中的深度值为什么是非线性的?

正确的投影特性的非线性深度方程是和1/z成正比的,这样基本上做的是在Z很近的时候是高精度和Z很远的时候是底精度。这样就是模拟了人眼观察,近处的物体很清晰,而远处的物体很模糊。

参考LearnOpenGL-深度测试章节

深度冲突

两个平面或三角形很紧密相互平行,深度缓冲区不具有足够的精度以至于无法得到哪一个靠前。导致了着两个形状不断切换顺序出现怪异问题。这被称为深度冲突(Z-fighting),因为它看上去像形状争夺顶靠前的位置。

解决方法

  • 让物体之间不要离得太近。
  • 尽可能把近平面设置得远一些。
  • 放弃一部分性能来获得更高精度的深度值,把默认的24位深度缓冲改成32位。

深度测试Demo

请见他人笔记
图形 3.1 深度与模板测试 (yuque.com)

注意:Unity中比如一个shader有两个Pass,其中一个Pass的渲染队列是Transparent,一个是Opaque
实际上使用这个shader的材质的物体会取二者中小的那个,也就是统一变成了Opaque,也就是Geometry渲染队列
然后Pass渲染按照代码中顺序执行,先执行上面的

深度测试扩展

2.混合模式及剔除

什么是混合模式

混合(Blend)就是把两种颜色混在一起,具体就是把某一像素位置原来存储的原色和当前即将要画上去的颜色通过某种方式或算法混在一起

最终颜色 = Shader计算后的颜色值 * 源因子(SrcFactor)+ 累积颜色 * 目标因子(DstFactor)
累积颜色:GBuffer中存储的颜色值(可以理解为当前物体后面及背景在该像素的颜色)

混合模式有哪些呢

PS中的混合模式

ShaderLab中的混合模式

1.如果颜色某一分量超过1,会被自动截取到1,无需考虑越界问题
2.语法

Blend和Blend Op

常见的Blend命令
BlendOp

决定Blend操作源颜色值和目标颜色值之间如何混合
是Add 加,还是Sub减?
还有很多很多,可以去查API,如果只写了Blend但是没写BlendOp,默认BlendOp为Add

Unity里Blend枚举变量序列化语法
各种混合效果实例
Normal正常
Darken变暗
Multipy正片叠底
Screen滤色
Lighten变亮
LinearDodge线性减淡
ColorBurn颜色加深

剔除

法线剔除:也被称为背面消隐。根据法线朝向判断哪个面被剔除,可以用来控制是否双面渲染。
语法:Cull Off/Front/Back

面裁剪:clip()函数,将输入参数小于0的对应的片元在片元着色器直接丢弃(discard),常用于制作溶解,裁剪效果
语法:clip(); 默认会裁切小于0的部分

3.曲面细分着色器与几何着色器

渲染管线中的顺序

Tess——曲面细分着色器
又分为Hull Shader——细分控制着色器
Tessellation Primitive Generator——细分图元生成器,硬件完成
Domain Shder——细分计算着色器

曲面细分着色器 Tessellation Shader

TESS的输入和输出

TESS的渲染流程

重心空间就是以重心为原点的坐标空间

Hull Shader部分

1.Tessellation Factor 参数
决定将一条边分成几个部分,有如下三种切分的方法(算法)

区别:
equal_Spacing:将一条边等分,Subdivide参数是多少,就是多少等分
fractional_even_Spacing:最小值是2,Subdivide参数向上取最近的偶数,将周长分为中间分段为等长的Subdivide - 2段,最左和最右两段为剩下的长度分出来的部分(因为Subdivide可取小数,所以这两段长度和小数有关系),目的是让细分更平滑
fractional_odd_Spacing:最小值为1,Subdivide参数向上取最近的奇数,将周长分为Subdivide - 2的等长的部分,以及两端不等长的部分,目的是让细分更平滑

2.Inner Tessellation Factor 参数

内部细分因素,当该参数为3时,无论上面的Tessellation Factor怎样去进行切分的,我们把三角形切分为三等分,然后分别找最近的两个切分的点,做其延长线,其焦点便是在新内部三角形的一个点;
(概括下就是取边上点的垂线的延长线做交点,直至最后无交点或者交于中心一点)

曲面细分着色器的Demo

1.细分一个Quad

着重观察Hull Shader中参数对细分的影响

//曲面细分Demo1
Shader "Unlit/TessShader"
{
Properties
{
_TessellationUniform ("TessellationUniform", Range(1, 64)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
// 定义两个函数 hull domain
#pragma hull hullProgram
#pragma domain ds

#pragma vertex tessvert
#pragma fragment frag


#include "UnityCG.cginc"
// 引入曲面细分的头文件
#include "Tessellation.cginc"

#pragma target 5.0

struct VertexInput
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct VertexOutput
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};


// 这个函数应用在domain函数钟,用来空间转换的函数
VertexOutput vert (VertexInput v)
{
VertexOutput o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.normal = v.normal;
o.tangent = v.tangent;
return o;
}

// 有些硬件不支持曲面细分着色器,定义了该宏就能够在不支持的硬件上不会变粉,也不会报错
#ifdef UNITY_CAN_COMPILE_TESSELLATION
// 顶点着色器结构的定义
struct TessVertex
{
float4 vertex : INTERNALTESSPOS;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};

struct OutputPatchConstant
{
// 不同的图元,该结构会有所不同
// 该部分用于Hull Shader里面
// 定义patch的属性
// Tessellation Factor和Inner Tessellation Factor
float edge[3] : SV_TESSFACTOR;
float inside : SV_INSIDETESSFACTOR;
};

// 顶点着色器函数
TessVertex tessvert(VertexInput v)
{
TessVertex o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
o.uv = v.uv;
return o;
}

// 定义曲面细分的参数
float _TessellationUniform;
OutputPatchConstant hsconst (InputPatch<TessVertex, 3> patch)
{
OutputPatchConstant o;
o.edge[0] = _TessellationUniform;
o.edge[1] = _TessellationUniform;
o.edge[2] = _TessellationUniform;
o.inside = _TessellationUniform;
return o;
}

[UNITY_domain("tri")] // 确定图元,quad,triangle等
[UNITY_partitioning("fractional_odd")] // 拆分edge的规则, equal_spacing,fractional_odd,fractional_even
[UNITY_outputtopology("triangle_cw")]
[UNITY_patchconstantfunc("hsconst")] // 一个patch一共三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)] // 不同的图元对应不同的控制点

// 定义hullshaderV函数
TessVertex hullProgram (InputPatch<TessVertex, 3> patch,uint id : SV_OutputControlPointID)
{
return patch[id];
}

[UNITY_domain("tri")] // 同样需要定义图元
// 进行空间转换,将切线空间下的顶点转换至模型空间 bary:重心空间下的顶点位置信息
VertexOutput ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex, 3> patch, float3 bary : SV_DOMAINLOCATION)
{
VertexInput v;
v.vertex = patch[0].vertex * bary.x + patch[1].vertex * bary.y + patch[2].vertex * bary.z;
v.normal = patch[0].normal * bary.x + patch[1].normal * bary.y + patch[2].normal * bary.z;
v.tangent = patch[0].tangent * bary.x + patch[1].tangent * bary.y + patch[2].tangent * bary.z;
v.uv = patch[0].uv * bary.x + patch[1].uv * bary.y + patch[2].uv * bary.z;
VertexOutput o = vert(v);
return o;
}
#endif

fixed4 frag (VertexOutput i) : SV_Target
{
return float4(1.0, 1.0, 1.0, 1.0);
}
ENDCG
}
}
}

2.细分着色器配合displacement贴图
//曲面细分Demo2:与置换贴图结合使用
Shader "Unlit/Tess_Diss_Shader"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_DisplacementMap ("DisplacementMap", 2D) = "gray" {}
_DisplacementStrength ("DisplacementStrength", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 5)) = 0.5
_TessellationUniform ("TessellationUniform", Range(1, 64)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
LOD 100

Pass
{
CGPROGRAM
// 定义两个函数 hull domain
#pragma hull hullProgram
#pragma domain ds

#pragma vertex tessvert
#pragma fragment frag


#include "UnityCG.cginc"
#include "Lighting.cginc"
// 引入曲面细分的头文件
#include "Tessellation.cginc"

#pragma target 5.0

sampler2D _MainTex; float4 _MainTex_ST;
sampler2D _DisplacementMap; float4 _DisplacementMap_ST;

float _DisplacementStrength;
float _Smoothness;
float _TessellationUniform;

struct VertexInput
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct VertexOutput
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float4 worldPos : TEXCOORD1;
half3 tspace0 : TEXCOORD2;
half3 tspace1 : TEXCOORD3;
half3 tspace2 : TEXCOORD4;
};


// 这个函数应用在domain函数钟,用来空间转换的函数
VertexOutput vert (VertexInput v)
{
VertexOutput o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// Displacement 由于并不是在Fragment shader中读取图片,GPU无法获取mipmap信息,
// 因此需要使用tex2Dlod来读取图片,使用z坐标作为mipmap的level,这里取了0
float Displacement = tex2Dlod(_DisplacementMap, float4(o.uv.xy, 0.0, 0.0)).g;
Displacement = (Displacement - 0.5) * _DisplacementStrength;
v.normal = normalize(v.normal);
v.vertex.xyz += v.normal * Displacement;

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

// 计算切线空间转换矩阵
half3 vNormal = UnityObjectToWorldNormal(v.normal);
half3 vTangent = UnityObjectToWorldDir(v.tangent.xyz);
half tangentSign = v.tangent.w * unity_WorldTransformParams.w;
half3 vBitangent = cross(vNormal, vTangent) * tangentSign;
// output the tangent space matrix
o.tspace0 = half3(vTangent.x, vBitangent.x, vNormal.x);
o.tspace1 = half3(vTangent.y, vBitangent.y, vNormal.y);
o.tspace2 = half3(vTangent.z, vBitangent.z, vNormal.z);
return o;
}

// 有些硬件不支持曲面细分着色器,定义了该宏就能够在不支持的硬件上不会变粉,也不会报错
#ifdef UNITY_CAN_COMPILE_TESSELLATION
// 顶点着色器结构的定义
struct TessVertex
{
float4 vertex : INTERNALTESSPOS;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};

struct OutputPatchConstant
{
// 不同的图元,该结构会有所不同
// 该部分用于Hull Shader里面
// 定义patch的属性
// Tessellation Factor和Inner Tessellation Factor
float edge[3] : SV_TESSFACTOR;
float inside : SV_INSIDETESSFACTOR;
};

// 顶点着色器函数
TessVertex tessvert(VertexInput v)
{
TessVertex o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
o.uv = v.uv;
return o;
}

// 定义曲面细分的参数
//float _TessellationUniform;
OutputPatchConstant hsconst (InputPatch<TessVertex, 3> patch)
{
OutputPatchConstant o;
o.edge[0] = _TessellationUniform;
o.edge[1] = _TessellationUniform;
o.edge[2] = _TessellationUniform;
o.inside = _TessellationUniform;
return o;
}

[UNITY_domain("tri")] // 确定图元,quad,triangle等
[UNITY_partitioning("fractional_odd")] // 拆分edge的规则, equal_spacing,fractional_odd,fractional_even
[UNITY_outputtopology("triangle_cw")]
[UNITY_patchconstantfunc("hsconst")] // 一个patch一共三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)] // 不同的图元对应不同的控制点

// 定义hullshaderV函数
TessVertex hullProgram (InputPatch<TessVertex, 3> patch,uint id : SV_OutputControlPointID)
{
return patch[id];
}

[UNITY_domain("tri")] // 同样需要定义图元
// 进行空间转换,将切线空间下的顶点转换至模型空间 bary:重心空间下的顶点位置信息
VertexOutput ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex, 3> patch, float3 bary : SV_DOMAINLOCATION)
{
VertexInput v;
v.vertex = patch[0].vertex * bary.x + patch[1].vertex * bary.y + patch[2].vertex * bary.z;
v.normal = patch[0].normal * bary.x + patch[1].normal * bary.y + patch[2].normal * bary.z;
v.tangent = patch[0].tangent * bary.x + patch[1].tangent * bary.y + patch[2].tangent * bary.z;
v.uv = patch[0].uv * bary.x + patch[1].uv * bary.y + patch[2].uv * bary.z;
VertexOutput o = vert(v);
return o;
}
#endif

fixed4 frag (VertexOutput i) : SV_Target
{
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 tnormal = UnpackNormal(tex2D(_DisplacementMap, i.uv));
half3 worldNormal;
worldNormal.x = dot(i.tspace0, tnormal);
worldNormal.y = dot(i.tspace1, tnormal);
worldNormal.z = dot(i.tspace2, tnormal);
float3 albedo = tex2D(_MainTex, i.uv).rgb;
float3 lightColor = _LightColor0.rgb;
float3 diffuse = albedo * lightColor * DotClamped(lightDir, worldNormal);
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
float3 halfDir = normalize(lightDir + viewDir);
float3 specular = albedo * pow(DotClamped(halfDir, worldNormal), _Smoothness * 100);
float3 result = diffuse + specular;
return float4(result, 1.0);
}
ENDCG
}
}
}

几何着色器 Geometry Shader

GS的输入和输出

输入:图元Primitive(三角形,矩形,线,点 等)根据图元的不同,shader中会传入对应不同数量的顶点

输出:图元Primitive,一个或多个,需要自己从顶点构建,顺序很重要,同时需要定义最大输出的顶点数

几何着色器的Demo

1.三角形生成简单的草
Shader "Unlit/Grass"
{
Properties
{
_TopColor ("上部颜色", Color) = (1.0, 1.0, 1.0, 1.0)
_BottomColor ("下部颜色", Color) = (1.0, 1.0, 1.0, 1.0)
_TranslucentGain ("半透明度", Range(0, 1)) = 0.5
_BladeWidth ("基础宽度", float) = 0.05
_BladeWidthRandom ("随机宽度系数", float) = 0.02
_BladeHeight ("基础高度", float) = 0.5
_BladeHeightRandom ("随机高度系数", float) = 0.3
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
cull off

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma geometry geo // 定义一个几何着色器

#include "UnityCG.cginc"

uniform float4 _TopColor;
uniform float4 _BottomColor;
uniform float _TranslucentGain;
uniform float _BladeWidth;
uniform float _BladeWidthRandom;
uniform float _BladeHeight;
uniform float _BladeHeightRandom;

float rand(float3 co)
{
float f = frac(sin(dot(co.xyz, float3(12.9898, 78.233, 53.539))) * 43758.5453);
return f;
}

float3x3 AngleAxis3x3(float angle, float3 axis)
{
float c, s;
sincos(angle, s, c);

float t = 1 - c;
float x = axis.x;
float y = axis.y;
float z = axis.z;

return float3x3(
t * x * x + c, t * x * y - s *z, t * x * z + s * y,
t * x * x + s * z, t * y * y + c, t * y * z - s * x,
t * x * z - s * y, t * y * z + s * x, t * z * z + c
);
}


struct vertexInput
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct vertexOutput
{
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};


vertexOutput vert (vertexInput v)
{
vertexOutput o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
return o;
}

struct geometryOutput
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};

geometryOutput CreateGeoOutput(float3 pos, float2 uv)
{
geometryOutput o;
o.pos = UnityObjectToClipPos(pos);
o.uv = uv;
return o;
}

[maxvertexcount(3)] // 定义最多顶点数
void geo(triangle vertexOutput IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream)
{
float3 pos = IN[0].vertex;
float3 vNormal = IN[0].normal;
float4 vTangent = IN[0].tangent;
float3 vBinormal = cross(vNormal, vTangent) * vTangent.w;

float height = (rand(pos.xyz) * 2 -1) * _BladeHeightRandom + _BladeHeight;
float width = (rand(pos.xyz) * 2 - 1) * _BladeWidthRandom + _BladeWidth;

// 构建矩阵 构建了TBN矩阵,并且于旋转矩阵相乘,获得转换矩阵
float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0, 0, 1));
float3x3 TBN = float3x3(
vTangent.x, vBinormal.x, vNormal.x,
vTangent.y, vBinormal.y, vNormal.y,
vTangent.z, vBinormal.z, vNormal.z
);
float3x3 transformationMat = mul(TBN, facingRotationMatrix);
geometryOutput o;
//"TriangleStream"类似用来装配三角形的工具,用来输出图元
triStream.Append(CreateGeoOutput(pos+mul(transformationMat,float3(width,0,0)),float2(0,0)));
triStream.Append(CreateGeoOutput(pos+mul(transformationMat,float3(-width,0,0)),float2(1,0)));
triStream.Append(CreateGeoOutput(pos+mul(transformationMat,float3(0,0,height)),float2(0.5,1)));

}

fixed4 frag (geometryOutput i) : SV_Target
{
fixed4 color = lerp(_BottomColor, _TopColor, i.uv.y);
return color;
}
ENDCG
}
}
}

二者搭配实现一个完整的草地(重点)

完整教程链接:https://roystan.net/articles/grass-shader

四. 高级扩展

五. 物理世界

六. 非真实感渲染

七. 其他