threepipe
Version:
A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.
209 lines • 9.25 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 { LinearFilter } from 'three';
import { PipelinePassPlugin } from '../base/PipelinePassPlugin';
import { uiFolderContainer, uiToggle } from 'uiconfig.js';
import { AddBlendTexturePass } from '../../postprocessing/AddBlendTexturePass';
import { now, serialize, timeout } from 'ts-browser-helpers';
import { ProgressivePlugin } from './ProgressivePlugin';
/**
* FrameFade Plugin
*
* Adds a post-render pass to smoothly fade to a new rendered frame over time.
* This is useful for example when changing the camera position, material, object properties, etc to avoid a sudden jump.
* @category Plugins
*/
let FrameFadePlugin = class FrameFadePlugin extends PipelinePassPlugin {
constructor(enabled = true) {
super();
this.passId = 'frameFade';
this.dependencies = [ProgressivePlugin];
// disables fadeOn... options but not serialized
this.isEditor = false;
this.fadeOnActiveCameraChange = true;
this.fadeOnMaterialUpdate = true;
this.fadeOnSceneUpdate = true;
this._pointerEnabled = true;
this.saveFrameTimeThreshold = 500; // ms
this._fadeCam = async (ev) => ev.frameFade !== false && !this.isEditor && this.fadeOnActiveCameraChange && this.startTransition(ev.fadeDuration || 1000);
this._fadeMat = async (ev) => ev.frameFade !== false && !this.isEditor && this.fadeOnMaterialUpdate && this.startTransition(ev.fadeDuration || 200);
this._fadeScene = async (ev) => ev.frameFade !== false && !this.isEditor && this.fadeOnSceneUpdate && this.startTransition(ev.fadeDuration || 500);
this._fadeObjectUpdate = async (ev) => ev.frameFade && !this.isEditor && this.startTransition(ev.fadeDuration || 500);
this._onPointerMove = (ev) => {
const canvas = this._viewer?.canvas;
if (!canvas) {
this._pointerEnabled = false;
return;
}
// no button is pressed
if (!ev.buttons || ev.target !== canvas) {
this._pointerEnabled = true;
return;
}
// check if pointer is over canvas
const rect = canvas.getBoundingClientRect();
const x = (ev.clientX - rect.left) / rect.width;
const y = (ev.clientY - rect.top) / rect.height;
this._pointerEnabled = x < 0 || x > 1 || y < 0 || y > 1;
};
this.enabled = enabled;
this.startTransition = this.startTransition.bind(this);
this.stopTransition = this.stopTransition.bind(this);
this._fadeCam = this._fadeCam.bind(this);
this._fadeMat = this._fadeMat.bind(this);
this.isDisabled = ((sup) => () => !this._pointerEnabled || sup())(this.isDisabled);
}
/**
* Start a frame fade transition.
* Note that the current frame data will only be used if the last running transition is ended or near the end. To do it anyway, call {@link stopTransition} first
* @param duration
*/
async startTransition(duration) {
if (!this._viewer || !this._pass || this.isDisabled())
return;
if (!this._target)
this._target = this._viewer.renderManager.getTempTarget({
sizeMultiplier: 1.,
minFilter: LinearFilter,
magFilter: LinearFilter,
colorSpace: this._viewer.renderManager.composerTarget.texture.colorSpace,
});
if (this._pass.fadeTimeState < this.saveFrameTimeThreshold) // only save if very near the end
this._pass.toSaveFrame = true;
this._pass.fadeTimeState = Math.max(duration, this._pass.fadeTimeState);
this._pass.fadeTime = this._pass.fadeTimeState;
// this._pass.enabled = true
this.setDirty();
await timeout(duration);
}
/**
* Stop a frame fade transition if running. Note that it will be stopped next frame.
*/
stopTransition() {
if (!this._pass)
return;
this._pass.fadeTimeState = 0.; // will be stopped in update on next frame
}
onAdded(viewer) {
super.onAdded(viewer);
viewer.scene.addEventListener('mainCameraUpdate', this.stopTransition);
viewer.scene.addEventListener('mainCameraChange', this._fadeCam);
viewer.scene.addEventListener('materialUpdate', this._fadeMat);
viewer.scene.addEventListener('sceneUpdate', this._fadeScene);
viewer.scene.addEventListener('objectUpdate', this._fadeObjectUpdate);
window.addEventListener('pointermove', this._onPointerMove); // has to be on window
}
onRemove(viewer) {
viewer.scene.removeEventListener('mainCameraUpdate', this.stopTransition);
viewer.scene.removeEventListener('mainCameraChange', this._fadeCam);
viewer.scene.removeEventListener('materialUpdate', this._fadeMat);
viewer.scene.removeEventListener('sceneUpdate', this._fadeScene);
viewer.scene.removeEventListener('objectUpdate', this._fadeObjectUpdate);
window.removeEventListener('pointermove', this._onPointerMove);
super.onRemove(viewer);
}
setDirty() {
super.setDirty();
if (this.isDisabled())
return;
this._viewer?.setDirty();
}
get dirty() {
return !this.isDisabled() && !!this._pass && this._pass.fadeTimeState > 0;
}
set dirty(_) {
console.error('FrameFadePlugin.dirty is readonly');
}
_createPass() {
return new FrameFadeBlendPass(this.passId, this, this._viewer?.renderManager.maxHDRIntensity);
}
get canFrameFade() {
return this._target && this._pointerEnabled &&
this.dirty && this._pass &&
this._pass.fadeTimeState > 0.001 &&
this._viewer && this._viewer.scene.renderCamera === this._viewer.scene.mainCamera;
}
get lastFrame() {
return this._viewer?.getPlugin(ProgressivePlugin)?.texture;
}
get target() {
return this._target;
}
_beforeRender() {
if (!super._beforeRender() || !this._pass)
return false;
if (this.isDisabled())
this.stopTransition();
if (this._pass.fadeTimeState < 0.001) {
this._pass.toSaveFrame = false;
if (this._target && this._viewer) {
this._viewer.renderManager.releaseTempTarget(this._target);
this._target = undefined;
}
}
return true;
}
};
FrameFadePlugin.PluginType = 'FrameFadePlugin';
__decorate([
serialize(),
uiToggle()
], FrameFadePlugin.prototype, "fadeOnActiveCameraChange", void 0);
__decorate([
serialize(),
uiToggle()
], FrameFadePlugin.prototype, "fadeOnMaterialUpdate", void 0);
__decorate([
serialize(),
uiToggle()
], FrameFadePlugin.prototype, "fadeOnSceneUpdate", void 0);
FrameFadePlugin = __decorate([
uiFolderContainer('FrameFade Plugin')
], FrameFadePlugin);
export { FrameFadePlugin };
export class FrameFadeBlendPass extends AddBlendTexturePass {
constructor(passId, plugin, maxIntensity = 120) {
super(undefined, maxIntensity);
this.passId = passId;
this.plugin = plugin;
this.before = ['progressive', 'taa'];
this.after = ['render'];
this.required = ['render', 'progressive'];
this.dirty = () => false;
this.fadeTime = 0; // ms
this.fadeTimeState = 0;
this.toSaveFrame = false;
this._lastTime = 0;
}
render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) {
this.needsSwap = false;
const target = this.plugin.target;
if (!this.plugin.canFrameFade || !target)
return;
const lastFrame = this.plugin.lastFrame;
if (this.toSaveFrame && lastFrame) {
renderer.renderManager.blit(target, { source: lastFrame, respectColorSpace: false });
this._lastTime = 0;
this.toSaveFrame = false;
}
this.uniforms.tDiffuse2.value = target.texture;
const weight = this.fadeTimeState / this.fadeTime;
this.uniforms.weight2.value.setScalar(weight);
this.uniforms.weight2.value.w = 1;
this.uniforms.weight.value.setScalar(1. - weight);
this.uniforms.weight.value.w = 1;
super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive);
this.needsSwap = true;
const time = now();
if (this._lastTime < 10)
this._lastTime = time - 10; // ms
const dt = time - this._lastTime;
this._lastTime = time;
this.fadeTimeState -= dt;
}
}
//# sourceMappingURL=FrameFadePlugin.js.map