@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
385 lines (291 loc) • 10.7 kB
JavaScript
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();
}
}