Quellcode durchsuchen

Add PopmotionPlugin and example.

master
Palash Bansal vor 2 Jahren
Ursprung
Commit
2cfb21b564
Es ist kein Account mit der E-Mail-Adresse des Committers verbunden

+ 57
- 0
README.md Datei anzeigen

- [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer
- [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
- [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening
- [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 - [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


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 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 ## RenderTargetPreviewPlugin


todo: image todo: image

+ 1
- 0
examples/index.html Datei anzeigen

<h2 class="category">Animation</h2> <h2 class="category">Animation</h2>
<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-animation-plugin/">Popmotion 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="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li> <li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li>
</ul> </ul>

+ 34
- 0
examples/popmotion-plugin/index.html Datei anzeigen

<!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 Datei anzeigen

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 Datei anzeigen

"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"local-web-server": "^5.3.0", "local-web-server": "^5.3.0",
"markdown-to-html-cli": "^3.7.0", "markdown-to-html-cli": "^3.7.0",
"popmotion": "^11.0.5",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"rollup": "^3.23.0", "rollup": "^3.23.0",
"rollup-plugin-glsl": "^1.3.0", "rollup-plugin-glsl": "^1.3.0",
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"url": "https://opencollective.com/unified" "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": { "node_modules/html-void-elements": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/postcss": {
"version": "8.4.24", "version": "8.4.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
"integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==",
"dev": true "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": { "node_modules/stylehacks": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
} }
} }
}, },
"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": { "fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"space-separated-tokens": "^2.0.0" "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": { "html-void-elements": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
"dev": true "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": { "postcss": {
"version": "8.4.24", "version": "8.4.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
"integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==",
"dev": true "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": { "stylehacks": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",

+ 2
- 1
package.json Datei anzeigen

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

+ 4
- 0
rollup.config.mjs Datei anzeigen

import terser from "@rollup/plugin-terser"; import terser from "@rollup/plugin-terser";
import postcss from 'rollup-plugin-postcss' import postcss from 'rollup-plugin-postcss'
import glsl from "rollup-plugin-glsl" import glsl from "rollup-plugin-glsl"
import replace from "rollup-plugin-replace";


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

+ 163
- 0
src/plugins/animation/PopmotionPlugin.ts Datei anzeigen

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 Datei anzeigen



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

Laden…
Abbrechen
Speichern