Просмотр исходного кода

Add PopmotionPlugin and example.

master
Palash Bansal 2 лет назад
Родитель
Сommit
2cfb21b564
Аккаунт пользователя с таким Email не найден

+ 57
- 0
README.md Просмотреть файл

@@ -72,6 +72,7 @@ To make changes and run the example, click on the CodePen button on the top righ
- [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal 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
- [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening
- [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
@@ -547,6 +548,62 @@ This plugin is made for playing, pausing, stopping, all the animations at once,

To play individual animations, with custom choreography, use the {@link GLTFAnimationPlugin.animations} property to get reference to the animation clips and actions. Create your own mixers and control the animation playback like in three.js

## PopmotionPlugin

todo: image

Example: https://threepipe.org/examples/#popmotion-plugin/

Source Code: [src/plugins/animation/PopmotionPlugin.ts](./src/plugins/animation/PopmotionPlugin.ts)

API Reference: [PopmotionPlugin](https://threepipe.org/docs/classes/PopmotionPlugin.html)

Provides animation/tweening capabilities to the viewer using the [popmotion.io](https://popmotion.io/) library.

Overrides the driver in popmotion to sync with the viewer and provide ways to store and stop animations.

```typescript
import {PopmotionPlugin, ThreeViewer} from 'threepipe'

const viewer = new ThreeViewer({...})

const cube = viewer.scene.getObjectByName('cube');

const popmotion = viewer.addPluginSync(new PopmotionPlugin())

// Move the object cube 1 unit up.
const anim = popmotion.animate({
from: cube.position.y,
to: cube.position.y + 1,
duration: 500, // ms
onUpdate: (v) => {
cube.position.setY(v)
cube.setDirty()
},
onComplete: () => isMovedUp = !isMovedUp,
})

// await for animation
await anim.promise;

// or stop the animation
// anim.stop()

// Animate the color
await popmotion.animateAsync({ // Also await for the animation.
from: '#' + cube.material.color.getHexString(),
to: '#' + new Color().setHSL(Math.random(), 1, 0.5).getHexString(),
duration: 500,
onUpdate: (v) => {
cube.material.color.set(v)
cube.material.setDirty()
},
})
```

Note: The animation is started when the animate or animateAsync function is called.


## RenderTargetPreviewPlugin

todo: image

+ 1
- 0
examples/index.html Просмотреть файл

@@ -267,6 +267,7 @@
<h2 class="category">Animation</h2>
<ul>
<li><a href="./gltf-animation-plugin/">glTF Animation Plugin </a></li>
<li><a href="./gltf-animation-plugin/">Popmotion Plugin </a></li>
<li><a href="./gltf-camera-animation/">glTF Camera Animation </a></li>
<li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li>
</ul>

+ 34
- 0
examples/popmotion-plugin/index.html Просмотреть файл

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Popmotion 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>

+ 65
- 0
examples/popmotion-plugin/script.ts Просмотреть файл

@@ -0,0 +1,65 @@
import {_testFinish, BoxGeometry, Color, Mesh, PhysicalMaterial, PopmotionPlugin, 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,
})
const popmotion = viewer.addPluginSync(PopmotionPlugin)

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})
))

let isMovedUp = false

createSimpleButtons({
['Move Up/Down']: async(btn) => {
btn.disabled = true
await popmotion.animateAsync({
from: cube.position.y,
to: cube.position.y + (isMovedUp ? -1 : 1),
duration: 500, // ms
onUpdate: (v) => {
cube.position.setY(v)
cube.setDirty()
},
onComplete: () => isMovedUp = !isMovedUp,
})
btn.disabled = false
},
['Rotate +90deg']: async(btn) => {
btn.disabled = true
await popmotion.animateAsync({
from: cube.rotation.y,
to: cube.rotation.y + Math.PI / 2,
duration: 500,
onUpdate: (v) => {
cube.rotation.y = v
cube.setDirty()
},
})
btn.disabled = false
},
['Change Color']: async(btn)=>{
btn.disabled = true
await popmotion.animateAsync({
from: '#' + cube.material.color.getHexString(),
to: '#' + new Color().setHSL(Math.random(), 1, 0.5).getHexString(),
duration: 500,
onUpdate: (v) => {
cube.material.color.set(v)
cube.material.setDirty()
},
})
btn.disabled = false
},
})

}

init().then(_testFinish)

+ 117
- 0
package-lock.json Просмотреть файл

@@ -31,6 +31,7 @@
"eslint-plugin-import": "^2.27.5",
"local-web-server": "^5.3.0",
"markdown-to-html-cli": "^3.7.0",
"popmotion": "^11.0.5",
"rimraf": "^5.0.1",
"rollup": "^3.23.0",
"rollup-plugin-glsl": "^1.3.0",
@@ -3170,6 +3171,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/framesync": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz",
"integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
"dev": true,
"dependencies": {
"tslib": "2.4.0"
}
},
"node_modules/framesync/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -3850,6 +3866,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hey-listen": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
"dev": true
},
"node_modules/html-void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
@@ -7028,6 +7050,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/popmotion": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz",
"integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==",
"dev": true,
"dependencies": {
"framesync": "6.1.2",
"hey-listen": "^1.0.8",
"style-value-types": "5.1.2",
"tslib": "2.4.0"
}
},
"node_modules/popmotion/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
},
"node_modules/postcss": {
"version": "8.4.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
@@ -9245,6 +9285,22 @@
"integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==",
"dev": true
},
"node_modules/style-value-types": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz",
"integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==",
"dev": true,
"dependencies": {
"hey-listen": "^1.0.8",
"tslib": "2.4.0"
}
},
"node_modules/style-value-types/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
},
"node_modules/stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -12634,6 +12690,23 @@
}
}
},
"framesync": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz",
"integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
"dev": true,
"requires": {
"tslib": "2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
}
}
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -13135,6 +13208,12 @@
"space-separated-tokens": "^2.0.0"
}
},
"hey-listen": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
"dev": true
},
"html-void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
@@ -15378,6 +15457,26 @@
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
"dev": true
},
"popmotion": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz",
"integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==",
"dev": true,
"requires": {
"framesync": "6.1.2",
"hey-listen": "^1.0.8",
"style-value-types": "5.1.2",
"tslib": "2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
}
}
},
"postcss": {
"version": "8.4.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
@@ -17008,6 +17107,24 @@
"integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==",
"dev": true
},
"style-value-types": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz",
"integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==",
"dev": true,
"requires": {
"hey-listen": "^1.0.8",
"tslib": "2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
}
}
},
"stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",

+ 2
- 1
package.json Просмотреть файл

@@ -98,7 +98,8 @@
"typescript": "^5.0.4",
"typescript-plugin-css-modules": "^5.0.1",
"uiconfig.js": "^0.0.6",
"rollup-plugin-replace": "^2.2.0"
"rollup-plugin-replace": "^2.2.0",
"popmotion": "^11.0.5"
},
"dependencies": {
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1012/package.tgz",

+ 4
- 0
rollup.config.mjs Просмотреть файл

@@ -10,6 +10,7 @@ import {fileURLToPath} from 'url';
import terser from "@rollup/plugin-terser";
import postcss from 'rollup-plugin-postcss'
import glsl from "rollup-plugin-glsl"
import replace from "rollup-plugin-replace";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -56,6 +57,9 @@ export default {
],
external: [],
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify( 'production' )
}),
glsl({ // todo: minify glsl.
include: "src/**/*.glsl"
}),

+ 163
- 0
src/plugins/animation/PopmotionPlugin.ts Просмотреть файл

@@ -0,0 +1,163 @@
import type {Driver} from 'popmotion/lib/animations/types'
import {now} from 'ts-browser-helpers'
import {animate, type AnimationOptions} from 'popmotion'
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin'
import type {ProgressivePlugin} from '../pipeline/ProgressivePlugin'
import {generateUUID} from '../../three'

export interface AnimationResult{
id: string
promise: Promise<string>
options: AnimationOptions<any>
stop: () => void
// eslint-disable-next-line @typescript-eslint/naming-convention
_stop?: () => void
}

/**
* Popmotion plugin
*
* Provides animation capabilities to the viewer using the popmotion library: https://popmotion.io/
*
* Overrides the driver in popmotion to sync with the viewer and provide ways to store and stop animations.
*
* @category Plugin
*/
export class PopmotionPlugin extends AViewerPluginSync<''> {
public static readonly PluginType = 'PopmotionPlugin'
enabled = true

toJSON: any = undefined // disable serialization
fromJSON: any = undefined // disable serialization

constructor(enabled = true) {
super()
this.enabled = enabled
this._postFrame = this._postFrame.bind(this)
}

// private _animating = false
private _lastFrameTime = 0 // for post frame
private _updaters: {u: ((timestamp: number) => void), time: number}[] = []

dependencies = []

private _fadeDisabled = false
/**
* Disable the frame fade plugin while animation is running
*/
disableFrameFade = true

// Same code used in CameraViewPlugin
private _postFrame = ()=>{
if (!this._viewer) return
if (!this.enabled || Object.keys(this.animations).length < 1) {
this._lastFrameTime = 0
// console.log('not anim')
if (this._fadeDisabled) {
this._viewer.getPlugin<FrameFadePlugin>('FrameFade')?.enable(PopmotionPlugin.PluginType)
this._fadeDisabled = false
}
return
}
const time = now() / 1000.0
if (this._lastFrameTime < 1) this._lastFrameTime = time - 1.0 / 60.0
let delta = time - this._lastFrameTime
this._lastFrameTime = time

// todo: scrolling
// delta = delta * (this.animateOnScroll ? this._scrollAnimationState : 1)

const d = this._viewer.getPlugin<ProgressivePlugin>('Progressive')?.postFrameConvergedRecordingDelta()
if (d && d > 0) delta = d
if (d === 0) return // not converged yet.
// if d < 0: not recording, do nothing

delta *= 1000

// delta = 16.666 // testing

if (delta <= 0.001) return

this._updaters.forEach(u=>{
let dt = delta
if (u.time + dt < 0) dt = -u.time
u.time += dt
if (Math.abs(dt) > 0.001)
u.u(dt)
})

if (!this._fadeDisabled && this.disableFrameFade) {
const ff = this._viewer.getPlugin<FrameFadePlugin>('FrameFade')
if (ff) {
ff.disable(PopmotionPlugin.PluginType)
this._fadeDisabled = true
}
}

// todo: scrolling
// if (this._scrollAnimationState < 0.001) this._scrollAnimationState = 0
// else this._scrollAnimationState *= 1.0 - this.scrollAnimationDamping
}

readonly defaultDriver: Driver = (update)=>{
return {
start: ()=>this._updaters.push({u:update, time:0}),
stop: ()=> this._updaters.splice(this._updaters.findIndex(u=>u.u === update), 1),
}
}

onAdded(viewer: ThreeViewer): void {
super.onAdded(viewer)
viewer.addEventListener('postFrame', this._postFrame)
}

onRemove(viewer: ThreeViewer): void {
viewer.removeEventListener('postFrame', this._postFrame)
super.onRemove(viewer)
}

readonly animations: Record<string, AnimationResult> = {}

animate<V>(options: AnimationOptions<V>): AnimationResult {
const uuid = generateUUID()
const a: any = {
id: uuid,
options,
stop: ()=>{
if (!this.animations[uuid]?._stop) console.warn('Animation not started')
else this.animations[uuid]?._stop?.()
},
}
this.animations[uuid] = a
a.promise = new Promise<void>((resolve) => {
const opts: AnimationOptions<V> = {
driver: this.defaultDriver,
...options,
onComplete: ()=>{
options.onComplete?.()
resolve()
},
onStop: ()=>{
options.onStop?.()
resolve()
},
}
const anim = animate(opts)
this.animations[uuid]._stop = anim.stop
this.animations[uuid].options = opts
}).then(()=>{
delete this.animations[uuid]
return uuid
})

return this.animations[uuid]
}

async animateAsync<V>(options: AnimationOptions<V>): Promise<string> {
return this.animate(options).promise
}

// todo : animateObject/animateTarget
}

+ 1
- 0
src/plugins/index.ts Просмотреть файл

@@ -31,3 +31,4 @@ export {TonemapPlugin} from './postprocessing/TonemapPlugin'

// animation
export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin'
export {PopmotionPlugin} from './animation/PopmotionPlugin'

Загрузка…
Отмена
Сохранить