| - [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer | - [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer | ||||
| - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | ||||
| - [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas | - [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas | ||||
| - [FrameFadePlugin](#framefadeplugin) - Post-render pass to smoothly fade to a new rendered frame over time | |||||
| - [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files | - [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files | ||||
| - [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files | - [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files | ||||
| - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | ||||
| previewPlugin.addTarget(()=>normalPlugin.target, 'normal', false, false) | previewPlugin.addTarget(()=>normalPlugin.target, 'normal', false, false) | ||||
| ``` | ``` | ||||
| ## FrameFadePlugin | |||||
| todo: image | |||||
| Example: https://threepipe.org/examples/#frame-fade-plugin/ | |||||
| Source Code: [src/plugins/pipeline/FrameFadePlugin.ts](./src/plugins/pipeline/FrameFadePlugin.ts) | |||||
| API Reference: [FrameFadePlugin](https://threepipe.org/docs/classes/FrameFadePlugin.html) | |||||
| FrameFadePlugin adds a post-render pass to the render manager and blends the last frame with the current frame over time. This is useful for creating smooth transitions between frames for example when changing the camera position, material, object properties, etc to avoid a sudden jump. | |||||
| ```typescript | |||||
| import {ThreeViewer, FrameFadePlugin} from 'threepipe' | |||||
| const viewer = new ThreeViewer({...}) | |||||
| const fadePlugin = viewer.addPluginSync(new FrameFadePlugin()) | |||||
| // Make some changes in the scene (any visual change that needs to be faded) | |||||
| // Start transition and wait for it to finish | |||||
| await fadePlugin.startTransition(400) // duration in ms | |||||
| ``` | |||||
| To stop a transition, call `fadePlugin.stopTransition()`. This will immediately set the current frame to the last frame and stop the transition. The transition is also automatically stopped when the camera is moved or some pointer event occurs on the canvas. | |||||
| The plugin automatically tracks `setDirty()` function calls in objects, materials and the scene. It can be triggerred by calling `setDirty` on any material or object in the scene. Check the [example](https://threepipe.org/examples/#frame-fade-plugin/) for a demo. This can be disabled by options in the plugin. | |||||
| ## Rhino3dmLoadPlugin | ## Rhino3dmLoadPlugin | ||||
| Example: https://threepipe.org/examples/#rhino3dm-load/ | Example: https://threepipe.org/examples/#rhino3dm-load/ |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Frame Fade Plugin</title> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, BoxGeometry, FrameFadePlugin, Mesh, PhysicalMaterial, ThreeViewer} from 'threepipe' | |||||
| import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| }) | |||||
| viewer.addPluginSync(FrameFadePlugin) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| const cube = viewer.scene.addObject(new Mesh( | |||||
| new BoxGeometry(1, 1, 1), | |||||
| new PhysicalMaterial({color: 0xff0000}) | |||||
| )) | |||||
| createSimpleButtons({ | |||||
| ['Change Color']: ()=>{ | |||||
| cube.material.color.setHSL(Math.random(), 1, 0.5) | |||||
| cube.material.setDirty() // this will trigger frame fade | |||||
| }, | |||||
| ['Change Size']: ()=>{ | |||||
| cube.scale.setScalar(Math.random() * 1.5 + 0.5) | |||||
| cube.setDirty({fadeDuration: 1000}) // duration can be controlled by an option like this. | |||||
| }, | |||||
| ['Change Color (no fade)']: ()=>{ | |||||
| cube.material.color.setHSL(Math.random(), 1, 0.5) | |||||
| cube.material.setDirty({frameFade: false}) // disable frame fade for this update but re-render the scene. | |||||
| }, | |||||
| }) | |||||
| } | |||||
| init().then(_testFinish) |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>GLTF Animation Page Scroll</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| body { | |||||
| overflow-y: scroll; | |||||
| overflow-x: hidden; | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| padding: 0; | |||||
| background-color: white; | |||||
| } | |||||
| #canvas-container { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| z-index: 5; | |||||
| } | |||||
| #content{ | |||||
| z-index: 10; | |||||
| position: relative; | |||||
| height: auto; | |||||
| } | |||||
| section{ | |||||
| z-index: 100; | |||||
| height: 100vh; | |||||
| line-height: 100vh; | |||||
| font-size: 4rem; | |||||
| font-weight: 600; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| <div id="content"> | |||||
| <section> | |||||
| Section 1 | |||||
| </section> | |||||
| <section> | |||||
| Section 2 | |||||
| </section> | |||||
| <section> | |||||
| Section 3 | |||||
| </section> | |||||
| <section> | |||||
| Section 4 | |||||
| </section> | |||||
| <section> | |||||
| Section 5 | |||||
| </section> | |||||
| <section> | |||||
| Section 6 | |||||
| </section> | |||||
| <section> | |||||
| Section 7 | |||||
| </section> | |||||
| <section> | |||||
| Section 8 | |||||
| </section> | |||||
| <section> | |||||
| Section 9 | |||||
| </section> | |||||
| <section> | |||||
| Section 10 | |||||
| </section> | |||||
| <section> | |||||
| Section 11 | |||||
| </section> | |||||
| <section> | |||||
| Section 12 | |||||
| </section> | |||||
| <section> | |||||
| Section 13 | |||||
| </section> | |||||
| <section> | |||||
| Section 14 | |||||
| </section> | |||||
| <section> | |||||
| Section 15 | |||||
| </section> | |||||
| <section> | |||||
| Section 16 | |||||
| </section> | |||||
| <section> | |||||
| Section 17 | |||||
| </section> | |||||
| <section> | |||||
| Section 18 | |||||
| </section> | |||||
| <section> | |||||
| Section 19 | |||||
| </section> | |||||
| <section> | |||||
| Section 20 | |||||
| </section> | |||||
| <section> | |||||
| Section 21 | |||||
| </section> | |||||
| <section> | |||||
| Section 22 | |||||
| </section> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, GLTFAnimationPlugin, ICamera, ThreeViewer} from 'threepipe' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| }) | |||||
| const gltfAnimation = viewer.addPluginSync(GLTFAnimationPlugin) | |||||
| gltfAnimation.autoplayOnLoad = false | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| await viewer.load('https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Blender-Exporter@master/polly/project_polly.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| const fileCamera = viewer.scene.getObjectByName<ICamera>('Correction__MovingCamera') | |||||
| if (!fileCamera) return | |||||
| fileCamera.autoAspect = true | |||||
| fileCamera.userData.autoLookAtTarget = false | |||||
| fileCamera.activateMain() | |||||
| gltfAnimation.loopAnimations = false | |||||
| gltfAnimation.animateOnPageScroll = true | |||||
| gltfAnimation.pageScrollAnimationDamping = 0.1 | |||||
| gltfAnimation.playAnimation() | |||||
| } | |||||
| init().then(_testFinish) |
| fileCamera.autoAspect = true | fileCamera.autoAspect = true | ||||
| fileCamera.userData.autoLookAtTarget = false | fileCamera.userData.autoLookAtTarget = false | ||||
| fileCamera.activateMain() | fileCamera.activateMain() | ||||
| viewer.scene.mainCamera.refreshAspect() | |||||
| gltfAnimation.loopAnimations = false | |||||
| gltfAnimation.loopAnimations = true | |||||
| gltfAnimation.playAnimation() | gltfAnimation.playAnimation() | ||||
| console.log(gltfAnimation) | console.log(gltfAnimation) |
| <h2 class="category">Post-Processing</h2> | <h2 class="category">Post-Processing</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./tonemap-plugin/">Tonemap Plugin </a></li> | <li><a href="./tonemap-plugin/">Tonemap Plugin </a></li> | ||||
| <li><a href="./frame-fade-plugin/">Frame Fade Plugin </a></li> | |||||
| </ul> | </ul> | ||||
| <h2 class="category">Rendering</h2> | <h2 class="category">Rendering</h2> | ||||
| <ul> | <ul> | ||||
| <ul> | <ul> | ||||
| <li><a href="./gltf-animation-plugin/">glTF Animation Plugin </a></li> | <li><a href="./gltf-animation-plugin/">glTF Animation Plugin </a></li> | ||||
| <li><a href="./gltf-camera-animation/">glTF Camera Animation </a></li> | <li><a href="./gltf-camera-animation/">glTF Camera Animation </a></li> | ||||
| <li><a href="./obj-to-glb/">Convert OBJ to GLB </a></li> | |||||
| <li><a href="./3dm-to-glb/">Convert 3DM to GLB </a></li> | |||||
| <li><a href="./hdr-to-exr/">Convert HDR to EXR </a></li> | |||||
| <li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li> | |||||
| </ul> | </ul> | ||||
| <h2 class="category">Utils</h2> | <h2 class="category">Utils</h2> | ||||
| <ul> | <ul> |
| _testFinish, | _testFinish, | ||||
| DepthBufferPlugin, | DepthBufferPlugin, | ||||
| DropzonePlugin, | DropzonePlugin, | ||||
| FrameFadePlugin, | |||||
| FullScreenPlugin, | FullScreenPlugin, | ||||
| GLTFAnimationPlugin, | GLTFAnimationPlugin, | ||||
| HalfFloatType, | HalfFloatType, | ||||
| new DepthBufferPlugin(HalfFloatType, true, true), | new DepthBufferPlugin(HalfFloatType, true, true), | ||||
| new NormalBufferPlugin(HalfFloatType, false), | new NormalBufferPlugin(HalfFloatType, false), | ||||
| new RenderTargetPreviewPlugin(false), | new RenderTargetPreviewPlugin(false), | ||||
| new FrameFadePlugin(), | |||||
| new KTX2LoadPlugin(), | new KTX2LoadPlugin(), | ||||
| new KTXLoadPlugin(), | new KTXLoadPlugin(), | ||||
| new PLYLoadPlugin(), | new PLYLoadPlugin(), | ||||
| editor.loadPlugins({ | editor.loadPlugins({ | ||||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ||||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ||||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin], | |||||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin], | |||||
| ['Animation']: [GLTFAnimationPlugin], | ['Animation']: [GLTFAnimationPlugin], | ||||
| ['Debug']: [RenderTargetPreviewPlugin], | ['Debug']: [RenderTargetPreviewPlugin], | ||||
| }) | }) |
| import type {ChangeEvent, IUiConfigContainer} from 'uiconfig.js' | import type {ChangeEvent, IUiConfigContainer} from 'uiconfig.js' | ||||
| import type {SerializationMetaType} from '../utils' | import type {SerializationMetaType} from '../utils' | ||||
| import type {IObject3D} from './IObject' | import type {IObject3D} from './IObject' | ||||
| import {ISetDirtyCommonOptions} from './IObject' | |||||
| import type {ITexture} from './ITexture' | import type {ITexture} from './ITexture' | ||||
| import type {IImportResultUserData} from '../assetmanager' | import type {IImportResultUserData} from '../assetmanager' | ||||
| uiChangeEvent?: ChangeEvent | uiChangeEvent?: ChangeEvent | ||||
| } | } | ||||
| export interface IMaterialSetDirtyOptions { | |||||
| export interface IMaterialSetDirtyOptions extends ISetDirtyCommonOptions{ | |||||
| /** | /** | ||||
| * @default true | * @default true | ||||
| */ | */ | ||||
| bubbleToObject?: boolean, | bubbleToObject?: boolean, | ||||
| /** | |||||
| * @default true | |||||
| */ | |||||
| refreshUi?: boolean, | |||||
| /** | /** | ||||
| * @default true | * @default true | ||||
| */ | */ | ||||
| needsUpdate?: boolean, | needsUpdate?: boolean, | ||||
| /** | |||||
| * Event from uiconfig.js | |||||
| */ | |||||
| uiChangeEvent?: ChangeEvent, | |||||
| [key: string]: any | [key: string]: any | ||||
| } | } | ||||
| export interface IMaterialUserData extends IImportResultUserData{ | export interface IMaterialUserData extends IImportResultUserData{ |
| import {IDisposable} from 'ts-browser-helpers' | import {IDisposable} from 'ts-browser-helpers' | ||||
| import {IMaterial} from './IMaterial' | import {IMaterial} from './IMaterial' | ||||
| import {Event, Object3D} from 'three' | import {Event, Object3D} from 'three' | ||||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||||
| import {ChangeEvent, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||||
| import {IGeometry, IGeometryEvent} from './IGeometry' | import {IGeometry, IGeometryEvent} from './IGeometry' | ||||
| import {IImportResultUserData} from '../assetmanager' | import {IImportResultUserData} from '../assetmanager' | ||||
| import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js' | import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js' | ||||
| oldGeometry?: IGeometry|undefined // from geometryChanged | oldGeometry?: IGeometry|undefined // from geometryChanged | ||||
| } | } | ||||
| export interface IObjectSetDirtyOptions { | |||||
| export interface ISetDirtyCommonOptions { | |||||
| /** | |||||
| * Trigger UI Config Refresh along with setDirty. | |||||
| * Default `true`. Set to `false` to prevent UI Config refresh. | |||||
| */ | |||||
| refreshUi?: boolean | |||||
| /** | |||||
| * Enable/disable frame fade using {@link FrameFadePlugin} | |||||
| * Default `true`. when the plugin is enabled and has corresponding flags enabled | |||||
| */ | |||||
| frameFade?: boolean // for plugins | |||||
| /** | |||||
| * Duration for `frameFade` in ms. Check {@link FrameFadePlugin} for more details. | |||||
| */ | |||||
| fadeDuration?: number // for plugins | |||||
| /** | |||||
| * Event from uiconfig.js when some value changes from the UI. | |||||
| */ | |||||
| uiChangeEvent?: ChangeEvent, | |||||
| } | |||||
| export interface IObjectSetDirtyOptions extends ISetDirtyCommonOptions{ | |||||
| bubbleToParent?: boolean // bubble event to parent root | bubbleToParent?: boolean // bubble event to parent root | ||||
| change?: string | change?: string | ||||
| refreshScene?: boolean // update scene after setting dirty | refreshScene?: boolean // update scene after setting dirty | ||||
| geometryChanged?: boolean // whether to refresh stuff like ground. | geometryChanged?: boolean // whether to refresh stuff like ground. | ||||
| frameFade?: boolean // for plugins | |||||
| refreshUi?: boolean // for plugins | |||||
| /** | /** | ||||
| * @deprecated use {@link refreshScene} instead | * @deprecated use {@link refreshScene} instead | ||||
| */ | */ |
| scene?: IScene | null | scene?: IScene | null | ||||
| // change?: string | // change?: string | ||||
| } | } | ||||
| export type ISceneSetDirtyOptions = IObjectSetDirtyOptions & { | |||||
| [key: string]: any | |||||
| } | |||||
| export type ISceneSetDirtyOptions = IObjectSetDirtyOptions | |||||
| export type ISceneUserData = IObject3DUserData | export type ISceneUserData = IObject3DUserData | ||||
| // environmentLight?: IEnvironmentLight; | // environmentLight?: IEnvironmentLight; | ||||
| // processors: ObjectProcessorMap<'environment' | 'background'> | // processors: ObjectProcessorMap<'environment' | 'background'> | ||||
| addObject<T extends IObject3D>(imported: T, options?: AddObjectOptions): T; | |||||
| addObject<T extends IObject3D>(imported: T, options?: AddObjectOptions): T&IObject3D; | |||||
| setDirty(e?: ISceneSetDirtyOptions): void | setDirty(e?: ISceneSetDirtyOptions): void | ||||
| * @param imported | * @param imported | ||||
| * @param options | * @param options | ||||
| */ | */ | ||||
| addObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T { | |||||
| addObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T&IObject3D { | |||||
| if (options?.clearSceneObjects || options?.disposeSceneObjects) { | if (options?.clearSceneObjects || options?.disposeSceneObjects) { | ||||
| this.clearSceneModels(options.disposeSceneObjects) | this.clearSceneModels(options.disposeSceneObjects) | ||||
| } | } | ||||
| if (!imported) return imported | if (!imported) return imported | ||||
| if (!imported.isObject3D) { | if (!imported.isObject3D) { | ||||
| console.error('Invalid object, cannot add to scene.', imported) | console.error('Invalid object, cannot add to scene.', imported) | ||||
| return imported | |||||
| return imported as T&IObject3D | |||||
| } | } | ||||
| this._addObject3D(<IObject3D>imported, options) | this._addObject3D(<IObject3D>imported, options) | ||||
| this.dispatchEvent({type: 'addSceneObject', object: <IObject3D>imported}) | this.dispatchEvent({type: 'addSceneObject', object: <IObject3D>imported}) | ||||
| return imported | |||||
| return imported as T&IObject3D | |||||
| } | } | ||||
| /** | /** |
| } | } | ||||
| this.dispatchEvent({...options, type: 'update'}) // does not bubble | this.dispatchEvent({...options, type: 'update'}) // does not bubble | ||||
| this.dispatchEvent({...options, type: 'cameraUpdate', bubbleToParent: true}) // this sets dirty in the viewer | this.dispatchEvent({...options, type: 'cameraUpdate', bubbleToParent: true}) // this sets dirty in the viewer | ||||
| iObjectCommons.setDirty.call(this, options) | |||||
| iObjectCommons.setDirty.call(this, {refreshScene: false, ...options}) | |||||
| }, | }, | ||||
| activateMain: function(this: ICamera, options: Partial<ICameraEvent> = {}, _internal = false, _refresh = true): void { | activateMain: function(this: ICamera, options: Partial<ICameraEvent> = {}, _internal = false, _refresh = true): void { | ||||
| if (!_internal) return this.dispatchEvent({ | if (!_internal) return this.dispatchEvent({ |
| import {ProgressivePlugin} from '../pipeline/ProgressivePlugin' | import {ProgressivePlugin} from '../pipeline/ProgressivePlugin' | ||||
| import {IObject3D} from '../../core' | import {IObject3D} from '../../core' | ||||
| import {generateUUID} from '../../three' | import {generateUUID} from '../../three' | ||||
| type FrameFadePlugin = any // todo | |||||
| import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | |||||
| /** | /** | ||||
| * Manages playback of GLTF animations. | * Manages playback of GLTF animations. | ||||
| @uiSlider('Speed', [0.1, 4], 0.1) @serialize() animationSpeed = 1 | @uiSlider('Speed', [0.1, 4], 0.1) @serialize() animationSpeed = 1 | ||||
| /** | /** | ||||
| * Automatically track scroll and mouse wheel events to seek animations | |||||
| * Automatically track mouse wheel events to seek animations | |||||
| * Control damping/smoothness with {@link scrollAnimationDamping} | * Control damping/smoothness with {@link scrollAnimationDamping} | ||||
| * See also {@link animateOnDrag} | |||||
| * See also {@link animateOnPageScroll}. {@link animateOnDrag} | |||||
| */ | */ | ||||
| @uiToggle() @serialize() animateOnScroll = false | @uiToggle() @serialize() animateOnScroll = false | ||||
| */ | */ | ||||
| @uiSlider('Scroll Damping', [0, 1]) @serialize() scrollAnimationDamping = 0.1 | @uiSlider('Scroll Damping', [0, 1]) @serialize() scrollAnimationDamping = 0.1 | ||||
| /** | |||||
| * Automatically track scroll event in window and use `window.scrollY` along with {@link pageScrollHeight} to seek animations | |||||
| * Control damping/smoothness with {@link pageScrollAnimationDamping} | |||||
| * See also {@link animateOnDrag}, {@link animateOnScroll} | |||||
| */ | |||||
| @uiToggle() @serialize() animateOnPageScroll = false | |||||
| /** | |||||
| * Damping for the scroll animation, when {@link animateOnPage Scroll} is true. | |||||
| */ | |||||
| @uiSlider('Page Scroll Damping', [0, 1]) @serialize() pageScrollAnimationDamping = 0.1 | |||||
| /** | /** | ||||
| * Automatically track drag events in either x or y axes to seek animations | * Automatically track drag events in either x or y axes to seek animations | ||||
| * Control axis with {@link dragAxis} and damping/smoothness with {@link dragAnimationDamping} | * Control axis with {@link dragAxis} and damping/smoothness with {@link dragAnimationDamping} | ||||
| private _animationTime = 0 | private _animationTime = 0 | ||||
| private _animationDuration = 0 | private _animationDuration = 0 | ||||
| private _scrollAnimationState = 0 | private _scrollAnimationState = 0 | ||||
| private _pageScrollAnimationState = 0 | |||||
| private _dragAnimationState = 0 | private _dragAnimationState = 0 | ||||
| private _pointerDragHelper = new PointerDragHelper() | private _pointerDragHelper = new PointerDragHelper() | ||||
| private _lastFrameTime = 0 | private _lastFrameTime = 0 | ||||
| this._onPropertyChange = this._onPropertyChange.bind(this) | this._onPropertyChange = this._onPropertyChange.bind(this) | ||||
| this._postFrame = this._postFrame.bind(this) | this._postFrame = this._postFrame.bind(this) | ||||
| this._wheel = this._wheel.bind(this) | this._wheel = this._wheel.bind(this) | ||||
| this._scroll = this._scroll.bind(this) | |||||
| this._pointerDragHelper.addEventListener('drag', this._drag.bind(this)) | this._pointerDragHelper.addEventListener('drag', this._drag.bind(this)) | ||||
| } | } | ||||
| viewer.scene.addEventListener('addSceneObject', this._objectAdded) | viewer.scene.addEventListener('addSceneObject', this._objectAdded) | ||||
| viewer.addEventListener('postFrame', this._postFrame) | viewer.addEventListener('postFrame', this._postFrame) | ||||
| window.addEventListener('wheel', this._wheel) | window.addEventListener('wheel', this._wheel) | ||||
| window.addEventListener('scroll', this._scroll) | |||||
| this._pointerDragHelper.element = viewer.canvas | this._pointerDragHelper.element = viewer.canvas | ||||
| return super.onAdded(viewer) | return super.onAdded(viewer) | ||||
| } | } | ||||
| viewer.scene.removeEventListener('addSceneObject', this._objectAdded) | viewer.scene.removeEventListener('addSceneObject', this._objectAdded) | ||||
| viewer.removeEventListener('postFrame', this._postFrame) | viewer.removeEventListener('postFrame', this._postFrame) | ||||
| window.removeEventListener('wheel', this._wheel) | window.removeEventListener('wheel', this._wheel) | ||||
| window.removeEventListener('scroll', this._scroll) | |||||
| this._pointerDragHelper.element = undefined | this._pointerDragHelper.element = undefined | ||||
| return super.onRemove(viewer) | return super.onRemove(viewer) | ||||
| } | } | ||||
| if (!this._viewer) return | if (!this._viewer) return | ||||
| const scrollAnimate = this.animateOnScroll // && this._animationState === 'paused' | const scrollAnimate = this.animateOnScroll // && this._animationState === 'paused' | ||||
| const pageScrollAnimate = this.animateOnPageScroll // && this._animationState === 'paused' | |||||
| const dragAnimate = this.animateOnDrag // && this._animationState === 'paused' | const dragAnimate = this.animateOnDrag // && this._animationState === 'paused' | ||||
| if (!this.enabled || this.animations.length < 1 || this._animationState !== 'playing' && !scrollAnimate && !dragAnimate) { | |||||
| if (!this.enabled || this.animations.length < 1 || this._animationState !== 'playing' && !scrollAnimate && !dragAnimate && !pageScrollAnimate) { | |||||
| this._lastFrameTime = 0 | this._lastFrameTime = 0 | ||||
| // console.log('not anim') | // console.log('not anim') | ||||
| if (this._fadeDisabled) { | if (this._fadeDisabled) { | ||||
| this._lastFrameTime = time | this._lastFrameTime = time | ||||
| if (scrollAnimate && dragAnimate) delta *= absMax(this._scrollAnimationState, this._dragAnimationState) | |||||
| if (pageScrollAnimate) delta *= this._pageScrollAnimationState | |||||
| else if (scrollAnimate && dragAnimate) delta *= absMax(this._scrollAnimationState, this._dragAnimationState) | |||||
| else if (scrollAnimate) delta *= this._scrollAnimationState | else if (scrollAnimate) delta *= this._scrollAnimationState | ||||
| else if (dragAnimate) delta *= this._dragAnimationState | else if (dragAnimate) delta *= this._dragAnimationState | ||||
| // if (this._animationTime > this._animationDuration) this._animationTime -= this._animationDuration | // if (this._animationTime > this._animationDuration) this._animationTime -= this._animationDuration | ||||
| // if (this._animationTime < 0) this._animationTime += this._animationDuration | // if (this._animationTime < 0) this._animationTime += this._animationDuration | ||||
| this._pageScrollAnimationState = this.pageScrollTime - this._animationTime | |||||
| if (Math.abs(this._pageScrollAnimationState) < 0.001) this._pageScrollAnimationState = 0 | |||||
| else this._pageScrollAnimationState *= 1.0 - this.pageScrollAnimationDamping | |||||
| if (Math.abs(this._scrollAnimationState) < 0.001) this._scrollAnimationState = 0 | if (Math.abs(this._scrollAnimationState) < 0.001) this._scrollAnimationState = 0 | ||||
| else this._scrollAnimationState *= 1.0 - this.scrollAnimationDamping | else this._scrollAnimationState *= 1.0 - this.scrollAnimationDamping | ||||
| else this._dragAnimationState *= 1.0 - this.dragAnimationDamping | else this._dragAnimationState *= 1.0 - this.dragAnimationDamping | ||||
| this.dispatchEvent({type: 'animationStep', delta: animDelta, time: t}) | this.dispatchEvent({type: 'animationStep', delta: animDelta, time: t}) | ||||
| // todo: find a wau to check if a camera is animating | |||||
| // todo: this is now checked preFrame in ThreeViewer.ts | |||||
| // if (this._viewer.scene.mainCamera.userData.isAnimating) { // if camera is animating | // if (this._viewer.scene.mainCamera.userData.isAnimating) { // if camera is animating | ||||
| this._viewer.scene.mainCamera.setDirty() | |||||
| // this._viewer.scene.mainCamera.setDirty() | |||||
| // console.log(this._viewer.scene.mainCamera, this._viewer.scene.mainCamera.getWorldPosition(new Vector3())) | // console.log(this._viewer.scene.mainCamera, this._viewer.scene.mainCamera.getWorldPosition(new Vector3())) | ||||
| // } | // } | ||||
| this._viewer.scene.refreshActiveCameraNearFar() // because it's based on scene bounding box. | |||||
| this._viewer.renderManager.resetShadows() | this._viewer.renderManager.resetShadows() | ||||
| this._viewer.setDirty() | this._viewer.setDirty() | ||||
| } | } | ||||
| } | } | ||||
| get pageScrollTime() { | |||||
| const scrollMax = this.pageScrollHeight() | |||||
| const time = window.scrollY / scrollMax * (this.animationDuration - 0.05) | |||||
| return time | |||||
| } | |||||
| private _scroll() { | |||||
| if (!this.enabled) return | |||||
| this._pageScrollAnimationState = this.pageScrollTime - this.animationTime | |||||
| } | |||||
| private _wheel({deltaY}: any | WheelEvent) { | private _wheel({deltaY}: any | WheelEvent) { | ||||
| if (!this.enabled) return | if (!this.enabled) return | ||||
| ev.delta.y * this._viewer.canvas.height / 4 | ev.delta.y * this._viewer.canvas.height / 4 | ||||
| } | } | ||||
| pageScrollHeight = () => Math.max( | |||||
| document.body.scrollHeight, | |||||
| document.body.offsetHeight, | |||||
| document.documentElement.clientHeight, | |||||
| document.documentElement.scrollHeight, | |||||
| document.documentElement.offsetHeight | |||||
| ) - window.innerHeight | |||||
| } | } |
| export {ProgressivePlugin} from './pipeline/ProgressivePlugin' | export {ProgressivePlugin} from './pipeline/ProgressivePlugin' | ||||
| export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin' | export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin' | ||||
| export {NormalBufferPlugin} from './pipeline/NormalBufferPlugin' | export {NormalBufferPlugin} from './pipeline/NormalBufferPlugin' | ||||
| export {FrameFadePlugin, type FrameFadePluginEventTypes} from './pipeline/FrameFadePlugin' | |||||
| export type {ProgressivePluginEventTypes, ProgressivePluginTarget} from './pipeline/ProgressivePlugin' | export type {ProgressivePluginEventTypes, ProgressivePluginTarget} from './pipeline/ProgressivePlugin' | ||||
| export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin' | export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin' | ||||
| export type {NormalBufferPluginEventTypes, NormalBufferPluginPass, NormalBufferPluginTarget} from './pipeline/NormalBufferPlugin' | export type {NormalBufferPluginEventTypes, NormalBufferPluginPass, NormalBufferPluginTarget} from './pipeline/NormalBufferPlugin' |
| import {LinearFilter, WebGLRenderTarget} from 'three' | |||||
| import {IPassID, IPipelinePass} from '../../postprocessing' | |||||
| import {ThreeViewer} from '../../viewer' | |||||
| import {PipelinePassPlugin} from '../base/PipelinePassPlugin' | |||||
| import {uiFolderContainer, uiToggle} from 'uiconfig.js' | |||||
| import {ITexture, IWebGLRenderer} from '../../core' | |||||
| import {AddBlendTexturePass} from '../../postprocessing/AddBlendTexturePass' | |||||
| import {now, serialize, timeout, ValOrFunc} from 'ts-browser-helpers' | |||||
| import {ProgressivePlugin} from './ProgressivePlugin' | |||||
| import {IRenderTarget} from '../../rendering' | |||||
| export type FrameFadePluginEventTypes = '' | |||||
| /** | |||||
| * 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 | |||||
| */ | |||||
| @uiFolderContainer('FrameFade Plugin') | |||||
| export class FrameFadePlugin | |||||
| extends PipelinePassPlugin<FrameFadeBlendPass, 'frameFade', FrameFadePluginEventTypes> { | |||||
| readonly passId = 'frameFade' | |||||
| public static readonly PluginType = 'FrameFadePlugin' | |||||
| dependencies = [ProgressivePlugin] | |||||
| @serialize() @uiToggle() fadeOnActiveCameraChange = true | |||||
| @serialize() @uiToggle() fadeOnMaterialUpdate = true | |||||
| @serialize() @uiToggle() fadeOnSceneUpdate = true | |||||
| protected _pointerEnabled = true | |||||
| protected _target?: IRenderTarget | |||||
| constructor( | |||||
| enabled = true, | |||||
| ) { | |||||
| super() | |||||
| 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) | |||||
| } | |||||
| public async startTransition(duration: number) { // duration in ms | |||||
| 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 as ITexture).colorSpace, | |||||
| }) | |||||
| this._pass.fadeTimeState = Math.max(duration, this._pass.fadeTimeState) | |||||
| this._pass.fadeTime = this._pass.fadeTimeState | |||||
| this._pass.toSaveFrame = true | |||||
| // this._pass.passObject.enabled = true | |||||
| this.setDirty() | |||||
| await timeout(duration) | |||||
| } | |||||
| public stopTransition() { | |||||
| if (!this._pass) return | |||||
| this._pass.fadeTimeState = 0. // will be stopped in update on next frame | |||||
| } | |||||
| onAdded(viewer: ThreeViewer) { | |||||
| 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) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer) { | |||||
| 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) | |||||
| } | |||||
| private _fadeCam = async(ev: any)=> | |||||
| ev.frameFade !== false && this.fadeOnActiveCameraChange && this.startTransition(ev.fadeDuration || 1000) | |||||
| private _fadeMat = async(ev: any)=> | |||||
| ev.frameFade !== false && this.fadeOnMaterialUpdate && this.startTransition(ev.fadeDuration || 200) | |||||
| private _fadeScene = async(ev: any)=> | |||||
| ev.frameFade !== false && this.fadeOnSceneUpdate && this.startTransition(ev.fadeDuration || 500) | |||||
| private _fadeObjectUpdate = async(ev: any)=> | |||||
| ev.frameFade && this.startTransition(ev.fadeDuration || 500) | |||||
| private _onPointerMove = (ev: PointerEvent)=> { | |||||
| 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 | |||||
| } | |||||
| private _disabledBy: string[] = [] | |||||
| disable(name: string) { | |||||
| if (!this._disabledBy.includes(name)) { | |||||
| this._disabledBy.push(name) | |||||
| } | |||||
| } | |||||
| enable(name: string) { | |||||
| const i = this._disabledBy.indexOf(name) | |||||
| if (i >= 0) { | |||||
| this._disabledBy.splice(i, 1) | |||||
| } | |||||
| } | |||||
| isDisabled() { | |||||
| return !this._pointerEnabled || this._disabledBy.length > 0 || !this.enabled | |||||
| } | |||||
| setDirty() { | |||||
| if (!this.enabled) return | |||||
| this._viewer?.setDirty() | |||||
| } | |||||
| get dirty() { | |||||
| return this.enabled && !!this._pass && this._pass.fadeTimeState > 0 | |||||
| } | |||||
| set dirty(_: boolean) { | |||||
| console.error('FrameFadePlugin.dirty is readonly') | |||||
| } | |||||
| protected _createPass() { | |||||
| return new FrameFadeBlendPass(this.passId, this) | |||||
| } | |||||
| get canFrameFade() { | |||||
| return this._target && this._pointerEnabled && this.enabled && this.dirty && this._pass && this._pass.fadeTimeState > 0.001 | |||||
| } | |||||
| get lastFrame() { | |||||
| return this._viewer?.getPlugin(ProgressivePlugin)?.texture | |||||
| } | |||||
| get target() { | |||||
| return this._target | |||||
| } | |||||
| protected _beforeRender(): boolean { | |||||
| 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 | |||||
| } | |||||
| } | |||||
| class FrameFadeBlendPass extends AddBlendTexturePass implements IPipelinePass { | |||||
| before = ['progressive', 'taa'] | |||||
| after = ['render'] | |||||
| required = ['render', 'progressive'] | |||||
| dirty: ValOrFunc<boolean> = () => false | |||||
| fadeTime = 0 // ms | |||||
| fadeTimeState = 0 | |||||
| toSaveFrame = false | |||||
| private _lastTime = 0 | |||||
| constructor(public readonly passId: IPassID, public plugin: FrameFadePlugin) { | |||||
| super() | |||||
| } | |||||
| render(renderer: IWebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget, deltaTime: number, maskActive: boolean) { | |||||
| 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 | |||||
| } | |||||
| } |