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

351 lines 16.2 kB
import { HalfFloatType, NoToneMapping } from "three"; import { showBalloonWarning } from "../../engine/debug/index.js"; // import { internal_SetSharpeningEffectModule } from "./Effects/Sharpening.js"; import { MODULES } from "../../engine/engine_modules.js"; import { getParam } from "../../engine/engine_utils.js"; globalThis["NEEDLE_USE_POSTPROCESSING"] = globalThis["NEEDLE_USE_POSTPROCESSING"] !== undefined ? globalThis["NEEDLE_USE_POSTPROCESSING"] : true; const debug = getParam("debugpost"); const dontMergePasses = getParam("debugpostpasses"); 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 { _composer = null; _lastVolumeComponents; _effects = []; get isActive() { return this._isActive; } get composer() { return this._composer; } _isActive = false; context; constructor(context) { this.context = context; } apply(components) { if ("env" in import.meta && import.meta.env.VITE_NEEDLE_USE_POSTPROCESSING === "false") { if (debug) console.warn("Postprocessing is disabled via vite env setting"); else console.debug("Postprocessing is disabled via vite env setting"); return Promise.resolve(); } if (!NEEDLE_USE_POSTPROCESSING) { if (debug) console.warn("Postprocessing is disabled via global vite define setting"); else console.debug("Postprocessing is disabled via vite define"); return Promise.resolve(); } this._isActive = true; return 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]; 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; } async onApply(context, components) { if (!components) return; // IMPORTANT // Load postprocessing modules ONLY here to get lazy loading of the postprocessing package await Promise.all([ MODULES.POSTPROCESSING.load(), MODULES.POSTPROCESSING_AO.load(), // import("./Effects/Sharpening.effect") ]); // try { // internal_SetSharpeningEffectModule(modules[2]); // } // catch (err) { // console.error(err); // } 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 = { 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 MODULES.POSTPROCESSING.MODULE.ToneMappingEffect)) { const tonemapping = new MODULES.POSTPROCESSING.MODULE.ToneMappingEffect(); this._effects.push(tonemapping); } } this.applyEffects(context); } /** Build composer passes */ applyEffects(context) { const effectsOrPasses = this._effects; if (effectsOrPasses.length <= 0) return; const camera = context.mainCameraComponent; 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; // create composer and set active on context if (!this._composer) { // const hdrRenderTarget = new WebGLRenderTarget(window.innerWidth, window.innerHeight, { type: HalfFloatType }); this._composer = new MODULES.POSTPROCESSING.MODULE.EffectComposer(renderer, { frameBufferType: HalfFloatType, stencilBuffer: true, }); } 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 MODULES.POSTPROCESSING.MODULE.RenderPass(scene, cam); screenpass.name = "Render To Screen"; screenpass.mainScene = scene; composer.addPass(screenpass); const automaticEffectsOrdering = true; if (automaticEffectsOrdering && !dontMergePasses) { try { this.orderEffects(); const effects = []; for (const ef of effectsOrPasses) { if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect) effects.push(ef); else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass) { composer.addPass(ef); } 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 MODULES.POSTPROCESSING.MODULE.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 { // we still want to sort passes, but we do not want to merge them for debugging if (automaticEffectsOrdering) this.orderEffects(); for (const ef of effectsOrPasses) { if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect) { const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ef); pass.name = ef.name; composer.addPass(pass); } else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass) composer.addPass(ef); 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 →", composer.passes); // DepthEffect for debugging purposes, disabled by default, can be selected in the debug pass select const depthEffect = new MODULES.POSTPROCESSING.MODULE.DepthEffect({ blendFunction: MODULES.POSTPROCESSING.MODULE.BlendFunction.NORMAL, inverted: true, }); depthEffect.name = "Depth Effect"; const depthPass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, depthEffect); depthPass.name = "Depth Effect Pass"; depthPass.enabled = false; composer.passes.push(depthPass); if (this._passIndices !== null) { const newPasses = [composer.passes[0]]; if (this._passIndices.length > 0) { newPasses.push(...this._passIndices .filter(x => x !== 0) .map(index => composer.passes[index]) .filter(pass => pass)); } if (newPasses.length > 0) { console.log("[PostProcessing] Passes (selected) →", newPasses); } composer.passes.length = 0; for (const pass of newPasses) { pass.enabled = true; pass.renderToScreen = false; // allows automatic setting for the last pass composer.addPass(pass); } } const menu = this.context.menu; if (menu && this._passIndices === null) { if (this._menuEntry) this._menuEntry.remove(); const select = document.createElement("select"); select.multiple = true; const defaultOpt = document.createElement("option"); defaultOpt.innerText = "Final Output"; defaultOpt.value = "-1"; select.appendChild(defaultOpt); for (const eff of composer.passes) { const opt = document.createElement("option"); opt.innerText = eff.name; opt.value = `${composer.passes.indexOf(eff)}`; opt.title = eff.name; select.appendChild(opt); } menu.appendChild(select); this._menuEntry = select; select.addEventListener("change", () => { const indices = Array.from(select.selectedOptions).map(option => parseInt(option.value)); if (indices.length === 1 && indices[0] === -1) { this._passIndices = null; } else { this._passIndices = indices; } this.applyEffects(context); }); } } } _menuEntry = null; _passIndices = null; orderEffects() { if (debug) console.log("Before ordering effects", [...this._effects]); // Order of effects for correct results. // Aligned with https://github.com/pmndrs/postprocessing/wiki/Effect-Merging#effect-execution-order // We can not put this into global scope because then the module might not yet be initialized effectsOrder ??= [ MODULES.POSTPROCESSING.MODULE.NormalPass, MODULES.POSTPROCESSING.MODULE.DepthDownsamplingPass, MODULES.POSTPROCESSING.MODULE.SMAAEffect, MODULES.POSTPROCESSING.MODULE.SSAOEffect, MODULES.POSTPROCESSING_AO.MODULE.N8AOPostPass, MODULES.POSTPROCESSING.MODULE.TiltShiftEffect, MODULES.POSTPROCESSING.MODULE.DepthOfFieldEffect, MODULES.POSTPROCESSING.MODULE.ChromaticAberrationEffect, MODULES.POSTPROCESSING.MODULE.BloomEffect, MODULES.POSTPROCESSING.MODULE.SelectiveBloomEffect, MODULES.POSTPROCESSING.MODULE.VignetteEffect, MODULES.POSTPROCESSING.MODULE.PixelationEffect, MODULES.POSTPROCESSING.MODULE.ToneMappingEffect, MODULES.POSTPROCESSING.MODULE.HueSaturationEffect, MODULES.POSTPROCESSING.MODULE.BrightnessContrastEffect, // __SHARPENING_MODULE._SharpeningEffect, ]; // 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]; if (effect?.configuration?.gammaCorrection !== undefined) { const isLast = i === effects.length - 1; effect.configuration.gammaCorrection = isLast; } } } } let effectsOrder = null; //# sourceMappingURL=PostProcessingHandler.js.map