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.

386 lines • 18.6 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js"; import { serializeable } from "../../engine/engine_serialization_decorator.js"; import { DeviceUtilities, getParam } from "../../engine/engine_utils.js"; import { Behaviour } from "../Component.js"; import { EffectWrapper } from "./Effects/EffectWrapper.js"; import { PostProcessingEffect } from "./PostProcessingEffect.js"; import { PostProcessingHandler } from "./PostProcessingHandler.js"; import { setPostprocessingManagerType } from "./utils.js"; import { VolumeParameter } from "./VolumeParameter.js"; import { VolumeProfile } from "./VolumeProfile.js"; const debug = getParam("debugpost"); /** The Volume/PostprocessingManager component is responsible for managing post processing effects. * Add this component to any object in your scene to enable post processing effects. * * @example Add bloom * ```ts * const volume = new Volume(); * volume.addEffect(new BloomEffect({ * intensity: 3, * luminanceThreshold: .2 * })); * gameObject.addComponent(volume); * ``` * * @example Remove bloom * ```ts * volume.removeEffect(bloom); * ``` * * @example Add pixelation * ```ts * const pixelation = new PixelationEffect(); * pixelation.granularity.value = 10; * volume.addEffect(pixelation); * ``` * * @category Rendering * @category Effects * @group Components */ export class Volume extends Behaviour { get isPostProcessingManager() { return true; } /** Currently active postprocessing effects */ get effects() { return this._activeEffects; } get dirty() { return this._isDirty; } set dirty(value) { this._isDirty = value; } sharedProfile; /** * Set multisampling to "auto" to automatically adjust the multisampling level based on performance. * Set to a number to manually set the multisampling level. * @default "auto" * @min 0 * @max renderer.capabilities.maxSamples */ multisampling = "auto"; /** * Add a post processing effect to the stack and schedules the effect stack to be re-created. */ addEffect(effect) { let entry = effect; if (!(entry instanceof PostProcessingEffect)) { entry = new EffectWrapper(entry); } if (entry.gameObject === undefined) this.gameObject.addComponent(entry); if (this._effects.includes(entry)) return effect; this._effects.push(entry); this._isDirty = true; return effect; } /** * Remove a post processing effect from the stack and schedules the effect stack to be re-created. */ removeEffect(effect) { let index = -1; if (!(effect instanceof PostProcessingEffect)) { index = this._effects.findIndex(e => e instanceof EffectWrapper && e.effect === effect); } else { index = this._effects.indexOf(effect); } if (index !== -1) { this._effects.splice(index, 1); this._isDirty = true; return effect; } else if (effect instanceof PostProcessingEffect) { // if the effect is part of the shared profile remove it from there const si = this.sharedProfile?.components?.indexOf(effect); if (si !== undefined && si !== -1) { this._isDirty = true; this.sharedProfile?.components?.splice(si, 1); } } return effect; } _postprocessing; _activeEffects = []; _effects = []; /** * When dirty the post processing effects will be re-applied */ markDirty() { this._isDirty = true; } /** @internal */ awake() { if (debug) { console.log("PostprocessingManager Awake", this); console.log("Press P to toggle post processing"); window.addEventListener("keydown", (e) => { if (e.key === "p") { this.enabled = !this.enabled; showBalloonMessage("Toggle PostProcessing " + this.name + ": Enabled=" + this.enabled); this.markDirty(); } }); } // ensure the profile is initialized this.sharedProfile?.__init(this); } _componentEnabledTime = -1; _multisampleAutoChangeTime = 0; _multisampleAutoDecreaseTime = 0; /** @internal */ onEnable() { this._componentEnabledTime = this.context.time.realtimeSinceStartup; this._isDirty = true; } /** @internal */ onDisable() { this._postprocessing?.unapply(); this._isDirty = false; } /** @internal */ onBeforeRender() { if (!this.context.isInXR) { // TODO: not sure when this was/could be the case? Maybe when using the default three composer? // if (this.context.composer && (this.context.composer instanceof EffectComposer) === false) { // if (debug) console.warn("PostProcessing: The current composer is not an EffectComposer - this is not supported"); // return; // } // Wait for the first frame to be rendered before creating because then we know we have a camera (issue 135) if (this.context.mainCamera) { if (this._isDirty) { this.apply(); } } if (this.context.composer && this._postprocessing && this._postprocessing.composer === this.context.composer) { if (this.context.renderer.getContext().isContextLost()) { this.context.renderer.forceContextRestore(); } if (this.context.composer.getRenderer() !== this.context.renderer) this.context.composer.setRenderer(this.context.renderer); this.context.composer.setMainScene(this.context.scene); if (this.multisampling === "auto") { // If the postprocessing handler is using depth+normals (e.g. with SMAA) we ALWAYS disable multisampling to avoid ugly edges if (this._postprocessing && (this._postprocessing.hasSmaaEffect)) { if (this._postprocessing.multisampling !== 0) { this._postprocessing.multisampling = 0; if (debug || isDevEnvironment()) { console.log(`[PostProcessing] multisampling is disabled because it's set to 'auto' on your PostprocessingManager/Volume component that also has an SMAA effect.\n\nIf you need multisampling consider changing 'auto' to a fixed value (e.g. 4).`); } } } else { const timeSinceLastChange = this.context.time.realtimeSinceStartup - this._multisampleAutoChangeTime; if (this.context.time.realtimeSinceStartup - this._componentEnabledTime > 2 && timeSinceLastChange > .5) { const prev = this._postprocessing.multisampling; if (this._postprocessing.multisampling > 0 && this.context.time.smoothedFps <= 50) { this._multisampleAutoChangeTime = this.context.time.realtimeSinceStartup; this._multisampleAutoDecreaseTime = this.context.time.realtimeSinceStartup; let newMultiSample = this._postprocessing.multisampling * .5; newMultiSample = Math.floor(newMultiSample); if (newMultiSample != this._postprocessing.multisampling) { this._postprocessing.multisampling = newMultiSample; } if (debug) console.debug(`[PostProcessing] Reduced multisampling from ${prev} to ${this._postprocessing.multisampling}`); } // if performance is good for a while try increasing multisampling again else if (timeSinceLastChange > 1 && this.context.time.smoothedFps >= 59 && this._postprocessing.multisampling < this.context.renderer.capabilities.maxSamples && this.context.time.realtimeSinceStartup - this._multisampleAutoDecreaseTime > 10) { this._multisampleAutoChangeTime = this.context.time.realtimeSinceStartup; let newMultiSample = this._postprocessing.multisampling <= 0 ? 1 : this._postprocessing.multisampling * 2; newMultiSample = Math.floor(newMultiSample); if (newMultiSample !== this._postprocessing.multisampling) { this._postprocessing.multisampling = newMultiSample; } if (debug) console.debug(`[PostProcessing] Increased multisampling from ${prev} to ${this._postprocessing.multisampling}`); } } } } else { const newMultiSample = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples)); if (newMultiSample !== this._postprocessing.multisampling) this._postprocessing.multisampling = newMultiSample; } // only set the main camera if any pass has a different camera // trying to avoid doing this regularly since it involves doing potentially unnecessary work // https://github.com/pmndrs/postprocessing/blob/3d3df0576b6d49aec9e763262d5a1ff7429fd91a/src/core/EffectComposer.js#L406 if (this.context.mainCamera) { const passes = this.context.composer.passes; for (const pass of passes) { if (pass.mainCamera && pass.mainCamera !== this.context.mainCamera) { this.context.composer.setMainCamera(this.context.mainCamera); break; } } } } } } /** @internal */ onDestroy() { this._postprocessing?.dispose(); } _lastApplyTime; _rapidApplyCount = 0; _isDirty = false; apply() { if (debug) console.log(`Apply PostProcessing "${this.name || "unnamed"}"`); if (isDevEnvironment()) { if (this._lastApplyTime !== undefined && Date.now() - this._lastApplyTime < 100) { this._rapidApplyCount++; if (this._rapidApplyCount === 5) console.warn("Detected rapid post processing modifications - this might be a bug", this); } this._lastApplyTime = Date.now(); } this._isDirty = false; this._activeEffects.length = 0; // get from profile if (this.sharedProfile?.components) { const comps = this.sharedProfile.components; for (const effect of comps) { if (effect.active && effect.enabled && !this._activeEffects.includes(effect)) this._activeEffects.push(effect); } } // add effects registered via code for (const effect of this._effects) { if (effect.active && effect.enabled && !this._activeEffects.includes(effect)) this._activeEffects.push(effect); } if (this._activeEffects.length > 0) { if (!this._postprocessing) this._postprocessing = new PostProcessingHandler(this.context); this._postprocessing.apply(this._activeEffects) ?.then(() => { if (!this.activeAndEnabled) return; this._applyPostQueue(); if (this._postprocessing) { if (this.multisampling === "auto") { this._postprocessing.multisampling = DeviceUtilities.isMobileDevice() ? 2 : 4; } else { this._postprocessing.multisampling = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples)); } if (debug) console.debug(`[PostProcessing] Set multisampling to ${this._postprocessing.multisampling} (Is Mobile: ${DeviceUtilities.isMobileDevice()})`); } else if (debug) { console.warn(`[PostProcessing] No composer found`); } }); } else { this._postprocessing?.unapply(false); } } _applyPostQueue() { if (this._modificationQueue) { for (const entry of this._modificationQueue.values()) this.onEditorModification(entry); this._modificationQueue.clear(); } } /** called from needle editor sync package if its active */ onEditorModification(modification) { if (modification.propertyName.startsWith("postprocessing.")) { if (!this._postprocessing) { if (!this._modificationQueue) this._modificationQueue = new Map(); this._modificationQueue.set(modification.propertyName, modification); return true; } if (!this._activeEffects?.length) return; const path = modification.propertyName.split("."); if (path.length === 3 || path.length === 4) { const componentName = path[1]; const propertyName = path[2]; for (const comp of this._activeEffects) { if (comp.typeName?.toLowerCase() === componentName.toLowerCase()) { if (propertyName === "active") { comp.active = modification.value; this.scheduleRecreate(); return; } // cache the volume parameters if (!effectVolumeProperties.has(componentName)) { const volumeParameterKeys = new Array(); effectVolumeProperties.set(componentName, volumeParameterKeys); const keys = Object.keys(comp); for (const key of keys) { const prop = comp[key]; if (prop instanceof VolumeParameter) { volumeParameterKeys.push(key); } } } if (effectVolumeProperties.has(componentName)) { const paramName = propertyName.toLowerCase(); const volumeParameterKeys = effectVolumeProperties.get(componentName); for (const key of volumeParameterKeys) { if (key.toLowerCase() === paramName) { const prop = comp[key]; if (prop instanceof VolumeParameter) { const isActiveStateChange = path.length === 4 && path[3] === "active"; if (isActiveStateChange) { prop.overrideState = modification.value; this.scheduleRecreate(); } else if (prop && prop.value !== undefined) { prop.value = modification.value; } } return; } } } console.warn("Unknown modification", propertyName); return; } } } return true; } return false; } _modificationQueue; _recreateId = -1; scheduleRecreate() { // When the editor modifications come in with changed active effects we want/need to re-create the effects // We defer it slightly because multiple active changes could be made and we dont want to recreate the full effect stack multiple times const id = ++this._recreateId; setTimeout(() => { if (id !== this._recreateId) return; this.onDisable(); this.onEnable(); }, 200); } } __decorate([ serializeable(VolumeProfile) ], Volume.prototype, "sharedProfile", void 0); __decorate([ serializeable() ], Volume.prototype, "multisampling", void 0); /** cached VolumeParameter keys per object */ const effectVolumeProperties = new Map(); setPostprocessingManagerType(Volume); export { Volume as PostProcessingManager }; //# sourceMappingURL=Volume.js.map