你们好,本文学习MSAA以及在WebGPU中的实现。html
上一篇博文
WebGPU学习(二): 学习“绘制一个三角形”示例git
下一篇博文
WebGPU学习(四):Alpha To Coveragegithub
MSAA(多重采样抗锯齿),是硬件实现的抗锯齿技术web
参考深刻剖析MSAA :canvas
具体到实时渲染领域中,走样有如下三种:
1.几何体走样(几何物体的边缘有锯齿),几何走样因为对几何边缘采样不足致使。
2.着色走样,因为对着色器中着色公式(渲染方程)采样不足致使。比较明显的现象就是高光闪烁。
3.时间走样,主要是对高速运动的物体采样不足致使。好比游戏中播放的动画发生跳变等。ide
这里讨论几何体走样。
函数
如上图所示,咱们要绘制一个三角形。它由三个顶点组成,红线范围内的三角形是片元primitive覆盖的区域。
primitive会被光栅化为fragment,而一个fragment最终对应屏幕上的一个像素,如图中的小方块所示。学习
gpu会根据像素中心的采样点是否被pritimive覆盖来判断是否生成该fragment和执行对应的fragment shader。动画
图中红色的点是被覆盖的采样点,它所在的像素会被渲染。ui
下图是最终渲染的结果,咱们看到三角形边缘产生了锯齿:
MSAA经过增长采样点来减轻几何体走样。
它包括4个步骤:
1.针对采样点进行覆盖检测
2.每一个被覆盖的fragment执行一次fragment shader
3.针对采样点进行深度检测和模版检测
4.解析(resolve)
下面以4X MSAA为例(每一个像素有4个采样点),说明每一个步骤:
1.针对采样点进行覆盖检测
gpu会计算每一个fragment的coverage(覆盖率),从而得知对应像素的每一个采样点是否被覆盖的信息。
coverage相关知识能够参考WebGPU学习(四):Alpha To Coverage -> 学习Alpha To Coverage -> 原理
这里为了简化,咱们只考虑经过“检测每一个像素有哪些采样点被primitive覆盖”来计算coverager:
如上图所示,蓝色的采样点是在三角形中,是被覆盖的采样点。
2.每一个被覆盖的fragment执行一次fragment shader
若是一个像素至少有一个采样点被覆盖,那么会执行一次它对应的fragment(注意,只执行一次哈,不是执行4次)(它全部的输入varying变量都是针对其像素中心点而言的,因此计算的输出的颜色始终是针对该栅格化出的像素中心点而言的),输出的颜色保存在color buffer中(覆盖的采样点都要保存同一个输出的颜色)
3.针对采样点进行深度检测和模版检测
全部采样点的深度值和模版值都要存到depth buffer和stencil buffer中(不管是否被覆盖)。
被覆盖的采样点会进行深度检测和模版检测,经过了的采样点会进入“解析”步骤。
那为何要保存全部采样点的深度和模版值了(包括没有被覆盖的)?由于它们在深度检测和模版检测阶段决定所在的fragment是否被丢弃:
那是由于以后须要每一个sample(采样点)都执行一下depth-test,以肯定整个fragment是否要流向(通往缓冲区输出的)流水线下一阶段——只有当所有fragment-sample的Depth-Test都Fail掉的时候,才决定抛弃掉这个fragment(蒙版值stencil也是这样的,每一个sample都得进行Stencil-Test)。
4.解析
什么是解析?
根据深刻剖析MSAA 的说法:
像超采样同样,过采样的信号必须从新采样到指定的分辨率,这样咱们才能够显示它。
这个过程叫解析(resolving)。
根据乱弹纪录II:Alpha To Coverage 的说法:
在把全部像素输出到渲染缓冲区前执行Resolve以生成单一像素值。
。。。。。。
也该是时候谈到一直说的“计算输出的颜色”是怎么一回事了。MultiSample的Resolve阶段,若是是屏幕输出的话这个阶段会发生在设备的屏幕输出直前;若是是FBO输出,则是发生在把这个Multisample-FBO映射到非multisample的FBO(或屏幕)的时候(见[多重采样(MultiSample)下的FBO反锯齿] )。Resolve,说白了就是把MultiSample的存储区域的数据,根据必定法则映射到能够用于显示的Buffer上了(这里的输出缓冲区包括了Color、Depth或还有Stencil这几个)。Depth和Stencil前面已经说起了法则了,Color方面其实也简单,通常的显卡的默认处理就是把sample的color取平均了。注意,由于depth-test等Test以及Coverage mask的影响下,有些sample是不参与计算的(被摒弃),例如4XMSAA下上面的0101,就只有两个sample,又已知各sample都对应的只是同一个颜色值,因此输出的颜色 = 2 * fragment color / 4 = 0.5 * fragment color。也就是是说该fragemnt最终显示到屏幕(或Non-MS-FBO)上是fragment shader计算出的color值的一半——这不只是颜色亮度减半还包括真·透明度值的减半。
个人理解:
“解析”是把每一个像素通过上述步骤获得的采样点的颜色值,取平均值,获得这个像素的颜色值。
如上图右边所示,像素的2个采样点进入了“解析”,最终该像素的颜色值为 0.5(2/4) * 原始颜色值
通过上述全部步骤后,最终的渲染结果以下:
MSAA能减轻几何体走样,但会增长color buffer、depth buffer、stencil buffer开销。
深刻剖析MSAA
乱弹纪录II:Alpha To Coverage
Anti Aliasing
有下面几个要点:
目前我没找到查询的方法,但至少支持4个采样点
参考 Investigation: Multisampled Render Targets and Resolve Operations:
We can say that 4xMSAA is guaranteed on all WebGPU implementations, and we need to provide APIs for queries on whether we can create a multisampled texture with given format and sample count.
dictionary GPURenderPipelineDescriptor : GPUPipelineDescriptorBase { ... unsigned long sampleCount = 1; ... };
dictionary GPUTextureDescriptor : GPUObjectDescriptorBase { ... unsigned long sampleCount = 1; ... };
咱们在WebGPU 规范中看到render pipeline descriptor和texture descriptor能够设置sampleCount。
在“解析”步骤中,须要从新采样到指定的分辨率:
过采样的信号必须从新采样到指定的分辨率,这样咱们才能够显示它
因此咱们应该设置color的resolveTarget(depth、stencil不支持resolveTarget),它包含“分辨率”的信息。
咱们来看下WebGPU 规范:
dictionary GPURenderPassColorAttachmentDescriptor { required GPUTextureView attachment; GPUTextureView resolveTarget; required (GPULoadOp or GPUColor) loadValue; GPUStoreOp storeOp = "store"; };
resolveTarget在render pass colorAttachment descriptor中设置,它的类型是GPUTextureView。
而GPUTextureView是从GPUTexture得来的,咱们来看下GPUTexture的descriptor的定义:
dictionary GPUExtent3DDict { required unsigned long width; required unsigned long height; required unsigned long depth; }; typedef (sequence<unsigned long> or GPUExtent3DDict) GPUExtent3D; dictionary GPUTextureDescriptor : GPUObjectDescriptorBase { ... required GPUExtent3D size; ... };
GPUTextureDescriptor的size属性有width和height属性,只要texture对应了屏幕大小,咱们就能得到屏幕“分辨率”的信息(depth设为1,由于分辨率只有宽、高,没有深度)。
咱们对应到sample来看下。
打开webgpu-samplers->helloTriangleMSAA.ts文件。
代码基本上与咱们上篇文章学习的webgpu-samplers->helloTriangle.ts差很少,
const sampleCount = 4; const pipeline = device.createRenderPipeline({ ... sampleCount, });
这里设置了sample count为4
const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [{ attachment: attachment, resolveTarget: swapChain.getCurrentTexture().createView(), ... }], };
该texture的建立代码为:
const texture = device.createTexture({ size: { width: canvas.width, height: canvas.height, depth: 1, }, sampleCount, format: swapChainFormat, usage: GPUTextureUsage.OUTPUT_ATTACHMENT, }); const attachment = texture.createView();
注意:texture的sampleCount应该与render pipeline的sampleCount同样,都是4
swapChain.getCurrentTexture()得到的texture的大小是与屏幕相同,因此它包含了屏幕分辨率的信息。
helloTriangleMSAA.ts
Investigation: Multisampled Render Targets and Resolve Operations