| - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | ||||
| - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | ||||
| - [CameraViewPlugin](#cameraviewplugin) - Add support for saving, loading, animating, looping between camera views | - [CameraViewPlugin](#cameraviewplugin) - Add support for saving, loading, animating, looping between camera views | ||||
| - [TransformAnimationPlugin](#transformanimationplugin) - Add support for saving, loading, animating, between object transforms | |||||
| - [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 | ||||
| - [GeometryUVPreviewPlugin](#geometryuvpreviewplugin) - Preview UVs of any geometry in a UI panel over the canvas | - [GeometryUVPreviewPlugin](#geometryuvpreviewplugin) - Preview UVs of any geometry in a UI panel over the canvas | ||||
| - [FrameFadePlugin](#framefadeplugin) - Post-render pass to smoothly fade to a new rendered frame over time | - [FrameFadePlugin](#framefadeplugin) - Post-render pass to smoothly fade to a new rendered frame over time | ||||
| renderManager.blit(destination, {source: sourceTexture}) | renderManager.blit(destination, {source: sourceTexture}) | ||||
| // Clear color of the canvas | // Clear color of the canvas | ||||
| renderManager.clearColor({r: 0, g: 0, b: 0, a: 1, depth: true, viewport: new Vector4(...)}) | |||||
| renderManager.clearColor({r: 0, g: 0, b: 0, a: 1, depth: true, viewport: new Vector4()}) | |||||
| // Clear of a render target | // Clear of a render target | ||||
| renderManager.clearColor(renderTarget, {r: 0, g: 0, b: 0, a: 1, target: renderTarget}) | renderManager.clearColor(renderTarget, {r: 0, g: 0, b: 0, a: 1, target: renderTarget}) | ||||
| ``` | ``` | ||||
| ## TransformAnimationPlugin | |||||
| [//]: # (todo: image) | |||||
| [Example](https://threepipe.org/examples/#transform-animation-plugin/) — | |||||
| [Source Code](./src/plugins/animation/TransformAnimationPlugin.ts) — | |||||
| [API Reference](https://threepipe.org/docs/classes/TransformAnimationPlugin.html) | |||||
| TransformAnimationPlugin adds support to save and load transform(position, rotation, scale) states for objects in the scene, which can then be animated to. | |||||
| It uses PopmotionPlugin internally to animate any object to a saved transform object. | |||||
| The transformations are saved in the object userData, and can be created and interacted with from the plugin. | |||||
| It also provides a UI to manage the states, this UI is added to the object's uiConfig and can be accessed using the object UI or PickingPlugin. Check the example for a working demo. | |||||
| Sample Usage - | |||||
| ```javascript | |||||
| import {TransformAnimationPlugin, ThreeViewer, Vector3, Quaternion, EasingFunctions, timeout} from 'threepipe' | |||||
| const viewer = new ThreeViewer({...}) | |||||
| const model = viewer.scene.getObjectByName('model') | |||||
| const transformAnim = viewer.addPluginSync(new TransformAnimationPlugin()) | |||||
| // Save the current state of the model as a transform | |||||
| transformAnim.addTransform(model, 'initial') | |||||
| // Rotate/Move the model and save other transform states | |||||
| // left | |||||
| model.rotation.set(0, Math.PI / 2, 0) | |||||
| model.setDirty?.() | |||||
| transformAnim.addTransform(model, 'left') | |||||
| // top | |||||
| model.rotation.set(Math.PI / 2, 0, 0) | |||||
| model.setDirty?.() | |||||
| transformAnim.addTransform(model, 'top') | |||||
| // up | |||||
| model.position.set(0, 2, 0) | |||||
| model.lookAt(viewer.scene.mainCamera.position) | |||||
| model.setDirty?.() | |||||
| transformAnim.addTransform(model, 'up') | |||||
| // animate to a transform(from current position) in 1 sec | |||||
| const anim = transformAnim.animateTransform(model, 'left', 1000) | |||||
| // to stop the animation | |||||
| // anim.stop() | |||||
| // wait for the animation to finish | |||||
| await anim.promise | |||||
| // set a transform without animation | |||||
| transformAnim.setTransform(model, 'top') | |||||
| // await directly. | |||||
| await transformAnim.animateToTransform(model, 'up', 1000)?.promise | |||||
| ``` | |||||
| ## RenderTargetPreviewPlugin | ## RenderTargetPreviewPlugin | ||||
| ) | ) | ||||
| const rightView = new CameraView( | const rightView = new CameraView( | ||||
| 'leftView', | |||||
| 'rightView', | |||||
| new Vector3(6, 0, 0), | new Vector3(6, 0, 0), | ||||
| initialView.target, | initialView.target, | ||||
| ) | ) | ||||
| const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | ||||
| ui.appendChild(viewer.scene.mainCamera.uiConfig) | ui.appendChild(viewer.scene.mainCamera.uiConfig) | ||||
| const uiC = ui.setupPluginUi(CameraViewPlugin)! | |||||
| uiC.expanded = true | |||||
| uiC.uiRefresh?.() | |||||
| ui.setupPluginUi(CameraViewPlugin, {expanded: true}) | |||||
| } | } | ||||
| <ul> | <ul> | ||||
| <li><a href="./picking-plugin/">Picking (Selection) Plugin </a></li> | <li><a href="./picking-plugin/">Picking (Selection) Plugin </a></li> | ||||
| <li><a href="./camera-view-plugin/">Camera View (Animation) Plugin </a></li> | <li><a href="./camera-view-plugin/">Camera View (Animation) Plugin </a></li> | ||||
| <li><a href="./transform-animation-plugin/">Transform Animation Plugin </a></li> | |||||
| <li><a href="./dropzone-plugin/">Dropzone (Drag & Drop) Plugin </a></li> | <li><a href="./dropzone-plugin/">Dropzone (Drag & Drop) Plugin </a></li> | ||||
| <li><a href="./transform-controls-plugin/">Transform Controls Plugin </a></li> | <li><a href="./transform-controls-plugin/">Transform Controls Plugin </a></li> | ||||
| <li><a href="./editor-view-widget-plugin/">Editor View Widget Plugin </a></li> | <li><a href="./editor-view-widget-plugin/">Editor View Widget Plugin </a></li> |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Transform Animation Plugin</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- 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", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/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, IObject3D, PopmotionPlugin, ThreeViewer, TransformAnimationPlugin} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| plugins: [PopmotionPlugin], | |||||
| }) | |||||
| const transformAnimPlugin = viewer.addPluginSync(TransformAnimationPlugin) | |||||
| console.log(transformAnimPlugin) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| const model = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| if (!model) return | |||||
| // Save the initial transform | |||||
| transformAnimPlugin.addTransform(model, 'front') | |||||
| // Rotate/Move the model and save other transform states | |||||
| // left | |||||
| model.rotation.set(0, Math.PI / 2, 0) | |||||
| model.setDirty?.() | |||||
| transformAnimPlugin.addTransform(model, 'left') | |||||
| // top | |||||
| model.rotation.set(Math.PI / 2, 0, 0) | |||||
| model.setDirty?.() | |||||
| transformAnimPlugin.addTransform(model, 'top') | |||||
| // up | |||||
| model.position.set(0, 2, 0) | |||||
| model.lookAt(viewer.scene.mainCamera.position) | |||||
| model.setDirty?.() | |||||
| transformAnimPlugin.addTransform(model, 'up') | |||||
| // reset | |||||
| transformAnimPlugin.setTransform(model, 'front') | |||||
| createSimpleButtons({ | |||||
| ['Reset']: async() => transformAnimPlugin.animateTransform(model, 'front', 1000), | |||||
| ['Left']: async() => transformAnimPlugin.animateTransform(model, 'left', 1000), | |||||
| ['Top']: async() => transformAnimPlugin.animateTransform(model, 'top', 1000), | |||||
| ['Up']: async() => transformAnimPlugin.animateTransform(model, 'up', 1000), | |||||
| }) | |||||
| const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | |||||
| ui.appendChild(model.uiConfig) | |||||
| ui.setupPluginUi(TransformAnimationPlugin, {expanded: true}) | |||||
| } | |||||
| init().finally(_testFinish) |
| import {Quaternion, Vector3} from 'three' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {UiObjectConfig} from 'uiconfig.js' | |||||
| import {PopmotionPlugin} from './PopmotionPlugin' | |||||
| import {IObject3D} from '../../core' | |||||
| // todo make a serializable object like CameraView for proper ui state management | |||||
| export interface TSavedTransform { | |||||
| position: Vector3 | |||||
| quaternion: Quaternion | |||||
| scale: Vector3 | |||||
| name?: string | |||||
| } | |||||
| /** | |||||
| * Transform Animation Plugin | |||||
| * | |||||
| * Helper plugin to save, load and animate between different transforms(position, rotation, scale) on objects. | |||||
| * Also adds a UI to add and animate transforms on objects. | |||||
| * Requires the PopmotionPlugin to animate. | |||||
| * | |||||
| * @category Plugin | |||||
| */ | |||||
| export class TransformAnimationPlugin extends AViewerPluginSync<''> { | |||||
| public static readonly PluginType = 'TransformAnimationPlugin' | |||||
| toJSON: any = undefined | |||||
| enabled = true | |||||
| dependencies = [PopmotionPlugin] | |||||
| constructor() { | |||||
| super() | |||||
| } | |||||
| onAdded(viewer: ThreeViewer): void { | |||||
| super.onAdded(viewer) | |||||
| viewer.scene.addEventListener('addSceneObject', this._addSceneObject) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer): void { | |||||
| viewer.scene.removeEventListener('addSceneObject', this._addSceneObject) | |||||
| return super.onRemove(viewer) | |||||
| } | |||||
| private _addSceneObject = (e: any)=>{ | |||||
| const object = e.object as IObject3D | |||||
| object?.traverse && object.traverse((o: IObject3D)=>{ | |||||
| if (!o.userData[TransformAnimationPlugin.PluginType]) { | |||||
| o.userData[TransformAnimationPlugin.PluginType] = { | |||||
| transforms: [] as TSavedTransform[], | |||||
| } | |||||
| } | |||||
| // if (!o.userData[TransformAnimationPlugin.PluginType].transforms) { | |||||
| // o.userData[TransformAnimationPlugin.PluginType].transforms = [] | |||||
| // } | |||||
| // for old files, todo remove later | |||||
| o.userData[TransformAnimationPlugin.PluginType]!.transforms?.forEach((t, i)=>{ | |||||
| if (t.name === undefined) t.name = 'Transform ' + i | |||||
| }) | |||||
| const uiConfig: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: 'Transform Animation', | |||||
| children: [ | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Add Current Transform', | |||||
| value: ()=>{ | |||||
| this.addTransform(o) | |||||
| uiConfig?.uiRefresh?.() | |||||
| }, | |||||
| }, | |||||
| ()=>o.userData[TransformAnimationPlugin.PluginType]?.transforms.map((t: TSavedTransform, i: number)=>({ | |||||
| type: 'folder', | |||||
| label: t.name || `Transform ${i}`, | |||||
| children: [ | |||||
| { | |||||
| type: 'input', | |||||
| label: 'Name', | |||||
| property: [t, 'name'], | |||||
| }, | |||||
| { | |||||
| type: 'vec3', | |||||
| label: 'Position', | |||||
| property: [t, 'position'], | |||||
| }, | |||||
| { | |||||
| type: 'vec3', | |||||
| label: 'Quaternion', | |||||
| property: [t, 'quaternion'], | |||||
| }, | |||||
| { | |||||
| type: 'vec3', | |||||
| label: 'Scale', | |||||
| property: [t, 'scale'], | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Set', | |||||
| value: ()=>{ | |||||
| this.setTransform(o, t) | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Animate', | |||||
| value: ()=>{ | |||||
| this.animateTransform(o, t) | |||||
| }, | |||||
| }], | |||||
| })), | |||||
| ], | |||||
| } | |||||
| o.uiConfig?.children?.push(uiConfig) // todo check if already exists | |||||
| }) | |||||
| } | |||||
| addTransform(o: IObject3D, name?: string) { | |||||
| if (!o.userData[TransformAnimationPlugin.PluginType]) { | |||||
| o.userData[TransformAnimationPlugin.PluginType] = { | |||||
| transforms: [] as TSavedTransform[], | |||||
| } | |||||
| } | |||||
| const transform = { | |||||
| name: name || 'Transform ' + (o.userData[TransformAnimationPlugin.PluginType]!.transforms.length + 1), | |||||
| position: o.position.clone(), | |||||
| quaternion: o.quaternion.clone(), | |||||
| scale: o.scale.clone(), | |||||
| } | |||||
| o.userData[TransformAnimationPlugin.PluginType]!.transforms.push(transform) | |||||
| return transform | |||||
| } | |||||
| setTransform(o: IObject3D, tr: TSavedTransform|number|string) { | |||||
| const t = this.getSavedTransform(tr, o) | |||||
| if (!t) return | |||||
| o.position.copy(t.position) | |||||
| o.quaternion.copy(t.quaternion) | |||||
| o.scale.copy(t.scale) | |||||
| o.setDirty?.() | |||||
| o.uiConfig?.uiRefresh?.() | |||||
| } | |||||
| getSavedTransform(tr: TSavedTransform | number | string, o: IObject3D) { | |||||
| return typeof tr === 'number' ? | |||||
| o.userData[TransformAnimationPlugin.PluginType]?.transforms[tr] : | |||||
| typeof tr === 'string' ? | |||||
| o.userData[TransformAnimationPlugin.PluginType]?.transforms.find(t1 => t1.name === tr) : | |||||
| tr | |||||
| } | |||||
| animateTransform(o: IObject3D, tr: TSavedTransform|number|string, duration = 2000) { | |||||
| const popmotion = this._viewer?.getPlugin(PopmotionPlugin) | |||||
| if (!popmotion) { | |||||
| this._viewer?.console.error('PopmotionPlugin required for animation') | |||||
| } | |||||
| const t = this.getSavedTransform(tr, o) | |||||
| if (!t) return | |||||
| // todo stop all existing animations(for the current model) like CameraView? | |||||
| const pos = new Vector3() | |||||
| const q = new Quaternion() | |||||
| const s = new Vector3() | |||||
| const op = o.position.clone() | |||||
| const oq = o.quaternion.clone() | |||||
| const os = o.scale.clone() | |||||
| const ep = t.position | |||||
| const eq = t.quaternion | |||||
| const es = t.scale | |||||
| return popmotion?.animate({ | |||||
| from: 0, | |||||
| to: 1, | |||||
| duration: duration, | |||||
| onUpdate: (v: number) => { | |||||
| pos.lerpVectors(op, ep, v) | |||||
| q.slerpQuaternions(oq, eq, v) | |||||
| s.lerpVectors(os, es, v) | |||||
| o.position.copy(pos) | |||||
| o.quaternion.copy(q) | |||||
| o.scale.copy(s) | |||||
| this._viewer?.setDirty() | |||||
| this._viewer?.renderManager.resetShadows() | |||||
| // o.setDirty?.() | |||||
| // o.uiConfig?.uiRefresh?.() | |||||
| }, | |||||
| onStop: () => { | |||||
| o.position.copy(t.position) | |||||
| o.quaternion.copy(t.quaternion) | |||||
| o.scale.copy(t.scale) | |||||
| o.setDirty?.() | |||||
| o.uiConfig?.uiRefresh?.() | |||||
| }, | |||||
| }) | |||||
| } | |||||
| } | |||||
| declare module '../../core/IObject' { | |||||
| interface IObject3DUserData { | |||||
| [TransformAnimationPlugin.PluginType]?: { | |||||
| transforms: TSavedTransform[] | |||||
| } | |||||
| } | |||||
| } |
| // animation | // animation | ||||
| export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | ||||
| export {PopmotionPlugin, type AnimationResult} from './animation/PopmotionPlugin' | export {PopmotionPlugin, type AnimationResult} from './animation/PopmotionPlugin' | ||||
| export {TransformAnimationPlugin, type TSavedTransform} from './animation/TransformAnimationPlugin' | |||||
| export {CameraViewPlugin, type CameraViewPluginOptions} from './animation/CameraViewPlugin' | export {CameraViewPlugin, type CameraViewPluginOptions} from './animation/CameraViewPlugin' | ||||
| // material | // material |