| @@ -73,6 +73,7 @@ To make changes and run the example, click on the CodePen button on the top righ | |||
| - [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer | |||
| - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | |||
| - [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 | |||
| - [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files | |||
| - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | |||
| @@ -571,6 +572,36 @@ const previewPlugin = viewer.addPluginSync(new RenderTargetPreviewPlugin()) | |||
| 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 | |||
| Example: https://threepipe.org/examples/#rhino3dm-load/ | |||
| @@ -0,0 +1,34 @@ | |||
| <!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> | |||
| @@ -0,0 +1,35 @@ | |||
| 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) | |||
| @@ -0,0 +1,128 @@ | |||
| <!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> | |||
| @@ -0,0 +1,34 @@ | |||
| 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) | |||
| @@ -31,9 +31,8 @@ async function init() { | |||
| fileCamera.autoAspect = true | |||
| fileCamera.userData.autoLookAtTarget = false | |||
| fileCamera.activateMain() | |||
| viewer.scene.mainCamera.refreshAspect() | |||
| gltfAnimation.loopAnimations = false | |||
| gltfAnimation.loopAnimations = true | |||
| gltfAnimation.playAnimation() | |||
| console.log(gltfAnimation) | |||
| @@ -220,6 +220,7 @@ | |||
| <h2 class="category">Post-Processing</h2> | |||
| <ul> | |||
| <li><a href="./tonemap-plugin/">Tonemap Plugin </a></li> | |||
| <li><a href="./frame-fade-plugin/">Frame Fade Plugin </a></li> | |||
| </ul> | |||
| <h2 class="category">Rendering</h2> | |||
| <ul> | |||
| @@ -267,9 +268,7 @@ | |||
| <ul> | |||
| <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="./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> | |||
| <h2 class="category">Utils</h2> | |||
| <ul> | |||
| @@ -2,6 +2,7 @@ import { | |||
| _testFinish, | |||
| DepthBufferPlugin, | |||
| DropzonePlugin, | |||
| FrameFadePlugin, | |||
| FullScreenPlugin, | |||
| GLTFAnimationPlugin, | |||
| HalfFloatType, | |||
| @@ -47,6 +48,7 @@ async function init() { | |||
| new DepthBufferPlugin(HalfFloatType, true, true), | |||
| new NormalBufferPlugin(HalfFloatType, false), | |||
| new RenderTargetPreviewPlugin(false), | |||
| new FrameFadePlugin(), | |||
| new KTX2LoadPlugin(), | |||
| new KTXLoadPlugin(), | |||
| new PLYLoadPlugin(), | |||
| @@ -61,7 +63,7 @@ async function init() { | |||
| editor.loadPlugins({ | |||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | |||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | |||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin], | |||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin], | |||
| ['Animation']: [GLTFAnimationPlugin], | |||
| ['Debug']: [RenderTargetPreviewPlugin], | |||
| }) | |||
| @@ -4,6 +4,7 @@ import type {MaterialExtension} from '../materials' | |||
| import type {ChangeEvent, IUiConfigContainer} from 'uiconfig.js' | |||
| import type {SerializationMetaType} from '../utils' | |||
| import type {IObject3D} from './IObject' | |||
| import {ISetDirtyCommonOptions} from './IObject' | |||
| import type {ITexture} from './ITexture' | |||
| import type {IImportResultUserData} from '../assetmanager' | |||
| @@ -20,23 +21,16 @@ export type IMaterialEvent<T extends string = IMaterialEventTypes> = Event & { | |||
| uiChangeEvent?: ChangeEvent | |||
| } | |||
| export interface IMaterialSetDirtyOptions { | |||
| export interface IMaterialSetDirtyOptions extends ISetDirtyCommonOptions{ | |||
| /** | |||
| * @default true | |||
| */ | |||
| bubbleToObject?: boolean, | |||
| /** | |||
| * @default true | |||
| */ | |||
| refreshUi?: boolean, | |||
| /** | |||
| * @default true | |||
| */ | |||
| needsUpdate?: boolean, | |||
| /** | |||
| * Event from uiconfig.js | |||
| */ | |||
| uiChangeEvent?: ChangeEvent, | |||
| [key: string]: any | |||
| } | |||
| export interface IMaterialUserData extends IImportResultUserData{ | |||
| @@ -1,7 +1,7 @@ | |||
| import {IDisposable} from 'ts-browser-helpers' | |||
| import {IMaterial} from './IMaterial' | |||
| import {Event, Object3D} from 'three' | |||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||
| import {ChangeEvent, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||
| import {IGeometry, IGeometryEvent} from './IGeometry' | |||
| import {IImportResultUserData} from '../assetmanager' | |||
| import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js' | |||
| @@ -21,16 +21,37 @@ export interface IObject3DEvent<T extends string = IObject3DEventTypes> extends | |||
| 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 | |||
| change?: string | |||
| refreshScene?: boolean // update scene after setting dirty | |||
| geometryChanged?: boolean // whether to refresh stuff like ground. | |||
| frameFade?: boolean // for plugins | |||
| refreshUi?: boolean // for plugins | |||
| /** | |||
| * @deprecated use {@link refreshScene} instead | |||
| */ | |||
| @@ -62,9 +62,7 @@ export interface ISceneEvent<T extends string = ISceneEventTypes> extends IObjec | |||
| scene?: IScene | null | |||
| // change?: string | |||
| } | |||
| export type ISceneSetDirtyOptions = IObjectSetDirtyOptions & { | |||
| [key: string]: any | |||
| } | |||
| export type ISceneSetDirtyOptions = IObjectSetDirtyOptions | |||
| export type ISceneUserData = IObject3DUserData | |||
| @@ -85,7 +83,7 @@ export interface IScene<E extends ISceneEvent = ISceneEvent, ET extends ISceneEv | |||
| // environmentLight?: IEnvironmentLight; | |||
| // 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 | |||
| @@ -160,18 +160,18 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| * @param imported | |||
| * @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) { | |||
| this.clearSceneModels(options.disposeSceneObjects) | |||
| } | |||
| if (!imported) return imported | |||
| if (!imported.isObject3D) { | |||
| console.error('Invalid object, cannot add to scene.', imported) | |||
| return imported | |||
| return imported as T&IObject3D | |||
| } | |||
| this._addObject3D(<IObject3D>imported, options) | |||
| this.dispatchEvent({type: 'addSceneObject', object: <IObject3D>imported}) | |||
| return imported | |||
| return imported as T&IObject3D | |||
| } | |||
| /** | |||
| @@ -16,7 +16,7 @@ export const iCameraCommons = { | |||
| } | |||
| this.dispatchEvent({...options, type: 'update'}) // does not bubble | |||
| 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 { | |||
| if (!_internal) return this.dispatchEvent({ | |||
| @@ -5,8 +5,7 @@ import {AnimationAction, AnimationClip, AnimationMixer, LoopOnce, LoopRepeat} fr | |||
| import {ProgressivePlugin} from '../pipeline/ProgressivePlugin' | |||
| import {IObject3D} from '../../core' | |||
| import {generateUUID} from '../../three' | |||
| type FrameFadePlugin = any // todo | |||
| import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | |||
| /** | |||
| * Manages playback of GLTF animations. | |||
| @@ -68,9 +67,9 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| @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} | |||
| * See also {@link animateOnDrag} | |||
| * See also {@link animateOnPageScroll}. {@link animateOnDrag} | |||
| */ | |||
| @uiToggle() @serialize() animateOnScroll = false | |||
| @@ -79,6 +78,18 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| */ | |||
| @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 | |||
| * Control axis with {@link dragAxis} and damping/smoothness with {@link dragAnimationDamping} | |||
| @@ -142,6 +153,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| private _animationTime = 0 | |||
| private _animationDuration = 0 | |||
| private _scrollAnimationState = 0 | |||
| private _pageScrollAnimationState = 0 | |||
| private _dragAnimationState = 0 | |||
| private _pointerDragHelper = new PointerDragHelper() | |||
| private _lastFrameTime = 0 | |||
| @@ -159,6 +171,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| this._onPropertyChange = this._onPropertyChange.bind(this) | |||
| this._postFrame = this._postFrame.bind(this) | |||
| this._wheel = this._wheel.bind(this) | |||
| this._scroll = this._scroll.bind(this) | |||
| this._pointerDragHelper.addEventListener('drag', this._drag.bind(this)) | |||
| } | |||
| @@ -171,6 +184,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| viewer.scene.addEventListener('addSceneObject', this._objectAdded) | |||
| viewer.addEventListener('postFrame', this._postFrame) | |||
| window.addEventListener('wheel', this._wheel) | |||
| window.addEventListener('scroll', this._scroll) | |||
| this._pointerDragHelper.element = viewer.canvas | |||
| return super.onAdded(viewer) | |||
| } | |||
| @@ -180,6 +194,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| viewer.scene.removeEventListener('addSceneObject', this._objectAdded) | |||
| viewer.removeEventListener('postFrame', this._postFrame) | |||
| window.removeEventListener('wheel', this._wheel) | |||
| window.removeEventListener('scroll', this._scroll) | |||
| this._pointerDragHelper.element = undefined | |||
| return super.onRemove(viewer) | |||
| } | |||
| @@ -356,9 +371,10 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| if (!this._viewer) return | |||
| const scrollAnimate = this.animateOnScroll // && this._animationState === 'paused' | |||
| const pageScrollAnimate = this.animateOnPageScroll // && 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 | |||
| // console.log('not anim') | |||
| if (this._fadeDisabled) { | |||
| @@ -380,7 +396,8 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| 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 (dragAnimate) delta *= this._dragAnimationState | |||
| @@ -413,6 +430,10 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| // if (this._animationTime > this._animationDuration) 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 | |||
| else this._scrollAnimationState *= 1.0 - this.scrollAnimationDamping | |||
| @@ -420,12 +441,13 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| else this._dragAnimationState *= 1.0 - this.dragAnimationDamping | |||
| 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 | |||
| this._viewer.scene.mainCamera.setDirty() | |||
| // this._viewer.scene.mainCamera.setDirty() | |||
| // 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.setDirty() | |||
| @@ -484,6 +506,16 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| } | |||
| } | |||
| 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) { | |||
| if (!this.enabled) return | |||
| @@ -498,4 +530,14 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| 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 | |||
| } | |||
| @@ -5,6 +5,7 @@ export {PipelinePassPlugin} from './base/PipelinePassPlugin' | |||
| export {ProgressivePlugin} from './pipeline/ProgressivePlugin' | |||
| export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin' | |||
| export {NormalBufferPlugin} from './pipeline/NormalBufferPlugin' | |||
| export {FrameFadePlugin, type FrameFadePluginEventTypes} from './pipeline/FrameFadePlugin' | |||
| export type {ProgressivePluginEventTypes, ProgressivePluginTarget} from './pipeline/ProgressivePlugin' | |||
| export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin' | |||
| export type {NormalBufferPluginEventTypes, NormalBufferPluginPass, NormalBufferPluginTarget} from './pipeline/NormalBufferPlugin' | |||
| @@ -0,0 +1,231 @@ | |||
| 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 | |||
| } | |||
| } | |||