大家好,在之前聊过shader后,相信大家对webgl的原生和底层渲染机制有了一些的了解,但是只是看概念,似乎很难加深印象。今天我们接着更进一步,聊下后处理。
首先,我们来看看啥是后处理。threejs的官方示例里面,有个章节就叫后处理,里面有许多的后处理示例。其实后处理主要集中在一些效果展示上,例如下面的:
bloom泛光
godray体积光
outline描边
原理与基本概念
在讲解源代码之前,我们还是先从基础理论开始讲起,否则对于很多新手同学,可能理解代码有点费劲。我们先从概念理解入手,一步步的深入到代码,看看如何在threejs中简单的应用后处理技术。
什么是后处理
后处理也叫PostProcessing,是图形渲染或者游戏引擎中非常常见的一种技术。它的实现方式通常是在普通的场景渲染结束后对结果再执行进一步的处理,一般是通过绘制一个铺满屏幕的四方形(quad),并将渲染的buffer作为texture传入,调用shader计算,将计算结果写入到另一个buffer中,最终显示在屏幕上。每个计算流程和普通的模型场景渲染一样,不同之处在于在vertex shader中通常只是简单的拷贝,主要的逻辑计算写在fragement shader中。一般我们将一组特定的功能代码组织在一起配合shader处理,这样一次处理我们叫一个pass,然后将多个pass一起组合在一起组成了整个后处理的渲染流水线。
FBO与RTT
这里,我们需要着重讲一些知识点,就是FBO(FrameBuffer Object)和RTT(Render to Texture)。这些知识点对于理解webgl的渲染,以及后处理流程都是非常重要的。
FrameBuffer就像是一个webgl绘制的容器一样,平时我们默认绘制都是将3d场景绘制在了默认的窗口中输出(此时绑定framebuffer为null),而当我们指定一个FrameBuffer为当前绘制容器,再绘制时则会将对象绘制于指定的FrameBuffer中,而不是直接绘制到屏幕。
framebuffer对象是一个可以包含颜色(color)、透明度(alpha),深度(depth)、模板(stencil)等缓冲(buffer)的集合。并且提供2个方法gl.framebufferTexture2D
,gl.framebufferRenderbuffer
用来绑定texture或者renderbuffer数据到指定的附加点(attachment)。
相关组合有以下几种 官网解释
1. 仅texture(COLOR_ATTACHMENT0 = RGBA/UNSIGNED_BYTE)
2. texture(COLOR_ATTACHMENT0 = RGBA/UNSIGNED_BYTE) + renderbuffer(DEPTH_ATTACHMENT = DEPTH_COMPONENT16)
3. texture(COLOR_ATTACHMENT0 = RGBA/UNSIGNED_BYTE) + renderbuffer(DEPTH_STENCIL_ATTACHMENT = DEPTH_STENCIL)
更多framebuffer的信息可以查看Webgl官方介绍 地址
当我们使用framebuffer对象渲染当前场景到一个texture上的时候,我们实际上就已经在使用rtt技术了。
threejs中如何实现一个后处理
当理解完基本的底层概念后,我们来看看,如何在threejs中应用一个简单的后处理实现天空盒的模糊效果。
首先我们创建一个普通的场景结构,包含一个renderer,一个相机,已经一个scene。这部分代码和一般场景没有什么区别,这里不做赘述。
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
canvas: document.getElementById("renderCanvas"),
});
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 200, 300);
camera.lookAt(new THREE.Vector3(0, 300, 0));
// controls
const controls = new OrbitControls(camera, renderer.domElement);
然后我们初始化一个EffectComposer
对象的实例,这个对象的定义实现代码在 examplesjsmpostprocessing
中。一个composer对象其实是一个后处理流程的调度器或者也可以叫manager。它包含一个addPass方法,用来添加一个后处理pass。然后它有一个叫做render的方法,用来渲染整个后处理流程。
const composer = new EffectComposer(renderer);
我们的后处理具有5个pass,一个渲染背景色,一个渲染天空盒,一个对天空盒做模糊处理,一个渲染主场景,最后一个pass将内容输出到屏幕。
// 清屏背景色pass
const clearPass = new ClearPass(0xff0000, 1);
composer.addPass(clearPass);
// 天空盒pass
const cubeTexturePassP = new CubeTexturePass(camera);
composer.addPass(cubeTexturePassP);
// 模糊处理pass
const blurPass = new BlurPass(
1,
new THREE.Vector2(renderer.domElement.width, renderer.domElement.height)
);
composer.addPass(blurPass);
// 场景渲染
const renderPass = new RenderPass(scene, camera);
renderPass.clear = false;
composer.addPass(renderPass);
// copy内容到屏幕输出
const copyPass = new ShaderPass(CopyShader);
composer.addPass(copyPass);
当添加完pass后,我们修改主渲染循环,将普通渲染场景的renderer.render()方法换成composer.render()。
function animate() {
requestAnimationFrame(animate);
// 设置模糊
blurPass.blur = params.blur;
// 设置明暗
renderer.toneMappingExposure = params.exposure;
// 设置天空盒透明度
cubeTexturePassP.opacity = params.cubeTexturePassOpacity;
// 渲染
composer.render();
}
配合dat.gui组件,实时修改参数,我们可以控制天空盒的模糊度。
var gui = new GUI();
gui.add(params, "exposure", 0, 2, 0.01);
gui.add(params, "blur", 0, 100);
gui.add(params, "cubeTexturePassOpacity", 0, 1);
gui.open();
demo
代码在这里
后处理的核心,是在每个pass的shader中,这部分才是整个后处理的难点和算法核心。在熟悉流程后,其实大家只需要专心攻克shader中的图像算法就好了,其他流程其实只是辅助。在了解完整个流程后,我们来看看threejs的后处理实现的背后都做了哪些事情。
EffectComposer
首先是我们来看看EffectComposer
,它内部到底在做些什么。从源码中我们可以看到,一个composer中会初始化两个WebGLRenderTarget
(FBO),一个叫writebuffer,另一个叫readbuffer。在渲染执行的时候,composer会遍历内部所有的pass,然后执行每个pass的渲染,每个pass都会传入 writeBuffer
和 readBuffer
,一般每个pass渲染会读取readbuffer中的数据,并写入writebuffer。在每次pass的渲染后,可以根据pass的 needsSwap
属性来互换这两个buffer,从而形成一个读取写入的流式处理。当处理到最后一个pass的时候,一般会把当前的渲染的fbo设置为null,也就是渲染到屏幕。所以经过最后一次渲染后,我们的渲染结果最终会被输出到屏幕。
pass
pass对象是threejs为我们提供的一个pass的基类,里面其实非常简单只有几个简单属性。
// 是否启用
this.enabled = true;
// 是否交换buffer
this.needsSwap = true;
// 是否清屏
this.clear = false;
// 是否渲染到屏幕
this.renderToScreen = false;
renderpass
renderpass一般用来渲染场景,把scene中的场景渲染到某个buffer中,注意这个renderpass如果是最后一个的话,会将内容直接渲染到屏幕输出,所以如果想叠加之前的处理结果,是需要额外加一个copypass的。
shaderpass
shaderpass的作用是使用一个shader,处理平面图形。一般来说shaderpass的输入是一些之前的处理后rendertarget的texture,输出也是通过一个正交相机对着的平面输出到buffer中。所以这个时候,一些深度计算和测试都会有些问题,大家在处理的时候需要额外注意。
我们的实现blurpass对象
理解完其他的概念后,我们不难写出一个属于自己的blurpass。继承了pass基类后,在blurpass中我们主要是做了两次的高斯径向模糊。具体高斯模糊实现的代码在shander中
varying vec2 vUv;
uniform sampler2D colorTexture;
uniform vec2 texSize;
uniform vec2 direction;
uniform float kernelRadius;
float gaussianPdf(in float x, in float sigma) {
return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
}
void main() {
vec2 invSize = 1.0 / texSize;
float weightSum = gaussianPdf(0.0, kernelRadius);
vec4 diffuseSum = texture2D( colorTexture, vUv) * weightSum;
vec2 delta = direction * invSize * kernelRadius/float(MAX_RADIUS);
vec2 uvOffset = delta;
for( int i = 1; i <= MAX_RADIUS; i ++ ) {
float w = gaussianPdf(uvOffset.x, kernelRadius);
vec4 sample1 = texture2D( colorTexture, vUv + uvOffset);
vec4 sample2 = texture2D( colorTexture, vUv - uvOffset);
diffuseSum += ((sample1 + sample2) * w);
weightSum += (2.0 * w);
uvOffset += delta;
}
gl_FragColor = diffuseSum/weightSum;
}"
关于高斯模糊的原理,可以看这篇 http://www.ruanyifeng.com/blog/2012/11/gaussian_blur.html 阮一峰的科普文。
以上便是对后处理的总结,总得来说是一些科普和概念总结,并没有什么高深的技术,旨在帮助大家理解整个流程和使用中常见的问题。如果有兴趣的同学,可以继续研究,探索下后处理里面可能会遇到的,比如后处理之后的抗锯齿、后处理的时候深度贴图处理等等,这里只是抛砖引玉,希望对大家入门后处理有所帮助。