SSAA & MSAA

前两篇关于几何反走样 MLAA/SMAA 和 FXAA 都是基于 post-processing 后处理完成的。 在这篇文章中我们将介绍 SSAA 和 MSAA 这两种更加依赖 GPU 实现的反走样技术。

SSAA

来自 https://zhuanlan.zhihu.com/p/28800047

SSAA(Supersampling Anti-Aliasing)可以说是图形学中最简单粗暴的反走样方法,但同时也最有效,它唯一也是致命的缺点是性能太差。开篇已经说过,任何类型的走样归根结底都是因为欠采样,那么我们只需要增加采样数,就可以减轻走样现象。这就是 SSAA,所以 SSAA 简单的来说可以分三步:

(1)在一个像素内取若干个子采样点
(2)对子像素点进行颜色计算(采样)
(3)根据子像素的颜色和位置,利用一个称之为 resolve 的合成阶段,计算当前像素的最终颜色输出

不同 SSAA 方式在子采样位置的选取和最终 resolve 使用的滤波器上有所不同,可以使用不同的采样模板(规则采样,旋转采样,随机采样,抖动采样等)或者不同的滤波函数(方波滤波器或者高斯滤波器)。
SSAA 同时是几何反走样和着色反走样方法,因为它不但增加了当前几何覆盖函数(Coverage)的采样率,也对渲染方程进行了更高频率的采样(单独计算每个子像素的颜色)。

image.png

WebGL 实现

来自 Three.js:🔗

composer = new THREE.EffectComposer( renderer );

ssaaRenderPass = new THREE.SSAARenderPass( scene, camera );
ssaaRenderPass.unbiased = false;
composer.addPass( ssaaRenderPass );

copyPass = new THREE.ShaderPass( THREE.CopyShader );
copyPass.renderToScreen = true;
composer.addPass( copyPass );

采样模版

这里选用了抖动采样作为采样模版:🔗

THREE.SSAARenderPass.JitterVectors = [
	[
		[ 0, 0 ]
	],
	[
		[ 4, 4 ], [ - 4, - 4 ]
	],
	[
		[ - 2, - 6 ], [ 6, - 2 ], [ - 6, 2 ], [ 2, 6 ]
	],
	[
		[ 1, - 3 ], [ - 1, 3 ], [ 5, 1 ], [ - 3, - 5 ],
		[ - 5, 5 ], [ - 7, - 1 ], [ 3, 7 ], [ 7, - 7 ]
	],
    ...
];

多次渲染

有了采样模版,就可以进行多次采样计算了,这里使用到了 PerspectiveCamera.setViewOffset,在每一次采样过程中改变偏移量,当然别忘了循环结束后重置:

for ( var i = 0; i < jitterOffsets.length; i ++ ) {
  var jitterOffset = jitterOffsets[ i ];

  if ( this.camera.setViewOffset ) {
    this.camera.setViewOffset( width, height,
      jitterOffset[ 0 ] * 0.0625, jitterOffset[ 1 ] * 0.0625,   // 0.0625 = 1 / 16
      width, height );
  }
  //...
}
// 计算结束后清除偏移量
if ( this.camera.clearViewOffset ) this.camera.clearViewOffset();

那么多次采样的权重是否应该都相同呢?这样只需要简单平均多次采样的结果就可以了。

the theory is that equal weights for each sample lead to an accumulation of rounding errors. The following equation varies the sampleWeight per sample so that it is uniformly distributed across a range of values whose rounding errors cancel each other out.

https://en.wikipedia.org/wiki/Round-off_error#Accumulation_of_roundoff_error

var sampleWeight = baseSampleWeight; // 1 / jitterOffsets.length
if ( this.unbiased ) {
	var uniformCenteredDistribution = ( - 0.5 + ( i + 0.5 ) / jitterOffsets.length );
	sampleWeight += roundingRange * uniformCenteredDistribution;
}

最后输出到 sampleRenderTarget 中供下一步混合使用:

renderer.setClearColor( this.clearColor, this.clearAlpha );
renderer.render( this.scene, this.camera, this.sampleRenderTarget, true );

混合

在混合场景中,我们需要创建一个使用正交投影的相机以及供 CopyPass 使用的一个简单平面:

this.camera2 = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
this.scene2	= new THREE.Scene();
this.quad2 = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), this.copyMaterial );
this.quad2.frustumCulled = false; // Avoid getting clipped
this.scene2.add( this.quad2 );

在负责拷贝的后处理 Shader 中,blending: THREE.AdditiveBlending 是关键,因为多次采样结果需要加权平均:

this.copyMaterial = new THREE.ShaderMaterial({
		uniforms: this.copyUniforms,
		vertexShader: copyShader.vertexShader,
		fragmentShader: copyShader.fragmentShader,
		premultipliedAlpha: true,
		transparent: true,
		blending: THREE.AdditiveBlending, // 累加
		depthTest: false,
		depthWrite: false
});

要特别注意由于需要进行累加,只有第一次渲染需要清空 color buffer:

// 混合权重
this.copyUniforms[ "opacity" ].value = sampleWeight;
this.copyUniforms[ "tDiffuse" ].value = this.sampleRenderTarget.texture;
// 只有第一次渲染需要清空
if ( i === 0 ) {
	renderer.setClearColor( 0x000000, 0.0 );
}
// 混合
renderer.render( this.scene2, this.camera2, this.renderToScreen ? null : writeBuffer, ( i === 0 ) );

实际效果

性能太差,采样点增加到 16 个就已经卡顿严重。和拥有硬件支持的 MSAA 完全没得比,即使对于几何和颜色走样都有改善。

MSAA(Multisample Anti-Aliasing)

SSAA中每个像素需要多次计算着色,这对实时渲染的开销是巨大的(想想4K和1080P的性能差异),我们开始也说过,实际上人眼对几何走样更敏感,能否解耦几何覆盖函数的采样率和着色方程的采样率呢?答案是肯定的。MSAA的原理很简单,它仍然把一个像素划分为若干个子采样点,但是相较于SSAA,每个子采样点的颜色值完全依赖于对应像素的颜色值进行简单的复制(该子采样点位于当前像素光栅化结果的覆盖范围内),不进行单独计算。此外它的做法和SSAA相同,每个子像素会在光栅化阶段分别计算自身的Z值和模板值,有完整的Z-Test和Stencil-Test并单独保存在Z-Buffer和Stencil-Buffer里,就是我们需要的几何覆盖信息。类似于SSAA,MSAA也需要一个resolve的过程,在早期(DX9/10?)这个过程是显卡的一个固有单元在执行,执行的方式一般也就是简单的Box Filter,随着可编程管线的功能逐渐强大,现在可以通过Pixel Shader来访问相应的MSAA Texture,并且定制resolve的算法。由于 MSAA 拥有硬件支持,相对开销比较小,又能很好地解决几何走样问题,在游戏中应用非常广泛(我们在游戏画质选项中常看到的 4x/8x/16x 抗锯齿一般说的就是 MSAA 的子采样点数量分别为4/8/16个)。

image.png

WebGL 实现

内置实现

https://stackoverflow.com/questions/50255282/webgl2-has-anti-alias-automatically-built-in

By setting it to false you’re telling the browser “Don’t turn on antialiasing” period. For example if you’re making a pixelated game you might want to tell the browser to not antialias.


首先开发者可以强制设置为 false,这样浏览器一定不会应用,在一些像素游戏场景下常见。在 FXAA/MLAA 等使用后处理进行几何反走样的例子中也会手动关闭,以保证真实效果。

然后即使设置为 true,实际是否应用 MSAA/SSAA 反走样还是由浏览器实现决定,这也是规范规定的:

const gl = canvas.getContext(webglVersion, {
	antialias: true
});

因此为了测试反走样是否生效,可以用一段简单的测试代码,画一个红色的三角形,然后检查四个点是否只有红黑两种颜色,如果是则说明没有进行颜色反走样:

gl.drawArrays(gl.TRIANGLES, 0, 3);
const pixels = new Uint8Array(2 * 2 * 4);
gl.readPixels(0, 0, 2, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const isNotAntialiased = 
  isRedOrBlack(pixels[ 0]) && 
  isRedOrBlack(pixels[ 4]) && 
  isRedOrBlack(pixels[ 8]) && 
  isRedOrBlack(pixels[12]) ;

看起来 WebGL 内置的反走样实现开发者并不是完全可控。

WebGL2

WebGL2 的实现可以参考:

总共需要两个 Pass,第一个绘制到开启了 MSAA 的 renderBuffer 中,第二个通过后处理读取纹理绘制到屏幕上。

pre-z pass –> rendering pass to FBO –> postprocessing pass –> render to window


1st Pass:renderBuffer

在这个 Pass 中,需要使用到 WebGL2 中新加入的 API:

首先绑定 renderBuffer 并应用 MSAA,下面的例子中子采样点数量为 4:

var colorRenderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, colorRenderbuffer);
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 4, gl.RGBA8, FRAMEBUFFER_SIZE.x, FRAMEBUFFER_SIZE.y);

// 绑定到 fbo
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[FRAMEBUFFER.RENDERBUFFER]);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorRenderbuffer);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);

然后开始绘制到 renderBuffer 中:

gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[FRAMEBUFFER.RENDERBUFFER]);
// 清除 Color buffer,序号为 0,清除颜色设置为黑色
gl.clearBufferfv(gl.COLOR, 0, [0.0, 0.0, 0.0, 1.0]);
gl.useProgram(programs[PROGRAM.TEXTURE]);
// 切换 vao
gl.bindVertexArray(vertexArrays[PROGRAM.TEXTURE]);
// 省略传入 uniform
gl.drawArrays(gl.LINE_LOOP, 0, vertexCount);

接下来如何输出到 TEXTURE0 是一个问题,WebGL2 并没有支持 multisample 的纹理:

Pay attention to the fact that the multisample renderbuffers cannot be directly bound to textures, but they can be resolved to single-sample textures using the blitFramebuffer call



这里就需要使用 WebGL2 中新加入的另一个 API:**blitFramebuffer **了,它的作用就是从 readBuffer 传递像素到 writeBuffer 中:

// 绑定读写两个 buffer
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffers[FRAMEBUFFER.RENDERBUFFER]);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, framebuffers[FRAMEBUFFER.COLORBUFFER]);
// 清除 COLORBUFFER
gl.clearBufferfv(gl.COLOR, 0, [0.0, 0.0, 0.0, 1.0]);
// 开始传递 RENDERBUFFER 到 COLORBUFFER
gl.blitFramebuffer(
  0, 0, FRAMEBUFFER_SIZE.x, FRAMEBUFFER_SIZE.y,
  0, 0, FRAMEBUFFER_SIZE.x, FRAMEBUFFER_SIZE.y,
  gl.COLOR_BUFFER_BIT, gl.NEAREST
);

2nd Pass:绘制到屏幕

现在我们经过 multisample 的纹理已经在 TEXTURE0 中了:

gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.useProgram(programs[PROGRAM.SPLASH]);
gl.uniform1i(diffuseLocation, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindVertexArray(vertexArrays[PROGRAM.SPLASH]);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

就是一个后处理 CopyShader 的实现:

#version 300 es
precision highp float;
precision highp int;
uniform sampler2D diffuse;
in vec2 uv;
out vec4 color;
void main()
{
	color = texture(diffuse, uv);
}

最终效果如下,可见颜色反走样效果很明显:
屏幕快照 2019-02-04 上午10.37.09.png

Three.js 实现

Three.js 在最近的一次 PR https://github.com/mrdoob/three.js/pull/15541 中引入了这个 WebGL2 特性。

用法如下 🔗

var parameters = {
  format: THREE.RGBFormat,
  stencilBuffer: false
};
var size = renderer.getDrawingBufferSize();
var renderTarget = new THREE.WebGLMultisampleRenderTarget( size.width, size.height, parameters );
composer1 = new THREE.EffectComposer( renderer, renderTarget );