Przeglądaj źródła

Add FrameFadePlugin, page scroll animation support in GLTFAnimationPlugin, examples for both, minor refactor in types.

master
Palash Bansal 2 lat temu
rodzic
commit
e1a651620f
No account linked to committer's email address

+ 31
- 0
README.md Wyświetl plik

@@ -73,6 +73,7 @@ To make changes and run the example, click on the CodePen button on the top righ
- [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
- [FrameFadePlugin](#framefadeplugin) - Post-render pass to smoothly fade to a new rendered frame over time
- [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files
- [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files
- [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files
@@ -571,6 +572,36 @@ const previewPlugin = viewer.addPluginSync(new RenderTargetPreviewPlugin())
previewPlugin.addTarget(()=>normalPlugin.target, 'normal', false, false)
```

## FrameFadePlugin

todo: image

Example: https://threepipe.org/examples/#frame-fade-plugin/

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

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

FrameFadePlugin adds a post-render pass to the render manager and blends the last frame with the current frame over time. This is useful for creating smooth transitions between frames for example when changing the camera position, material, object properties, etc to avoid a sudden jump.

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

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

const fadePlugin = viewer.addPluginSync(new FrameFadePlugin())

// Make some changes in the scene (any visual change that needs to be faded)

// Start transition and wait for it to finish
await fadePlugin.startTransition(400) // duration in ms

```

To stop a transition, call `fadePlugin.stopTransition()`. This will immediately set the current frame to the last frame and stop the transition. The transition is also automatically stopped when the camera is moved or some pointer event occurs on the canvas.

The plugin automatically tracks `setDirty()` function calls in objects, materials and the scene. It can be triggerred by calling `setDirty` on any material or object in the scene. Check the [example](https://threepipe.org/examples/#frame-fade-plugin/) for a demo. This can be disabled by options in the plugin.

## Rhino3dmLoadPlugin

Example: https://threepipe.org/examples/#rhino3dm-load/

+ 34
- 0
examples/frame-fade-plugin/index.html Wyświetl plik

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

+ 35
- 0
examples/frame-fade-plugin/script.ts Wyświetl plik

@@ -0,0 +1,35 @@
import {_testFinish, BoxGeometry, FrameFadePlugin, Mesh, PhysicalMaterial, 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,
})
viewer.addPluginSync(FrameFadePlugin)

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

createSimpleButtons({
['Change Color']: ()=>{
cube.material.color.setHSL(Math.random(), 1, 0.5)
cube.material.setDirty() // this will trigger frame fade
},
['Change Size']: ()=>{
cube.scale.setScalar(Math.random() * 1.5 + 0.5)
cube.setDirty({fadeDuration: 1000}) // duration can be controlled by an option like this.
},
['Change Color (no fade)']: ()=>{
cube.material.color.setHSL(Math.random(), 1, 0.5)
cube.material.setDirty({frameFade: false}) // disable frame fade for this update but re-render the scene.
},
})

}

init().then(_testFinish)

+ 128
- 0
examples/gltf-animation-page-scroll/index.html Wyświetl plik

@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GLTF Animation Page Scroll</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 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">
#canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
body {
overflow-y: scroll;
overflow-x: hidden;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: white;
}
#canvas-container {
position: fixed;
top: 0;
z-index: 5;
}
#content{
z-index: 10;
position: relative;
height: auto;
}
section{
z-index: 100;
height: 100vh;
line-height: 100vh;
font-size: 4rem;
font-weight: 600;
}
</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>
<div id="content">
<section>
Section 1
</section>
<section>
Section 2
</section>
<section>
Section 3
</section>
<section>
Section 4
</section>
<section>
Section 5
</section>
<section>
Section 6
</section>
<section>
Section 7
</section>
<section>
Section 8
</section>
<section>
Section 9
</section>
<section>
Section 10
</section>
<section>
Section 11
</section>
<section>
Section 12
</section>
<section>
Section 13
</section>
<section>
Section 14
</section>
<section>
Section 15
</section>
<section>
Section 16
</section>
<section>
Section 17
</section>
<section>
Section 18
</section>
<section>
Section 19
</section>
<section>
Section 20
</section>
<section>
Section 21
</section>
<section>
Section 22
</section>
</div>
</body>

+ 34
- 0
examples/gltf-animation-page-scroll/script.ts Wyświetl plik

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

async function init() {

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

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

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

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

fileCamera.autoAspect = true
fileCamera.userData.autoLookAtTarget = false
fileCamera.activateMain()

gltfAnimation.loopAnimations = false
gltfAnimation.animateOnPageScroll = true
gltfAnimation.pageScrollAnimationDamping = 0.1

gltfAnimation.playAnimation()

}

init().then(_testFinish)

+ 1
- 2
examples/gltf-camera-animation/script.ts Wyświetl plik

@@ -31,9 +31,8 @@ async function init() {
fileCamera.autoAspect = true
fileCamera.userData.autoLookAtTarget = false
fileCamera.activateMain()
viewer.scene.mainCamera.refreshAspect()

gltfAnimation.loopAnimations = false
gltfAnimation.loopAnimations = true
gltfAnimation.playAnimation()

console.log(gltfAnimation)

+ 2
- 3
examples/index.html Wyświetl plik

@@ -220,6 +220,7 @@
<h2 class="category">Post-Processing</h2>
<ul>
<li><a href="./tonemap-plugin/">Tonemap Plugin </a></li>
<li><a href="./frame-fade-plugin/">Frame Fade Plugin </a></li>
</ul>
<h2 class="category">Rendering</h2>
<ul>
@@ -267,9 +268,7 @@
<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>
<li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li>
</ul>
<h2 class="category">Utils</h2>
<ul>

+ 3
- 1
examples/tweakpane-editor/script.ts Wyświetl plik

@@ -2,6 +2,7 @@ import {
_testFinish,
DepthBufferPlugin,
DropzonePlugin,
FrameFadePlugin,
FullScreenPlugin,
GLTFAnimationPlugin,
HalfFloatType,
@@ -47,6 +48,7 @@ async function init() {
new DepthBufferPlugin(HalfFloatType, true, true),
new NormalBufferPlugin(HalfFloatType, false),
new RenderTargetPreviewPlugin(false),
new FrameFadePlugin(),
new KTX2LoadPlugin(),
new KTXLoadPlugin(),
new PLYLoadPlugin(),
@@ -61,7 +63,7 @@ async function init() {
editor.loadPlugins({
['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin],
['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin],
['Post-processing']: [TonemapPlugin, ProgressivePlugin],
['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin],
['Animation']: [GLTFAnimationPlugin],
['Debug']: [RenderTargetPreviewPlugin],
})

+ 3
- 9
src/core/IMaterial.ts Wyświetl plik

@@ -4,6 +4,7 @@ import type {MaterialExtension} from '../materials'
import type {ChangeEvent, IUiConfigContainer} from 'uiconfig.js'
import type {SerializationMetaType} from '../utils'
import type {IObject3D} from './IObject'
import {ISetDirtyCommonOptions} from './IObject'
import type {ITexture} from './ITexture'
import type {IImportResultUserData} from '../assetmanager'

@@ -20,23 +21,16 @@ export type IMaterialEvent<T extends string = IMaterialEventTypes> = Event & {

uiChangeEvent?: ChangeEvent
}
export interface IMaterialSetDirtyOptions {
export interface IMaterialSetDirtyOptions extends ISetDirtyCommonOptions{
/**
* @default true
*/
bubbleToObject?: boolean,
/**
* @default true
*/
refreshUi?: boolean,
/**
* @default true
*/
needsUpdate?: boolean,
/**
* Event from uiconfig.js
*/
uiChangeEvent?: ChangeEvent,

[key: string]: any
}
export interface IMaterialUserData extends IImportResultUserData{

+ 26
- 5
src/core/IObject.ts Wyświetl plik

@@ -1,7 +1,7 @@
import {IDisposable} from 'ts-browser-helpers'
import {IMaterial} from './IMaterial'
import {Event, Object3D} from 'three'
import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js'
import {ChangeEvent, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js'
import {IGeometry, IGeometryEvent} from './IGeometry'
import {IImportResultUserData} from '../assetmanager'
import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js'
@@ -21,16 +21,37 @@ export interface IObject3DEvent<T extends string = IObject3DEventTypes> extends
oldGeometry?: IGeometry|undefined // from geometryChanged
}

export interface IObjectSetDirtyOptions {
export interface ISetDirtyCommonOptions {
/**
* Trigger UI Config Refresh along with setDirty.
* Default `true`. Set to `false` to prevent UI Config refresh.
*/
refreshUi?: boolean

/**
* Enable/disable frame fade using {@link FrameFadePlugin}
* Default `true`. when the plugin is enabled and has corresponding flags enabled
*/
frameFade?: boolean // for plugins
/**
* Duration for `frameFade` in ms. Check {@link FrameFadePlugin} for more details.
*/
fadeDuration?: number // for plugins

/**
* Event from uiconfig.js when some value changes from the UI.
*/
uiChangeEvent?: ChangeEvent,

}

export interface IObjectSetDirtyOptions extends ISetDirtyCommonOptions{
bubbleToParent?: boolean // bubble event to parent root
change?: string
refreshScene?: boolean // update scene after setting dirty

geometryChanged?: boolean // whether to refresh stuff like ground.

frameFade?: boolean // for plugins
refreshUi?: boolean // for plugins

/**
* @deprecated use {@link refreshScene} instead
*/

+ 2
- 4
src/core/IScene.ts Wyświetl plik

@@ -62,9 +62,7 @@ export interface ISceneEvent<T extends string = ISceneEventTypes> extends IObjec
scene?: IScene | null
// change?: string
}
export type ISceneSetDirtyOptions = IObjectSetDirtyOptions & {
[key: string]: any
}
export type ISceneSetDirtyOptions = IObjectSetDirtyOptions


export type ISceneUserData = IObject3DUserData
@@ -85,7 +83,7 @@ export interface IScene<E extends ISceneEvent = ISceneEvent, ET extends ISceneEv
// environmentLight?: IEnvironmentLight;
// processors: ObjectProcessorMap<'environment' | 'background'>

addObject<T extends IObject3D>(imported: T, options?: AddObjectOptions): T;
addObject<T extends IObject3D>(imported: T, options?: AddObjectOptions): T&IObject3D;

setDirty(e?: ISceneSetDirtyOptions): void


+ 3
- 3
src/core/object/RootScene.ts Wyświetl plik

@@ -160,18 +160,18 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
* @param imported
* @param options
*/
addObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T {
addObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T&IObject3D {
if (options?.clearSceneObjects || options?.disposeSceneObjects) {
this.clearSceneModels(options.disposeSceneObjects)
}
if (!imported) return imported
if (!imported.isObject3D) {
console.error('Invalid object, cannot add to scene.', imported)
return imported
return imported as T&IObject3D
}
this._addObject3D(<IObject3D>imported, options)
this.dispatchEvent({type: 'addSceneObject', object: <IObject3D>imported})
return imported
return imported as T&IObject3D
}

/**

+ 1
- 1
src/core/object/iCameraCommons.ts Wyświetl plik

@@ -16,7 +16,7 @@ export const iCameraCommons = {
}
this.dispatchEvent({...options, type: 'update'}) // does not bubble
this.dispatchEvent({...options, type: 'cameraUpdate', bubbleToParent: true}) // this sets dirty in the viewer
iObjectCommons.setDirty.call(this, options)
iObjectCommons.setDirty.call(this, {refreshScene: false, ...options})
},
activateMain: function(this: ICamera, options: Partial<ICameraEvent> = {}, _internal = false, _refresh = true): void {
if (!_internal) return this.dispatchEvent({

+ 51
- 9
src/plugins/animation/GLTFAnimationPlugin.ts Wyświetl plik

@@ -5,8 +5,7 @@ import {AnimationAction, AnimationClip, AnimationMixer, LoopOnce, LoopRepeat} fr
import {ProgressivePlugin} from '../pipeline/ProgressivePlugin'
import {IObject3D} from '../../core'
import {generateUUID} from '../../three'

type FrameFadePlugin = any // todo
import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin'

/**
* Manages playback of GLTF animations.
@@ -68,9 +67,9 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
@uiSlider('Speed', [0.1, 4], 0.1) @serialize() animationSpeed = 1

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

@@ -79,6 +78,18 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
*/
@uiSlider('Scroll Damping', [0, 1]) @serialize() scrollAnimationDamping = 0.1

/**
* Automatically track scroll event in window and use `window.scrollY` along with {@link pageScrollHeight} to seek animations
* Control damping/smoothness with {@link pageScrollAnimationDamping}
* See also {@link animateOnDrag}, {@link animateOnScroll}
*/
@uiToggle() @serialize() animateOnPageScroll = false

/**
* Damping for the scroll animation, when {@link animateOnPage Scroll} is true.
*/
@uiSlider('Page Scroll Damping', [0, 1]) @serialize() pageScrollAnimationDamping = 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}
@@ -142,6 +153,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
private _animationTime = 0
private _animationDuration = 0
private _scrollAnimationState = 0
private _pageScrollAnimationState = 0
private _dragAnimationState = 0
private _pointerDragHelper = new PointerDragHelper()
private _lastFrameTime = 0
@@ -159,6 +171,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
this._onPropertyChange = this._onPropertyChange.bind(this)
this._postFrame = this._postFrame.bind(this)
this._wheel = this._wheel.bind(this)
this._scroll = this._scroll.bind(this)
this._pointerDragHelper.addEventListener('drag', this._drag.bind(this))
}

@@ -171,6 +184,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
viewer.scene.addEventListener('addSceneObject', this._objectAdded)
viewer.addEventListener('postFrame', this._postFrame)
window.addEventListener('wheel', this._wheel)
window.addEventListener('scroll', this._scroll)
this._pointerDragHelper.element = viewer.canvas
return super.onAdded(viewer)
}
@@ -180,6 +194,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
viewer.scene.removeEventListener('addSceneObject', this._objectAdded)
viewer.removeEventListener('postFrame', this._postFrame)
window.removeEventListener('wheel', this._wheel)
window.removeEventListener('scroll', this._scroll)
this._pointerDragHelper.element = undefined
return super.onRemove(viewer)
}
@@ -356,9 +371,10 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
if (!this._viewer) return

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

if (!this.enabled || this.animations.length < 1 || this._animationState !== 'playing' && !scrollAnimate && !dragAnimate) {
if (!this.enabled || this.animations.length < 1 || this._animationState !== 'playing' && !scrollAnimate && !dragAnimate && !pageScrollAnimate) {
this._lastFrameTime = 0
// console.log('not anim')
if (this._fadeDisabled) {
@@ -380,7 +396,8 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec

this._lastFrameTime = time

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

@@ -413,6 +430,10 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
// if (this._animationTime > this._animationDuration) this._animationTime -= this._animationDuration
// if (this._animationTime < 0) this._animationTime += this._animationDuration

this._pageScrollAnimationState = this.pageScrollTime - this._animationTime
if (Math.abs(this._pageScrollAnimationState) < 0.001) this._pageScrollAnimationState = 0
else this._pageScrollAnimationState *= 1.0 - this.pageScrollAnimationDamping

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

@@ -420,12 +441,13 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
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

// todo: this is now checked preFrame in ThreeViewer.ts
// if (this._viewer.scene.mainCamera.userData.isAnimating) { // if camera is animating
this._viewer.scene.mainCamera.setDirty()
// 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()

@@ -484,6 +506,16 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
}
}

get pageScrollTime() {
const scrollMax = this.pageScrollHeight()
const time = window.scrollY / scrollMax * (this.animationDuration - 0.05)
return time
}

private _scroll() {
if (!this.enabled) return
this._pageScrollAnimationState = this.pageScrollTime - this.animationTime
}

private _wheel({deltaY}: any | WheelEvent) {
if (!this.enabled) return
@@ -498,4 +530,14 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec
ev.delta.y * this._viewer.canvas.height / 4
}


pageScrollHeight = () => Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.documentElement.clientHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight
) - window.innerHeight


}

+ 1
- 0
src/plugins/index.ts Wyświetl plik

@@ -5,6 +5,7 @@ export {PipelinePassPlugin} from './base/PipelinePassPlugin'
export {ProgressivePlugin} from './pipeline/ProgressivePlugin'
export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin'
export {NormalBufferPlugin} from './pipeline/NormalBufferPlugin'
export {FrameFadePlugin, type FrameFadePluginEventTypes} from './pipeline/FrameFadePlugin'
export type {ProgressivePluginEventTypes, ProgressivePluginTarget} from './pipeline/ProgressivePlugin'
export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin'
export type {NormalBufferPluginEventTypes, NormalBufferPluginPass, NormalBufferPluginTarget} from './pipeline/NormalBufferPlugin'

+ 231
- 0
src/plugins/pipeline/FrameFadePlugin.ts Wyświetl plik

@@ -0,0 +1,231 @@
import {LinearFilter, WebGLRenderTarget} from 'three'
import {IPassID, IPipelinePass} from '../../postprocessing'
import {ThreeViewer} from '../../viewer'
import {PipelinePassPlugin} from '../base/PipelinePassPlugin'
import {uiFolderContainer, uiToggle} from 'uiconfig.js'
import {ITexture, IWebGLRenderer} from '../../core'
import {AddBlendTexturePass} from '../../postprocessing/AddBlendTexturePass'
import {now, serialize, timeout, ValOrFunc} from 'ts-browser-helpers'
import {ProgressivePlugin} from './ProgressivePlugin'
import {IRenderTarget} from '../../rendering'

export type FrameFadePluginEventTypes = ''

/**
* FrameFade Plugin
*
* Adds a post-render pass to smoothly fade to a new rendered frame over time.
* This is useful for example when changing the camera position, material, object properties, etc to avoid a sudden jump.
* @category Plugins
*/
@uiFolderContainer('FrameFade Plugin')
export class FrameFadePlugin
extends PipelinePassPlugin<FrameFadeBlendPass, 'frameFade', FrameFadePluginEventTypes> {

readonly passId = 'frameFade'
public static readonly PluginType = 'FrameFadePlugin'

dependencies = [ProgressivePlugin]

@serialize() @uiToggle() fadeOnActiveCameraChange = true
@serialize() @uiToggle() fadeOnMaterialUpdate = true
@serialize() @uiToggle() fadeOnSceneUpdate = true

protected _pointerEnabled = true
protected _target?: IRenderTarget

constructor(
enabled = true,
) {
super()
this.enabled = enabled
this.startTransition = this.startTransition.bind(this)
this.stopTransition = this.stopTransition.bind(this)
this._fadeCam = this._fadeCam.bind(this)
this._fadeMat = this._fadeMat.bind(this)
}

public async startTransition(duration: number) { // duration in ms
if (!this._viewer || !this._pass || this.isDisabled()) return
if (!this._target)
this._target = this._viewer.renderManager.getTempTarget({
sizeMultiplier: 1.,
minFilter: LinearFilter,
magFilter: LinearFilter,
colorSpace: (this._viewer.renderManager.composerTarget.texture as ITexture).colorSpace,
})
this._pass.fadeTimeState = Math.max(duration, this._pass.fadeTimeState)
this._pass.fadeTime = this._pass.fadeTimeState
this._pass.toSaveFrame = true
// this._pass.passObject.enabled = true
this.setDirty()
await timeout(duration)
}
public stopTransition() {
if (!this._pass) return
this._pass.fadeTimeState = 0. // will be stopped in update on next frame
}

onAdded(viewer: ThreeViewer) {
super.onAdded(viewer)
viewer.scene.addEventListener('mainCameraUpdate', this.stopTransition)
viewer.scene.addEventListener('mainCameraChange', this._fadeCam)
viewer.scene.addEventListener('materialUpdate', this._fadeMat)
viewer.scene.addEventListener('sceneUpdate', this._fadeScene)
viewer.scene.addEventListener('objectUpdate', this._fadeObjectUpdate)
window.addEventListener('pointermove', this._onPointerMove)
}

onRemove(viewer: ThreeViewer) {
viewer.scene.removeEventListener('mainCameraUpdate', this.stopTransition)
viewer.scene.removeEventListener('mainCameraChange', this._fadeCam)
viewer.scene.removeEventListener('materialUpdate', this._fadeMat)
viewer.scene.removeEventListener('sceneUpdate', this._fadeScene)
viewer.scene.removeEventListener('objectUpdate', this._fadeObjectUpdate)
window.removeEventListener('pointermove', this._onPointerMove)
super.onRemove(viewer)
}


private _fadeCam = async(ev: any)=>
ev.frameFade !== false && this.fadeOnActiveCameraChange && this.startTransition(ev.fadeDuration || 1000)
private _fadeMat = async(ev: any)=>
ev.frameFade !== false && this.fadeOnMaterialUpdate && this.startTransition(ev.fadeDuration || 200)
private _fadeScene = async(ev: any)=>
ev.frameFade !== false && this.fadeOnSceneUpdate && this.startTransition(ev.fadeDuration || 500)
private _fadeObjectUpdate = async(ev: any)=>
ev.frameFade && this.startTransition(ev.fadeDuration || 500)

private _onPointerMove = (ev: PointerEvent)=> {
const canvas = this._viewer?.canvas
if (!canvas) {
this._pointerEnabled = false
return
}

// no button is pressed
if (!ev.buttons || ev.target !== canvas) {
this._pointerEnabled = true
return
}

// check if pointer is over canvas
const rect = canvas.getBoundingClientRect()
const x = (ev.clientX - rect.left) / rect.width
const y = (ev.clientY - rect.top) / rect.height
this._pointerEnabled = x < 0 || x > 1 || y < 0 || y > 1

}


private _disabledBy: string[] = []

disable(name: string) {
if (!this._disabledBy.includes(name)) {
this._disabledBy.push(name)
}
}
enable(name: string) {
const i = this._disabledBy.indexOf(name)
if (i >= 0) {
this._disabledBy.splice(i, 1)
}
}
isDisabled() {
return !this._pointerEnabled || this._disabledBy.length > 0 || !this.enabled
}

setDirty() {
if (!this.enabled) return
this._viewer?.setDirty()
}

get dirty() {
return this.enabled && !!this._pass && this._pass.fadeTimeState > 0
}

set dirty(_: boolean) {
console.error('FrameFadePlugin.dirty is readonly')
}

protected _createPass() {
return new FrameFadeBlendPass(this.passId, this)
}

get canFrameFade() {
return this._target && this._pointerEnabled && this.enabled && this.dirty && this._pass && this._pass.fadeTimeState > 0.001
}

get lastFrame() {
return this._viewer?.getPlugin(ProgressivePlugin)?.texture
}

get target() {
return this._target
}

protected _beforeRender(): boolean {
if (!super._beforeRender() || !this._pass) return false

if (this.isDisabled()) this.stopTransition()

if (this._pass.fadeTimeState < 0.001) {
this._pass.toSaveFrame = false
if (this._target && this._viewer) {
this._viewer.renderManager.releaseTempTarget(this._target)
this._target = undefined
}
}
return true
}

}

class FrameFadeBlendPass extends AddBlendTexturePass implements IPipelinePass {
before = ['progressive', 'taa']
after = ['render']
required = ['render', 'progressive']
dirty: ValOrFunc<boolean> = () => false


fadeTime = 0 // ms
fadeTimeState = 0
toSaveFrame = false

private _lastTime = 0


constructor(public readonly passId: IPassID, public plugin: FrameFadePlugin) {
super()
}
render(renderer: IWebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget, deltaTime: number, maskActive: boolean) {
this.needsSwap = false
const target = this.plugin.target
if (!this.plugin.canFrameFade || !target) return

const lastFrame = this.plugin.lastFrame
if (this.toSaveFrame && lastFrame) {
renderer.renderManager.blit(target, {source: lastFrame, respectColorSpace: false})
this._lastTime = 0
this.toSaveFrame = false
}

this.uniforms.tDiffuse2.value = target.texture

const weight = this.fadeTimeState / this.fadeTime
this.uniforms.weight2.value.setScalar(weight)
this.uniforms.weight2.value.w = 1
this.uniforms.weight.value.setScalar(1. - weight)
this.uniforms.weight.value.w = 1
super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
this.needsSwap = true

const time = now()
if (this._lastTime < 10) this._lastTime = time - 10 // ms
const dt = time - this._lastTime
this._lastTime = time

this.fadeTimeState -= dt
}

}

Ładowanie…
Anuluj
Zapisz