| - [Background, Environment maps](#background-environment-maps) | - [Background, Environment maps](#background-environment-maps) | ||||
| - [SVG strings](#svg-strings) | - [SVG strings](#svg-strings) | ||||
| - [Plugins](#threepipe-plugins) | - [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 | - [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) | - [Packages](#threepipe-packages) | ||||
| - [@threepipe/plugin-tweakpane](#threepipeplugin-tweakpane) Tweakpane UI Plugin | - [@threepipe/plugin-tweakpane](#threepipeplugin-tweakpane) Tweakpane UI Plugin | ||||
| - [@threepipe/plugin-tweakpane-editor](#threepipeplugin-tweakpane-editor) - Tweakpane Editor Plugin | - [@threepipe/plugin-tweakpane-editor](#threepipeplugin-tweakpane-editor) - Tweakpane Editor Plugin | ||||
| const viewer = new ThreeViewer({ | const viewer = new ThreeViewer({ | ||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | ||||
| dropzone: { // this can also be set to true and configured by getting a reference to the DropzonePlugin | 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: { | addOptions: { | ||||
| disposeSceneObjects: true, // auto dispose of old scene objects | disposeSceneObjects: true, // auto dispose of old scene objects | ||||
| autoSetEnvironment: true, // when hdr is dropped | autoSetEnvironment: true, // when hdr is dropped | ||||
| }) | }) | ||||
| ``` | ``` | ||||
| ## 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 | ## DepthBufferPlugin | ||||
| todo: image | todo: image | ||||
| ``` | ``` | ||||
| ## DepthNormalBufferPlugin | |||||
| ## GBufferPlugin | |||||
| todo | 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 | ## RenderTargetPreviewPlugin | ||||
| todo: image | todo: image |
| <!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> |
| 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) |
| <!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> |
| 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) |
| </ul> | </ul> | ||||
| <h2 class="category">Rendering</h2> | <h2 class="category">Rendering</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./progressive-plugin/">Progressive Plugin </a></li> | |||||
| <li><a href="./depth-buffer-plugin/">Depth Buffer 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="./normal-buffer-plugin/">Normal Buffer Plugin </a></li> | ||||
| <li><a href="./custom-pipeline/">Custom Pipeline specification </a></li> | <li><a href="./custom-pipeline/">Custom Pipeline specification </a></li> | ||||
| <li><a href="./scene-uiconfig/">Scene UI </a></li> | <li><a href="./scene-uiconfig/">Scene UI </a></li> | ||||
| <li><a href="./viewer-uiconfig/">Viewer UI </a></li> | <li><a href="./viewer-uiconfig/">Viewer UI </a></li> | ||||
| </ul> | </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> | <h2 class="category">Utils</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./render-target-preview/">Render Target Preview Plugin </a></li> | <li><a href="./render-target-preview/">Render Target Preview Plugin </a></li> | ||||
| <ul> | <ul> | ||||
| <li><a href="./html-sample/">HTML/JS Sample </a></li> | <li><a href="./html-sample/">HTML/JS Sample </a></li> | ||||
| </ul> | </ul> | ||||
| <h2 class="category">Lights</h2> | |||||
| <ul> | |||||
| <li><a href="./directional-light/">Directional Light </a></li> | |||||
| </ul> | |||||
| <h2 class="category">Tests</h2> | <h2 class="category">Tests</h2> | ||||
| <ul> | <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="./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="./half-float-hdr-test/">Half-float HDR Test </a></li> | ||||
| <li><a href="./sphere-rgbm-test/">RGBM Test </a></li> | <li><a href="./sphere-rgbm-test/">RGBM Test </a></li> | ||||
| </ul> | </ul> | ||||
| </div> | </div> | ||||
| <div class="iframe-container"> | <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 *"> | 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> | </iframe> | ||||
| </div> | </div> |
| <!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> |
| 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) |
| DepthBufferPlugin, | DepthBufferPlugin, | ||||
| DropzonePlugin, | DropzonePlugin, | ||||
| FullScreenPlugin, | FullScreenPlugin, | ||||
| GLTFAnimationPlugin, | |||||
| HalfFloatType, | HalfFloatType, | ||||
| KTX2LoadPlugin, | KTX2LoadPlugin, | ||||
| KTXLoadPlugin, | KTXLoadPlugin, | ||||
| NormalBufferPlugin, | NormalBufferPlugin, | ||||
| PLYLoadPlugin, | PLYLoadPlugin, | ||||
| ProgressivePlugin, | |||||
| RenderTargetPreviewPlugin, | RenderTargetPreviewPlugin, | ||||
| Rhino3dmLoadPlugin, | Rhino3dmLoadPlugin, | ||||
| SceneUiConfigPlugin, | SceneUiConfigPlugin, | ||||
| }, | }, | ||||
| }) | }) | ||||
| viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| // @ts-expect-error unused | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| const editor = viewer.addPluginSync(new TweakpaneEditorPlugin()) | const editor = viewer.addPluginSync(new TweakpaneEditorPlugin()) | ||||
| await viewer.addPlugins([ | await viewer.addPlugins([ | ||||
| new ProgressivePlugin(), | |||||
| new GLTFAnimationPlugin(), | |||||
| new ViewerUiConfigPlugin(), | new ViewerUiConfigPlugin(), | ||||
| // new SceneUiConfigPlugin(), // this is already in ViewerUiPlugin | // new SceneUiConfigPlugin(), // this is already in ViewerUiPlugin | ||||
| new DepthBufferPlugin(HalfFloatType, true, true), | new DepthBufferPlugin(HalfFloatType, true, true), | ||||
| editor.loadPlugins({ | editor.loadPlugins({ | ||||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ||||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ||||
| ['Post-processing']: [TonemapPlugin], | |||||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin], | |||||
| ['Animation']: [GLTFAnimationPlugin], | |||||
| ['Debug']: [RenderTargetPreviewPlugin], | ['Debug']: [RenderTargetPreviewPlugin], | ||||
| }) | }) | ||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | 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, | // autoCenter: true, | ||||
| // autoScale: true, | // autoScale: true, | ||||
| // }) | // }) | ||||
| // const model = result?.getObjectByName('node_damagedHelmet_-6514') | |||||
| // | |||||
| // const model = result?.getObjectByName('Correction__MovingCamera') | |||||
| // const config = model?.uiConfig | // const config = model?.uiConfig | ||||
| // console.log(model, config, result) | |||||
| // if (config) ui.appendChild(config) | // if (config) ui.appendChild(config) | ||||
| } | } |
| 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 | |||||
| } | |||||
| } |
| export {PipelinePassPlugin} from './base/PipelinePassPlugin' | export {PipelinePassPlugin} from './base/PipelinePassPlugin' | ||||
| // pipeline | // pipeline | ||||
| export {ProgressivePlugin} from './pipeline/ProgressivePlugin' | |||||
| export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin' | export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin' | ||||
| export {NormalBufferPlugin} from './pipeline/NormalBufferPlugin' | export {NormalBufferPlugin} from './pipeline/NormalBufferPlugin' | ||||
| export type {ProgressivePluginEventTypes, ProgressivePluginTarget} from './pipeline/ProgressivePlugin' | |||||
| export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin' | export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin' | ||||
| export type {NormalBufferPluginEventTypes, NormalBufferPluginPass, NormalBufferPluginTarget} from './pipeline/NormalBufferPlugin' | export type {NormalBufferPluginEventTypes, NormalBufferPluginPass, NormalBufferPluginTarget} from './pipeline/NormalBufferPlugin' | ||||
| // postprocessing | // postprocessing | ||||
| export {TonemapPlugin} from './postprocessing/TonemapPlugin' | export {TonemapPlugin} from './postprocessing/TonemapPlugin' | ||||
| // animation | |||||
| export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' |
| 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 | |||||
| } | |||||
| } |