门户网站整改情况报告,wordpress演示,知乎 做照片好的网站,网站开发seo要求本系列为作者学习UnityShader入门精要而作的笔记#xff0c;内容将包括#xff1a;
书本中句子照抄 个人批注项目源码一堆新手会犯的错误潜在的太监断更#xff0c;有始无终
总之适用于同样开始学习Shader的同学们进行有取舍的参考。 文章目录 上节复习GPU流水线顶点着色… 本系列为作者学习UnityShader入门精要而作的笔记内容将包括
书本中句子照抄 个人批注项目源码一堆新手会犯的错误潜在的太监断更有始无终
总之适用于同样开始学习Shader的同学们进行有取舍的参考。 文章目录 上节复习GPU流水线顶点着色器裁剪屏幕映射三角形设置三角形遍历简单拓展重心坐标系片元着色器逐片元操作 总结 上节复习
在上节笔记中我们学习了图像渲染流水线的基本过程从应用阶段的CPU处理输出渲染图元到几何阶段再输出屏幕空间的顶点信息到光栅化阶段。
上节详细介绍了应用阶段在这一阶段主要由我们人为控制在程序中设定材质网格纹理着色器等数据的渲染并对不必要的渲染进行剔除。整个渲染流程是从硬盘加载数据到RAM再到VRAM由显卡进行调用RAM中的数据在被调用到VRAM后就会被丢弃除了部分用于进行物理计算的网格信息所有工作在渲染状态中打包成数据准备好后将由CPU进行DrawCall来通知GPU对相应的图元primitives进行处理。那么接下来就是GPU流水线阶段也对应了我们后面的几何阶段和光栅化阶段。
GPU流水线
在GPU流水线阶段开发者无法完全操控整个GPU流水线不过GPU还是为我们提供了一些阶段的控制权如下图所示 上图可以抽象成2个大阶段其中起点是接收应用阶段加载到显存的顶点数据准备好了由CPU通过DrawCall调用GPU。接下来就是GPU的处理流程几何阶段和光栅化阶段。最后处理完成输出为屏幕图像。
接下来是书中的介绍请对照上图查看可能初学会难懂拗口不过没关系先记个名字后续会逐一介绍。
在几何阶段顶点着色器Vertex Shader 是完全可编程的它通常用于实现顶点的空间变换顶点着色等功能。曲面细分着色器Tessellation Shader 是一个可选的着色器用于细分图元。几何着色器Geometry Shader 也是可选的着色器 可以被用于执行逐图元Pre-Primitive 的着色操作或者被用于产生更多图元。其实这三个阶段可以简单理解为点到线到面的处理在我理解里几何阶段就是一种对图元在几何性质的点线面上的规划和渲染
经历了着色器渲染后接着进行裁剪Clipping 这一阶段的目的是将那些不被渲染的顶点裁剪掉并剔除某些三角面元的面片。这一阶段我们可配置但不可编程也就是说我们只决定裁剪哪些而裁剪的算法是固定的。我们可以使用自定义的裁剪平面来配置裁剪区域也可以通过指令控制裁剪三角图元的正面还是侧面。接触过Shader中的Clip的同学们应该更有体会
几何阶段的最后一个流水线是屏幕映射Screen Mapping 这一阶段是不可配置和编程的它负责把每个图元的坐标转换到屏幕坐标中毕竟在从CPU到GPU处理这些数据的时候它们常常不是在同一个坐标空间下的。
接着是光栅化阶段其中的三角形设置Triangle Setup 和三角形遍历Triangle Traversal 阶段都是固定函数Fixed-Function的阶段。接下来的片元着色器Fragment Shader 则是完全可编程的用于实现逐片元Pre-Fragment) 的着色操作在逐片元操作Pre-Fragment Operations 阶段负责执行很多重要的操作例如修改颜色、深度缓冲、进行混合等它不是可编程的但具有很高的可配置性。
上述内容看着复杂但是学习shader必须掌握的接着我们要一一描述这些概念 顶点着色器
顶点着色器Vertex Shader 是流水线的第一个阶段学习几何的时候总是由点到线到面嘛。它的输入来自于CPU对每个输入的顶点都会调用一次顶点着色器。它本身不创建或销毁顶点无法得到顶点与顶点的关系因此它无法判断某几个点是不是同属于一个三角网格。由于其独立性只要计算就好了因此顶点着色器处理速度也很快。
顶点着色器主要完成的工作是坐标变换和逐顶点光照。除此之外还可以输出后续所需的数据。下图展示了顶点着色器对顶点进行坐标变换并计算顶点颜色的过程 我们可以通过坐标变换改变顶点位置从而实现顶点动画例如模拟水面布料等等。但无论我们在顶点着色器中如何改变顶点的位置一个最基本的顶点着色器必须完成的一个工作是把顶点坐标从模型空间转换到齐次裁剪空间 。
在顶点着色器中可能会经常看到如下代码
o.pos mul(UNITY_MVP, v.position);上述代码的功能就是将顶点坐标转化为齐次坐标通常再由硬件做透视算法后最终得到归一化的设备坐标Normalized Device CoordinatesNDC。其实看到归一化不少同学可能就理解了就是为了将不同坐标转化到同一个齐次坐标再进行处理嘛 上图给出的分量范围是OpenGL同时也是Unity使用的NDCz分量范围为[-1,1]。在DirectX中z分量范围为[0,1]
左图是模型空间右图是NDC齐次裁剪坐标空间是四维的
顶点着色器可以有不同的输出方式最常见的输出路径是经光栅化后交给片元着色器进行处理而在现代的Shader Model中还可以将数据发送给曲面细分着色器或几何着色器。
裁剪
为什么裁剪。简单来说我们不需要渲染不在摄像机视野内的物体因此这些部分需要被裁剪掉。
一个图元和摄像机的视野有三种位置关系完全在视野内部分在视野内不在视野内。完全在视野内的处理完就传递给下一个流水线阶段不在视野内的就不传递因为它不显示也就不参与渲染而部分在视野内的则需要进行裁剪处理例如一条线段的一个顶点在视野内而另一个顶点不在视野内那么在视野外部的顶点应该使用一个新的顶点来替代这个新顶点位于线段和视野边界的交点处。 上图的单位立方体代表的是NDC而实际裁剪工作是在裁剪空间内完成的
如上图所示视野边界就是NDC的坐标分量的上界和下界我们绘制NDC的单位立方体则保留在立方体内的部分进行渲染被立方体边缘裁剪的部分产生新顶点。
虽然我们无法通过编程来控制裁剪的过程不过是可以自定义裁剪操作的。
屏幕映射
屏幕映射是几何阶段的最后一步其输出将作为光栅化阶段的输入。这一步接收的输入坐标是归一化的齐次坐标。屏幕映射Screen Mapping 的任务是将每个图元的x和y坐标转换到屏幕坐标系Screen Coordinates) 下。屏幕坐标系是一个二维坐标系它和我们显示画面的分辨率相关。
实际上屏幕映射就是二维上对齐次坐标的缩放变换。屏幕坐标系的最小坐标是左下角x1y1而最大坐标是右上角x2y2与图元的齐次坐标系是不一样的。 屏幕映射对齐次坐标进行了x和y分量上的缩放变换并且对应的坐标也改变了其中x1x2且y1y2
那么z轴呢z跑哪里去了屏幕映射不会对z轴进行任何处理屏幕坐标会和z轴一起构成一个新坐标系称为窗口坐标系(Window Coordinates) ,这些值会被一起传递到光栅化阶段。
屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及离这个像素有多远。
此外OpenGL和DirectX的屏幕坐标也存在差异OpenGL将屏幕左下角作为最小窗口值而DirectX将屏幕右下角作为最小窗口值。 如果你发现得到的图像是倒转的那么很有可能就是这个原因造成的。
三角形设置
三角形设置是光栅化的第一个阶段上一个阶段屏幕映射给出了屏幕坐标系下的顶点信息和其他额外信息例如深度值z坐标)法线方向视角方向等。光栅化阶段有两个重要的目标计算每个图元覆盖了哪些像素以及为这些像素计算它们的颜色。
三角形设置会计算光栅化一个三角网格所需的信息。具体来说在几何阶段输出的都是三角网格的顶点即我们所得到的是三角形网格每条边的两个端点。但如果需要得到整个三角网格对像素的覆盖情况我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程叫做三角形设置。
说白了说人话就是连线把点连成三角形或者边。
三角形遍历
三角形遍历阶段会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话就会生成一个片元fragment 。而找到哪些像素被三角网格覆盖的过程就是三角形遍历这个阶段也被称为扫描变换Scan Conversion 我想是因为扫描线算法。
根据三角形设置的计算结果来判断每个像素是否被一个三角网格覆盖并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。下图展示了三角形遍历阶段简化的计算过程 这一步的输出结果得到一个片元序列。一个片元并不是真正意义上的像素而是包含了很多种状态的集合这些状态用于计算每个像素的最终颜色最终结果。状态包括且不限于屏幕坐标深度信息以及其他从几何阶段输出的顶点信息例如法线、纹理坐标等。
图元和片元虽然在英文上是fragment如果翻译成碎片很容易被误解为个体实际上它们应当被视为多种状态的集合一个元包括了很多的信息而不仅仅是某个图形或者某个像素
简单拓展重心坐标系
推荐阅读计算机图形学 1重心坐标系Barycentric coordinate system详解
上图展示的片元颜色渲染我们看到重心插值的深度为-10。既然要计算插值如果我们以三角形某一个顶点为原点去构建一个直角坐标系再计算显然并不是那么好最简单的方案构建一个非正交的坐标系这个坐标系就是重心坐标系请看下图 上图三角形以abc代表顶点。假设我们以点 a a a为原点那么 a b → b − a \overrightarrow {ab} b-a ab b−a a c → c − a \overrightarrow {ac} c-a ac c−a。如果以 a b → a c → \overrightarrow {ab} \overrightarrow {ac} ab ac 作为基向量建立坐标系。 p p p点为三角形的重心设 p p p点的坐标为 ( β , γ ) (\beta,\gamma) (β,γ),那么点 p p p的坐标表示即为 p a β a b → γ a c → p a \beta \overrightarrow {ab} \gamma \overrightarrow {ac} paβab γac p a β ( b − a ) γ ( c − a ) p a \beta (b-a) \gamma(c-a) paβ(b−a)γ(c−a) p ( 1 − β − γ ) a β b γ c p (1-\beta - \gamma)a \beta b \gamma c p(1−β−γ)aβbγc 因此令 ( 1 − β − γ ) α (1-\beta - \gamma) \alpha (1−β−γ)α 即为 p ( α , β , γ ) α a β b γ c p(\alpha,\beta,\gamma) \alpha a \beta b \gamma c p(α,β,γ)αaβbγc 其中 α β γ 1 \alpha\beta \gamma 1 αβγ1
好了这样重心坐标系就建立好了。在这个坐标系下三角形内任意一点的位置可视为三个顶点的线性组合通过重心坐标系我们可以很简单判断某个点是否在三角形内部只需 0 α 1 , 0 β 1 , 0 γ 1 0\alpha1,\newline 0\beta1,\newline 0\gamma1\newline 0α1,0β1,0γ1即可 当 α β γ 1 3 \alpha \beta \gamma \frac{1}{3} αβγ31时则为重心位置。 片元着色器
片元着色器Fragment Shader 是另一个非常重要的可编程着色器阶段在DirectX中片元着色器也被称为像素着色器Pixel Shader) 但是我们说过片元包含像素但不等同于像素所以片元着色器是更适合的名字。
前面的光栅化阶段实际并不影响屏幕上每个像素颜色而是产生一系列数据信息用于描述一个三角形网格是怎样覆盖每个像素的而每个片元负责存储这一系列信息。真正会对像素产生影响的是逐片元操作Pre-Fragment Operation阶段 。
片元着色器的输入是三角形遍历阶段对顶点信息进行插值后得到的结果更具体的说是对顶点着色器的输出数据进行插值后得到的。而片元着色器的输出是一个或多个的颜色值如下图所示。
在这一阶段会完成许多重要的渲染技术其中最重要的技术之一就是纹理采样为了在片元着色器进行纹理采样我们会在顶点着色器阶段输出每个顶点对应的纹理坐标经过插值之后就能得到其覆盖的每个片元的纹理坐标了。
虽然片元着色器很重要但其局限性在于仅能影响单个片元。也就是说当执行片元着色器时它不可以将自己的任何结果直接发送给它的邻居相邻的其他片元。除了当片元着色器可以访问到导数信息gradient或者说derivative时例外本章拓展阅读部分补充。
逐片元操作
最后一步是逐片元操作在DirectX中称为输出合并阶段Output-Merger)。最主要的目的还是Merge合并合并的目标就是每一个片元。
这一阶段有几个主要任务
决定每个片元的可见性。这涉及很多测试工作例如深度测试、模板测试等。如果一个片元通过了所有的测试就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并或者说是混合。
首先要进行的就是片元测试这些测试决定了哪些片元可以被渲染哪些片元会被舍弃。简单来说就是一个资格考试淘汰不合格的片元。只要有一个测试没通过就会被舍弃之前为这个片元做的一切工作都会白费。只有通过测试的片元才有资格和颜色缓冲区合并。 书中给出了模板测试和深度测试的简化流程图
片元所需要经历的模板测试和深度测试这两大测试都是可以由开发者自行配置的。在模板测试中GPU首先读取使用读取掩码模板缓冲区中该片元位置的模板之然后将该值与参考值也使用读取掩码读取进行比较判断是否舍弃(舍弃条件可以是小于或者大于等于。然后根据模板测试和深度测试结果来修改模板缓冲区。这个修改操作也是由开发者指定的模板测试通常用于限制渲染的区域另外还有例如渲染阴影轮廓渲染等高级用法。
如果片元通过模板测试那么还会进行深度测试。这个测试同样是高度可配置的GPU会将片元的深度值与深度缓冲区的深度值进行比较。比较舍弃和上述模板测试一样可以定义通常是小于等于保留大于等于舍弃。因为我们想渲染离摄像机更近深度更低的物体。与模板测试不同的是模板测试在保留或舍弃时都可以修改模板缓冲区但是如果一个片元没有通过深度测试它没有权利更改深度缓冲区的值如果它通过了测试开发者可以指定是否用这个片元的深度值覆盖掉原有片元的深度值这是通过开启/关闭深度写入做到的。透明效果和深度测试以及深度写入的关系非常密切。
最后这些片元需要被合并。现在模板缓冲区已经修改了深度缓冲区也进行了对应操作。其实所谓的渲染过程是一个物体一个物体地画到屏幕上的因此我们还需要对像素颜色进行处理而每个像素的颜色信息都被存储在一个名为颜色缓冲区的地方。当我们执行完这次渲染后相同位置的颜色缓冲已经有了上次处理的结果那么这次是直接覆盖还是其它操作这就是合并需要解决的问题。
例如对于不透明的物体我们可以关闭混合Blend) 操作让颜色直接覆盖颜色缓冲。而对于半透明物体我们需要混合颜色像素让其看起来像是透明的。
混合操作也是高度可配置的我们可以选择是否开启混合如果开启混合,GPU就会去除源颜色和目标颜色源颜色指的是片元着色器得到的颜色值而目标颜色是已经存在于颜色缓冲区的颜色值。之后使用一个混合函数来进行混合操作这个混合函数与透明通道息息相关例如根据透明通道的值进行相加、相减、相乘等。 那么我们就有疑问了既然有的片元会在测试阶段被舍弃这样不是很浪费吗那么为什么不先进行测试再进行渲染呢?这样不是可以提高性能吗就像下图的例子一样 上图先渲染球再渲染长方体但是由于球先被渲染且深度更低因此长方体大部分片元无法通过深度测试对这些片元执行片元着色器造成了很大的性能浪费
提前进行测试当然是可以的例如文中就提到了可以提前进行深度测试的技术Early-Z。但是如果将测试提前的话其检验结果可能与片元着色器中的一些操作冲突。例如如果片元着色器中的代码进行了透明度测试而片元没能通过那么它将在着色器中被调用API例如clip函数被手动舍弃。这就导致GPU无法提前进行各种测试。因此现代的GPU会判断片元着色器中的操作是否和提前测试冲突了如果有冲突就会禁用提前测试。这样反而导致性能下降了本来可以提前测试由于添加了透明度测试反而不能提前测试了。
最终图元被渲染完成后会被呈现在屏幕上我们的屏幕显示的就是颜色缓冲区中的颜色值。但是为了避免我们看到那些正在进行光栅化的图元GPU会使用双重缓冲Double Buffering 的策略这意味着对场景的渲染是在幕后发生的即在后置缓冲Back Buffer 中。一旦场景被渲染到了后置缓冲中GPU就会交换后置缓冲区和前置缓冲Front Buffer 中的内容。而前置缓冲区是之前显示在屏幕上的图像。因此保证了我们看到的图像总是连续的。巧妙的方法避免了渲染导致的画面不连续问题
总结
虽然上述流程描述了很多其实曲面细分着色器和几何着色器都没讲但实际过程要更加复杂。当然上述内容与其他资料会产生差异这是由于图像编程接口的实现不尽相同而GPU在底层也做了很多优化。基本原理都是融会贯通的未来可在学习Games101或者RTR4时重拾。
在Unity中为我们封装了很多功能更多时候我们只需要在一个Unity Shader设置一些输入编写顶点着色器和片元着色器设置一些状态就能达到大部分常见的屏幕效果。更别说现在Unity提供了URP和SRP等渲染管线
虽然概念生疏但是坚持才是胜利。无论经历了什么选择了什么既然选择了从事这个行业那么都应该贯彻到底。勉励自己也勉励诸位。