| @@ -40,7 +40,7 @@ To make changes and run the example, click on the CodePen button on the top righ | |||
| ## Table of Contents | |||
| - [ThreePipe](#threepipe) | |||
| - [Examples](#examples) | |||
| - [Examples](https://threepipe.org/examples/) | |||
| - [Table of Contents](#table-of-contents) | |||
| - [Getting Started](#getting-started) | |||
| - [HTML/JS Quickstart (CDN)](#htmljs-quickstart-cdn) | |||
| @@ -101,7 +101,12 @@ To make changes and run the example, click on the CodePen button on the top righ | |||
| - [VignettePlugin](#vignetteplugin) - Add Vignette effect by patching the final screen pass | |||
| - [ChromaticAberrationPlugin](#chromaticaberrationplugin) - Add Chromatic Aberration effect by patching the final screen pass | |||
| - [FilmicGrainPlugin](#filmicgrainplugin) - Add Filmic Grain effect by patching the final screen pass | |||
| - [NoiseBumpMaterialPlugin](#noisebumpmaterialplugin) - Sparkle Bump/Noise Bump material extension for PhysicalMaterial | |||
| - [CustomBumpMapPlugin](#custombumpmapplugin) - Custom Bump Map material extension for PhysicalMaterial | |||
| - [ClearcoatTintPlugin](#clearcoattintplugin) - Clearcoat Tint material extension for PhysicalMaterial | |||
| - [FragmentClippingExtensionPlugin](#fragmentclippingextensionplugin) - Fragment/SDF Clipping material extension for PhysicalMaterial | |||
| - [HDRiGroundPlugin](#hdrigroundplugin) - Add support for ground projected hdri/skybox to the webgl background shader. | |||
| - [VirtualCamerasPlugin](#virtualcamerasplugin) - Add support for rendering virtual cameras before the main one every frame. | |||
| - [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files | |||
| - [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files | |||
| - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | |||
| @@ -2232,9 +2237,10 @@ const anim1 = popmotion.animate({ | |||
| }, | |||
| onComplete: () => isMovedUp = true, | |||
| onStop: () => throw(new Error('Animation stopped')), | |||
| onEnd: () => console.log('Animation ended'), // This runs after both onComplete and onStop | |||
| }) | |||
| // await for animation. This promise will reject only if an exception is thrown in onStop | |||
| // await for animation. This promise will reject only if an exception is thrown in onStop or onComplete. onStop rejects if throwOnStop is true | |||
| await anim.promise.catch((e)=>{ | |||
| console.log(e, 'animation stopped before completion') | |||
| }); | |||
| @@ -2481,15 +2487,163 @@ filmicGrainPlugin.intensity = 10 | |||
| filmicGrainPlugin.multiply = false | |||
| ``` | |||
| ## NoiseBumpMaterialPlugin | |||
| [//]: # (todo: image) | |||
| Example: https://threepipe.org/examples/#noise-bump-material-plugin/ | |||
| Source Code: [src/plugins/material/NoiseBumpMaterialPlugin.ts](./src/plugins/material/NoiseBumpMaterialPlugin.ts) | |||
| API Reference: [NoiseBumpMaterialPlugin](https://threepipe.org/docs/classes/NoiseBumpMaterialPlugin.html) | |||
| NoiseBumpMaterialPlugin adds a material extension to PhysicalMaterial to add support for sparkle bump / noise bump by creating procedural bump map from noise to simulate sparkle flakes. | |||
| It uses voronoise function from blender along with several additions to generate the noise for the generation. | |||
| It also adds a UI to the material to edit the settings. | |||
| It uses `WEBGI_materials_noise_bump` glTF extension to save the settings in glTF/glb files. | |||
| ```typescript | |||
| import {ThreeViewer, NoiseBumpMaterialPlugin} from 'threepipe' | |||
| const viewer = new ThreeViewer({...}) | |||
| const noiseBump = viewer.addPluginSync(NoiseBumpMaterialPlugin) | |||
| // Add noise bump to a material | |||
| NoiseBumpMaterialPlugin.AddNoiseBumpMaterial(material, { | |||
| flakeScale: 300, | |||
| }) | |||
| // Change properties with code or use the UI | |||
| material.userData._noiseBumpMat!.bumpNoiseParams = [1, 1] | |||
| material.setDirty() | |||
| // Disable | |||
| material.userData._noiseBumpMat!.hasBump = false | |||
| material.setDirty() | |||
| ``` | |||
| ## CustomBumpMapPlugin | |||
| [//]: # (todo: image) | |||
| Example: https://threepipe.org/examples/#custom-bump-map-plugin/ | |||
| Source Code: [src/plugins/material/CustomBumpMapPlugin.ts](./src/plugins/material/CustomBumpMapPlugin.ts) | |||
| API Reference: [CustomBumpMapPlugin](https://threepipe.org/docs/classes/CustomBumpMapPlugin.html) | |||
| CustomBumpMapPlugin adds a material extension to PhysicalMaterial to support custom bump maps. | |||
| A Custom bump map is similar to the built-in bump map, but allows using an extra bump map and scale to give a combined effect. | |||
| This plugin also has support for bicubic filtering of the custom bump map and is enabled by default. | |||
| It also adds a UI to the material to edit the settings. | |||
| It uses `WEBGI_materials_custom_bump_map` glTF extension to save the settings in glTF/glb files. | |||
| ```typescript | |||
| import {ThreeViewer, CustomBumpMapPlugin} from 'threepipe' | |||
| const viewer = new ThreeViewer({...}) | |||
| const customBump = viewer.addPluginSync(CustomBumpMapPlugin) | |||
| // Add noise bump to a material | |||
| customBump.enableCustomBump(material, bumpMap, 0.2) | |||
| // Change properties with code or use the UI | |||
| material.userData._customBumpMat = texture | |||
| material.setDirty() | |||
| // Disable | |||
| material.userData._hasCustomBump = false | |||
| // or | |||
| material.userData._customBumpMat = null | |||
| material.setDirty() | |||
| ``` | |||
| ## ClearcoatTintPlugin | |||
| [//]: # (todo: image) | |||
| Example: https://threepipe.org/examples/#clearcoat-tint-plugin/ | |||
| Source Code: [src/plugins/material/ClearcoatTintPlugin.ts](./src/plugins/material/ClearcoatTintPlugin.ts) | |||
| API Reference: [ClearcoatTintPlugin](https://threepipe.org/docs/classes/ClearcoatTintPlugin.html) | |||
| ClearcoatTintPlugin adds a material extension to PhysicalMaterial which adds tint and thickness to the built-in clearcoat properties. | |||
| It also adds a UI to the material to edit the settings. | |||
| It uses `WEBGI_materials_clearcoat_tint` glTF extension to save the settings in glTF/glb files. | |||
| ```typescript | |||
| import {ThreeViewer, ClearcoatTintPlugin} from 'threepipe' | |||
| const viewer = new ThreeViewer({...}) | |||
| const clearcoatTint = viewer.addPluginSync(ClearcoatTintPlugin) | |||
| material.clearcoat = 1 | |||
| // add initial properties | |||
| ClearcoatTintPlugin.AddClearcoatTint(material, { | |||
| tintColor: '#ff0000', | |||
| thickness: 1, | |||
| }) | |||
| // Change properties with code or use the UI | |||
| material.userData._clearcoatTint!.tintColor = '#ff0000' | |||
| material.setDirty() | |||
| // Disable | |||
| material.userData._clearcoatTint.enableTint = false | |||
| material.setDirty() | |||
| ``` | |||
| ## FragmentClippingExtensionPlugin | |||
| [//]: # (todo: image) | |||
| Example: https://threepipe.org/examples/#fragment-clipping-extension-plugin/ | |||
| Source Code: [src/plugins/materials/FragmentClippingExtensionPlugin.ts](./src/plugins/material/FragmentClippingExtensionPlugin.ts) | |||
| API Reference: [FragmentClippingExtensionPlugin](https://threepipe.org/docs/classes/FragmentClippingExtensionPlugin.html) | |||
| FragmentClippingExtensionPlugin adds a material extension to PhysicalMaterial to add support for fragment clipping. | |||
| Fragment clipping allows to clip fragments of the material in screen space or world space based on a circle, rectangle, plane, sphere, etc. | |||
| It uses fixed SDFs with params defined by the user for clipping. | |||
| It also adds a UI to the material to edit the settings. | |||
| It uses `WEBGI_materials_fragment_clipping_extension` glTF extension to save the settings in glTF/glb files. | |||
| ```typescript | |||
| import {ThreeViewer, FragmentClippingExtensionPlugin} from 'threepipe' | |||
| const viewer = new ThreeViewer({...}) | |||
| const fragmentClipping = viewer.addPluginSync(FragmentClippingExtensionPlugin) | |||
| // add initial properties | |||
| FragmentClippingExtensionPlugin.AddFragmentClipping(material, { | |||
| clipPosition: new Vector4(0.5, 0.5, 0, 0), | |||
| clipParams: new Vector4(0.1, 0.05, 0, 1), | |||
| }) | |||
| // Change properties with code or use the UI | |||
| material.userData._fragmentClipping!.clipPosition.set(0, 0, 0, 0) | |||
| material.setDirty() | |||
| // Disable | |||
| material.userData._clearcoatTint.clipEnabled = false | |||
| material.setDirty() | |||
| ``` | |||
| ## HDRiGroundPlugin | |||
| [//]: # (todo: image) | |||
| Example: https://threepipe.org/examples/#hdri-ground-plugin/ | |||
| Source Code: [src/plugins/pipeline/HDRiGroundPlugin.ts](./src/plugins/extras/HDRiGroundPlugin.ts) | |||
| Source Code: [src/plugins/extras/HDRiGroundPlugin.ts](./src/plugins/extras/HDRiGroundPlugin.ts) | |||
| API Reference: [FrameFadePlugin](https://threepipe.org/docs/classes/HDRiGroundPlugin.html) | |||
| API Reference: [HDRiGroundPlugin](https://threepipe.org/docs/classes/HDRiGroundPlugin.html) | |||
| HDRiGroundPlugin patches the background shader in the renderer to add support for ground projected environment map/skybox. Works simply by setting the background same as the environemnt and enabling the plugin. | |||
| @@ -2499,7 +2653,7 @@ The plugin is disabled by default when added. Set `.enabled` to enable it or pas | |||
| If the background is not the same as the environment when enabled, the user will be prompted for this, unless `promptOnBackgroundMismatch` is set to `false` in the plugin. | |||
| ```typescript | |||
| import {ThreeViewer, FrameFadePlugin} from 'threepipe' | |||
| import {ThreeViewer, HDRiGrounPlugin} from 'threepipe' | |||
| const viewer = new ThreeViewer({...}) | |||
| @@ -2518,6 +2672,43 @@ hdriGround.enabled = true | |||
| Check the [example](https://threepipe.org/examples/#hdri-ground-plugin/) for a demo. | |||
| ## VirtualCamerasPlugin | |||
| [//]: # (todo: image) | |||
| Example: https://threepipe.org/examples/#virtual-cameras-plugin/ | |||
| Source Code: [src/plugins/rendering/VirtualCamerasPlugin.ts](./src/plugins/rendering/VirtualCamerasPlugin.ts) | |||
| API Reference: [VirtualCamerasPlugin](https://threepipe.org/docs/classes/VirtualCamerasPlugin.html) | |||
| VirtualCamerasPlugin adds support for rendering to multiple virtual cameras in the viewer. These cameras are rendered in preRender callback just before the main camera is rendered. The virtual cameras can be added to the plugin and removed from it. | |||
| The feed to the virtual camera is rendered to a Render Target texture which can be accessed and re-rendered in the scene or used in other plugins. | |||
| ```typescript | |||
| import {ThreeViewer, VirtualCamerasPlugin} from 'threepipe' | |||
| const viewer = new ThreeViewer({...}) | |||
| const hdriGround = viewer.addPluginSync(new VirtualCamerasPlugin()) | |||
| const camera = new PerspectiveCamera2('orbit', viewer.canvas, false, 45, 1) | |||
| camera.name = name | |||
| camera.position.set(0, 5, 0) | |||
| camera.target.set(0, 0.25, 0) | |||
| camera.userData.autoLookAtTarget = true // automatically look at the target (in setDirty) | |||
| camera.setDirty() | |||
| camera.addEventListener('update', ()=>{ | |||
| viewer.setDirty() // if the camera is not added to the scene it wont update automatically when camera.setDirty is called(like from the UI) | |||
| }) | |||
| const vCam = virtualCameras.addCamera(camera) | |||
| console.log(vCam.target) // target is a WebGLRenderTarget/IRenderTarget | |||
| ``` | |||
| Check the [virtual camera](https://threepipe.org/examples/#hdri-ground-plugin/) example for using the texture in the scene. | |||
| ## Rhino3dmLoadPlugin | |||
| Example: https://threepipe.org/examples/#rhino3dm-load/ | |||
| @@ -231,6 +231,8 @@ | |||
| <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> | |||
| <li><a href="./virtual-cameras-plugin/">Virtual Cameras Plugin </a></li> | |||
| <li><a href="./virtual-camera/">Virtual Camera (Animated) </a></li> | |||
| </ul> | |||
| <h2 class="category">Interaction</h2> | |||
| <ul> | |||
| @@ -279,6 +281,13 @@ | |||
| <li><a href="./gltf-camera-animation/">glTF Camera Animation </a></li> | |||
| <li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li> | |||
| </ul> | |||
| <h2 class="category">Materials</h2> | |||
| <ul> | |||
| <li><a href="./clearcoat-tint-plugin/">Clearcoat Tint Plugin</a></li> | |||
| <li><a href="./fragment-clipping-extension-plugin/">Fragment Clipping Extension Plugin </a></li> | |||
| <li><a href="./noise-bump-material-plugin/">SparkleBump(NoiseBump) Material Plugin </a></li> | |||
| <li><a href="./custom-bump-map-plugin/">Custom Bump Map Plugin </a></li> | |||
| </ul> | |||
| <h2 class="category">Utils</h2> | |||
| <ul> | |||
| <li><a href="./hdri-ground-plugin/">HDRi Ground Plugin <br/>(Projected Skybox)</a></li> | |||
| @@ -2,9 +2,12 @@ import { | |||
| _testFinish, | |||
| CameraViewPlugin, | |||
| ChromaticAberrationPlugin, | |||
| ClearcoatTintPlugin, | |||
| CustomBumpMapPlugin, | |||
| DepthBufferPlugin, | |||
| DropzonePlugin, | |||
| FilmicGrainPlugin, | |||
| FragmentClippingExtensionPlugin, | |||
| FrameFadePlugin, | |||
| FullScreenPlugin, | |||
| GLTFAnimationPlugin, | |||
| @@ -13,6 +16,7 @@ import { | |||
| HemisphereLight, | |||
| KTX2LoadPlugin, | |||
| KTXLoadPlugin, | |||
| NoiseBumpMaterialPlugin, | |||
| NormalBufferPlugin, | |||
| PickingPlugin, | |||
| PLYLoadPlugin, | |||
| @@ -26,9 +30,10 @@ import { | |||
| USDZLoadPlugin, | |||
| ViewerUiConfigPlugin, | |||
| VignettePlugin, | |||
| VirtualCamerasPlugin, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| import {TweakpaneEditorPlugin} from '@threepipe/plugin-tweakpane-editor' | |||
| import {HierarchyUiPlugin, TweakpaneEditorPlugin} from '@threepipe/plugin-tweakpane-editor' | |||
| import {BlendLoadPlugin} from '@threepipe/plugin-blend-importer' | |||
| import {extraImportPlugins} from '@threepipe/plugin-extra-importers' | |||
| @@ -56,6 +61,11 @@ async function init() { | |||
| PickingPlugin, | |||
| CameraViewPlugin, | |||
| ViewerUiConfigPlugin, | |||
| ClearcoatTintPlugin, | |||
| FragmentClippingExtensionPlugin, | |||
| NoiseBumpMaterialPlugin, | |||
| CustomBumpMapPlugin, | |||
| VirtualCamerasPlugin, | |||
| // new SceneUiConfigPlugin(), // this is already in ViewerUiPlugin | |||
| new DepthBufferPlugin(HalfFloatType, true, true), | |||
| new NormalBufferPlugin(HalfFloatType, false), | |||
| @@ -72,6 +82,7 @@ async function init() { | |||
| STLLoadPlugin, | |||
| USDZLoadPlugin, | |||
| BlendLoadPlugin, | |||
| HierarchyUiPlugin, | |||
| ...extraImportPlugins, | |||
| ]) | |||
| @@ -81,11 +92,11 @@ async function init() { | |||
| editor.loadPlugins({ | |||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | |||
| ['Interaction']: [PickingPlugin], | |||
| ['Interaction']: [HierarchyUiPlugin, PickingPlugin], | |||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | |||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin], | |||
| ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | |||
| ['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin], | |||
| ['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin], | |||
| ['Debug']: [RenderTargetPreviewPlugin], | |||
| }) | |||
| @@ -0,0 +1,36 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Virtual Camera</title> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <!-- 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,69 @@ | |||
| import { | |||
| _testFinish, | |||
| IObject3D, | |||
| Mesh, | |||
| PerspectiveCamera2, | |||
| PhysicalMaterial, | |||
| PlaneGeometry, | |||
| PopmotionPlugin, | |||
| ProgressivePlugin, | |||
| Texture, | |||
| ThreeViewer, | |||
| VirtualCamerasPlugin, | |||
| } from 'threepipe' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| debug: true, | |||
| plugins: [new ProgressivePlugin(16)], | |||
| }) | |||
| const virtualCameras = viewer.addPluginSync(VirtualCamerasPlugin) | |||
| const popmotion = viewer.addPluginSync(PopmotionPlugin) | |||
| 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', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| const ground = new Mesh( | |||
| new PlaneGeometry(5, 5) | |||
| .translate(0, 0, -4), | |||
| new PhysicalMaterial({ | |||
| color: '#ffffff', | |||
| }) | |||
| ) | |||
| ground.castShadow = false | |||
| ground.receiveShadow = true | |||
| viewer.scene.addObject(ground) | |||
| const camera = new PerspectiveCamera2('', viewer.canvas, false, 45, 1) | |||
| camera.position.set(0, 0, 5) | |||
| camera.target.set(0, 0.25, 0) | |||
| camera.userData.autoLookAtTarget = true | |||
| camera.near = 1 | |||
| camera.far = 10 | |||
| camera.setDirty() | |||
| const vCam = virtualCameras.addCamera(camera) | |||
| ground.material.map = vCam.target.texture as Texture | |||
| popmotion.animate({ | |||
| from: 0, | |||
| to: 1, | |||
| repeat: Infinity, | |||
| duration: 6000, | |||
| onUpdate: (v)=>{ | |||
| // Set camera position xz in a circle around the target | |||
| const angle = v * Math.PI * 2 + Math.PI / 2 | |||
| const radius = 5 | |||
| camera.position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius) | |||
| camera.setDirty() | |||
| viewer.setDirty() // since camera is not in the scene | |||
| }, | |||
| }) | |||
| } | |||
| init().then(_testFinish) | |||
| @@ -0,0 +1,36 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Virtual Cameras Plugin</title> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <!-- 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,69 @@ | |||
| import { | |||
| _testFinish, | |||
| HemisphereLight, | |||
| IObject3D, | |||
| PerspectiveCamera2, | |||
| ProgressivePlugin, | |||
| RenderTargetPreviewPlugin, | |||
| ThreeViewer, | |||
| Vector3, | |||
| VirtualCamerasPlugin, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| debug: true, | |||
| plugins: [new ProgressivePlugin(16)], | |||
| }) | |||
| const virtualCameras = viewer.addPluginSync(VirtualCamerasPlugin) | |||
| viewer.scene.addObject(new HemisphereLight(0xffffff, 0x444444, 10)) | |||
| await viewer.load<IObject3D>('https://threejs.org/examples/models/fbx/Samba Dancing.fbx', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| viewer.scene.mainCamera.position.set(5, 5, 5) | |||
| viewer.scene.mainCamera.target.set(0, 0.25, 0) | |||
| viewer.scene.mainCamera.setDirty() | |||
| const views = [ | |||
| [new Vector3(5, 0, 0), 'right'], | |||
| [new Vector3(0, 5, 0), 'top'], | |||
| [new Vector3(0, 0, 5), 'front'], | |||
| ] as const | |||
| const rt = viewer.addPluginSync(RenderTargetPreviewPlugin) | |||
| const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | |||
| ui.appendChild(viewer.scene.mainCamera.uiConfig) | |||
| for (const [view, name] of views) { | |||
| const camera = new PerspectiveCamera2('', viewer.canvas, false, 45, 1) | |||
| camera.name = name | |||
| camera.position.copy(view) | |||
| camera.target.set(0, 0.25, 0) | |||
| camera.userData.autoLookAtTarget = true | |||
| camera.setDirty() | |||
| camera.addEventListener('update', ()=>{ | |||
| viewer.setDirty() // since the camera is not added to the scene it wont update automatically when setDirty is called(like from the UI) | |||
| }) | |||
| viewer.scene.mainCamera.addEventListener('update', ()=>{ | |||
| camera.target.copy(viewer.scene.mainCamera.target) // sync the lookAt target of all the cameras | |||
| camera.setDirty() | |||
| }) | |||
| const vCam = virtualCameras.addCamera(camera) | |||
| rt.addTarget(()=>vCam.target, name, false, false, true) | |||
| ui.appendChild(camera.uiConfig) | |||
| } | |||
| } | |||
| init().then(_testFinish) | |||
| @@ -7,7 +7,8 @@ | |||
| }, | |||
| "dependencies": { | |||
| "threepipe": "file:./../../src/", | |||
| "@threepipe/plugin-tweakpane": "file:./../tweakpane/src/" | |||
| "@threepipe/plugin-tweakpane": "file:./../tweakpane/src/", | |||
| "treejs": "git://github.com/repalash/treejs.git#d303016bb74e75725d13e97291ac1d4727985918" | |||
| }, | |||
| "clean-package": { | |||
| "remove": [ | |||
| @@ -0,0 +1,139 @@ | |||
| import {createDiv, createStyles, css, timeout} from 'ts-browser-helpers' | |||
| import Tree from 'treejs' | |||
| import {AViewerPluginSync, IObject3D, Object3D, ThreeViewer} from 'threepipe' | |||
| import {UiObjectConfig} from 'uiconfig.js' | |||
| export class HierarchyUiPlugin extends AViewerPluginSync<''> { | |||
| enabled = true | |||
| public static readonly PluginType = 'HierarchyUiPlugin' | |||
| toJSON: any = undefined | |||
| treeView: any = undefined | |||
| hierarchyDiv = createDiv({ | |||
| innerHTML: '', | |||
| id: 'tpHierarchyContainer', | |||
| addToBody: false, | |||
| }) | |||
| constructor(enabled = true) { | |||
| super() | |||
| this.enabled = enabled | |||
| this.reset = this.reset.bind(this) | |||
| this._postFrame = this._postFrame.bind(this) | |||
| this.uiConfig.domChildren = [this.hierarchyDiv] | |||
| createStyles(css` | |||
| #tpHierarchyContainer{ | |||
| width: 100%; | |||
| height: auto; | |||
| background-color: transparent; | |||
| color: #e9e9ed; | |||
| margin-top: 0; | |||
| } | |||
| `) | |||
| } | |||
| reset(e?: any) { | |||
| if (e?.fromHierarchyPlugin) return // for infinite loop | |||
| if (!e?.hierarchyChanged) return | |||
| this._needsReset = true | |||
| } | |||
| protected async _reset() { | |||
| this._needsReset = false | |||
| while (this.hierarchyDiv.firstChild) this.hierarchyDiv.firstChild.remove() | |||
| const obj = this._viewer?.scene.modelRoot | |||
| if (!obj) return | |||
| const data = obj.children.reduce(this._buildData, []) | |||
| const visible = obj.children.reduce(this._findVisible, []) | |||
| let firstChange = false | |||
| return new Promise<void>((resolve) => { | |||
| this.treeView = new Tree(this.hierarchyDiv, { | |||
| closeDepth: 1, | |||
| data, | |||
| // values: visible, // uuids of visible nodes | |||
| loaded: function() { | |||
| this.values = visible | |||
| resolve() | |||
| }, | |||
| onChange: () => { | |||
| if (!firstChange) { // first time called when loaded | |||
| firstChange = true | |||
| return | |||
| } | |||
| timeout(200).then(() => { // wait for the UI to update | |||
| if (this.treeView) | |||
| this._setVisible(this.treeView.values) | |||
| }) | |||
| }, | |||
| onItemLabelClick: (item: any) => { | |||
| const obj1 = this._viewer?.scene.modelRoot.getObjectByProperty('uuid', item) | |||
| if (!obj1 || !obj.visible) return | |||
| obj1.dispatchEvent({type: 'select', value: obj1, ui: true}) | |||
| }, | |||
| }) | |||
| }) | |||
| } | |||
| onAdded(viewer: ThreeViewer) { | |||
| super.onAdded(viewer) | |||
| this.reset() | |||
| viewer.scene.addEventListener('sceneUpdate', this.reset) | |||
| viewer.addEventListener('postFrame', this._postFrame) | |||
| } | |||
| onRemove(viewer: ThreeViewer) { | |||
| // todo: remove UI element. | |||
| viewer.scene.removeEventListener('sceneUpdate', this.reset) | |||
| viewer.removeEventListener('postFrame', this._postFrame) | |||
| return super.onRemove(viewer) | |||
| } | |||
| protected _needsReset = false | |||
| protected _postFrame() { | |||
| if (this._needsReset) this._reset() | |||
| } | |||
| dispose() { | |||
| // todo destroy UI element. | |||
| } | |||
| uiConfig: UiObjectConfig = { | |||
| type: 'folder', | |||
| label: 'Hierarchy', | |||
| children: [], | |||
| } | |||
| private _buildData = (data: any[], obj: IObject3D): any[] => { | |||
| data.push({ | |||
| text: obj.name || 'unnamed', | |||
| id: obj.uuid, | |||
| children: obj.children.reduce(this._buildData, []), | |||
| }) | |||
| return data | |||
| } | |||
| private _findVisible = (data: any[], obj: Object3D): any[] => { // only leaf. | |||
| if (!obj.visible) return data | |||
| if (obj.children.length < 1) data.push(obj.uuid) | |||
| else data.push(...obj.children.reduce(this._findVisible, [])) | |||
| return data | |||
| } | |||
| private _setVisible = (values: string[]): void => { | |||
| this._viewer?.doOnce('postFrame', () => { | |||
| const obj = this._viewer?.scene.modelRoot | |||
| if (!obj || values === undefined || values === null) return | |||
| const ancestorSet: Set<Object3D> = new Set() | |||
| obj.traverse((o)=>{ | |||
| if (o === obj) return | |||
| o.visible = values.includes(o.uuid) | |||
| if (o.visible) o.traverseAncestors(p=>ancestorSet.add(p)) | |||
| }) | |||
| ancestorSet.forEach(o=> o.visible = true) | |||
| this._viewer?.scene?.setDirty({refreshScene: true, fromHierarchyPlugin: true, updateGround: false}) | |||
| }) | |||
| } | |||
| } | |||
| @@ -1 +1,2 @@ | |||
| export {TweakpaneEditorPlugin} from './TweakpaneEditorPlugin' | |||
| export {HierarchyUiPlugin} from './HierarchyUiPlugin' | |||
| @@ -51,7 +51,7 @@ export interface AddObjectOptions { | |||
| // | string | |||
| export type ISceneEventTypes = IObject3DEventTypes | 'sceneUpdate' | 'addSceneObject' | | |||
| 'mainCameraChange' | 'mainCameraUpdate' | 'environmentChanged' | 'backgroundChanged' | | |||
| 'mainCameraChange' | 'mainCameraUpdate' | 'environmentChanged' | 'backgroundChanged' | 'renderCameraChange' | | |||
| 'update' | // todo: deprecate, use 'sceneUpdate' instead | |||
| 'textureAdded' | // todo remove | |||
| 'activeCameraChange' | 'activeCameraUpdate' | // todo: deprecate | |||
| @@ -80,7 +80,14 @@ export interface IScene<E extends ISceneEvent = ISceneEvent, ET extends ISceneEv | |||
| extends Scene<E, ET>, IObject3D<E, ET>, IShaderPropertiesUpdater { | |||
| readonly visible: boolean; | |||
| readonly isScene: true; | |||
| /** | |||
| * Main camera that the user controls | |||
| */ | |||
| mainCamera: ICamera; | |||
| /** | |||
| * Camera that in currently being rendered. | |||
| */ | |||
| renderCamera: ICamera; | |||
| type: 'Scene'; | |||
| toJSON(): any; // todo | |||
| @@ -104,7 +104,7 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||
| constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, fov?: number, aspect?: number) { | |||
| super(fov, aspect) | |||
| this._canvas = domElement | |||
| this.autoAspect = autoAspect || !!domElement | |||
| this.autoAspect = autoAspect ?? !!domElement | |||
| iCameraCommons.upgradeCamera.call(this) // todo: test if autoUpgrade = false works as expected if we call upgradeObject3D externally after constructor, because we have setDirty, refreshTarget below. | |||
| @@ -101,6 +101,16 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| this.setDirty() | |||
| } | |||
| private _renderCamera: ICamera | undefined | |||
| get renderCamera() { | |||
| return this._renderCamera ?? this.mainCamera | |||
| } | |||
| set renderCamera(camera: ICamera) { | |||
| const cam = this._renderCamera | |||
| this._renderCamera = camera | |||
| this.dispatchEvent({type: 'renderCameraChange', lastCamera: cam, camera}) | |||
| } | |||
| /** | |||
| * Create a scene instance. This is done automatically in the {@link ThreeViewer} and must not be created separately. | |||
| * @param camera | |||
| @@ -280,6 +280,9 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| }) | |||
| this._animating = false | |||
| this._viewer?.setDirty() | |||
| this.dispatchEvent({type: 'viewChange', view}) | |||
| await timeout(10) | |||
| @@ -164,6 +164,15 @@ export class PopmotionPlugin extends AViewerPluginSync<''> { | |||
| } | |||
| this.animations[uuid] = a | |||
| a.promise = new Promise<void>((resolve, reject) => { | |||
| const end2 = ()=>{ | |||
| try { | |||
| options.onEnd && options.onEnd() | |||
| } catch (e: any) { | |||
| reject(e) | |||
| return false | |||
| } | |||
| return true | |||
| } | |||
| const opts: AnimationOptions<V> = { | |||
| driver: this.defaultDriver, | |||
| ...options, | |||
| @@ -171,15 +180,18 @@ export class PopmotionPlugin extends AViewerPluginSync<''> { | |||
| try { | |||
| options.onComplete && options.onComplete() | |||
| } catch (e: any) { | |||
| if (!end2()) return | |||
| reject(e) | |||
| return | |||
| } | |||
| if (!end2()) return | |||
| resolve() | |||
| }, | |||
| onStop: ()=>{ | |||
| try { | |||
| options.onStop && options.onStop() | |||
| } catch (e: any) { | |||
| if (!end2()) return | |||
| reject(e) | |||
| return | |||
| } | |||
| @@ -48,5 +48,8 @@ export {NoiseBumpMaterialPlugin} from './material/NoiseBumpMaterialPlugin' | |||
| export {CustomBumpMapPlugin} from './material/CustomBumpMapPlugin' | |||
| export {FragmentClippingExtensionPlugin, FragmentClippingMode} from './material/FragmentClippingExtensionPlugin' | |||
| // extras | |||
| export {VirtualCamerasPlugin} from './rendering/VirtualCamerasPlugin' | |||
| // extras | |||
| export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin' | |||
| @@ -153,7 +153,10 @@ export class FrameFadePlugin | |||
| } | |||
| get canFrameFade() { | |||
| return this._target && this._pointerEnabled && this.enabled && this.dirty && this._pass && this._pass.fadeTimeState > 0.001 | |||
| return this._target && this._pointerEnabled && | |||
| this.enabled && this.dirty && this._pass && | |||
| this._pass.fadeTimeState > 0.001 && | |||
| this._viewer && this._viewer.scene.renderCamera === this._viewer.scene.mainCamera | |||
| } | |||
| get lastFrame() { | |||
| @@ -5,7 +5,7 @@ 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' | |||
| import {getOrCall, serialize, ValOrFunc} from 'ts-browser-helpers' | |||
| export type ProgressivePluginEventTypes = '' | |||
| export type ProgressivePluginTarget = WebGLRenderTarget | |||
| @@ -24,12 +24,33 @@ export class ProgressivePlugin | |||
| readonly passId = 'progressive' | |||
| public static readonly PluginType = 'ProgressivePlugin' | |||
| target?: ProgressivePluginTarget | |||
| protected _targets = new Map<string, ProgressivePluginTarget>() | |||
| @serialize() @uiInput('Frame count') maxFrameCount: number | |||
| // todo: deserialize jitter | |||
| @uiImage('Last Texture' /* {readOnly: true}*/) texture?: Texture | |||
| // @uiImage('Last Texture' /* {readOnly: true}*/) texture?: Texture | |||
| get texture(): Texture | undefined { | |||
| return this.target?.texture | |||
| } | |||
| get target(): ProgressivePluginTarget | undefined { | |||
| return this._viewer ? this._targets.get(this._viewer.scene.renderCamera.uuid) : undefined | |||
| } | |||
| getTarget(camera?: ICamera) { | |||
| return this._viewer ? this._targets.get((camera ? camera : this._viewer.scene.renderCamera).uuid) : undefined | |||
| } | |||
| get textures() { | |||
| return this._viewer ? Array.from(this._targets.values()).map(t => t.texture) : [] | |||
| } | |||
| @uiImage('Last Texture' /* {readOnly: true}*/) | |||
| get mainTexture() { | |||
| return this._viewer ? this.getTarget(this._viewer.scene.mainCamera)?.texture : undefined | |||
| } | |||
| // @onChange2(ProgressivePlugin.prototype._createTarget) | |||
| // @uiDropdown('Buffer Type', threeConstMappings.TextureDataType.uiConfig) | |||
| @@ -46,30 +67,36 @@ export class ProgressivePlugin | |||
| this.bufferType = bufferType | |||
| } | |||
| protected _createTarget(recreate = true) { | |||
| protected _createTarget(camera?: ICamera, recreate = false) { | |||
| 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 | |||
| camera = camera ?? this._viewer.scene.renderCamera | |||
| if (recreate) this._disposeTarget(camera) | |||
| if (this._targets.has(camera.uuid)) return this._targets.get(camera.uuid) | |||
| const target = this._viewer.renderManager.composerTarget.clone(true) as WebGLRenderTarget | |||
| target.texture.name = 'progressiveLastBuffer_' + camera.uuid | |||
| // target.texture.type = this.bufferType | |||
| this._targets.set(camera.uuid, target) | |||
| // if (this._pass) this._pass.target = this.target | |||
| return target | |||
| } | |||
| protected _disposeTarget() { | |||
| protected _disposeTarget(camera?: ICamera) { | |||
| if (!this._viewer) return | |||
| if (this.target) { | |||
| this._viewer.renderManager.disposeTarget(this.target) | |||
| this.target = undefined | |||
| if (!camera) { | |||
| this._targets.forEach((t) => this._viewer!.renderManager.disposeTarget(t)) | |||
| this._targets.clear() | |||
| } else { | |||
| const t = this._targets.get(camera.uuid) | |||
| if (t) { | |||
| this._viewer!.renderManager.disposeTarget(t) | |||
| this._targets.delete(camera.uuid) | |||
| } | |||
| } | |||
| 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) | |||
| // this._createTarget(true) | |||
| const pass = new ProgressiveBlendPass(this.passId, ()=>this.target ?? this._createTarget()) // todo: disposeTarget somewhere | |||
| pass.dirty = () => (this._viewer?.renderManager.frameCount || 0) < this.maxFrameCount // todo use isConverged function | |||
| return pass | |||
| } | |||
| @@ -109,14 +136,20 @@ class ProgressiveBlendPass extends AddBlendTexturePass implements IPipelinePass | |||
| after = ['render'] | |||
| required = ['render'] | |||
| dirty: ValOrFunc<boolean> = () => false | |||
| constructor(public readonly passId: IPassID, public target: WebGLRenderTarget) { | |||
| constructor(public readonly passId: IPassID, public target?: ValOrFunc<WebGLRenderTarget|undefined>) { | |||
| super() | |||
| } | |||
| render(renderer: IWebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget, deltaTime: number, maskActive: boolean) { | |||
| if (!this.enabled) return | |||
| const target = getOrCall(this.target) | |||
| if (!target) { | |||
| console.warn('ProgressiveBlendPass: target not defined') | |||
| return | |||
| } | |||
| if (renderer.renderManager.frameCount < 1) { | |||
| this.needsSwap = false | |||
| if (readBuffer?.texture) | |||
| renderer.renderManager.blit(this.target, { | |||
| renderer.renderManager.blit(target, { | |||
| source: readBuffer.texture, | |||
| respectColorSpace: false, | |||
| }) | |||
| @@ -124,7 +157,7 @@ class ProgressiveBlendPass extends AddBlendTexturePass implements IPipelinePass | |||
| } | |||
| this.needsSwap = true | |||
| super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) | |||
| renderer.renderManager.blit(this.target, { | |||
| renderer.renderManager.blit(target, { | |||
| source: writeBuffer.texture, | |||
| respectColorSpace: false, | |||
| }) | |||
| @@ -140,7 +173,7 @@ class ProgressiveBlendPass extends AddBlendTexturePass implements IPipelinePass | |||
| 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.uniforms.tDiffuse2.value = getOrCall(this.target)?.texture | |||
| this.material.uniformsNeedUpdate = true | |||
| } | |||
| @@ -0,0 +1,58 @@ | |||
| import {AViewerPluginSync} from '../../viewer' | |||
| import {IRenderTarget} from '../../rendering' | |||
| import {ICamera} from '../../core' | |||
| import {uiFolderContainer, uiToggle} from 'uiconfig.js' | |||
| export interface VirtualCamera { | |||
| camera: ICamera | |||
| target: IRenderTarget | |||
| enabled: boolean | |||
| } | |||
| @uiFolderContainer('Virtual Cameras') | |||
| export class VirtualCamerasPlugin extends AViewerPluginSync<''> { | |||
| public static readonly PluginType = 'VirtualCamerasPlugin' | |||
| @uiToggle() | |||
| enabled = true | |||
| toJSON: any = undefined // disable serialization | |||
| constructor(enabled = true) { | |||
| super() | |||
| this.enabled = enabled | |||
| } | |||
| cameras: VirtualCamera[] = [] | |||
| protected _viewerListeners = { | |||
| preRender: () => { | |||
| if (!this.enabled || !this._viewer) return | |||
| const viewer = this._viewer | |||
| for (const v of this.cameras) { | |||
| if (!v.enabled) continue | |||
| const camera = v.camera | |||
| try { | |||
| viewer.scene.renderCamera = camera | |||
| viewer.renderManager.render(viewer.scene, false) | |||
| const source = viewer.renderManager.composer.readBuffer.texture | |||
| viewer.renderManager.blit(v.target, {source}) | |||
| } catch (e: any) { | |||
| viewer.console.error(e) | |||
| v.enabled = false | |||
| if (viewer.debug) throw e | |||
| } | |||
| } | |||
| }, | |||
| } | |||
| addCamera(camera: ICamera) { | |||
| if (!this._viewer) throw 'Plugin not added to viewer' | |||
| const target = this._viewer.renderManager.composerTarget.clone(true) | |||
| target.name = camera.name + '_virtualCamTarget' | |||
| const vCam: VirtualCamera = {camera, target, enabled: true} | |||
| this.cameras.push(vCam) | |||
| // todo: track for jitter in progressive or something else for jittering | |||
| return vCam | |||
| } | |||
| } | |||
| @@ -35,6 +35,10 @@ export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'scr | |||
| this.material.addEventListener('materialUpdate', this.setDirty) | |||
| } | |||
| /** | |||
| * Output Color Space | |||
| * Note: this is ignored when renderToScreen is false (it will take the color space of the render target) | |||
| */ | |||
| @uiDropdown('Output Color Space', threeConstMappings.ColorSpace.uiConfig, (t: ScreenPass)=>({onChange: t.setDirty})) | |||
| outputColorSpace: ColorSpace = SRGBColorSpace | |||
| @@ -43,7 +47,7 @@ export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'scr | |||
| render(renderer: IWebGLRenderer, writeBuffer?: WebGLMultipleRenderTargets | WebGLRenderTarget | null, readBuffer?: WebGLMultipleRenderTargets | WebGLRenderTarget, deltaTime?: number, maskActive?: boolean) { | |||
| const colorSpace = renderer.outputColorSpace | |||
| if (!writeBuffer || this.renderToScreen) renderer.outputColorSpace = this.outputColorSpace | |||
| else console.warn('ScreenPass: outputColorSpace is ignored when renderToScreen is false') | |||
| // else console.warn('ScreenPass: outputColorSpace is ignored when renderToScreen is false') | |||
| super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) | |||
| this._lastReadBuffer = readBuffer | |||
| renderer.outputColorSpace = colorSpace | |||
| @@ -192,17 +192,21 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen | |||
| // // todo gizmos | |||
| // } | |||
| render(scene: IScene): void { | |||
| render(scene: IScene, renderToScreen = true): void { | |||
| if (this._passesNeedsUpdate) { | |||
| this._refreshPipeline() | |||
| this.refreshPasses() | |||
| } | |||
| for (const pass of this._passes) { | |||
| if (pass.enabled && pass.beforeRender) pass.beforeRender(scene, scene.mainCamera, this) | |||
| if (pass.enabled && pass.beforeRender) pass.beforeRender(scene, scene.renderCamera, this) | |||
| } | |||
| this._composer.renderToScreen = renderToScreen | |||
| this._composer.render() | |||
| this._frameCount += 1 | |||
| this._totalFrameCount += 1 | |||
| this._composer.renderToScreen = true | |||
| if (renderToScreen) { | |||
| this._frameCount += 1 | |||
| this._totalFrameCount += 1 | |||
| } | |||
| this._dirty = false | |||
| } | |||
| @@ -22,6 +22,14 @@ import {timeout} from 'ts-browser-helpers' | |||
| import {MathUtils} from 'three' | |||
| export {animate} | |||
| declare module 'popmotion'{ | |||
| interface PlaybackOptions<V> { | |||
| // throwOnStop?: boolean; // instead of this, user can simply throw an error in onStop. | |||
| onEnd?: () => void; | |||
| } | |||
| } | |||
| export type {AnimationOptions, KeyframeOptions, Easing} | |||
| function easeInOutSine(x: number): number { | |||
| @@ -100,24 +108,38 @@ export async function animateTarget<V>(target: any, key: string, options: Animat | |||
| export async function animateAsync<V=number>(options: AnimationOptions<V>, animations?: AnimateResult[]) { | |||
| const complete = options.onComplete | |||
| const stop = options.onStop | |||
| const end = options.onEnd | |||
| options = {...options} | |||
| return new Promise<void>((resolve, reject) => { | |||
| const end2 = ()=>{ | |||
| try { | |||
| end?.() | |||
| } catch (e: any) { | |||
| reject(e) | |||
| return false | |||
| } | |||
| return true | |||
| } | |||
| options.onComplete = ()=>{ | |||
| try { | |||
| complete?.() | |||
| } catch (e: any) { | |||
| if (!end2()) return | |||
| reject(e) | |||
| return | |||
| } | |||
| if (!end2()) return | |||
| resolve() | |||
| } | |||
| options.onStop = ()=>{ | |||
| try { | |||
| stop?.() | |||
| } catch (e: any) { | |||
| if (!end2()) return | |||
| reject(e) | |||
| return | |||
| } | |||
| if (!end2()) return | |||
| resolve() | |||
| } | |||
| const an = animate(options) | |||
| @@ -1,6 +1,7 @@ | |||
| import type {ISerializedConfig, ThreeViewer} from './ThreeViewer' | |||
| import type {ISerializedConfig, IViewerEvent, ThreeViewer} from './ThreeViewer' | |||
| import {IViewerEventTypes} from './ThreeViewer' | |||
| import {Event, EventDispatcher} from 'three' | |||
| import {SerializationMetaType, ThreeSerialization} from '../utils' | |||
| import {PartialRecord, SerializationMetaType, ThreeSerialization} from '../utils' | |||
| import {IViewerPlugin, IViewerPluginAsync} from './IViewerPlugin' | |||
| import {UiObjectConfig} from 'uiconfig.js' | |||
| @@ -57,6 +58,14 @@ export abstract class AViewerPlugin<T extends string = string, TViewer extends T | |||
| else this.fromJSON?.(state) | |||
| } | |||
| protected _viewerListeners: PartialRecord<IViewerEventTypes, (e: Event)=>void> = {} | |||
| protected _onViewerEvent = (e: IViewerEvent)=> { | |||
| const et = e.eType | |||
| et && this._viewerListeners[et]?.(e) | |||
| return e | |||
| } | |||
| // todo: move to ThreeViewer | |||
| // storeState(prefix?: string, storage?: Storage, data?: any): void { | |||
| // storage = storage || (window ? window.localStorage : undefined) | |||
| @@ -114,9 +123,11 @@ export abstract class AViewerPluginSync<T extends string, TViewer extends ThreeV | |||
| onAdded(viewer: TViewer): void { | |||
| this._viewer = viewer | |||
| this._viewer.addEventListener('*', this._onViewerEvent) | |||
| } | |||
| onRemove(viewer: TViewer): void { | |||
| if (this._viewer !== viewer) viewer.console.error('Wrong viewer') | |||
| this._viewer?.removeEventListener('*', this._onViewerEvent) | |||
| this._viewer = undefined | |||
| } | |||
| @@ -131,9 +142,11 @@ export abstract class AViewerPluginAsync<T extends string, TViewer extends Three | |||
| async onAdded(viewer: TViewer): Promise<void> { | |||
| this._viewer = viewer | |||
| this._viewer.addEventListener('*', this._onViewerEvent) | |||
| } | |||
| async onRemove(viewer: TViewer): Promise<void> { | |||
| if (this._viewer !== viewer) viewer.console.error('Wrong viewer') | |||
| this._viewer?.removeEventListener('*', this._onViewerEvent) | |||
| this._viewer = undefined | |||
| } | |||
| } | |||
| @@ -61,7 +61,9 @@ import {Easing} from 'popmotion' | |||
| import {OrbitControls3} from '../three' | |||
| export type IViewerEvent = BaseEvent & { | |||
| type: 'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'renderEnabled'|'renderDisabled' | |||
| type: '*'|'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'renderEnabled'|'renderDisabled' | |||
| eType?: '*'|'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'renderEnabled'|'renderDisabled' | |||
| [p: string]: any | |||
| } | |||
| export type IViewerEventTypes = IViewerEvent['type'] | |||
| export interface ISerializedConfig { | |||
| @@ -612,14 +614,13 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| this.dispatchEvent({type: 'preRender', target: this}) | |||
| if (this.debug) this.renderManager.render(this._scene) | |||
| else { | |||
| try { | |||
| this.renderManager.render(this._scene) | |||
| } catch (e) { | |||
| this.console.error(e) | |||
| // this.enabled = false | |||
| } | |||
| try { | |||
| this._scene.renderCamera = this._scene.mainCamera | |||
| this.renderManager.render(this._scene) | |||
| } catch (e) { | |||
| this.console.error(e) | |||
| if (this.debug) throw e | |||
| // this.enabled = false | |||
| } | |||
| this.dispatchEvent({type: 'postRender', target: this}) | |||
| @@ -1049,6 +1050,11 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| }) | |||
| } | |||
| dispatchEvent(event: IViewerEvent) { | |||
| super.dispatchEvent(event) | |||
| super.dispatchEvent({...event, type: '*', eType: event.type}) | |||
| } | |||
| private _setActiveCameraView(event: any = {}): void { | |||
| if (event.type === 'setView') { | |||
| if (!event.camera) { | |||