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