@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
JavaScript
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