用three.js构建自己的后处理渲染器第二篇---性能好要解决的问题

2,004 阅读6分钟

上一篇我们讲了抗锯齿的选择,这一篇我们讲一下常用的后处理功能的实现思路,如果想了解抗锯齿blog.csdn.net/zhgu1992/ar… 首先看一下three.js自己实现的EffectComposer,它主要是创建了两个renderTarget,一个readBuffer一个writeBuffer,而three.js默认的后期则用的有点混乱。我们为了实现一个后处理渲染器需要明确每一个pass都要将最终的结果拷贝到readBuffer,然后下一个后期的pass如果要拿上一个pass的结果直接从readBuffer拿就可以了,这样所有后处理功能才能连贯起来,最终我们将最后一个pass输出到屏幕。其中我们还是需要多创建一个renderTarget保存原图(第一次渲染所有物体到renderTarget的那张图,我们后面都称其为原图sourceTarget)。(这里有个小建议,每个pass里面的每一步最好能做一个debug功能,可以debug每一次渲染的结果看看和自己预期的是否一致,当然babylon也推出了一款插件叫Spector.js也可以帮助你查看每一次渲染的结果以及执行的webgl方法) 我们把常用的后处理功能一般分为两类:一种是全屏后处理,一种是针对物体级别的 ##全屏后处理 对于全屏后处理,我们先不去实现一些需要延迟渲染支持的复杂的后处理功能(虽然也可以实现但是拿到depthTexture,normalTexture等等需要执行额外的渲染,耗费大量的性能,所以我们先不考虑实现),这种的一般是SAO,SSR之类的,three.js都有实现,但是性能太差,只是demo状态。我们可以实现一些简单的,比如全屏调色,动态模糊之类的,把它们作为我们后处理渲染器最后的几个pass 全屏调色:比如调整一下对比度饱和度等(随便调一下玩玩) 在这里插入图片描述 动态模糊:可以做简单的类似景深的效果等等 在这里插入图片描述 ##针对物体的后处理 针对物体的后处理比较麻烦,如果熟悉3d引擎就知道一般是几种策略,1.第一种使用scheme图的策略也就是黑白图,如果支持MRT,这是最完美的实现方式,性能会最优,而且会更加灵活,虽然使用three.js目前实现不了,但是我们还是要考虑这部分的实现,后面有了MRT可以快速切换(这种思路不做详细介绍,等后续实现MRT再具体讲解,传统的很多大型引擎都是采用这种策略如OGRE) 2.另一种就是所有要针对物体的那张图都和原图(也就是第一次把所有物体都渲染到rendertarget的那张图)绑定同一个depthTexture或者depthRenderbuffer,这样在渲染时webgl就可以进行深度比较,可以实现带深度的物体后处理效果(如果想不带深度,渲染的时候自行清除深度即可),这种方式是我们目前要关注的核心,也是目前没有MRT的最优策略(先卖个关子)。 3.我们看看three.js自己的demo,比如outlinePass,看看它怎么实现的 在这里插入图片描述这个outlinePass确实效果不错,又有遮挡关系,但可惜的是这并不是一个比较好的方式,性能实在太差,我们看看three.js是怎么实现针对物体的后处理功能的

	// Make selected objects invisible
			this.changeVisibilityOfSelectedObjects( false );

			var currentBackground = this.renderScene.background;
			this.renderScene.background = null;

			// 1. Draw Non Selected objects in the depth buffer
			this.renderScene.overrideMaterial = this.depthMaterial;
			renderer.setRenderTarget( this.renderTargetDepthBuffer );
			renderer.clear();
			renderer.render( this.renderScene, this.renderCamera );

			// Make selected objects visible
			this.changeVisibilityOfSelectedObjects( true );

			// Update Texture Matrix for Depth compare
			this.updateTextureMatrix();

			// Make non selected objects invisible, and draw only the selected objects, by comparing the depth buffer of non selected objects
			this.changeVisibilityOfNonSelectedObjects( false );
			this.renderScene.overrideMaterial = this.prepareMaskMaterial;
			this.prepareMaskMaterial.uniforms[ "cameraNearFar" ].value = new Vector2( this.renderCamera.near, this.renderCamera.far );
			this.prepareMaskMaterial.uniforms[ "depthTexture" ].value = this.renderTargetDepthBuffer.texture;
			this.prepareMaskMaterial.uniforms[ "textureMatrix" ].value = this.textureMatrix;
			renderer.setRenderTarget( this.renderTargetMaskBuffer );
			renderer.clear();
			renderer.render( this.renderScene, this.renderCamera );
			this.renderScene.overrideMaterial = null;
			this.changeVisibilityOfNonSelectedObjects( true );

先把选择的物体隐藏,把所有其他物体渲染一遍深度,然后把该物体显示,再渲一遍,此时要和之前渲染的深度图进行比对。然后知道哪些部分是遮挡的哪些部分是不遮挡的。 这里必不可少的多了一次所有物体的渲染(性能下降一倍左右),然后多了很多全局递归遍历的开销,这性能哗哗的往下降啊,已经看不下去了。然后还有一个问题是,这种比对如果在大尺度下还是有很大的精度问题,大尺度下必然不可用(除了这些点后续的shader也开销也比较大)。这样违背了我们性能要好的原则,所以还是无法使用。况且我要同时勾几种颜色的边那,就得创建几个outlinepass,性能又是直线下降 好了,现在还是开始介绍我们的第二种策略吧: 1.首先要解决的事情是如何拿到渲染列表直接渲染? 为什么那?原因是three.js render的时候要更新scene矩阵(当然你可以停掉),要计算物体是否在视锥范围之内,还要排序,最终才生成渲染列表。但是这些操作在第一次渲染到原图的时候已经执行过了,后续如果还调用renderer,我们不希望再有这些开销了,我们希望直接拿到渲染列表进行渲染,这样能让第二次针对后处理功能物体的渲染性能达到最优(这里需要对three.js renderer做一些改造,限于篇幅不做介绍)。因为次渲染前都递归遍历隐藏不必要的物体,渲染后再把它们显示,这样相当于递归两遍场景,在大场景下开销也很大。 2.如何拿到深度,如何进行比对? 我们知道,帧缓冲区中有深度关联对象,我们可以将其绑定纹理对象或者渲染缓冲区对象 在这里插入图片描述那么three.js实现了吗?仔细看了下源码发现three.js有实现,我们的renderTarget支持传入depthTexture,所以可以这样使用

let sourceTarget = new WebGLRenderTarget(width, height,{
		depthTexture:new THREE.DepthTexture()
});

后续所有针对物体渲染的那个renderTarget和sourceTarget绑定同一张depthTexture,并且渲染的时候不要清深度即可让这些物体有后期效果的同时也保留深度信息。three.js会将帧缓存区的深度信息写入depthTexture,进行深度比对。当然这个depthTexture也可以拿来使用,虽然它精度不高。 但是如果使用WebGLMultisampleRenderTarget(MSAA),由于是多重采样,最后拷贝出来的depthTexture和帧缓冲区的深度是不一致的,因此这种方法深度比对会出现问题(其实blitFramebuffer没必要拷贝深度,因为拷贝后的结果也是不可用的)。因此,要想保留深度信息必须绑定渲染缓冲区对象。但是three.js没有实现渲染缓冲区对象的传入和自由绑定。如果想实现,那可以自行绑定同一个渲染缓冲区对象就行(如果要改造可以详细看一下three.js的webGLTextures.js这一篇,renderTarget以及MSAA的实现都在这里,我们需要进行一些的逻辑修改,支持我们传入rendrerBuffer),不过要注意stencil的问题,因为stencil没有自己的renderBuffer需要和深度一起使用,如果深度和stencil一起用的话blitFramebuffer需要的开销较大,如果不使用stencil建议设置为false,因为three.js的WebGLRenderTarget默认depthBuffer和stencilBuffer都是true。 好了,解决了这两个问题,我们就可以开始设计后处理渲染器的架构了,下次再继续更新吧