| ## Table of Contents | ## Table of Contents | ||||
| - [ThreePipe](#threepipe) | - [ThreePipe](#threepipe) | ||||
| - [Examples](#examples) | |||||
| - [Examples](https://threepipe.org/examples/) | |||||
| - [Table of Contents](#table-of-contents) | - [Table of Contents](#table-of-contents) | ||||
| - [Getting Started](#getting-started) | - [Getting Started](#getting-started) | ||||
| - [HTML/JS Quickstart (CDN)](#htmljs-quickstart-cdn) | - [HTML/JS Quickstart (CDN)](#htmljs-quickstart-cdn) | ||||
| - [VignettePlugin](#vignetteplugin) - Add Vignette effect by patching the final screen pass | - [VignettePlugin](#vignetteplugin) - Add Vignette effect by patching the final screen pass | ||||
| - [ChromaticAberrationPlugin](#chromaticaberrationplugin) - Add Chromatic Aberration 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 | - [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. | - [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 | - [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files | ||||
| - [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files | - [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files | ||||
| - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | ||||
| }, | }, | ||||
| onComplete: () => isMovedUp = true, | onComplete: () => isMovedUp = true, | ||||
| onStop: () => throw(new Error('Animation stopped')), | 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)=>{ | await anim.promise.catch((e)=>{ | ||||
| console.log(e, 'animation stopped before completion') | console.log(e, 'animation stopped before completion') | ||||
| }); | }); | ||||
| filmicGrainPlugin.multiply = false | 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 | ## HDRiGroundPlugin | ||||
| [//]: # (todo: image) | [//]: # (todo: image) | ||||
| Example: https://threepipe.org/examples/#hdri-ground-plugin/ | 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. | 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. | ||||
| 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. | 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 | ```typescript | ||||
| import {ThreeViewer, FrameFadePlugin} from 'threepipe' | |||||
| import {ThreeViewer, HDRiGrounPlugin} from 'threepipe' | |||||
| const viewer = new ThreeViewer({...}) | const viewer = new ThreeViewer({...}) | ||||
| Check the [example](https://threepipe.org/examples/#hdri-ground-plugin/) for a demo. | 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 | ## Rhino3dmLoadPlugin | ||||
| Example: https://threepipe.org/examples/#rhino3dm-load/ | Example: https://threepipe.org/examples/#rhino3dm-load/ |
| <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="./virtual-cameras-plugin/">Virtual Cameras Plugin </a></li> | |||||
| <li><a href="./virtual-camera/">Virtual Camera (Animated) </a></li> | |||||
| </ul> | </ul> | ||||
| <h2 class="category">Interaction</h2> | <h2 class="category">Interaction</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./gltf-camera-animation/">glTF Camera Animation </a></li> | <li><a href="./gltf-camera-animation/">glTF Camera Animation </a></li> | ||||
| <li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li> | <li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li> | ||||
| </ul> | </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> | <h2 class="category">Utils</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./hdri-ground-plugin/">HDRi Ground Plugin <br/>(Projected Skybox)</a></li> | <li><a href="./hdri-ground-plugin/">HDRi Ground Plugin <br/>(Projected Skybox)</a></li> |
| _testFinish, | _testFinish, | ||||
| CameraViewPlugin, | CameraViewPlugin, | ||||
| ChromaticAberrationPlugin, | ChromaticAberrationPlugin, | ||||
| ClearcoatTintPlugin, | |||||
| CustomBumpMapPlugin, | |||||
| DepthBufferPlugin, | DepthBufferPlugin, | ||||
| DropzonePlugin, | DropzonePlugin, | ||||
| FilmicGrainPlugin, | FilmicGrainPlugin, | ||||
| FragmentClippingExtensionPlugin, | |||||
| FrameFadePlugin, | FrameFadePlugin, | ||||
| FullScreenPlugin, | FullScreenPlugin, | ||||
| GLTFAnimationPlugin, | GLTFAnimationPlugin, | ||||
| HemisphereLight, | HemisphereLight, | ||||
| KTX2LoadPlugin, | KTX2LoadPlugin, | ||||
| KTXLoadPlugin, | KTXLoadPlugin, | ||||
| NoiseBumpMaterialPlugin, | |||||
| NormalBufferPlugin, | NormalBufferPlugin, | ||||
| PickingPlugin, | PickingPlugin, | ||||
| PLYLoadPlugin, | PLYLoadPlugin, | ||||
| USDZLoadPlugin, | USDZLoadPlugin, | ||||
| ViewerUiConfigPlugin, | ViewerUiConfigPlugin, | ||||
| VignettePlugin, | VignettePlugin, | ||||
| VirtualCamerasPlugin, | |||||
| } from 'threepipe' | } from 'threepipe' | ||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | 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 {BlendLoadPlugin} from '@threepipe/plugin-blend-importer' | ||||
| import {extraImportPlugins} from '@threepipe/plugin-extra-importers' | import {extraImportPlugins} from '@threepipe/plugin-extra-importers' | ||||
| PickingPlugin, | PickingPlugin, | ||||
| CameraViewPlugin, | CameraViewPlugin, | ||||
| ViewerUiConfigPlugin, | ViewerUiConfigPlugin, | ||||
| ClearcoatTintPlugin, | |||||
| FragmentClippingExtensionPlugin, | |||||
| NoiseBumpMaterialPlugin, | |||||
| CustomBumpMapPlugin, | |||||
| VirtualCamerasPlugin, | |||||
| // new SceneUiConfigPlugin(), // this is already in ViewerUiPlugin | // new SceneUiConfigPlugin(), // this is already in ViewerUiPlugin | ||||
| new DepthBufferPlugin(HalfFloatType, true, true), | new DepthBufferPlugin(HalfFloatType, true, true), | ||||
| new NormalBufferPlugin(HalfFloatType, false), | new NormalBufferPlugin(HalfFloatType, false), | ||||
| STLLoadPlugin, | STLLoadPlugin, | ||||
| USDZLoadPlugin, | USDZLoadPlugin, | ||||
| BlendLoadPlugin, | BlendLoadPlugin, | ||||
| HierarchyUiPlugin, | |||||
| ...extraImportPlugins, | ...extraImportPlugins, | ||||
| ]) | ]) | ||||
| editor.loadPlugins({ | editor.loadPlugins({ | ||||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ||||
| ['Interaction']: [PickingPlugin], | |||||
| ['Interaction']: [HierarchyUiPlugin, PickingPlugin], | |||||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ||||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin], | ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin], | ||||
| ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | ||||
| ['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin], | |||||
| ['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin], | |||||
| ['Debug']: [RenderTargetPreviewPlugin], | ['Debug']: [RenderTargetPreviewPlugin], | ||||
| }) | }) | ||||
| <!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> |
| 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) |
| <!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> |
| 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) |
| }, | }, | ||||
| "dependencies": { | "dependencies": { | ||||
| "threepipe": "file:./../../src/", | "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": { | "clean-package": { | ||||
| "remove": [ | "remove": [ |
| 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}) | |||||
| }) | |||||
| } | |||||
| } |
| export {TweakpaneEditorPlugin} from './TweakpaneEditorPlugin' | export {TweakpaneEditorPlugin} from './TweakpaneEditorPlugin' | ||||
| export {HierarchyUiPlugin} from './HierarchyUiPlugin' |
| // | string | // | string | ||||
| export type ISceneEventTypes = IObject3DEventTypes | 'sceneUpdate' | 'addSceneObject' | | export type ISceneEventTypes = IObject3DEventTypes | 'sceneUpdate' | 'addSceneObject' | | ||||
| 'mainCameraChange' | 'mainCameraUpdate' | 'environmentChanged' | 'backgroundChanged' | | |||||
| 'mainCameraChange' | 'mainCameraUpdate' | 'environmentChanged' | 'backgroundChanged' | 'renderCameraChange' | | |||||
| 'update' | // todo: deprecate, use 'sceneUpdate' instead | 'update' | // todo: deprecate, use 'sceneUpdate' instead | ||||
| 'textureAdded' | // todo remove | 'textureAdded' | // todo remove | ||||
| 'activeCameraChange' | 'activeCameraUpdate' | // todo: deprecate | 'activeCameraChange' | 'activeCameraUpdate' | // todo: deprecate | ||||
| extends Scene<E, ET>, IObject3D<E, ET>, IShaderPropertiesUpdater { | extends Scene<E, ET>, IObject3D<E, ET>, IShaderPropertiesUpdater { | ||||
| readonly visible: boolean; | readonly visible: boolean; | ||||
| readonly isScene: true; | readonly isScene: true; | ||||
| /** | |||||
| * Main camera that the user controls | |||||
| */ | |||||
| mainCamera: ICamera; | mainCamera: ICamera; | ||||
| /** | |||||
| * Camera that in currently being rendered. | |||||
| */ | |||||
| renderCamera: ICamera; | |||||
| type: 'Scene'; | type: 'Scene'; | ||||
| toJSON(): any; // todo | toJSON(): any; // todo |
| constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, fov?: number, aspect?: number) { | constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, fov?: number, aspect?: number) { | ||||
| super(fov, aspect) | super(fov, aspect) | ||||
| this._canvas = domElement | 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. | 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. | ||||
| this.setDirty() | 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. | * Create a scene instance. This is done automatically in the {@link ThreeViewer} and must not be created separately. | ||||
| * @param camera | * @param camera |
| }) | }) | ||||
| this._animating = false | this._animating = false | ||||
| this._viewer?.setDirty() | |||||
| this.dispatchEvent({type: 'viewChange', view}) | this.dispatchEvent({type: 'viewChange', view}) | ||||
| await timeout(10) | await timeout(10) |
| } | } | ||||
| this.animations[uuid] = a | this.animations[uuid] = a | ||||
| a.promise = new Promise<void>((resolve, reject) => { | 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> = { | const opts: AnimationOptions<V> = { | ||||
| driver: this.defaultDriver, | driver: this.defaultDriver, | ||||
| ...options, | ...options, | ||||
| try { | try { | ||||
| options.onComplete && options.onComplete() | options.onComplete && options.onComplete() | ||||
| } catch (e: any) { | } catch (e: any) { | ||||
| if (!end2()) return | |||||
| reject(e) | reject(e) | ||||
| return | return | ||||
| } | } | ||||
| if (!end2()) return | |||||
| resolve() | resolve() | ||||
| }, | }, | ||||
| onStop: ()=>{ | onStop: ()=>{ | ||||
| try { | try { | ||||
| options.onStop && options.onStop() | options.onStop && options.onStop() | ||||
| } catch (e: any) { | } catch (e: any) { | ||||
| if (!end2()) return | |||||
| reject(e) | reject(e) | ||||
| return | return | ||||
| } | } |
| export {CustomBumpMapPlugin} from './material/CustomBumpMapPlugin' | export {CustomBumpMapPlugin} from './material/CustomBumpMapPlugin' | ||||
| export {FragmentClippingExtensionPlugin, FragmentClippingMode} from './material/FragmentClippingExtensionPlugin' | export {FragmentClippingExtensionPlugin, FragmentClippingMode} from './material/FragmentClippingExtensionPlugin' | ||||
| // extras | |||||
| export {VirtualCamerasPlugin} from './rendering/VirtualCamerasPlugin' | |||||
| // extras | // extras | ||||
| export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin' | export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin' |
| } | } | ||||
| get canFrameFade() { | 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() { | get lastFrame() { |
| import {uiFolderContainer, uiImage, uiInput} from 'uiconfig.js' | import {uiFolderContainer, uiImage, uiInput} from 'uiconfig.js' | ||||
| import {ICamera, IRenderManager, IScene, IWebGLRenderer} from '../../core' | import {ICamera, IRenderManager, IScene, IWebGLRenderer} from '../../core' | ||||
| import {AddBlendTexturePass} from '../../postprocessing/AddBlendTexturePass' | 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 ProgressivePluginEventTypes = '' | ||||
| export type ProgressivePluginTarget = WebGLRenderTarget | export type ProgressivePluginTarget = WebGLRenderTarget | ||||
| readonly passId = 'progressive' | readonly passId = 'progressive' | ||||
| public static readonly PluginType = 'ProgressivePlugin' | public static readonly PluginType = 'ProgressivePlugin' | ||||
| target?: ProgressivePluginTarget | |||||
| protected _targets = new Map<string, ProgressivePluginTarget>() | |||||
| @serialize() @uiInput('Frame count') maxFrameCount: number | @serialize() @uiInput('Frame count') maxFrameCount: number | ||||
| // todo: deserialize jitter | // 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) | // @onChange2(ProgressivePlugin.prototype._createTarget) | ||||
| // @uiDropdown('Buffer Type', threeConstMappings.TextureDataType.uiConfig) | // @uiDropdown('Buffer Type', threeConstMappings.TextureDataType.uiConfig) | ||||
| this.bufferType = bufferType | this.bufferType = bufferType | ||||
| } | } | ||||
| protected _createTarget(recreate = true) { | |||||
| protected _createTarget(camera?: ICamera, recreate = false) { | |||||
| if (!this._viewer) return | 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._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() { | 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 | pass.dirty = () => (this._viewer?.renderManager.frameCount || 0) < this.maxFrameCount // todo use isConverged function | ||||
| return pass | return pass | ||||
| } | } | ||||
| after = ['render'] | after = ['render'] | ||||
| required = ['render'] | required = ['render'] | ||||
| dirty: ValOrFunc<boolean> = () => false | dirty: ValOrFunc<boolean> = () => false | ||||
| constructor(public readonly passId: IPassID, public target: WebGLRenderTarget) { | |||||
| constructor(public readonly passId: IPassID, public target?: ValOrFunc<WebGLRenderTarget|undefined>) { | |||||
| super() | super() | ||||
| } | } | ||||
| render(renderer: IWebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget, deltaTime: number, maskActive: boolean) { | 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) { | if (renderer.renderManager.frameCount < 1) { | ||||
| this.needsSwap = false | this.needsSwap = false | ||||
| if (readBuffer?.texture) | if (readBuffer?.texture) | ||||
| renderer.renderManager.blit(this.target, { | |||||
| renderer.renderManager.blit(target, { | |||||
| source: readBuffer.texture, | source: readBuffer.texture, | ||||
| respectColorSpace: false, | respectColorSpace: false, | ||||
| }) | }) | ||||
| } | } | ||||
| this.needsSwap = true | this.needsSwap = true | ||||
| super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) | super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) | ||||
| renderer.renderManager.blit(this.target, { | |||||
| renderer.renderManager.blit(target, { | |||||
| source: writeBuffer.texture, | source: writeBuffer.texture, | ||||
| respectColorSpace: false, | respectColorSpace: false, | ||||
| }) | }) | ||||
| this.uniforms.weight.value.set(f, f, f, f) | this.uniforms.weight.value.set(f, f, f, f) | ||||
| f = 1. - f | f = 1. - f | ||||
| this.uniforms.weight2.value.set(f, f, f, 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 | this.material.uniformsNeedUpdate = true | ||||
| } | } | ||||
| 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 | |||||
| } | |||||
| } |
| this.material.addEventListener('materialUpdate', this.setDirty) | 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})) | @uiDropdown('Output Color Space', threeConstMappings.ColorSpace.uiConfig, (t: ScreenPass)=>({onChange: t.setDirty})) | ||||
| outputColorSpace: ColorSpace = SRGBColorSpace | outputColorSpace: ColorSpace = SRGBColorSpace | ||||
| render(renderer: IWebGLRenderer, writeBuffer?: WebGLMultipleRenderTargets | WebGLRenderTarget | null, readBuffer?: WebGLMultipleRenderTargets | WebGLRenderTarget, deltaTime?: number, maskActive?: boolean) { | render(renderer: IWebGLRenderer, writeBuffer?: WebGLMultipleRenderTargets | WebGLRenderTarget | null, readBuffer?: WebGLMultipleRenderTargets | WebGLRenderTarget, deltaTime?: number, maskActive?: boolean) { | ||||
| const colorSpace = renderer.outputColorSpace | const colorSpace = renderer.outputColorSpace | ||||
| if (!writeBuffer || this.renderToScreen) renderer.outputColorSpace = this.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) | super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) | ||||
| this._lastReadBuffer = readBuffer | this._lastReadBuffer = readBuffer | ||||
| renderer.outputColorSpace = colorSpace | renderer.outputColorSpace = colorSpace |
| // // todo gizmos | // // todo gizmos | ||||
| // } | // } | ||||
| render(scene: IScene): void { | |||||
| render(scene: IScene, renderToScreen = true): void { | |||||
| if (this._passesNeedsUpdate) { | if (this._passesNeedsUpdate) { | ||||
| this._refreshPipeline() | this._refreshPipeline() | ||||
| this.refreshPasses() | this.refreshPasses() | ||||
| } | } | ||||
| for (const pass of this._passes) { | 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._composer.render() | ||||
| this._frameCount += 1 | |||||
| this._totalFrameCount += 1 | |||||
| this._composer.renderToScreen = true | |||||
| if (renderToScreen) { | |||||
| this._frameCount += 1 | |||||
| this._totalFrameCount += 1 | |||||
| } | |||||
| this._dirty = false | this._dirty = false | ||||
| } | } | ||||
| import {MathUtils} from 'three' | import {MathUtils} from 'three' | ||||
| export {animate} | 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} | export type {AnimationOptions, KeyframeOptions, Easing} | ||||
| function easeInOutSine(x: number): number { | function easeInOutSine(x: number): number { | ||||
| export async function animateAsync<V=number>(options: AnimationOptions<V>, animations?: AnimateResult[]) { | export async function animateAsync<V=number>(options: AnimationOptions<V>, animations?: AnimateResult[]) { | ||||
| const complete = options.onComplete | const complete = options.onComplete | ||||
| const stop = options.onStop | const stop = options.onStop | ||||
| const end = options.onEnd | |||||
| options = {...options} | options = {...options} | ||||
| return new Promise<void>((resolve, reject) => { | return new Promise<void>((resolve, reject) => { | ||||
| const end2 = ()=>{ | |||||
| try { | |||||
| end?.() | |||||
| } catch (e: any) { | |||||
| reject(e) | |||||
| return false | |||||
| } | |||||
| return true | |||||
| } | |||||
| options.onComplete = ()=>{ | options.onComplete = ()=>{ | ||||
| try { | try { | ||||
| complete?.() | complete?.() | ||||
| } catch (e: any) { | } catch (e: any) { | ||||
| if (!end2()) return | |||||
| reject(e) | reject(e) | ||||
| return | return | ||||
| } | } | ||||
| if (!end2()) return | |||||
| resolve() | resolve() | ||||
| } | } | ||||
| options.onStop = ()=>{ | options.onStop = ()=>{ | ||||
| try { | try { | ||||
| stop?.() | stop?.() | ||||
| } catch (e: any) { | } catch (e: any) { | ||||
| if (!end2()) return | |||||
| reject(e) | reject(e) | ||||
| return | return | ||||
| } | } | ||||
| if (!end2()) return | |||||
| resolve() | resolve() | ||||
| } | } | ||||
| const an = animate(options) | const an = animate(options) |
| import type {ISerializedConfig, ThreeViewer} from './ThreeViewer' | |||||
| import type {ISerializedConfig, IViewerEvent, ThreeViewer} from './ThreeViewer' | |||||
| import {IViewerEventTypes} from './ThreeViewer' | |||||
| import {Event, EventDispatcher} from 'three' | import {Event, EventDispatcher} from 'three' | ||||
| import {SerializationMetaType, ThreeSerialization} from '../utils' | |||||
| import {PartialRecord, SerializationMetaType, ThreeSerialization} from '../utils' | |||||
| import {IViewerPlugin, IViewerPluginAsync} from './IViewerPlugin' | import {IViewerPlugin, IViewerPluginAsync} from './IViewerPlugin' | ||||
| import {UiObjectConfig} from 'uiconfig.js' | import {UiObjectConfig} from 'uiconfig.js' | ||||
| else this.fromJSON?.(state) | 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 | // todo: move to ThreeViewer | ||||
| // storeState(prefix?: string, storage?: Storage, data?: any): void { | // storeState(prefix?: string, storage?: Storage, data?: any): void { | ||||
| // storage = storage || (window ? window.localStorage : undefined) | // storage = storage || (window ? window.localStorage : undefined) | ||||
| onAdded(viewer: TViewer): void { | onAdded(viewer: TViewer): void { | ||||
| this._viewer = viewer | this._viewer = viewer | ||||
| this._viewer.addEventListener('*', this._onViewerEvent) | |||||
| } | } | ||||
| onRemove(viewer: TViewer): void { | onRemove(viewer: TViewer): void { | ||||
| if (this._viewer !== viewer) viewer.console.error('Wrong viewer') | if (this._viewer !== viewer) viewer.console.error('Wrong viewer') | ||||
| this._viewer?.removeEventListener('*', this._onViewerEvent) | |||||
| this._viewer = undefined | this._viewer = undefined | ||||
| } | } | ||||
| async onAdded(viewer: TViewer): Promise<void> { | async onAdded(viewer: TViewer): Promise<void> { | ||||
| this._viewer = viewer | this._viewer = viewer | ||||
| this._viewer.addEventListener('*', this._onViewerEvent) | |||||
| } | } | ||||
| async onRemove(viewer: TViewer): Promise<void> { | async onRemove(viewer: TViewer): Promise<void> { | ||||
| if (this._viewer !== viewer) viewer.console.error('Wrong viewer') | if (this._viewer !== viewer) viewer.console.error('Wrong viewer') | ||||
| this._viewer?.removeEventListener('*', this._onViewerEvent) | |||||
| this._viewer = undefined | this._viewer = undefined | ||||
| } | } | ||||
| } | } |
| import {OrbitControls3} from '../three' | import {OrbitControls3} from '../three' | ||||
| export type IViewerEvent = BaseEvent & { | 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 type IViewerEventTypes = IViewerEvent['type'] | ||||
| export interface ISerializedConfig { | export interface ISerializedConfig { | ||||
| this.dispatchEvent({type: 'preRender', target: this}) | 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}) | this.dispatchEvent({type: 'postRender', target: this}) | ||||
| }) | }) | ||||
| } | } | ||||
| dispatchEvent(event: IViewerEvent) { | |||||
| super.dispatchEvent(event) | |||||
| super.dispatchEvent({...event, type: '*', eType: event.type}) | |||||
| } | |||||
| private _setActiveCameraView(event: any = {}): void { | private _setActiveCameraView(event: any = {}): void { | ||||
| if (event.type === 'setView') { | if (event.type === 'setView') { | ||||
| if (!event.camera) { | if (!event.camera) { |