Shader入门:光栅化都在做什么?
渲染流水线总共有三大概念性阶段应用阶段、几何阶段、光栅化阶段。上一篇已经讲了应用阶段和几何阶段这一篇开始进入 GPU 主导的部分。几何阶段和光栅化阶段通常被叫做 GPU 流水线它们各自又可以细分成多个子流水线阶段如图 2 所示。应用阶段和几何阶段已经在另两篇讲完本篇主要讲解光栅化阶段。建议读者先学习应用阶段和几何阶段再阅读本篇文章。应用阶段几何阶段图2光栅化阶段 光栅化阶段的两个最重要目标计算每个图元覆盖了哪些像素为这些像素计算它们的颜色1.三角形相关1.1输入屏幕映射阶段结束后输出的是图元该图元的每个顶点的位置信息已经变换到屏幕坐标中。但是图元只包含三角形的三个顶点信息可以想到实际上我们想要得到整个三角形对像素的覆盖情况而不只是三角形三个顶点的像素覆盖情况。而这种求得三角形对像素的覆盖情况的过程是三角形设置和遍历阶段这里统称三角形相关。1.2三角形遍历过程直接看光栅化阶段的伪代码:// 定义片元结构体 struct Fragment { i32 x, y; // 屏幕像素坐标 f32 depth; // 插值后的深度 Vec2 uv; // 插值后的纹理坐标 Color vert_color; // 插值后的顶点色 }; void Rasterization( Vec2 a, Vec2 b, Vec2 c, Color ca, Color cb, Color cc, ......(传入三角形三个顶点的信息包括坐标颜色法线,纹理坐标等等) ) { i32 min_x min(a.x, b.x, c.x); i32 min_y min(a.y, b.y, c.y); i32 max_x max(a.x, b.x, c.x); i32 max_y max(a.y, b.y, c.y); f32 total_area SignedTriangleArea(a, b, c); for (i32 x min_x; x max_x; x) { for (i32 y min_y; y max_y; y) { f32 alpha SignedTriangleArea(b, c, {x,y}) / total_area; f32 beta SignedTriangleArea(c, a, {x,y}) / total_area; f32 gamma SignedTriangleArea(a, b, {x,y}) / total_area; // 内外判定三角形外的直接丢弃 if (alpha 0 || beta 0 || gamma 0) continue; // 生成片元用同一组重心坐标插值所有属性 Fragment frag; frag.x x; frag.y y;//屏幕像素坐标 frag.vert_color alpha * ca beta * cb gamma * cc;//插值后的顶点色深度、纹理坐标的插值同理 // 后面再单独执行片元着色器 → 深度测试 → 混合 → 写入帧缓冲 } } }代码解释:1.开始遍历之前实际上我们不可能遍历整个屏幕像素这太消耗性能了而是在遍历三角形之前生成一个包围盒这会显著提高性能如图:求包围盒四个顶点的代码:i32 min_x min(a.x, b.x, c.x); i32 min_y min(a.y, b.y, c.y); i32 max_x max(a.x, b.x, c.x); i32 max_y max(a.y, b.y, c.y);2.如何确定一个点是否在三角形内部通过判断该像素的重心坐标的其中一个坐标是否为负数是则该点必定不在三角形内部。重心坐标有三个点每个点表示自己的三角形占总三角形面积的比例比如如下图所示A点的比例三角形PBG/三角形总面积所以我们可以看到代码中调用SignedTriangleArea(a, b, c)来求出重心坐标然后在循环内部判断该点其中一个坐标是否为负数来确定该点是否在三角形内部if (alpha 0 || beta 0 || gamma 0) continue;3.筛选出在三角形内部的像素后但我们此时只有屏幕像素坐标这个信息我们是不是还需要颜色等信息拿颜色来举例实则我们可以通过插值来计算该像素的颜色它是基于该像素的重心坐标和原三个顶点的颜色信息计算得到。而用于存储这些插值后的数据以及其它数据的结构体我们可以叫它片元。// 生成片元用同一组重心坐标插值所有属性 Fragment frag; frag.x x; frag.y y;//屏幕像素坐标 frag.vert_color alpha * ca beta * cb gamma * cc;//插值后的顶点色深度、纹理坐标的插值同理1.3输出三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话就会生成一个片元fragment。所以三角形遍历最终会输出片元一个片元并不是真正意义上的像素而是包含了很多状态的集合这些状态用于计算每个像素的最终颜色。这些状态包括了但不限于它的屏幕坐标深度信息以及其他从几何阶段输出的顶点信息经过插值处理例如法线、纹理坐标颜色等。2.片元着色器2.1输入输出片元着色器会选择性的读取片元携带的插值属性比如 UV、法线、顶点色等这些输入属性是只读的。着色器可以选择用哪些、怎么算然后输出新的颜色、深度等到片元的输出值属性。下图显示了这个过程。2.2主要工作该阶段比较重要的技术是我们可以通过片元中经过光栅化插值处理的纹理坐标进行纹理采样输出纹理颜色。再或者我们可以通过片元中经过光栅化插值处理后的法线等信息进行光照计算输出光照颜色。4.逐片元操作4.1输入输出光栅化产生的片元 F 一开始主要带着屏幕位置、覆盖 mask、插值后的输入属性、深度等。它不一定一开始就有一个固定的“输出颜色属性”等着被赋值。片元着色器执行时声明输出例如out vec4 fragColor; void main() { fragColor ...; }执行完成后fragColor的值就作为这个片元的输出颜色/输出附件交给逐片元操作。逐片元操作处理的还是同一个片元 F只是现在它带有片元着色器写出的输出颜色大致流程如下:光栅化生成片元 FF { 位置, 覆盖 mask, 插值输入属性, 深度 ... }片元着色器执行读 F 的插值输入写 out 变量例如 fragColor执行后F 的输出颜色 fragColor 的值逐片元操作用 F 的输出颜色做混合/写入仍然用 F 的位置、覆盖、深度等4.2主要任务决定每个片元的可见性涉及很多测试工作例如深度测试、模板测试等如果一个片元通过了所有的测试就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并或者说是混合需要指明的是逐片元操作阶段是高度可配置的即我们可以设置每一步的操作细节4.2.1决定片元可见性一个片元只有通过了深度测试和模板测试才能最终获得和颜色缓冲区进行合并的资格。而这些测试流程大致如下:模板测试:本篇不讲解感兴趣读者可以查阅相关资料。深度测试:深度可以简单理解为离摄像机的远近距离默认情况下离摄像机越近值越低。深度测试主要经历两个子阶段深度测试和深度写入深度写入我们可以选择是否开启。通过深度测试可以确定出自己是否是最靠近摄像机的那个片元,而如果通过了深度测试那么可以把自己的深度信息写到深度缓冲中写入深度缓冲的操作叫深度写入举个简单的例子说明深度测试和写入的作用:假设A的深度:0.5 B的深度0.7A和B都关闭了颜色混合和开启了深度写入A和B都是不透明物体那么如果先渲染A→渲染B:A进行深度测试发现深度缓冲中的值是1(默认值)而自己的深度值是0.5比1小通过了深度测试然后将自己的深度值0.5写入了深度缓冲中并把自己的颜色值写入颜色缓冲中B进行深度测试发现深度缓冲中的值是0.5而自己的深度值是0.7比0.5大没有经过深度测试该片元B被舍弃最终颜色缓冲区中是A的颜色是完全合理的。读者可以自行通过分析先渲染B→渲染A发现最终还是A的颜色所以你知道深度测试和深度写入有什么用了吗4.2.2合并如果片元通过了深度测试那么它将进行合并操作该片元的颜色值可以选择覆盖颜色缓冲区的值或者与颜色缓冲区的值进行混合通过是否开启混合操作来决定这种方式。对于不透明物体和经过透明度测试的全透明物体我们一般关闭混合如4.2.2的例子所示。而对于半透明物体来说我们一般开启混合并且需要设置正确的渲染顺序下面举个例子说明设置正确渲染顺序的重要性:假设A深度0.5 B深度0.7A开启混合B关闭混合A是黄色半透明 B是不透明紫色假设先渲染A→渲染B:A通过了深度测试并进行深度写入然后将自己的颜色和颜色缓冲区中的默认颜色混合成黄暗色半透明B发现自己没有通过深度测试最终颜色是黄暗色半透明错误的假设先渲染B→渲染A:B通过了深度测试并进行深度写入然后将自己的颜色覆盖颜色缓冲区中的默认颜色变成紫色A发现自己通过深度测试并进行深度写入然后将自己的颜色和颜色缓冲区中的紫色颜色混合最终颜色是黄色半透明和紫色的混合结果正确所以设置渲染顺序很重要如果读者把上面的例子中A的深度写入关闭在设置正确的渲染顺序下也是可以达到正确的效果的但是我建议还是关闭半透明物体的深度写入我总结后发现半透明物体的几种情况:顺序正确 ZWrite Off理想简单情况下正确复杂情况下同一物体内部重叠、双面、粒子、相交片元级顺序无法保证混合顺序可能错但所有层都参与了视觉上通常可接受。顺序正确 ZWrite On理想简单情况下正确复杂情况下近处透明片元写入深度远处透明片元被剔除后面的透明层直接消失透明感严重丢失。顺序错误 无论 ZWrite 开还是关都错只是错的样子不同。所以正确顺序通常是先渲染不透明物体开启深度测试 深度写入、关闭混合再渲染半透明物体开启深度测试、关闭深度写入、开启混合并按从远到近排序。4.3测试顺序与 Early-Z 技术Early-Z 是一种 GPU 优化尽量在片元着色器前完成深度/模板测试提前丢弃不可见片元避免无谓的片元着色器计算。无法使用Early-Z技术的情况:透明度测试里的clip让 GPU 无法提前判断片元是否会被丢弃Early-Z 被迫禁用片元着色器执行量增加性能下降。