Kaynağa Gözat

Add ProgressivePlugin, GLTFAnimationPlugin, update Readme, Add examples gltf-animation-plugin, gltf-camera-animation, progressive-plugin

master
Palash Bansal 2 yıl önce
ebeveyn
işleme
66bbbafd75
No account linked to committer's email address

+ 49
- 12
README.md Dosyayı Görüntüle

@@ -65,17 +65,19 @@ To make changes and run the example, click on the CodePen button on the top righ
- [Background, Environment maps](#background-environment-maps)
- [SVG strings](#svg-strings)
- [Plugins](#threepipe-plugins)
- [TonemapPlugin](#tonemapplugin) - Tonemapping Plugin for post-processing
- [TonemapPlugin](#tonemapplugin) - Add tonemap to the final screen pass
- [DropzonePlugin](#dropzoneplugin) - Drag and drop local files to import and load
- [DepthBufferPlugin](#depthbufferplugin) - Depth Buffer Plugin for pre-rendering depth buffer
- [NormalBufferPlugin](#normalbufferplugin) - Normal Buffer Plugin for pre-rendering normal buffer
- [DepthNormalBufferPlugin](#depthnormalbufferplugin) - Depth and Normal Buffer Plugin for pre-rendering depth and normal buffers in a single pass
- [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Render Target Preview Plugin for previewing render targets
- [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Rhino3dm Load Plugin for loading .3dm files
- [PLYLoadPlugin](#plyloadplugin) - PLY Load Plugin for loading .ply files
- [STLLoadPlugin](#stlloadplugin) - STL Load Plugin for loading .stl files
- [KTX2LoadPlugin](#ktx2loadplugin) - KTX2 Load Plugin for loading .ktx2 files
- [KTXLoadPlugin](#ktxloadplugin) - KTX Load Plugin for loading .ktx files
- [ProgressivePlugin](#progressiveplugin) - Post-render pass to blend the last frame with the current frame
- [DepthBufferPlugin](#depthbufferplugin) - Pre-rendering of depth buffer
- [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
- [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas
- [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files
- [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files
- [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files
- [KTX2LoadPlugin](#ktx2loadplugin) - Add support for loading .ktx2 files
- [KTXLoadPlugin](#ktxloadplugin) - Add support for loading .ktx files
- [Packages](#threepipe-packages)
- [@threepipe/plugin-tweakpane](#threepipeplugin-tweakpane) Tweakpane UI Plugin
- [@threepipe/plugin-tweakpane-editor](#threepipeplugin-tweakpane-editor) - Tweakpane Editor Plugin
@@ -435,7 +437,7 @@ import {DropzonePlugin, ThreeViewer} from 'threepipe'
const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
dropzone: { // this can also be set to true and configured by getting a reference to the DropzonePlugin
allowedExtensions: ['gltf', 'glb', 'hdr', 'png', 'jpg', 'json', 'fbx', 'obj'], // only allow these file types. If undefined, all files are allowed.
allowedExtensions: ['gltf', 'glb', 'hdr', 'png', 'jpg', 'json', 'fbx', 'obj', 'bin', 'exr'], // only allow these file types. If undefined, all files are allowed.
addOptions: {
disposeSceneObjects: true, // auto dispose of old scene objects
autoSetEnvironment: true, // when hdr is dropped
@@ -451,6 +453,20 @@ const viewer = new ThreeViewer({
})
```

## ProgressivePlugin

todo: image

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

Source Code: [src/plugins/postprocessing/ProgressivePlugin.ts](./src/plugins/pipeline/ProgressivePlugin.ts)

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

Progressive Plugin adds a post-render pass to blend the last frame with the current frame.

This is used as a dependency in other plugins for progressive rendering effect which is useful for progressive shadows, gi, denoising, baking, anti-aliasing, and many other effects.

## DepthBufferPlugin

todo: image
@@ -504,11 +520,32 @@ const normalTarget = normalPlugin.target;
```


## DepthNormalBufferPlugin
## GBufferPlugin

todo


## GLTFAnimationPlugin

todo: image

Example: https://threepipe.org/examples/#gltf-animation-plugin/

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

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

Manages playback of GLTF animations.

The GLTF animations can be created in any 3d software that supports GLTF export like Blender.
If animations from multiple files are loaded, they will be merged in a single root object and played together.

The time playback is managed automatically, but can be controlled manually by setting {@link autoIncrementTime} to false and using {@link setTime} to set the time.

This plugin is made for playing, pausing, stopping, all the animations at once, while it is possible to play individual animations, it is not recommended.

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

## RenderTargetPreviewPlugin

todo: image

+ 34
- 0
examples/gltf-animation-plugin/index.html Dosyayı Görüntüle

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

+ 29
- 0
examples/gltf-animation-plugin/script.ts Dosyayı Görüntüle

@@ -0,0 +1,29 @@
import {_testFinish, GLTFAnimationPlugin, ThreeViewer} from 'threepipe'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
dropzone: {
allowedExtensions: ['gltf', 'glb', 'hdr', 'bin', 'png', 'jpeg', 'webp', 'jpg', 'exr'],
addOptions: {
disposeSceneObjects: true,
autoSetEnvironment: true, // when hdr is dropped
},
},
})

const gltfAnimation = viewer.addPluginSync(GLTFAnimationPlugin)
gltfAnimation.autoplayOnLoad = true

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
const result = await viewer.load('https://threejs.org/examples/models/gltf/Horse.glb', {
autoCenter: true,
autoScale: true,
})
console.log(result)

}

init().then(_testFinish)

+ 34
- 0
examples/gltf-camera-animation/index.html Dosyayı Görüntüle

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

+ 43
- 0
examples/gltf-camera-animation/script.ts Dosyayı Görüntüle

@@ -0,0 +1,43 @@
import {_testFinish, GLTFAnimationPlugin, ICamera, ThreeViewer} from 'threepipe'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
dropzone: {
allowedExtensions: ['gltf', 'glb', 'hdr', 'bin', 'png', 'jpeg', 'webp', 'jpg', 'exr'],
addOptions: {
disposeSceneObjects: true,
autoSetEnvironment: true, // when hdr is dropped
},
},
})

const gltfAnimation = viewer.addPluginSync(GLTFAnimationPlugin)
gltfAnimation.autoplayOnLoad = false

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
const result = await viewer.load('https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Blender-Exporter@master/polly/project_polly.gltf', {
autoCenter: true,
autoScale: true,
})
console.log(result)


const fileCamera = viewer.scene.getObjectByName<ICamera>('Correction__MovingCamera')
if (!fileCamera) return

fileCamera.autoAspect = true
fileCamera.userData.autoLookAtTarget = false
fileCamera.activateMain()
viewer.scene.mainCamera.refreshAspect()

gltfAnimation.loopAnimations = false
gltfAnimation.playAnimation()

console.log(gltfAnimation)

}

init().then(_testFinish)

+ 15
- 2
examples/index.html Dosyayı Görüntüle

@@ -223,6 +223,7 @@
</ul>
<h2 class="category">Rendering</h2>
<ul>
<li><a href="./progressive-plugin/">Progressive Plugin </a></li>
<li><a href="./depth-buffer-plugin/">Depth Buffer Plugin </a></li>
<li><a href="./normal-buffer-plugin/">Normal Buffer Plugin </a></li>
<li><a href="./custom-pipeline/">Custom Pipeline specification </a></li>
@@ -262,6 +263,14 @@
<li><a href="./scene-uiconfig/">Scene UI </a></li>
<li><a href="./viewer-uiconfig/">Viewer UI </a></li>
</ul>
<h2 class="category">Animation</h2>
<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>
</ul>
<h2 class="category">Utils</h2>
<ul>
<li><a href="./render-target-preview/">Render Target Preview Plugin </a></li>
@@ -274,9 +283,13 @@
<ul>
<li><a href="./html-sample/">HTML/JS Sample </a></li>
</ul>
<h2 class="category">Lights</h2>
<ul>
<li><a href="./directional-light/">Directional Light </a></li>
</ul>
<h2 class="category">Tests</h2>
<ul>
<li><a href="./gltf-transmission-test/">GLTF Transmission Test </a></li>
<li><a href="./gltf-transmission-test/">glTF Transmission Test </a></li>
<li><a href="./uint8-rgbm-hdr-test/">Uint8 RGBM HDR Test </a></li>
<li><a href="./half-float-hdr-test/">Half-float HDR Test </a></li>
<li><a href="./sphere-rgbm-test/">RGBM Test </a></li>
@@ -287,7 +300,7 @@
</ul>
</div>
<div class="iframe-container">
<iframe id="example-iframe" src="./gltf-load/" frameborder="0" allowfullscreen="allowfullscreen"
<iframe id="example-iframe" src="./tweakpane-editor/" frameborder="0" allowfullscreen="allowfullscreen"
allow="accelerometer *; ambient-light-sensor *; autoplay *; camera *; clipboard-read *; clipboard-write *; encrypted-media *; fullscreen *; geolocation *; gyroscope *; magnetometer *; microphone *; midi *; payment *; picture-in-picture *; screen-wake-lock *; speaker *; sync-xhr *; usb *; web-share *; vibrate *; vr *">
</iframe>
</div>

+ 35
- 0
examples/progressive-plugin/index.html Dosyayı Görüntüle

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

+ 84
- 0
examples/progressive-plugin/script.ts Dosyayı Görüntüle

@@ -0,0 +1,84 @@
import {
_testFinish,
BasicShadowMap,
Box3B,
DirectionalLight,
IObject3D,
Mesh,
PhysicalMaterial,
PlaneGeometry,
ProgressivePlugin,
RenderTargetPreviewPlugin,
ThreeViewer,
timeout,
Vector3,
} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
})

// viewer.scene.addObject(new HemisphereLight(0xffffff, 0x444444, 10))
const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/fbx/Samba Dancing.fbx', {
autoCenter: true,
autoScale: true,
})

const ground = new Mesh(
new PlaneGeometry(100, 100)
.rotateX(-Math.PI / 2)
.translate(0, new Box3B().expandByObject(result!).getSize(new Vector3()).y / -2, 0),
new PhysicalMaterial({
color: '#ffffff',
})
)
ground.castShadow = false
ground.receiveShadow = true
viewer.scene.addObject(ground)

const directionalLight = viewer.scene.addObject(new DirectionalLight(0xffffff, 4))
directionalLight.position.set(2, 2, 2)
directionalLight.lookAt(0, 0, 0)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.setScalar(1024)
directionalLight.shadow.camera.near = 0.1
directionalLight.shadow.camera.far = 10
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.bottom = -2
directionalLight.shadow.camera.left = -2
directionalLight.shadow.camera.right = 2

viewer.renderManager.renderer.shadowMap.type = BasicShadowMap

viewer.scene.mainCamera.position.set(1, 2, 2.5)
viewer.scene.mainCamera.target.set(0, 0.25, 0)
viewer.scene.mainCamera.setDirty()

const rt = viewer.addPluginSync(RenderTargetPreviewPlugin)
rt.addTarget(()=>directionalLight.shadow.map || undefined, 'shadow', true, true, true)

viewer.addPluginSync(new ProgressivePlugin(200))
viewer.addEventListener('postFrame', ()=>{
if (viewer.renderManager.frameCount < 1) return

directionalLight.position.set(
2 + Math.sin(viewer.renderManager.frameCount) / 5,
2,
2 + Math.cos(viewer.renderManager.frameCount) / 5
)
directionalLight.lookAt(0, 0, 0)
viewer.renderManager.resetShadows()

})

viewer.addPluginSync(TweakpaneUiPlugin).setupPlugins(ProgressivePlugin)

await timeout(3000) // for convergence

}

init().then(_testFinish)

+ 12
- 5
examples/tweakpane-editor/script.ts Dosyayı Görüntüle

@@ -3,11 +3,13 @@ import {
DepthBufferPlugin,
DropzonePlugin,
FullScreenPlugin,
GLTFAnimationPlugin,
HalfFloatType,
KTX2LoadPlugin,
KTXLoadPlugin,
NormalBufferPlugin,
PLYLoadPlugin,
ProgressivePlugin,
RenderTargetPreviewPlugin,
Rhino3dmLoadPlugin,
SceneUiConfigPlugin,
@@ -33,10 +35,13 @@ async function init() {
},
})

viewer.addPluginSync(new TweakpaneUiPlugin(true))
// @ts-expect-error unused
const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))
const editor = viewer.addPluginSync(new TweakpaneEditorPlugin())

await viewer.addPlugins([
new ProgressivePlugin(),
new GLTFAnimationPlugin(),
new ViewerUiConfigPlugin(),
// new SceneUiConfigPlugin(), // this is already in ViewerUiPlugin
new DepthBufferPlugin(HalfFloatType, true, true),
@@ -56,19 +61,21 @@ async function init() {
editor.loadPlugins({
['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin],
['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin],
['Post-processing']: [TonemapPlugin],
['Post-processing']: [TonemapPlugin, ProgressivePlugin],
['Animation']: [GLTFAnimationPlugin],
['Debug']: [RenderTargetPreviewPlugin],
})

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')

// await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
// const result = await viewer.load<IObject3D>('https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Blender-Exporter@master/polly/project_polly.gltf', {
// autoCenter: true,
// autoScale: true,
// })
// const model = result?.getObjectByName('node_damagedHelmet_-6514')
//
// const model = result?.getObjectByName('Correction__MovingCamera')
// const config = model?.uiConfig
// console.log(model, config, result)
// if (config) ui.appendChild(config)

}

+ 501
- 0
src/plugins/animation/GLTFAnimationPlugin.ts Dosyayı Görüntüle

@@ -0,0 +1,501 @@
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {absMax, now, onChange, onChange2, PointerDragHelper, serialize} from 'ts-browser-helpers'
import {uiButton, uiDropdown, uiFolderContainer, uiMonitor, UiObjectConfig, uiSlider, uiToggle} from 'uiconfig.js'
import {AnimationAction, AnimationClip, AnimationMixer, LoopOnce, LoopRepeat} from 'three'
import {ProgressivePlugin} from '../pipeline/ProgressivePlugin'
import {IObject3D} from '../../core'
import {generateUUID} from '../../three'

type FrameFadePlugin = any // todo

/**
* Manages playback of GLTF animations.
*
* The GLTF animations can be created in any 3d software that supports GLTF export like Blender.
* If animations from multiple files are loaded, they will be merged in a single root object and played together.
*
* The time playback is managed automatically, but can be controlled manually by setting {@link autoIncrementTime} to false and using {@link setTime} to set the time.
*
* This plugin is made for playing, pausing, stopping, all the animations at once, while it is possible to play individual animations, it is not recommended.
*
* 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
*
* @category Plugins
*/
@uiFolderContainer('GLTF Animations')
export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'checkpointBegin'|'animationStep'> {
enabled = true
uiConfig!: UiObjectConfig

static readonly PluginType = 'GLTFAnimation'
/**
* List of GLTF animations loaded with the models.
* The animations are standard threejs AnimationClip and their AnimationAction. Each set of actions also has a mixer.
*/
public readonly animations: {mixer: AnimationMixer, clips: AnimationClip[], actions: AnimationAction[], duration: number}[] = []

/**
* If true, the animation time will be automatically incremented by the time delta, otherwise it has to be set manually between 0 and the animationDuration using `setTime`. (default: true)
*/
@serialize() autoIncrementTime = true

/**
* Loop the complete animation. (not individual actions)
* This happens {@link loopRepetitions} times.
*/
@onChange2(GLTFAnimationPlugin.prototype._onPropertyChange)
@uiToggle('Loop')
@serialize() loopAnimations = true

/**
* Number of times to loop the animation. (not individual actions)
* Only applicable when {@link loopAnimations} is true.
*/
@onChange2(GLTFAnimationPlugin.prototype._onPropertyChange)
@serialize() loopRepetitions = Infinity

/**
* Timescale for the animation. (not individual actions)
* If set to 0, it will be ignored.
*/
@uiSlider('Timescale', [-2, 2], 0.01)
@serialize() timeScale = 1

/**
* Speed of the animation. (not individual actions)
* This can be set to 0.
*/
@uiSlider('Speed', [0.1, 4], 0.1) @serialize() animationSpeed = 1

/**
* Automatically track scroll and mouse wheel events to seek animations
* Control damping/smoothness with {@link scrollAnimationDamping}
* See also {@link animateOnDrag}
*/
@uiToggle() @serialize() animateOnScroll = false

/**
* Damping for the scroll animation, when {@link animateOnScroll} is true.
*/
@uiSlider('Scroll Damping', [0, 1]) @serialize() scrollAnimationDamping = 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}
*/
@uiToggle() @serialize() animateOnDrag = false

/**
* Axis to track for drag events, when {@link animateOnDrag} is true.
* `x` will track horizontal drag, `y` will track vertical drag.
*/
@uiDropdown('Drag Axis', [{label: 'x'}, {label: 'y'}])
@serialize() dragAxis: 'x'|'y' = 'y'

/**
* Damping for the drag animation, when {@link animateOnDrag} is true.
*/
@uiSlider('Drag Damping', [0, 1]) @serialize() dragAnimationDamping = 0.3

/**
* If true, the animation will be played automatically when the model(any model with animations) is loaded.
*/
@uiToggle() @serialize() autoplayOnLoad = false

/**
* Get the current state of the animation. (read only)
* use {@link playAnimation}, {@link pauseAnimation}, {@link stopAnimation} to change the state.
*/
@uiMonitor() get animationState(): 'none' | 'playing' | 'paused' | 'stopped' {
return this._animationState
}

/**
* Get the current animation time. (read only)
* The time is managed automatically.
* To manage the time manually set {@link autoIncrementTime} to false and use {@link setTime} to change the time.
*/
@uiMonitor() get animationTime(): number {
return this._animationTime
}

/**
* Get the current animation duration (max of all animations). (read only)
*/
@uiMonitor() get animationDuration(): number {
return this._animationDuration
}


@uiButton('Play/Pause', (that: GLTFAnimationPlugin)=>({
label:()=> that.animationState === 'playing' ? 'Pause' : 'Play',
limitedUi: true,
}))
playPauseAnimation() {
this._animationState === 'playing' ? this.pauseAnimation() : this.playAnimation()
}

@onChange(GLTFAnimationPlugin.prototype.onStateChange)
protected _animationState: 'none' | 'playing' | 'paused' | 'stopped' = 'none'

private _lastAnimationTime = 0
private _animationTime = 0
private _animationDuration = 0
private _scrollAnimationState = 0
private _dragAnimationState = 0
private _pointerDragHelper = new PointerDragHelper()
private _lastFrameTime = 0
private _fadeDisabled = false

constructor() {
super()
this.playClips = this.playClips.bind(this)
this.playClip = this.playClip.bind(this)
this.playAnimation = this.playAnimation.bind(this)
this.playPauseAnimation = this.playPauseAnimation.bind(this)
this.pauseAnimation = this.pauseAnimation.bind(this)
this.stopAnimation = this.stopAnimation.bind(this)
this.resetAnimation = this.resetAnimation.bind(this)
this._onPropertyChange = this._onPropertyChange.bind(this)
this._postFrame = this._postFrame.bind(this)
this._wheel = this._wheel.bind(this)
this._pointerDragHelper.addEventListener('drag', this._drag.bind(this))
}

setTime(time: number) {
this._animationTime = Math.max(0, Math.min(time, this._animationDuration))
}


async onAdded(viewer: ThreeViewer): Promise<void> {
viewer.scene.addEventListener('addSceneObject', this._objectAdded)
viewer.addEventListener('postFrame', this._postFrame)
window.addEventListener('wheel', this._wheel)
this._pointerDragHelper.element = viewer.canvas
return super.onAdded(viewer)
}

async onRemove(viewer: ThreeViewer): Promise<void> {
while (this.animations.length) this.animations.pop()
viewer.scene.removeEventListener('addSceneObject', this._objectAdded)
viewer.removeEventListener('postFrame', this._postFrame)
window.removeEventListener('wheel', this._wheel)
this._pointerDragHelper.element = undefined
return super.onRemove(viewer)
}

public onStateChange(): void {
this.uiConfig?.uiRefresh?.(true, 'postFrame')
// this.uiConfig?.children?.map(value => value && getOrCall(value)).flat(2).forEach(v=>v?.uiRefresh?.())
}

/**
* This will play a single clip by name
* It might reset all other animations, this is a bug; https://codepen.io/repalash/pen/mdjgpvx
* @param name
* @param resetOnEnd
*/
async playClip(name: string, resetOnEnd = false) {
return this.playClips([name], resetOnEnd)
}
async playClips(names: string[], resetOnEnd = false) {
const anims: AnimationAction[] = []
this.animations.forEach(({actions})=>{
actions.forEach((action)=>{
if (names.includes(action.getClip().name)) {
anims.push(action)
}
})
})
return this.playAnimation(resetOnEnd, anims)
}

private _lastAnimId = ''

/**
* Starts all the animations and returns a promise that resolves when all animations are done.
* @param resetOnEnd - if true, will reset the animation to the start position when it ends.
* @param animations - play specific animations, otherwise play all animations. Note: the promise returned (if this is set) from this will resolve before time if the animations was ever paused, or converged mode is on in recorder.
*/
async playAnimation(resetOnEnd = false, animations?: AnimationAction[]): Promise<void> {
if (!this.enabled) return
let wasPlaying = false
if (this._animationState === 'playing') {
this.stopAnimation(false) // stop and play again. reset is done below.
wasPlaying = true
}
// safeSetProperty(this._viewer?.getPlugin<PickingPlugin>('Picking')?.transformControls, 'enabled', false)
let duration = 0
const isAllAnimations = !animations
if (!animations) {
animations = []
this.animations.forEach(({actions}) => {
// console.log(mixer, actions, clips)
animations!.push(...actions)
})
}
if (wasPlaying)
this.resetAnimation()
else if (this.animationState !== 'paused') {
animations.forEach((ac)=>{
ac.reset()
})
this._animationTime = 0
}

const id = generateUUID()
this._lastAnimId = id // todo: check logic
for (const ac of animations) {
// if (Math.abs(this.timeScale) > 0) {
// if (!(ac as any)._tTimeScale) (ac as any)._tTimeScale = ac.timeScale
// ac.timeScale = this.timeScale
// } else if ((ac as any)._tTimeScale) ac.timeScale = (ac as any)._tTimeScale
ac.setLoop(this.loopAnimations ? LoopRepeat : LoopOnce, this.loopRepetitions)
ac.play()
duration = Math.max(duration, ac.getClip().duration / Math.abs(ac.timeScale))
// if (!this._playingActions.includes(ac)) this._playingActions.push(ac)
// console.log(ac)
}
this._animationState = 'playing'
this._viewer?.setDirty()
if (!isAllAnimations) {
const loops = this.loopAnimations ? this.loopRepetitions : 1
duration *= loops
if (!isFinite(duration)) {
// infinite animation
return
}

await new Promise<void>((resolve) => {
const listen = (e: any) => {
if (e.time >= duration) {
this.removeEventListener('animationStep', listen)
resolve()
}
}
this.addEventListener('animationStep', listen)
})

// const animDuration = 1000 * duration - this._animationTime / this.animationSpeed + 0.01
//
// if (animDuration > 0) {
// await timeout(animDuration)
// return
// } // todo: handle pausing/early stop, converge mode for single animation playback
} else {
if (!isFinite(this._animationDuration)) {
// infinite animation
return
}
await new Promise<void>((resolve) => {
const listen = () => {
this.removeEventListener('checkpointEnd', listen)
resolve()
}
this.addEventListener('checkpointEnd', listen)
})
}
if (id === this._lastAnimId) { // in-case multiple animations are started.
this.stopAnimation(resetOnEnd)
}
return
}

pauseAnimation() {
if (this._animationState !== 'playing') {
console.warn('pauseAnimation called when animation was not playing.')
return
}
this._animationState = 'paused'
// safeSetProperty(this._viewer?.getPlugin<PickingPlugin>('Picking')?.transformControls, 'enabled', true)
this._viewer?.setDirty()
// this._lastAnimId = '' // this disables stop on timeout end, for now.
}
resumeAnimation() {
if (this._animationState !== 'paused') {
console.warn('resumeAnimation called when animation was not paused.')
return
}
this._animationState = 'playing'
// safeSetProperty(this._viewer?.getPlugin<PickingPlugin>('Picking')?.transformControls, 'enabled', true)
this._viewer?.setDirty()
}

@uiButton('Stop')
stopAnimation(reset = false) {
this._animationState = 'stopped'
// safeSetProperty(this._viewer?.getPlugin<PickingPlugin>('Picking'), 'enabled', true)
if (reset) this.resetAnimation()
else this._viewer?.setDirty()
this._lastAnimId = ''

if (this._viewer && this._fadeDisabled) {
this._viewer.getPlugin<FrameFadePlugin>('FrameFade')?.enable(GLTFAnimationPlugin.PluginType)
this._fadeDisabled = false
}

}

@uiButton('Reset')
resetAnimation() {
if (this._animationState !== 'stopped' && this._animationState !== 'none') {
this.stopAnimation(true) // reset and stop
return
}
this.animations.forEach(({mixer}) => {
// console.log(mixer, actions, clips)
mixer.stopAllAction()
mixer.setTime(0)
})
this._animationTime = 0
this._viewer?.setDirty()
}


protected _postFrame() {
if (!this._viewer) return

const scrollAnimate = this.animateOnScroll // && this._animationState === 'paused'
const dragAnimate = this.animateOnDrag // && this._animationState === 'paused'

if (!this.enabled || this.animations.length < 1 || this._animationState !== 'playing' && !scrollAnimate && !dragAnimate) {
this._lastFrameTime = 0
// console.log('not anim')
if (this._fadeDisabled) {
this._viewer.getPlugin<FrameFadePlugin>('FrameFade')?.enable(GLTFAnimationPlugin.PluginType)
this._fadeDisabled = false
}
return
}

if (this._animationTime < 0.0001) {
this.dispatchEvent({type: 'checkpointBegin'})
}
if (this.autoIncrementTime) {

const time = now() / 1000.0
if (this._lastFrameTime < 1) this._lastFrameTime = time - 1.0 / 30.0
let delta = time - this._lastFrameTime
delta *= this.animationSpeed

this._lastFrameTime = time

if (scrollAnimate && dragAnimate) delta *= absMax(this._scrollAnimationState, this._dragAnimationState)
else if (scrollAnimate) delta *= this._scrollAnimationState
else if (dragAnimate) delta *= this._dragAnimationState

if (Math.abs(delta) < 0.0001) return

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

const ts = Math.abs(this.timeScale)
this._animationTime += delta * (ts > 0 ? ts : 1)
}

const animDelta = this._animationTime - this._lastAnimationTime

this._lastAnimationTime = this._animationTime

const t = this.timeScale < 0 ?
(isFinite(this._animationDuration) ? this._animationDuration : 0) - this._animationTime :
this._animationTime

this.animations.map(a=>{
// a.mixer.timeScale = -1
a.mixer.setTime(t)
})

if (Math.abs(animDelta) < 0.00001) return

// if (this._animationTime > this._animationDuration) this._animationTime -= this._animationDuration
// if (this._animationTime < 0) this._animationTime += this._animationDuration

if (Math.abs(this._scrollAnimationState) < 0.001) this._scrollAnimationState = 0
else this._scrollAnimationState *= 1.0 - this.scrollAnimationDamping

if (Math.abs(this._dragAnimationState) < 0.001) this._dragAnimationState = 0
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
// if (this._viewer.scene.mainCamera.userData.isAnimating) { // if camera is animating
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()

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

if (this._animationTime >= this._animationDuration) {
this.dispatchEvent({type: 'checkpointEnd'})
}
}

protected _objectAdded = (ev: any)=>{
const object = ev.object as IObject3D
if (!this._viewer) return
let changed = false

object.traverse((obj)=>{
if (!this._viewer) return

const clips: AnimationClip[] = obj.animations
if (clips.length < 1) return

const duration = Math.max(...clips.map(an=>an.duration))
// clips.forEach(cp=>cp.duration = duration) // todo: check why do we need to do this? wont this create problems with looping or is it for that so that looping works in sync.

const mixer = new AnimationMixer(this._viewer.scene.modelRoot) // add to modelRoot so it works with GLTF export...
const actions = clips.map(an=>mixer.clipAction(an).setLoop(this.loopAnimations ? LoopRepeat : LoopOnce, this.loopRepetitions))

actions.forEach(ac=>ac.clampWhenFinished = true)

this.animations.push({
mixer, clips, actions, duration,
})
// todo remove on object dispose

changed = true

})
// this.playAnimation()
if (changed) {
this._onPropertyChange(!this.autoplayOnLoad)
if (this.autoplayOnLoad) this.playAnimation()
}
return
}

private _onPropertyChange(replay = true): void {
this._animationDuration = Math.max(...this.animations.map(({duration})=>duration)) * (this.loopAnimations ? this.loopRepetitions : 1)
if (this._animationState === 'playing' && replay) {
this.playAnimation()
}
}


private _wheel({deltaY}: any | WheelEvent) {
if (!this.enabled) return
if (Math.abs(deltaY) > 0.001)
this._scrollAnimationState = -1. * Math.sign(deltaY)
}

private _drag(ev: any) {
if (!this.enabled || !this._viewer) return
this._dragAnimationState = this.dragAxis === 'x' ?
ev.delta.x * this._viewer.canvas.width / 4 :
ev.delta.y * this._viewer.canvas.height / 4
}

}

+ 5
- 0
src/plugins/index.ts Dosyayı Görüntüle

@@ -2,8 +2,10 @@
export {PipelinePassPlugin} from './base/PipelinePassPlugin'

// pipeline
export {ProgressivePlugin} from './pipeline/ProgressivePlugin'
export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin'
export {NormalBufferPlugin} from './pipeline/NormalBufferPlugin'
export type {ProgressivePluginEventTypes, ProgressivePluginTarget} from './pipeline/ProgressivePlugin'
export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin'
export type {NormalBufferPluginEventTypes, NormalBufferPluginPass, NormalBufferPluginTarget} from './pipeline/NormalBufferPlugin'

@@ -25,3 +27,6 @@ export {KTX2LoadPlugin} from './import/KTX2LoadPlugin'

// postprocessing
export {TonemapPlugin} from './postprocessing/TonemapPlugin'

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

+ 147
- 0
src/plugins/pipeline/ProgressivePlugin.ts Dosyayı Görüntüle

@@ -0,0 +1,147 @@
import {IUniform, Texture, TextureDataType, UnsignedByteType, WebGLRenderTarget} from 'three'
import {IPassID, IPipelinePass} from '../../postprocessing'
import {ThreeViewer} from '../../viewer'
import {PipelinePassPlugin} from '../base/PipelinePassPlugin'
import {uiFolderContainer, uiImage, uiInput} from 'uiconfig.js'
import {ICamera, IRenderManager, IScene, IWebGLRenderer} from '../../core'
import {AddBlendTexturePass} from '../../postprocessing/AddBlendTexturePass'
import {serialize, ValOrFunc} from 'ts-browser-helpers'

export type ProgressivePluginEventTypes = ''
export type ProgressivePluginTarget = WebGLRenderTarget

/**
* Progressive Plugin
*
* Adds a post-render pass to blend the last frame with the current frame.
* This can be used to create a progressive rendering effect which is useful for progressive shadows, gi, denoising, baking, anti-aliasing, and many other effects.
* @category Plugins
*/
@uiFolderContainer('Progressive Plugin')
export class ProgressivePlugin
extends PipelinePassPlugin<ProgressiveBlendPass, 'progressive', ProgressivePluginEventTypes> {

readonly passId = 'progressive'
public static readonly PluginType = 'ProgressivePlugin'

target?: ProgressivePluginTarget

@serialize() @uiInput('Frame count') maxFrameCount: number
// todo: deserialize jitter

@uiImage('Last Texture' /* {readOnly: true}*/) texture?: Texture

// @onChange2(ProgressivePlugin.prototype._createTarget)
// @uiDropdown('Buffer Type', threeConstMappings.TextureDataType.uiConfig)
readonly bufferType: TextureDataType // cannot be changed after creation (for now)

constructor(
maxFrameCount = 32,
bufferType: TextureDataType = UnsignedByteType,
enabled = true,
) {
super()
this.maxFrameCount = maxFrameCount
this.enabled = enabled
this.bufferType = bufferType
}

protected _createTarget(recreate = true) {
if (!this._viewer) return
if (recreate) this._disposeTarget()
if (!this.target) this.target = this._viewer.renderManager.composerTarget.clone(true) as WebGLRenderTarget

this.texture = this.target.texture
this.texture.name = 'progressiveLastBuffer'

if (this._pass) this._pass.target = this.target
}

protected _disposeTarget() {
if (!this._viewer) return
if (this.target) {
this._viewer.renderManager.disposeTarget(this.target)
this.target = undefined
}
this.texture = undefined
}

protected _createPass() {
this._createTarget(true)
if (!this.target) throw new Error('ProgressivePlugin: target not created')
const pass = new ProgressiveBlendPass(this.passId, this.target)
pass.dirty = () => (this._viewer?.renderManager.frameCount || 0) < this.maxFrameCount // todo use isConverged function
return pass
}

onRemove(viewer: ThreeViewer): void {
this._disposeTarget()
return super.onRemove(viewer)
}
/**
*
* @param postRender - if called after rendering frame.
*/
public isConverged(postRender = false): boolean {
return (this._viewer?.renderer.frameCount || 0) >= this.maxFrameCount - 1 + (postRender ? 1 : 0)
}

updateShaderProperties(material: {defines: Record<string, string | number | undefined>; uniforms: {[p: string]: IUniform}}): this {
if (material.uniforms.tLastFrame) material.uniforms.tLastFrame.value = this.target?.texture ?? undefined
return this
}

/**
* Get recording delta post render, For use with animations to sync with converge mode in canvas recorder. See PopmotionPlugin for usage.
* @returns {number} - delta time in milliseconds, or 0 when converging, or -1 in case of not recording in converge mode
*/
postFrameConvergedRecordingDelta(_ = 'CanvasRecorder'): number {
// const recorder = this._viewer!.getPluginByType<IConvergedCanvasRecorder&IViewerPlugin>(recorderPlugin)
// if (recorder && recorder.isRecording() && recorder.convergeMode)
// return this.isConverged(true) ? 1. / recorder.videoFrameRate : 0
return -1
}

}

class ProgressiveBlendPass extends AddBlendTexturePass implements IPipelinePass {
before = ['screen']
after = ['render']
required = ['render']
dirty: ValOrFunc<boolean> = () => false
constructor(public readonly passId: IPassID, public target: WebGLRenderTarget) {
super()
}
render(renderer: IWebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget, deltaTime: number, maskActive: boolean) {
if (renderer.renderManager.frameCount < 1) {
this.needsSwap = false
if (readBuffer?.texture)
renderer.renderManager.blit(this.target, {
source: readBuffer.texture,
respectColorSpace: false,
})
return
}
this.needsSwap = true
super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
renderer.renderManager.blit(this.target, {
source: writeBuffer.texture,
respectColorSpace: false,
})
}

beforeRender(_: IScene, _1: ICamera, renderManager: IRenderManager) {
if (!this.enabled) return
if (!this.target) {
console.error('ProgressiveBlendPass: render target undefined')
return
}
let f = 1. / (Math.max(renderManager.frameCount, 0) + 1)
this.uniforms.weight.value.set(f, f, f, f)
f = 1. - f
this.uniforms.weight2.value.set(f, f, f, f)
this.uniforms.tDiffuse2.value = this.target.texture
this.material.uniformsNeedUpdate = true
}

}

Loading…
İptal
Kaydet