| @@ -108,6 +108,7 @@ To make changes and run the example, click on the CodePen button on the top righ | |||
| - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | |||
| - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | |||
| - [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 | |||
| - [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 | |||
| @@ -1550,7 +1551,7 @@ renderManager.maxTempPerKey = 10 // default = 5 | |||
| renderManager.blit(destination, {source: sourceTexture}) | |||
| // 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 | |||
| renderManager.clearColor(renderTarget, {r: 0, g: 0, b: 0, a: 1, target: renderTarget}) | |||
| @@ -2779,6 +2780,64 @@ cameraViewPlugin.viewLooping = false | |||
| ``` | |||
| ## 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 | |||
| @@ -33,7 +33,7 @@ async function init() { | |||
| ) | |||
| const rightView = new CameraView( | |||
| 'leftView', | |||
| 'rightView', | |||
| new Vector3(6, 0, 0), | |||
| initialView.target, | |||
| ) | |||
| @@ -70,9 +70,7 @@ async function init() { | |||
| const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | |||
| ui.appendChild(viewer.scene.mainCamera.uiConfig) | |||
| const uiC = ui.setupPluginUi(CameraViewPlugin)! | |||
| uiC.expanded = true | |||
| uiC.uiRefresh?.() | |||
| ui.setupPluginUi(CameraViewPlugin, {expanded: true}) | |||
| } | |||
| @@ -338,6 +338,7 @@ | |||
| <ul> | |||
| <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="./transform-animation-plugin/">Transform Animation 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="./editor-view-widget-plugin/">Editor View Widget Plugin </a></li> | |||
| @@ -0,0 +1,36 @@ | |||
| <!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> | |||
| @@ -0,0 +1,60 @@ | |||
| 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) | |||
| @@ -0,0 +1,201 @@ | |||
| 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[] | |||
| } | |||
| } | |||
| } | |||
| @@ -60,6 +60,7 @@ export {FilmicGrainPlugin} from './postprocessing/FilmicGrainPlugin' | |||
| // animation | |||
| export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | |||
| export {PopmotionPlugin, type AnimationResult} from './animation/PopmotionPlugin' | |||
| export {TransformAnimationPlugin, type TSavedTransform} from './animation/TransformAnimationPlugin' | |||
| export {CameraViewPlugin, type CameraViewPluginOptions} from './animation/CameraViewPlugin' | |||
| // material | |||