UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

385 lines (291 loc) • 10.7 kB
import { EnginePlugin } from "../../../../../plugin/EnginePlugin.js"; import { GLSL3, LinearFilter, NoBlending, RedFormat, ShaderMaterial, UniformsUtils, UnsignedByteType, WebGLMultisampleRenderTarget } from "three"; import { SAOShader } from "./SAOShader.js"; import { CompositLayer } from "../../../../composit/CompositLayer.js"; import { BlendingType } from "../../../../texture/sampler/BlendingType.js"; import { DepthLimitedBlur } from "../DepthLimitedBlur.js"; import { renderScreenSpace } from "../../../utils/renderScreenSpace.js"; import { StandardFrameBuffers } from "../../../../StandardFrameBuffers.js"; import { CompositingStages } from "../../../../composit/CompositingStages.js"; export class AmbientOcclusionPostProcessEffect extends EnginePlugin { constructor() { super(); this.id = "ambient-occlusion-post-process"; const defines = Object.assign({}, SAOShader.defines); this.__material = new ShaderMaterial({ defines: defines, fragmentShader: SAOShader.fragmentShader, vertexShader: SAOShader.vertexShader, uniforms: UniformsUtils.clone(SAOShader.uniforms), glslVersion: GLSL3, depthWrite: false, depthTest: false }); // AO parameters const uniforms = this.__material.uniforms; uniforms.bias.value = 0.5; uniforms.intensity.value = 2; uniforms.kernelRadius.value = 30; uniforms.minResolution.value = 0; this.__material.blending = NoBlending; this.__material.extensions.derivatives = true; // should depth be unpacked? defines.DEPTH_PACKING = 0; // is normal buffer available? defines.NORMAL_TEXTURE = 0; // assume that the camera is a perspective camera defines.PERSPECTIVE_CAMERA = 1; // More samples = better quality defines.NUM_SAMPLES = 13; // More rings = better quality defines.NUM_RINGS = 5; /** * Should normal buffer be used or not? Using normal buffer might be significantly more expensive, as this would require normal buffer to be built every frame. If there are other users of the buffer - this is not an issue * @type {boolean} * @private */ this.__use_normals = false; /** * Resolution scale at which AO is to be rendered. Higher scale will lead to sharper and better looking results * @type {number} * @private */ this.__resolution_scale = 0.5; /** * * @type {CompositLayer} * @private */ this.__composit_layer = null; /** * * @type {Camera|null} * @private */ this.__render_camera = null; this.__render_target = new WebGLMultisampleRenderTarget(1, 1, { minFilter: LinearFilter, magFilter: LinearFilter, format: RedFormat, type: UnsignedByteType, internalFormat: 'R8', depthBuffer: false, stencilBuffer: false, generateMipmaps: false, }); /** * * @type {FrameBuffer[]} * @private */ this.__used_buffers = []; /** * * @type {DepthLimitedBlur} * @private */ this.__blur = new DepthLimitedBlur({ format: RedFormat, clear_color: 0xFFFFFF }); } /** * Higher value will make AO appear darker, lower values will make it lighter * @param {number} v */ set intensity(v) { this.__material.uniforms.intensity.value = v; } /** * * @return {number} */ get intensity() { return this.__material.uniforms.intensity.value; } /** * * @param {Camera} camera * @private */ __setCamera(camera) { this.__render_camera = camera; const material = this.__material; const defines = material.defines; if (camera.isPerspectiveCamera === true && defines.PERSPECTIVE_CAMERA !== 1) { defines.PERSPECTIVE_CAMERA = 1; // material has changed and will need to be re-compiled material.needsUpdate = true; } else if (camera.isPerspectiveCamera !== true && defines.PERSPECTIVE_CAMERA !== 0) { defines.PERSPECTIVE_CAMERA = 0; // material has changed and will need to be re-compiled material.needsUpdate = true; } this.__blur.setRenderCamera(camera); } initialize(engine) { this.__blur.configureMaterials(); super.initialize(engine); } finalize() { super.finalize(); } __prepare_uniforms() { const material = this.__material; const uniforms = material.uniforms; const camera = this.__render_camera; uniforms.size.value.set(this.__render_target.width, this.__render_target.height); // setup camera const near = camera.near; const far = camera.far; uniforms.cameraNear.value = near; uniforms.cameraFar.value = far; uniforms.cameraInverseProjectionMatrix.value.copy(camera.projectionMatrixInverse); uniforms.cameraProjectionMatrix.value.copy(camera.projectionMatrix); // trigger uniform update material.uniformsNeedUpdate = true; } __prepare_draw() { const engine = this.engine; const graphics = engine.graphics; const camera = graphics.camera; this.__setCamera(camera); this.__prepare_uniforms(); } /** * * @private */ __render() { const engine = this.engine; const graphics = engine.graphics; const renderer = graphics.renderer; this.__prepare_draw(); const __old_state_rt = renderer.getRenderTarget(); // do draw renderer.setRenderTarget(this.__render_target); renderer.clearColor(); renderScreenSpace(renderer, this.__material); //restore rt renderer.setRenderTarget(__old_state_rt); // do blur this.__blur.execute(renderer); } /** * * @param {boolean} value */ setNormalBufferUsage(value) { if (value === this.__use_normals) { return; } this.__use_normals = value; const material = this.__material; const uniforms = material.uniforms; const defines = material.defines; const engine = this.engine; const graphics = engine.graphics; const buffer = graphics.frameBuffers.getById(StandardFrameBuffers.Normal); if (value) { uniforms.tNormal.value = buffer.getRenderTarget().texture; this.__add_used_frame_buffer(buffer); defines.NORMAL_TEXTURE = 1; } else { uniforms.tNormal.value = null; this.__remove_used_frame_buffer(buffer); defines.NORMAL_TEXTURE = 0; } // trigger re-compilation material.needsUpdate = true; } /** * * @param {FrameBuffer} buffer * @private */ __add_used_frame_buffer(buffer) { if (this.__used_buffers.includes(buffer)) { // already used return; } buffer.referenceCount++; this.__used_buffers.push(buffer); } /** * * @param {FrameBuffer} buffer * @private */ __remove_used_frame_buffer(buffer) { const i = this.__used_buffers.indexOf(buffer); if (i === -1) { // not used return; } buffer.referenceCount--; this.__used_buffers.splice(i, 1); } __update_render_target_size() { const engine = this.engine; const graphics = engine.graphics; const size = graphics.viewport.size; const target_resolution_x = Math.ceil(size.x * this.__resolution_scale); const target_resolution_y = Math.ceil(size.y * this.__resolution_scale); this.__render_target.setSize( target_resolution_x, target_resolution_y ); } async startup() { const engine = this.engine; const graphics = engine.graphics; // bind compositing layer this.__composit_layer = graphics.layerComposer.createLayer( { format: RedFormat, type: UnsignedByteType, internalFormat: 'R8' }, BlendingType.Multiply ); this.__composit_layer.name = 'Ambient Occlusion'; // apply ambient occlusion on opaque pass, otherwise it would occlude transparent geometry this.__composit_layer.stage = CompositingStages.POST_OPAQUE; const material = this.__material; const uniforms = material.uniforms; const fb_color_depth = graphics.frameBuffers.getById(StandardFrameBuffers.ColorAndDepth); const depthTexture = fb_color_depth.getRenderTarget().depthTexture; uniforms.tDepth.value = depthTexture; if (this.__use_normals) { // force usage of normal buffer this.__use_normals = false; this.setNormalBufferUsage(true); } this.__composit_layer.on.preRender.add(this.__render, this); graphics.viewport.size.onChanged.add(this.__update_render_target_size, this); this.__update_render_target_size(); // configure blur this.__blur.setDepthBuffer(depthTexture); this.__blur.setInput(this.__render_target); this.__blur.setOutput(this.__composit_layer.renderTarget); return super.startup(); } async shutdown() { const engine = this.engine; const graphics = engine.graphics; // remove the layer graphics.layerComposer.remove(this.__composit_layer); this.__composit_layer.on.preRender.remove(this.__render, this); graphics.viewport.size.onChanged.add(this.__update_render_target_size, this); // release memory this.__blur.dispose(); this.__render_target.dispose(); return super.shutdown(); } }