UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in

299 lines (261 loc) • 11.9 kB
import { N8AOPostPass } from "n8ao"; import { BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SMAAEffect, SSAOEffect, TiltShiftEffect, ToneMappingEffect as _TonemappingEffect, VignetteEffect} from "postprocessing"; import { HalfFloatType, NoToneMapping } from "three"; import { showBalloonWarning } from "../../engine/debug/index.js"; import { Context } from "../../engine/engine_setup.js"; import type { Constructor } from "../../engine/engine_types.js"; import { DeviceUtilities, getParam } from "../../engine/engine_utils.js"; import { Camera } from "../Camera.js"; import { _SharpeningEffect } from "./Effects/Sharpening.js"; import { PostProcessingEffect, PostProcessingEffectContext } from "./PostProcessingEffect.js"; const debug = getParam("debugpost"); const activeKey = Symbol("needle:postprocessing-handler"); const autoclearSetting = Symbol("needle:previous-autoclear-state") /** * PostProcessingHandler is responsible for applying post processing effects to the scene. It is internally used by the {@link Volume} component */ export class PostProcessingHandler { private _composer: EffectComposer | null = null; private _lastVolumeComponents?: PostProcessingEffect[]; private _effects: Array<Effect | Pass> = []; get isActive() { return this._isActive; } get composer() { return this._composer; } private _isActive: boolean = false; private readonly context: Context; constructor(context: Context) { this.context = context; } apply(components: PostProcessingEffect[]) { this._isActive = true; this.onApply(this.context, components); } unapply() { if(debug) console.log("Unapplying postprocessing effects"); this._isActive = false; if (this._lastVolumeComponents) { for (const component of this._lastVolumeComponents) { component.unapply(); } this._lastVolumeComponents.length = 0; } const context = this.context; const active = context[activeKey] as PostProcessingHandler | null; if (active === this) { delete context[activeKey]; } if (context.composer === this._composer) { context.composer?.dispose(); context.composer = null; } if (typeof context.renderer[autoclearSetting] === "boolean") { context.renderer.autoClear = context.renderer[autoclearSetting]; } } dispose() { this.unapply(); for (const effect of this._effects) { effect.dispose(); } this._effects.length = 0; this._composer = null; } private onApply(context: Context, components: PostProcessingEffect[]) { if (!components) return; context[activeKey] = this; if (debug) console.log("Apply Postprocessing Effects", components); this._lastVolumeComponents = [...components]; // store all effects in an array to apply them all in one pass // const effects: Array<Effect | Pass> = []; this._effects.length = 0; // TODO: if an effect is added or removed during the loop this might not be correct anymore const ctx: PostProcessingEffectContext = { handler: this, components: this._lastVolumeComponents, } for (let i = 0; i < this._lastVolumeComponents.length; i++) { const component = this._lastVolumeComponents[i]; //@ts-ignore component.context = context; if (component.apply) { if (component.active) { if (!context.mainCameraComponent) { console.error("No camera in scene found or available yet - can not create postprocessing effects"); return; } // apply or collect effects const res = component.apply(ctx); if (!res) continue; if (Array.isArray(res)) { this._effects.push(...res); } else this._effects.push(res); } } else { if (component.active) showBalloonWarning("Volume component is not a VolumeComponent: " + component["__type"]); } } // Ensure that we have a tonemapping effect if the renderer is set to use a tone mapping if (this.context.renderer.toneMapping != NoToneMapping) { if (!this._effects.find(e => e instanceof _TonemappingEffect)) { const tonemapping = new _TonemappingEffect(); this._effects.push(tonemapping); } } this.applyEffects(context); } /** Build composer passes */ private applyEffects(context: Context) { const effectsOrPasses = this._effects; if (effectsOrPasses.length <= 0) return; const camera = context.mainCameraComponent as Camera; const renderer = context.renderer; const scene = context.scene; const cam = camera.threeCamera; // Store the auto clear setting because the postprocessing composer just disables it // and when we disable postprocessing we want to restore the original setting // https://github.com/pmndrs/postprocessing/blob/271944b74b543a5b743a62803a167b60cc6bb4ee/src/core/EffectComposer.js#L230C12-L230C12 renderer[autoclearSetting] = renderer.autoClear; const maxSamples = renderer.capabilities.maxSamples; // create composer and set active on context if (!this._composer) { // const hdrRenderTarget = new WebGLRenderTarget(window.innerWidth, window.innerHeight, { type: HalfFloatType }); this._composer = new EffectComposer(renderer, { frameBufferType: HalfFloatType, stencilBuffer: true, multisampling: Math.min(DeviceUtilities.isMobileDevice() ? 4 : 8, maxSamples), }); } if (context.composer && context.composer !== this._composer) { console.warn("There's already an active EffectComposer in your scene: replacing it with a new one. This might cause unexpected behaviour. Make sure to only use one PostprocessingManager/Volume in your scene."); } context.composer = this._composer; const composer = context.composer; composer.setMainCamera(cam); composer.setRenderer(renderer); composer.setMainScene(scene); for (const prev of composer.passes) prev.dispose(); composer.removeAllPasses(); // Render to screen pass const screenpass = new RenderPass(scene, cam); screenpass.name = "Render To Screen"; screenpass.mainScene = scene; composer.addPass(screenpass); const automaticEffectsOrdering = true; if (automaticEffectsOrdering) { try { this.orderEffects(); const effects: Array<Effect> = []; for (const ef of effectsOrPasses) { if (ef instanceof Effect) effects.push(ef as Effect); else if (ef instanceof Pass) { const pass = new EffectPass(cam, ...effects); pass.mainScene = scene; pass.name = effects.map(e => e.constructor.name).join(", "); pass.enabled = true; // composer.addPass(pass); effects.length = 0; composer.addPass(ef as Pass); } else { // seems some effects are not correctly typed, but three can deal with them, // so we might need to just pass them through // composer.addPass(ef); } } // create and apply uber pass if (effects.length > 0) { const pass = new EffectPass(cam, ...effects); pass.name = effects.map(e => e.name).join(" "); pass.mainScene = scene; pass.enabled = true; composer.addPass(pass); } } catch (e) { console.error("Error while applying postprocessing effects", e); composer.removeAllPasses(); } } else { for (const ef of effectsOrPasses) { if (ef instanceof Effect) composer.addPass(new EffectPass(cam, ef as Effect)); else if (ef instanceof Pass) composer.addPass(ef as Pass); else // seems some effects are not correctly typed, but three can deal with them, // so we just pass them through composer.addPass(ef); } } if (debug) console.log("PostProcessing Passes", effectsOrPasses, "->", composer.passes); } private orderEffects() { if (debug) console.log("Before ordering effects", [...this._effects]); // TODO: enforce correct order of effects (e.g. DOF before Bloom) const effects = this._effects; effects.sort((a, b) => { // we use find index here because sometimes constructor names are prefixed with `_` // TODO: find a more robust solution that isnt name based (not sure if that exists tho... maybe we must give effect TYPES some priority/index) const aidx = effectsOrder.findIndex(e => a.constructor.name.endsWith(e.name)); const bidx = effectsOrder.findIndex(e => b.constructor.name.endsWith(e.name)); // Unknown effects should be rendered first if (aidx < 0) { if (debug) console.warn("Unknown effect found: ", a.constructor.name); return -1; } else if (bidx < 0) { if (debug) console.warn("Unknown effect found: ", b.constructor.name); return 1; } if (aidx < 0) return 1; if (bidx < 0) return -1; return aidx - bidx; }); if (debug) console.log("After ordering effects", [...this._effects]); for (let i = 0; i < effects.length; i++) { const effect = effects[i] as any; if (effect?.configuration?.gammaCorrection !== undefined) { const isLast = i === effects.length - 1; effect.configuration.gammaCorrection = isLast; } } } } // Order of effects for correct results. // Aligned with https://github.com/pmndrs/postprocessing/wiki/Effect-Merging#effect-execution-order export const effectsOrder: Array<Constructor<Effect | Pass>> = [ NormalPass, DepthDownsamplingPass, SMAAEffect, SSAOEffect, N8AOPostPass, TiltShiftEffect, DepthOfFieldEffect, ChromaticAberrationEffect, BloomEffect, SelectiveBloomEffect, VignetteEffect, PixelationEffect, _TonemappingEffect, HueSaturationEffect, BrightnessContrastEffect, _SharpeningEffect, ];