@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
262 lines • 11.4 kB
JavaScript
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 { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
import { _SharpeningEffect } from "./Effects/Sharpening.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 {
_composer = null;
_lastVolumeComponents;
_effects = [];
get isActive() {
return this._isActive;
}
get composer() {
return this._composer;
}
_isActive = false;
context;
constructor(context) {
this.context = context;
}
apply(components) {
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];
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;
}
onApply(context, components) {
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 = {
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 */
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;
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 = [];
for (const ef of effectsOrPasses) {
if (ef instanceof Effect)
effects.push(ef);
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);
}
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));
else if (ef instanceof 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", effectsOrPasses, "->", composer.passes);
}
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];
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 = [
NormalPass,
DepthDownsamplingPass,
SMAAEffect,
SSAOEffect,
N8AOPostPass,
TiltShiftEffect,
DepthOfFieldEffect,
ChromaticAberrationEffect,
BloomEffect,
SelectiveBloomEffect,
VignetteEffect,
PixelationEffect,
_TonemappingEffect,
HueSaturationEffect,
BrightnessContrastEffect,
_SharpeningEffect,
];
//# sourceMappingURL=PostProcessingHandler.js.map