| [](https://opensource.org/license/apache-2-0/) | [](https://opensource.org/license/apache-2-0/) | ||||
| [](https://twitter.com/repalash) | [](https://twitter.com/repalash) | ||||
| ThreePipe is a 3D framework built on top of [three.js](https://threejs.org/) in TypeScript with a focus on rendering quality, modularity, and extensibility. | |||||
| ThreePipe is a modern 3D framework built on top of [three.js](https://threejs.org/), written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable. | |||||
| Key features include: | Key features include: | ||||
| - Simple, intuitive API for creating 3D model viewers/configurators/editors on web pages, with many built-in presets for common workflows and use-cases. | - Simple, intuitive API for creating 3D model viewers/configurators/editors on web pages, with many built-in presets for common workflows and use-cases. | ||||
| - Automatic disposal of all three.js resources with built-in reference management. | - Automatic disposal of all three.js resources with built-in reference management. | ||||
| - Realtime Realistic Rendering with screen-space post-processing effects from [webgi](https://webgi.dev/). | - Realtime Realistic Rendering with screen-space post-processing effects from [webgi](https://webgi.dev/). | ||||
| Checkout the documentation and guides on the [threepipe website](https://threepipe.org) for more details. | |||||
| ## Examples | ## Examples | ||||
| Code samples and demos covering various usecases and test are present in the [examples](./examples/) folder. | Code samples and demos covering various usecases and test are present in the [examples](./examples/) folder. | ||||
| ## Getting Started | ## Getting Started | ||||
| Checkout the full [Getting Started Guide](https://threepipe.org/guide/getting-started.html) on [threepipe.com](https://threepipe.com) | |||||
| ### Local Setup | |||||
| To create a new project locally | |||||
| ```npm create threepipe@latest``` | |||||
| And follow the instructions to create a new project. | |||||
| ### HTML/JS Quickstart (CDN) | ### HTML/JS Quickstart (CDN) | ||||
| ```html | ```html | ||||
| <canvas id="three-canvas" style="width: 800px; height: 600px;"></canvas> | <canvas id="three-canvas" style="width: 800px; height: 600px;"></canvas> | ||||
| <script type="module"> | <script type="module"> | ||||
| import {ThreeViewer, DepthBufferPlugin} from 'https://threepipe.org/dist/index.mjs' | |||||
| import {ThreeViewer, DepthBufferPlugin} from 'https://unpkg.com/threepipe@latest/dist/index.mjs' | |||||
| const viewer = new ThreeViewer({canvas: document.getElementById('three-canvas')}) | const viewer = new ThreeViewer({canvas: document.getElementById('three-canvas')}) | ||||
| ### React | ### React | ||||
| A sample [react](https://react.dev) component in tsx to render a model with an environment map. | |||||
| The best way to use the viewer in react is to wrap it in a custom component. | |||||
| Here is a sample [react](https://react.dev) component in tsx to render a model with an environment map. | |||||
| ```tsx | ```tsx | ||||
| import React from 'react' | import React from 'react' | ||||
| Another example with Vue SFC(Single file component): https://threepipe.org/examples/#vue-sfc-sample/ | Another example with Vue SFC(Single file component): https://threepipe.org/examples/#vue-sfc-sample/ | ||||
| ### Svelte | |||||
| ### Svelte (4) | |||||
| A sample [svelte](https://svelte.dev/) component in js to render a model with an environment map. | A sample [svelte](https://svelte.dev/) component in js to render a model with an environment map. | ||||
| The viewer initializes with a Scene, Camera, Camera controls(Orbit Controls), several importers, exporters and a default rendering pipeline. Additional functionality can be added with plugins. | The viewer initializes with a Scene, Camera, Camera controls(Orbit Controls), several importers, exporters and a default rendering pipeline. Additional functionality can be added with plugins. | ||||
| Check out the GLTF Load example to see it in action or to check the JS equivalent code: https://threepipe.org/examples/#gltf-load/ | |||||
| Check out the glTF Load example to see it in action or to check the JS equivalent code: https://threepipe.org/examples/#gltf-load/ | |||||
| Check out the [Plugins](https://threepipe.org/guide/features.html#plugin-system) section to learn how to add additional functionality to the viewer. | Check out the [Plugins](https://threepipe.org/guide/features.html#plugin-system) section to learn how to add additional functionality to the viewer. | ||||
| - [TonemapPlugin](https://threepipe.org/plugin/TonemapPlugin.html) - Add tonemap to the final screen pass | - [TonemapPlugin](https://threepipe.org/plugin/TonemapPlugin.html) - Add tonemap to the final screen pass | ||||
| - [DropzonePlugin](https://threepipe.org/plugin/DropzonePlugin.html) - Drag and drop local files to import and load | - [DropzonePlugin](https://threepipe.org/plugin/DropzonePlugin.html) - Drag and drop local files to import and load | ||||
| - [ProgressivePlugin](https://threepipe.org/plugin/ProgressivePlugin.html) - Post-render pass to blend the last frame with the current frame | - [ProgressivePlugin](https://threepipe.org/plugin/ProgressivePlugin.html) - Post-render pass to blend the last frame with the current frame | ||||
| - [SSAAPlugin](https://threepipe.org/plugin/SSAAPlugin.html) - Add Super Sample Anti-Aliasing by applying jitter to the camera. | |||||
| - [DepthBufferPlugin](https://threepipe.org/plugin/DepthBufferPlugin.html) - Pre-rendering of depth buffer | |||||
| - [NormalBufferPlugin](https://threepipe.org/plugin/NormalBufferPlugin.html) - Pre-rendering of normal buffer | |||||
| - [GBufferPlugin](https://threepipe.org/plugin/GBufferPlugin.html) - Pre-rendering of depth-normal and flags buffers in a single pass | |||||
| - [SSAOPlugin](https://threepipe.org/plugin/SSAOPlugin.html) - Add SSAO(Screen Space Ambient Occlusion) for physical materials. | |||||
| - [CanvasSnapshotPlugin](https://threepipe.org/plugin/CanvasSnapshotPlugin.html) - Add support for taking snapshots of the canvas | |||||
| - [SSAAPlugin](https://threepipe.org/plugin/SSAAPlugin.html) - Add [Super Sample Anti-Aliasing](https://en.wikipedia.org/wiki/Supersampling) by applying jitter to the camera. | |||||
| - [DepthBufferPlugin](https://threepipe.org/plugin/DepthBufferPlugin.html) - Pre-rendering of [depth buffer](https://en.wikipedia.org/wiki/Z-buffering) | |||||
| - [NormalBufferPlugin](https://threepipe.org/plugin/NormalBufferPlugin.html) - Pre-rendering of normal buffer ([deferred shading](https://en.wikipedia.org/wiki/Deferred_shading)) | |||||
| - [GBufferPlugin](https://threepipe.org/plugin/GBufferPlugin.html) - Pre-rendering of depth-normal and flags buffers in a single pass ([deferred shading](https://en.wikipedia.org/wiki/Deferred_shading)) | |||||
| - [SSAOPlugin](https://threepipe.org/plugin/SSAOPlugin.html) - Add [SSAO(Screen Space Ambient Occlusion)](https://en.wikipedia.org/wiki/Screen_space_ambient_occlusion) for physical materials. | |||||
| - [CanvasSnapshotPlugin](https://threepipe.org/plugin/CanvasSnapshotPlugin.html) - Add support for taking snapshots of the canvas with anti-aliasing and other options | |||||
| - [PickingPlugin](https://threepipe.org/plugin/PickingPlugin.html) - Adds support for selecting objects in the viewer with user interactions and selection widgets | - [PickingPlugin](https://threepipe.org/plugin/PickingPlugin.html) - Adds support for selecting objects in the viewer with user interactions and selection widgets | ||||
| - [AssetExporterPlugin](https://threepipe.org/plugin/AssetExporterPlugin.html) - Provides options and methods to export the scene, object GLB or Viewer Configuration. | |||||
| - [LoadingScreenPlugin](https://threepipe.org/plugin/LoadingScreenPlugin.html) - Shows a configurable loading screen overlay over the canvas. | |||||
| - [AssetExporterPlugin](https://threepipe.org/plugin/AssetExporterPlugin.html) - Provides options and methods to export the scene/object GLB or Viewer Configuration JSON | |||||
| - [LoadingScreenPlugin](https://threepipe.org/plugin/LoadingScreenPlugin.html) - Shows a configurable loading screen overlay over the canvas | |||||
| - [FullScreenPlugin](https://threepipe.org/plugin/FullScreenPlugin.html) - Adds support for moving the canvas or the container fullscreen mode in browsers | - [FullScreenPlugin](https://threepipe.org/plugin/FullScreenPlugin.html) - Adds support for moving the canvas or the container fullscreen mode in browsers | ||||
| - [InteractionPromptPlugin](https://threepipe.org/plugin/InteractionPromptPlugin.html) - Adds an animated hand icon over canvas to prompt the user to interact | - [InteractionPromptPlugin](https://threepipe.org/plugin/InteractionPromptPlugin.html) - Adds an animated hand icon over canvas to prompt the user to interact | ||||
| - [TransformControlsPlugin](https://threepipe.org/plugin/TransformControlsPlugin.html) - Adds support for moving, rotating and scaling objects in the viewer with interactive widgets | - [TransformControlsPlugin](https://threepipe.org/plugin/TransformControlsPlugin.html) - Adds support for moving, rotating and scaling objects in the viewer with interactive widgets | ||||
| - [ContactShadowGroundPlugin](https://threepipe.org/plugin/ContactShadowGroundPlugin.html) - Adds a ground plane at runtime with contact shadows | - [ContactShadowGroundPlugin](https://threepipe.org/plugin/ContactShadowGroundPlugin.html) - Adds a ground plane at runtime with contact shadows | ||||
| - [GLTFAnimationPlugin](https://threepipe.org/plugin/GLTFAnimationPlugin.html) - Add support for playing and seeking gltf animations | |||||
| - [GLTFAnimationPlugin](https://threepipe.org/plugin/GLTFAnimationPlugin.html) - Add support for playing and seeking glTF animations | |||||
| - [PopmotionPlugin](https://threepipe.org/plugin/PopmotionPlugin.html) - Integrates with popmotion.io library for animation/tweening | - [PopmotionPlugin](https://threepipe.org/plugin/PopmotionPlugin.html) - Integrates with popmotion.io library for animation/tweening | ||||
| - [CameraViewPlugin](https://threepipe.org/plugin/CameraViewPlugin.html) - Add support for saving, loading, animating, looping between camera views | - [CameraViewPlugin](https://threepipe.org/plugin/CameraViewPlugin.html) - Add support for saving, loading, animating, looping between camera views | ||||
| - [TransformAnimationPlugin](https://threepipe.org/plugin/TransformAnimationPlugin.html) - Add support for saving, loading, animating, between object transforms | - [TransformAnimationPlugin](https://threepipe.org/plugin/TransformAnimationPlugin.html) - Add support for saving, loading, animating, between object transforms | ||||
| - [GeometryUVPreviewPlugin](https://threepipe.org/plugin/GeometryUVPreviewPlugin.html) - Preview UVs of any geometry in a UI panel over the canvas | - [GeometryUVPreviewPlugin](https://threepipe.org/plugin/GeometryUVPreviewPlugin.html) - Preview UVs of any geometry in a UI panel over the canvas | ||||
| - [FrameFadePlugin](https://threepipe.org/plugin/FrameFadePlugin.html) - Post-render pass to smoothly fade to a new rendered frame over time | - [FrameFadePlugin](https://threepipe.org/plugin/FrameFadePlugin.html) - Post-render pass to smoothly fade to a new rendered frame over time | ||||
| - [VignettePlugin](https://threepipe.org/plugin/VignettePlugin.html) - Add Vignette effect by patching the final screen pass | - [VignettePlugin](https://threepipe.org/plugin/VignettePlugin.html) - Add Vignette effect by patching the final screen pass | ||||
| - [ChromaticAberrationPlugin](https://threepipe.org/plugin/ChromaticAberrationPlugin.html) - Add Chromatic Aberration effect by patching the final screen pass | |||||
| - [FilmicGrainPlugin](https://threepipe.org/plugin/FilmicGrainPlugin.html) - Add Filmic Grain effect by patching the final screen pass | |||||
| - [ChromaticAberrationPlugin](https://threepipe.org/plugin/ChromaticAberrationPlugin.html) - Add [Chromatic Aberration](https://en.wikipedia.org/wiki/Chromatic_aberration) effect by patching the final screen pass | |||||
| - [FilmicGrainPlugin](https://threepipe.org/plugin/FilmicGrainPlugin.html) - Add [Filmic Grain](https://en.wikipedia.org/wiki/Film_grain) effect by patching the final screen pass | |||||
| - [NoiseBumpMaterialPlugin](https://threepipe.org/plugin/NoiseBumpMaterialPlugin.html) - Sparkle Bump/Noise Bump material extension for PhysicalMaterial | - [NoiseBumpMaterialPlugin](https://threepipe.org/plugin/NoiseBumpMaterialPlugin.html) - Sparkle Bump/Noise Bump material extension for PhysicalMaterial | ||||
| - [CustomBumpMapPlugin](https://threepipe.org/plugin/CustomBumpMapPlugin.html) - Custom Bump Map material extension for PhysicalMaterial | |||||
| - [CustomBumpMapPlugin](https://threepipe.org/plugin/CustomBumpMapPlugin.html) - Adds multiple bump map support and bicubic filtering material extension for PhysicalMaterial | |||||
| - [ClearcoatTintPlugin](https://threepipe.org/plugin/ClearcoatTintPlugin.html) - Clearcoat Tint material extension for PhysicalMaterial | - [ClearcoatTintPlugin](https://threepipe.org/plugin/ClearcoatTintPlugin.html) - Clearcoat Tint material extension for PhysicalMaterial | ||||
| - [FragmentClippingExtensionPlugin](https://threepipe.org/plugin/FragmentClippingExtensionPlugin.html) - Fragment/SDF Clipping material extension for PhysicalMaterial | - [FragmentClippingExtensionPlugin](https://threepipe.org/plugin/FragmentClippingExtensionPlugin.html) - Fragment/SDF Clipping material extension for PhysicalMaterial | ||||
| - [ParallaxMappingPlugin](https://threepipe.org/plugin/ParallaxMappingPlugin.html) - Relief Parallax Bump Mapping extension for PhysicalMaterial | - [ParallaxMappingPlugin](https://threepipe.org/plugin/ParallaxMappingPlugin.html) - Relief Parallax Bump Mapping extension for PhysicalMaterial | ||||
| - [HDRiGroundPlugin](https://threepipe.org/plugin/HDRiGroundPlugin.html) - Add support for ground projected hdri/skybox to the webgl background shader. | - [HDRiGroundPlugin](https://threepipe.org/plugin/HDRiGroundPlugin.html) - Add support for ground projected hdri/skybox to the webgl background shader. | ||||
| - [VirtualCamerasPlugin](https://threepipe.org/plugin/VirtualCamerasPlugin.html) - Add support for rendering virtual cameras before the main one every frame. | - [VirtualCamerasPlugin](https://threepipe.org/plugin/VirtualCamerasPlugin.html) - Add support for rendering virtual cameras before the main one every frame. | ||||
| - [EditorViewWidgetPlugin](https://threepipe.org/plugin/EditorViewWidgetPlugin.html) - Adds an interactive ViewHelper/AxisHelper that syncs with the main camera. | |||||
| - [EditorViewWidgetPlugin](https://threepipe.org/plugin/EditorViewWidgetPlugin.html) - Adds an interactive `ViewHelper`/`AxisHelper` that syncs with the main camera. | |||||
| - [Object3DWidgetsPlugin](https://threepipe.org/plugin/Object3DWidgetsPlugin.html) - Automatically create light and camera helpers/gizmos when they are added to the scene. | - [Object3DWidgetsPlugin](https://threepipe.org/plugin/Object3DWidgetsPlugin.html) - Automatically create light and camera helpers/gizmos when they are added to the scene. | ||||
| - [Object3DGeneratorPlugin](https://threepipe.org/plugin/Object3DGeneratorPlugin.html) - Provides UI and API to create scene objects like lights, cameras, meshes, etc. | |||||
| - [DeviceOrientationControlsPlugin](https://threepipe.org/plugin/DeviceOrientationControlsPlugin.html) - Adds a controlsMode to the mainCamera for device orientation controls(gyroscope rotation control). | |||||
| - [PointerLockControlsPlugin](https://threepipe.org/plugin/PointerLockControlsPlugin.html) - Adds a controlsMode to the mainCamera for pointer lock controls. | |||||
| - [ThreeFirstPersonControlsPlugin](https://threepipe.org/plugin/ThreeFirstPersonControlsPlugin.html) - Adds a controlsMode to the mainCamera for first person controls from threejs. | |||||
| - [GLTFKHRMaterialVariantsPlugin](https://threepipe.org/plugin/GLTFKHRMaterialVariantsPlugin.html) - Support using for variants from KHR_materials_variants extension in gltf models. | |||||
| - [Rhino3dmLoadPlugin](https://threepipe.org/plugin/Rhino3dmLoadPlugin.html) - Add support for loading .3dm files | |||||
| - [Object3DGeneratorPlugin](https://threepipe.org/plugin/Object3DGeneratorPlugin.html) - Provides an API and UI to create scene objects like lights, cameras, meshes, etc. | |||||
| - [DeviceOrientationControlsPlugin](https://threepipe.org/plugin/DeviceOrientationControlsPlugin.html) - Adds a `controlsMode` to the `mainCamera` for device orientation controls(gyroscope rotation control). | |||||
| - [PointerLockControlsPlugin](https://threepipe.org/plugin/PointerLockControlsPlugin.html) - Adds a `controlsMode` to the `mainCamera` for pointer lock controls. | |||||
| - [ThreeFirstPersonControlsPlugin](https://threepipe.org/plugin/ThreeFirstPersonControlsPlugin.html) - Adds a `controlsMode` to the `mainCamera` for first person controls from threejs. | |||||
| - [GLTFKHRMaterialVariantsPlugin](https://threepipe.org/plugin/GLTFKHRMaterialVariantsPlugin.html) - Support using for variants from KHR_materials_variants extension in glTF models. | |||||
| - [Rhino3dmLoadPlugin](https://threepipe.org/plugin/Rhino3dmLoadPlugin.html) - Add support for loading .3dm files ([Rhino 3D](https://www.rhino3d.com/)) | |||||
| - [PLYLoadPlugin](https://threepipe.org/plugin/PLYLoadPlugin.html) - Add support for loading .ply files | - [PLYLoadPlugin](https://threepipe.org/plugin/PLYLoadPlugin.html) - Add support for loading .ply files | ||||
| - [STLLoadPlugin](https://threepipe.org/plugin/STLLoadPlugin.html) - Add support for loading .stl files | |||||
| - [KTX2LoadPlugin](https://threepipe.org/plugin/KTX2LoadPlugin.html) - Add support for loading .ktx2 files | |||||
| - [KTXLoadPlugin](https://threepipe.org/plugin/KTXLoadPlugin.html) - Add support for loading .ktx files | |||||
| - [USDZLoadPlugin](https://threepipe.org/plugin/USDZLoadPlugin.html) - Add support for loading .usdz files | |||||
| - [GLTFMeshOptDecodePlugin](https://threepipe.org/plugin/GLTFMeshOptDecodePlugin.html) - Decode gltf files with EXT_meshopt_compression extension. | |||||
| - [STLLoadPlugin](https://threepipe.org/plugin/STLLoadPlugin.html) - Add support for loading .stl files ([STL](https://en.wikipedia.org/wiki/STL_(file_format))) | |||||
| - [KTX2LoadPlugin](https://threepipe.org/plugin/KTX2LoadPlugin.html) - Add support for loading .ktx2 files ([KTX2 - GPU Compressed Textures](https://doc.babylonjs.com/features/featuresDeepDive/materials/using/ktx2Compression)) | |||||
| - [KTXLoadPlugin](https://threepipe.org/plugin/KTXLoadPlugin.html) - Add support for loading .ktx files (Note - use ktx2) | |||||
| - [USDZLoadPlugin](https://threepipe.org/plugin/USDZLoadPlugin.html) - Add partial support for loading .usdz, .usda files ([USDZ](https://en.wikipedia.org/wiki/Universal_Scene_Description)) | |||||
| - [GLTFMeshOptDecodePlugin](https://threepipe.org/plugin/GLTFMeshOptDecodePlugin.html) - Decode glTF files with [EXT_meshopt_compression](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md) extension. | |||||
| - [SimplifyModifierPlugin](https://threepipe.org/plugin/SimplifyModifierPlugin.html) - Boilerplate for plugin to simplify geometries | - [SimplifyModifierPlugin](https://threepipe.org/plugin/SimplifyModifierPlugin.html) - Boilerplate for plugin to simplify geometries | ||||
| - [MeshOptSimplifyModifierPlugin](https://threepipe.org/plugin/MeshOptSimplifyModifierPlugin.html) - Simplify geometries using meshoptimizer library | |||||
| - [MeshOptSimplifyModifierPlugin](https://threepipe.org/plugin/MeshOptSimplifyModifierPlugin.html) - Simplify geometries using [meshoptimizer](https://github.com/zeux/meshoptimizer) library | |||||
| - [Packages](https://threepipe.org/guide/threepipe-packages.html) | - [Packages](https://threepipe.org/guide/threepipe-packages.html) | ||||
| - [@threepipe/plugin-tweakpane](https://threepipe.org/package/plugin-tweakpane.html) Tweakpane UI Plugin | |||||
| - [@threepipe/plugin-blueprintjs](https://threepipe.org/package/plugin-blueprintjs.html) BlueprintJs UI Plugin | |||||
| - [@threepipe/plugin-tweakpane-editor](https://threepipe.org/package/plugin-tweakpane-editor.html) - Tweakpane Editor Plugin | |||||
| - [@threepipe/plugin-configurator](https://threepipe.org/package/plugin-configurator.html) - Provides Material Configurator and Switch Node Plugin to allow users to select variations | |||||
| - [@threepipe/plugin-gltf-transform](https://threepipe.org/package/plugin-gltf-transform.html) - Plugin to transform gltf models (draco compression) | |||||
| - [@threepipe/plugin-tweakpane](https://threepipe.org/package/plugin-tweakpane.html) [Tweakpane](https://tweakpane.github.io/docs/) UI Plugin | |||||
| - [@threepipe/plugin-blueprintjs](https://threepipe.org/package/plugin-blueprintjs.html) [BlueprintJs](https://blueprintjs.com/) UI Plugin | |||||
| - [@threepipe/plugin-tweakpane-editor](https://threepipe.org/package/plugin-tweakpane-editor.html) - Editor Plugin using Tweakpane for plugin UI | |||||
| - [@threepipe/plugin-configurator](../package/plugin-configurator) - Provides `MaterialConfiguratorPlugin` and `SwitchNodePlugin` to allow users to select variations | |||||
| - [@threepipe/plugin-gltf-transform](https://threepipe.org/package/plugin-gltf-transform.html) - Plugin to transform glTF models (draco compression) | |||||
| - [@threepipe/plugins-extra-importers](https://threepipe.org/package/plugins-extra-importers.html) - Plugin for loading more file types supported by loaders in three.js | - [@threepipe/plugins-extra-importers](https://threepipe.org/package/plugins-extra-importers.html) - Plugin for loading more file types supported by loaders in three.js | ||||
| - [@threepipe/plugin-blend-importer](https://threepipe.org/package/plugin-blend-importer.html) - Blender to add support for loading .blend file | |||||
| - [@threepipe/plugin-blend-importer](https://threepipe.org/package/plugin-blend-importer.html) - Add support for loading .blend file. (Partial/WIP) ([Blender](https://www.blender.org/)) | |||||
| - [@threepipe/plugin-geometry-generator](https://threepipe.org/package/plugin-geometry-generator.html) - Generate parametric geometry types that can be re-generated from UI/API. | - [@threepipe/plugin-geometry-generator](https://threepipe.org/package/plugin-geometry-generator.html) - Generate parametric geometry types that can be re-generated from UI/API. | ||||
| - [@threepipe/plugin-gaussian-splatting](https://threepipe.org/package/plugin-gaussian-splatting.html) - Gaussian Splatting plugin for loading and rendering splat files | - [@threepipe/plugin-gaussian-splatting](https://threepipe.org/package/plugin-gaussian-splatting.html) - Gaussian Splatting plugin for loading and rendering splat files | ||||
| - [@threepipe/plugin-network](https://threepipe.org/package/plugin-network.html) - Network/Cloud related plugin implementations for Threepipe. | - [@threepipe/plugin-network](https://threepipe.org/package/plugin-network.html) - Network/Cloud related plugin implementations for Threepipe. |
| { | { | ||||
| "name": "threepipe", | "name": "threepipe", | ||||
| "version": "0.0.40", | "version": "0.0.40", | ||||
| "description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.", | |||||
| "description": "A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.", | |||||
| "main": "dist/index.js", | "main": "dist/index.js", | ||||
| "module": "dist/index.mjs", | "module": "dist/index.mjs", | ||||
| "types": "dist/index.d.ts", | "types": "dist/index.d.ts", |
| this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: 0}) | this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: 0}) | ||||
| let res: ImportResult | ImportResult[] | undefined | let res: ImportResult | ImportResult[] | undefined | ||||
| try { | try { | ||||
| const loader = this.registerFile(path, file) | |||||
| const loader = this.registerFile(path, file, options.fileExtension, options.fileHandler) | |||||
| // const url = this.resolveURL(path) // todo: why is this required? maybe for query string? | // const url = this.resolveURL(path) // todo: why is this required? maybe for query string? | ||||
| // const path2 = path.replace(/\?.*$/, '') // remove query string to find the handler properly | // const path2 = path.replace(/\?.*$/, '') // remove query string to find the handler properly | ||||
| // baseUrl: LoaderUtils.extractUrlBase(url), | // baseUrl: LoaderUtils.extractUrlBase(url), | ||||
| } | } | ||||
| loader.loadFileOptions = options | |||||
| res = await loader.loadAsync(path + (options.queryString ? (path.includes('?') ? '&' : '?') + options.queryString : ''), (e)=>{ | res = await loader.loadAsync(path + (options.queryString ? (path.includes('?') ? '&' : '?') + options.queryString : ''), (e)=>{ | ||||
| if (onDownloadProgress) onDownloadProgress(e) | if (onDownloadProgress) onDownloadProgress(e) | ||||
| const total = e.lengthComputable ? e.total : undefined | |||||
| this.dispatchEvent({ | this.dispatchEvent({ | ||||
| type: 'importFile', path, | type: 'importFile', path, | ||||
| state:'downloading', | state:'downloading', | ||||
| loadedBytes: e.loaded || undefined, | loadedBytes: e.loaded || undefined, | ||||
| totalBytes: e.total || undefined, | |||||
| progress: e.total > 0 ? e.loaded / e.total : 1, | |||||
| totalBytes: total && total < e.loaded ? e.loaded : e.total || undefined, // sometimes total is more than e.loaded | |||||
| progress: total && total > 0 && total > e.loaded ? e.loaded / total : 1, | |||||
| }) | }) | ||||
| }) | }) | ||||
| if (loader.transform) res = await loader.transform(res, options) | if (loader.transform) res = await loader.transform(res, options) | ||||
| delete loader.loadFileOptions | |||||
| this._rootContext = undefined | this._rootContext = undefined | ||||
| * Register a file in the database and return a loader for it. If the loader does not exist, it will be created. | * Register a file in the database and return a loader for it. If the loader does not exist, it will be created. | ||||
| * @param path | * @param path | ||||
| * @param file | * @param file | ||||
| * @param extension | |||||
| * @param loader | |||||
| */ | */ | ||||
| registerFile(path: string, file?: IFile): ILoader | undefined { | |||||
| registerFile(path: string, file?: IFile, extension?: string, loader?: ILoader): ILoader | undefined { | |||||
| const isData = path.startsWith('data:') || false | const isData = path.startsWith('data:') || false | ||||
| if (!isData) path = path.replace(/\?.*$/, '') // remove query string | if (!isData) path = path.replace(/\?.*$/, '') // remove query string | ||||
| const ext = isData ? undefined : file?.ext ?? parseFileExtension(file?.name ?? path.trim())?.toLowerCase() | |||||
| const ext = extension || (isData ? undefined : file?.ext ?? parseFileExtension(file?.name ?? path.trim())?.toLowerCase()) | |||||
| const mime = file?.mime ?? isData ? path.slice(0, path.indexOf(';')).split(':')[1] || undefined : undefined | const mime = file?.mime ?? isData ? path.slice(0, path.indexOf(';')).split(':')[1] || undefined : undefined | ||||
| if (file) { | if (file) { | ||||
| this._fileDatabase.set(path, file) | this._fileDatabase.set(path, file) | ||||
| } | } | ||||
| return this._getLoader(path) || this._createLoader(path, ext, mime) | |||||
| return loader || this._getLoader(path, ext, mime) || this._createLoader(path, ext, mime) | |||||
| } | } | ||||
| /** | /** | ||||
| return this._loadingManager.resolveURL(url) | return this._loadingManager.resolveURL(url) | ||||
| } | } | ||||
| protected _urlModifiers: ((url: string) => string)[] = [] | |||||
| addURLModifier(modifier: (url: string) => string) { | |||||
| this._urlModifiers.push(modifier) | |||||
| } | |||||
| removeURLModifier(modifier: (url: string) => string) { | |||||
| const index = this._urlModifiers.indexOf(modifier) | |||||
| if (index >= 0) this._urlModifiers.splice(index, 1) | |||||
| } | |||||
| protected _urlModifier(url: string) { | protected _urlModifier(url: string) { | ||||
| url = this._urlModifiers.reduce((acc, modifier) => modifier(acc), url) | |||||
| let normalizedURL = decodeURI(url) | let normalizedURL = decodeURI(url) | ||||
| const rootUrl = this._rootContext?.rootUrl | const rootUrl = this._rootContext?.rootUrl | ||||
| if (!normalizedURL.includes('://') && rootUrl && !normalizedURL.startsWith(rootUrl)) | if (!normalizedURL.includes('://') && rootUrl && !normalizedURL.startsWith(rootUrl)) | ||||
| if (isRoot && !importer.root) return false | if (isRoot && !importer.root) return false | ||||
| if (mime && importer.mime?.find(m => mime === m)) return true | if (mime && importer.mime?.find(m => mime === m)) return true | ||||
| if (importer.ext.find(iext => | if (importer.ext.find(iext => | ||||
| ext && iext === ext | |||||
| || name?.toLowerCase()?.endsWith('.' + iext) | |||||
| ext ? iext === ext : name?.toLowerCase()?.endsWith('.' + iext) | |||||
| || iext?.startsWith('data:') && name?.startsWith(iext))) return true | || iext?.startsWith('data:') && name?.startsWith(iext))) return true | ||||
| return false | return false | ||||
| }) | }) |
| } | } | ||||
| export interface LoadFileOptions { | export interface LoadFileOptions { | ||||
| /** | |||||
| * The file extension to use for the file. If not specified, the importer will try to determine the file extension from the file name/url. | |||||
| */ | |||||
| fileExtension?: string, | |||||
| /** | /** | ||||
| * The custom {@link ILoader} to use for the file. If not specified, the importer will try to determine the loader from the file extension. | * The custom {@link ILoader} to use for the file. If not specified, the importer will try to determine the loader from the file extension. | ||||
| */ | */ | ||||
| fileHandler?: any, | |||||
| fileHandler?: ILoader, | |||||
| /** | /** | ||||
| * Query string to add to the url. Default = undefined | * Query string to add to the url. Default = undefined | ||||
| */ | */ | ||||
| */ | */ | ||||
| processRaw(res: any, options: ProcessRawOptions): Promise<any[]> | processRaw(res: any, options: ProcessRawOptions): Promise<any[]> | ||||
| addURLModifier(modifier: (url: string) => string): void | |||||
| removeURLModifier(modifier: (url: string) => string): void | |||||
| } | } | ||||
| import {Loader} from 'three' | import {Loader} from 'three' | ||||
| import {IAssetImporter} from './IAssetImporter' | |||||
| import {AnyOptions, IDisposable} from 'ts-browser-helpers' | |||||
| import {IAssetImporter, LoadFileOptions} from './IAssetImporter' | |||||
| import {IDisposable} from 'ts-browser-helpers' | |||||
| export interface ILoader<T = any, T2 = any> extends Loader, Partial<IDisposable> { | |||||
| export interface ILoader<T = any, T2 = T> extends Loader, Partial<IDisposable> { | |||||
| loadFileOptions?: LoadFileOptions | |||||
| loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<any>; | loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<any>; | ||||
| /** | /** | ||||
| * Transform after load, like convert geometry to mesh, etc. for reference see {@link DRACOLoader2} or {@link PLYLoadPlugin} | * Transform after load, like convert geometry to mesh, etc. for reference see {@link DRACOLoader2} or {@link PLYLoadPlugin} | ||||
| * @param res - result of load | * @param res - result of load | ||||
| * @param options | * @param options | ||||
| */ | */ | ||||
| transform?(res: T, options: AnyOptions): T2|Promise<T2> | |||||
| transform?(res: T, options: LoadFileOptions): T2|Promise<T2> | |||||
| } | } | ||||
| export interface IImporter { | export interface IImporter { | ||||
| ext: string[]; | ext: string[]; |
| this.setMeshoptDecoder(window.MeshoptDecoder) | this.setMeshoptDecoder(window.MeshoptDecoder) | ||||
| parser.options.meshoptDecoder = window.MeshoptDecoder | parser.options.meshoptDecoder = window.MeshoptDecoder | ||||
| } else { | } else { | ||||
| console.error('Add GLTFMeshOptPlugin(and initialize it) to viewer to enable EXT_meshopt_compression decode') | |||||
| console.error('Add GLTFMeshOptDecodePlugin(and initialize it) to viewer to enable EXT_meshopt_compression decode') | |||||
| } | } | ||||
| } | } | ||||
| import {IMaterial, IMaterialEventMap} from './IMaterial' | import {IMaterial, IMaterialEventMap} from './IMaterial' | ||||
| import {EventListener2, Object3D, Object3DEventMap, Vector3} from 'three' | |||||
| import {Box3, EventListener2, Object3D, Object3DEventMap, Sphere, Vector3} from 'three' | |||||
| import {ChangeEvent, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | import {ChangeEvent, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | ||||
| import {IGeometry, IGeometryEventMap} from './IGeometry' | import {IGeometry, IGeometryEventMap} from './IGeometry' | ||||
| import {IImportResultUserData} from '../assetmanager' | import {IImportResultUserData} from '../assetmanager' | ||||
| */ | */ | ||||
| dispose(removeFromParent?: boolean): void; | dispose(removeFromParent?: boolean): void; | ||||
| /** | |||||
| * A promise can be set by the object to indicate that the object is loading. | |||||
| * This can be used by the scene, viewer, plugins to defer actions until the object is loaded. | |||||
| */ | |||||
| _loadingPromise?: Promise<void> | |||||
| /** | |||||
| * For InstancedMesh, SkinnedMesh etc | |||||
| */ | |||||
| boundingBox?: Box3 | null | |||||
| /** | |||||
| * For InstancedMesh, SkinnedMesh etc | |||||
| */ | |||||
| boundingSphere?: Sphere | null | |||||
| /** | |||||
| * For InstancedMesh, SkinnedMesh etc | |||||
| * Computes bounding box, updating {@link boundingBox | .boundingBox} attribute. | |||||
| * @remarks Bounding boxes aren't computed by default. They need to be explicitly computed, otherwise they are `null`. | |||||
| */ | |||||
| computeBoundingBox?(): void; | |||||
| /** | |||||
| * For InstancedMesh, SkinnedMesh etc | |||||
| * Computes bounding sphere, updating {@link boundingSphere | .boundingSphere} attribute. | |||||
| * @remarks bounding spheres aren't computed by default. They need to be explicitly computed, otherwise they are `null`. | |||||
| */ | |||||
| computeBoundingSphere?(): void; | |||||
| /** | |||||
| * Set to `false` to disable propagation of any events from its children. | |||||
| */ | |||||
| acceptChildEvents?: boolean | |||||
| /** | |||||
| * Set to `false` to disable automatic call of `upgradeObject3D` when a child is added. | |||||
| */ | |||||
| autoUpgradeChildren?: boolean | |||||
| // region inherited type fixes | // region inherited type fixes | ||||
| traverse(callback: (object: IObject3D) => void): void | traverse(callback: (object: IObject3D) => void): void |
| contextLost: { | contextLost: { | ||||
| event: WebGLContextEvent | event: WebGLContextEvent | ||||
| } | } | ||||
| preRender: { | |||||
| scene: IScene | |||||
| renderToScreen: boolean | |||||
| } | |||||
| postRender: { | |||||
| scene: IScene | |||||
| renderToScreen: boolean | |||||
| } | |||||
| } | } | ||||
| export interface RendererBlitOptions {source?: Texture, viewport?: Vector4, material?: ShaderMaterial, clear?: boolean, respectColorSpace?: boolean, blending?: Blending, transparent?: boolean} | export interface RendererBlitOptions {source?: Texture, viewport?: Vector4, material?: ShaderMaterial, clear?: boolean, respectColorSpace?: boolean, blending?: Blending, transparent?: boolean} |
| import {Camera, IUniform, Object3D, PerspectiveCamera, Vector3} from 'three' | import {Camera, IUniform, Object3D, PerspectiveCamera, Vector3} from 'three' | ||||
| import {generateUiConfig, uiInput, UiObjectConfig, uiSlider, uiToggle, uiVector} from 'uiconfig.js' | |||||
| import {generateUiConfig, uiInput, uiNumber, UiObjectConfig, uiSlider, uiToggle, uiVector} from 'uiconfig.js' | |||||
| import {onChange, onChange2, onChange3, serialize} from 'ts-browser-helpers' | import {onChange, onChange2, onChange3, serialize} from 'ts-browser-helpers' | ||||
| import type {ICamera, ICameraEventMap, ICameraUserData, TCameraControlsMode} from '../ICamera' | import type {ICamera, ICameraEventMap, ICameraUserData, TCameraControlsMode} from '../ICamera' | ||||
| import {ICameraSetDirtyOptions} from '../ICamera' | import {ICameraSetDirtyOptions} from '../ICamera' | ||||
| @serialize() declare focus: number | @serialize() declare focus: number | ||||
| @onChange3(PerspectiveCamera2.prototype.setDirty) | @onChange3(PerspectiveCamera2.prototype.setDirty) | ||||
| @uiSlider('FoV Zoom', [0.001, 10], 0.001) | |||||
| @uiNumber('FoV Zoom') | |||||
| @serialize() declare zoom: number | @serialize() declare zoom: number | ||||
| @uiVector('Position', undefined, undefined, (that:PerspectiveCamera2)=>({onChange: ()=>that.setDirty()})) | @uiVector('Position', undefined, undefined, (that:PerspectiveCamera2)=>({onChange: ()=>that.setDirty()})) | ||||
| this.getWorldPosition(this._positionWorld) | this.getWorldPosition(this._positionWorld) | ||||
| iCameraCommons.setDirty.call(this, options) | iCameraCommons.setDirty.call(this, options) | ||||
| if (options?.last !== false) | if (options?.last !== false) | ||||
| this._camUi.forEach(u=>u?.uiRefresh?.(false, 'postFrame', 1)) // because camera changes a lot. so we dont want to deep refresh ui on every change | this._camUi.forEach(u=>u?.uiRefresh?.(false, 'postFrame', 1)) // because camera changes a lot. so we dont want to deep refresh ui on every change | ||||
| } | } |
| if (addToRoot) this.add(obj) | if (addToRoot) this.add(obj) | ||||
| else this.modelRoot.add(obj) | else this.modelRoot.add(obj) | ||||
| if (autoCenter && !obj.userData.isCentered && !obj.userData.pseudoCentered) { // pseudoCentered is legacy | |||||
| obj.autoCenter?.() | |||||
| } else { | |||||
| obj.userData.isCentered = true // mark as centered, so that autoCenter is not called again when file is reloaded. | |||||
| } | |||||
| if (autoScale && !obj.userData.autoScaled) { | |||||
| obj.autoScale?.(obj.userData.autoScaleRadius || autoScaleRadius) | |||||
| } else { | |||||
| obj.userData.autoScaled = true // mark as auto-scaled, so that autoScale is not called again when file is reloaded. | |||||
| } | |||||
| if (centerGeometries && !obj.userData.geometriesCentered) { | |||||
| this.centerAllGeometries(centerGeometriesKeepPosition, obj) | |||||
| obj.userData.geometriesCentered = true | |||||
| } else { | |||||
| obj.userData.geometriesCentered = true // mark as centered, so that geometry center is not called again when file is reloaded. | |||||
| const process = () => { | |||||
| if (autoCenter && !obj.userData.isCentered && !obj.userData.pseudoCentered) { // pseudoCentered is legacy | |||||
| obj.autoCenter && obj.autoCenter() | |||||
| } else { | |||||
| obj.userData.isCentered = true // mark as centered, so that autoCenter is not called again when file is reloaded. | |||||
| } | |||||
| if (autoScale && !obj.userData.autoScaled) { | |||||
| obj.autoScale && obj.autoScale(obj.userData.autoScaleRadius || autoScaleRadius) | |||||
| } else { | |||||
| obj.userData.autoScaled = true // mark as auto-scaled, so that autoScale is not called again when file is reloaded. | |||||
| } | |||||
| if (centerGeometries && !obj.userData.geometriesCentered) { | |||||
| this.centerAllGeometries(centerGeometriesKeepPosition, obj) | |||||
| obj.userData.geometriesCentered = true | |||||
| } else { | |||||
| obj.userData.geometriesCentered = true // mark as centered, so that geometry center is not called again when file is reloaded. | |||||
| } | |||||
| } | } | ||||
| if (obj._loadingPromise) obj._loadingPromise.finally(process) | |||||
| else process() | |||||
| if (license) obj.userData.license = [obj.userData.license, license].filter(v=>v).join(', ') | if (license) obj.userData.license = [obj.userData.license, license].filter(v=>v).join(', ') | ||||
| // todo check if this takes too much time with large scenes(when moving the camera and not animating), but we also need to support animations | // todo check if this takes too much time with large scenes(when moving the camera and not animating), but we also need to support animations | ||||
| const bbox = this.getBounds(false) // todo: can we use this._sceneBounds or will it have some issue with animation? | const bbox = this.getBounds(false) // todo: can we use this._sceneBounds or will it have some issue with animation? | ||||
| const size = bbox.getSize(this._v2).length() | |||||
| if (size < 0.001) { | |||||
| camera.near = camera.userData.minNearPlane ?? 0.5 | |||||
| camera.far = camera.userData.maxFarPlane ?? 1000 | |||||
| return | |||||
| } | |||||
| camera.getWorldPosition(this._v1).sub(bbox.getCenter(this._v2)) | camera.getWorldPosition(this._v1).sub(bbox.getCenter(this._v2)) | ||||
| const radius = 1.5 * bbox.getSize(this._v2).length() / 2. | |||||
| const radius = 1.5 * Math.max(0.25, size) / 2. | |||||
| const dist = this._v1.length() | const dist = this._v1.length() | ||||
| // new way | // new way | ||||
| const dist1 = Math.max(0.1, -this._v1.normalize().dot(camera.getWorldDirection(new Vector3()))) | const dist1 = Math.max(0.1, -this._v1.normalize().dot(camera.getWorldDirection(new Vector3()))) | ||||
| const near = Math.max(Math.max(camera.userData.minNearPlane ?? 0.5, 0.001), dist1 * (dist - radius)) | const near = Math.max(Math.max(camera.userData.minNearPlane ?? 0.5, 0.001), dist1 * (dist - radius)) | ||||
| const far = Math.min(Math.max(near + radius, dist1 * (dist + radius)), camera.userData.maxFarPlane ?? 1000) | |||||
| let far = Math.min(Math.max(near + radius, dist1 * (dist + radius)), camera.userData.maxFarPlane ?? 1000) | |||||
| // old way, has issues when panning very far from the camera target | // old way, has issues when panning very far from the camera target | ||||
| // const near = Math.max(camera.userData.minNearPlane ?? 0.2, dist - radius) | // const near = Math.max(camera.userData.minNearPlane ?? 0.2, dist - radius) | ||||
| // const far = Math.min(Math.max(near + 1, dist + radius), camera.userData.maxFarPlane ?? 1000) | // const far = Math.min(Math.max(near + 1, dist + radius), camera.userData.maxFarPlane ?? 1000) | ||||
| if (far < near || far - near < 0.1) { | |||||
| far = near + 0.1 | |||||
| } | |||||
| camera.near = near | camera.near = near | ||||
| camera.far = far | camera.far = far | ||||
| } | } | ||||
| // } | // } | ||||
| this.dispatchEvent({...options, type: 'update', bubbleToParent: false, camera: this}) // does not bubble | this.dispatchEvent({...options, type: 'update', bubbleToParent: false, camera: this}) // does not bubble | ||||
| this.dispatchEvent({...options, type: 'cameraUpdate', bubbleToParent: true}) // this sets dirty in the viewer | |||||
| this.dispatchEvent({...options, type: 'cameraUpdate', bubbleToParent: true, camera: this}) // this sets dirty in the viewer | |||||
| iObjectCommons.setDirty.call(this, {refreshScene: false, ...options}) | iObjectCommons.setDirty.call(this, {refreshScene: false, ...options}) | ||||
| }, | }, | ||||
| activateMain: function(this: ICamera, options: Omit<ICameraEventMap['activateMain'], 'bubbleToParent'> = {}, _internal = false, _refresh = true, canvas?: HTMLCanvasElement): void { | activateMain: function(this: ICamera, options: Omit<ICameraEventMap['activateMain'], 'bubbleToParent'> = {}, _internal = false, _refresh = true, canvas?: HTMLCanvasElement): void { |
| import {Event, Matrix4, Mesh, Vector3} from 'three' | import {Event, Matrix4, Mesh, Vector3} from 'three' | ||||
| import {IMaterial} from '../IMaterial' | import {IMaterial} from '../IMaterial' | ||||
| import {objectHasOwn} from 'ts-browser-helpers' | |||||
| import {IEvent, objectHasOwn} from 'ts-browser-helpers' | |||||
| import {IObject3D, IObject3DEventMap, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject' | import {IObject3D, IObject3DEventMap, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject' | ||||
| import {copyObject3DUserData} from '../../utils' | import {copyObject3DUserData} from '../../utils' | ||||
| import {IGeometry, IGeometryEventMap} from '../IGeometry' | import {IGeometry, IGeometryEventMap} from '../IGeometry' | ||||
| makeUiConfig: makeIObject3DUiConfig, | makeUiConfig: makeIObject3DUiConfig, | ||||
| autoCenter: function<T extends IObject3D>(this: T, setDirty = true, undo = false): T { | autoCenter: function<T extends IObject3D>(this: T, setDirty = true, undo = false): T { | ||||
| // todo use bounding sphere? | |||||
| if (undo) { | if (undo) { | ||||
| if (!this.userData.autoCentered || !this.userData._lastCenter) return this | if (!this.userData.autoCentered || !this.userData._lastCenter) return this | ||||
| if (!isFinite(this.userData._lastCenter.lengthSq())) return this | |||||
| this.position.add(this.userData._lastCenter) | this.position.add(this.userData._lastCenter) | ||||
| delete this.userData.autoCentered | delete this.userData.autoCentered | ||||
| delete this.userData.isCentered | delete this.userData.isCentered | ||||
| } else { | } else { | ||||
| const bb = new Box3B().expandByObject(this, true, true) | const bb = new Box3B().expandByObject(this, true, true) | ||||
| const center = bb.getCenter(new Vector3()) | const center = bb.getCenter(new Vector3()) | ||||
| if (!isFinite(center.lengthSq())) return this | |||||
| this.userData._lastCenter = center/* .clone()*/ | this.userData._lastCenter = center/* .clone()*/ | ||||
| this.position.sub(center) | this.position.sub(center) | ||||
| this.userData.autoCentered = true | this.userData.autoCentered = true | ||||
| dispatchEvent: (superDispatch: IObject3D['dispatchEvent']): IObject3D['dispatchEvent'] => | dispatchEvent: (superDispatch: IObject3D['dispatchEvent']): IObject3D['dispatchEvent'] => | ||||
| function(this: IObject3D, event): void { | function(this: IObject3D, event): void { | ||||
| if ((event as IEvent<any>).target && (event as IEvent<any>).target !== this && this.acceptChildEvents === false) return | |||||
| if ((event as IObject3DEventMap['objectUpdate']).bubbleToParent || this.userData?.__autoBubbleToParentEvents?.includes(event.type)) { | if ((event as IObject3DEventMap['objectUpdate']).bubbleToParent || this.userData?.__autoBubbleToParentEvents?.includes(event.type)) { | ||||
| // console.log('parent dispatch', e, this.parentRoot, this.parent) | // console.log('parent dispatch', e, this.parentRoot, this.parent) | ||||
| const pRoot = this.parentRoot || this.parent | const pRoot = this.parentRoot || this.parent | ||||
| }, | }, | ||||
| add: (superAdd: IObject3D['add']): IObject3D['add'] => | add: (superAdd: IObject3D['add']): IObject3D['add'] => | ||||
| function(this: IObject3D, ...args): IObject3D { | function(this: IObject3D, ...args): IObject3D { | ||||
| for (const a of args) iObjectCommons.upgradeObject3D.call(a, this.parentRoot || this, this.objectProcessor) | |||||
| if (this.autoUpgradeChildren !== false) { | |||||
| for (const a of args) iObjectCommons.upgradeObject3D.call(a, this.parentRoot || this, this.objectProcessor) | |||||
| } | |||||
| return superAdd.call(this, ...args) | return superAdd.call(this, ...args) | ||||
| }, | }, | ||||
| dispose: (superDispose?: IObject3D['dispose']) => | dispose: (superDispose?: IObject3D['dispose']) => | ||||
| * @param parent | * @param parent | ||||
| * @param objectProcessor | * @param objectProcessor | ||||
| */ | */ | ||||
| function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectProcessor?: IObjectProcessor): void { | |||||
| if (!this) return | |||||
| function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectProcessor?: IObjectProcessor): IObject3D { | |||||
| if (!this) return this | |||||
| // console.log('upgradeObject3D', this, parent, objectProcessor) | // console.log('upgradeObject3D', this, parent, objectProcessor) | ||||
| // if (this.__disposed) { | // if (this.__disposed) { | ||||
| // console.warn('re-init/re-add disposed object, things might not work as intended', this) | // console.warn('re-init/re-add disposed object, things might not work as intended', this) | ||||
| this.userData.uuid = this.uuid | this.userData.uuid = this.uuid | ||||
| // not checking assetType but custom var __objectSetup because its required in types sometimes, check PerspectiveCamera2 | // not checking assetType but custom var __objectSetup because its required in types sometimes, check PerspectiveCamera2 | ||||
| // if (this.assetType) return | |||||
| // if (this.assetType) return this | |||||
| if (this.userData.__objectSetup) return | |||||
| if (this.userData.__objectSetup) return this | |||||
| this.userData.__objectSetup = true | this.userData.__objectSetup = true | ||||
| if (!this.objectProcessor) this.objectProcessor = objectProcessor || this.parent?.objectProcessor || parent?.objectProcessor | if (!this.objectProcessor) this.objectProcessor = objectProcessor || this.parent?.objectProcessor || parent?.objectProcessor | ||||
| // todo: serialization? | // todo: serialization? | ||||
| const children = [...this.children] | |||||
| for (const c of children) upgradeObject3D.call(c, this) | |||||
| if (this.autoUpgradeChildren !== false) { | |||||
| const children = [...this.children] | |||||
| for (const c of children) upgradeObject3D.call(c, this) | |||||
| } | |||||
| // region Legacy | // region Legacy | ||||
| this.objectProcessor?.processObject(this) | this.objectProcessor?.processObject(this) | ||||
| return this | |||||
| } | } |
| import {Quaternion, Vector3} from 'three' | |||||
| import {EventListener2, Quaternion, Vector3} from 'three' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | import {AViewerPluginSync, ThreeViewer} from '../../viewer' | ||||
| import {UiObjectConfig} from 'uiconfig.js' | import {UiObjectConfig} from 'uiconfig.js' | ||||
| import {PopmotionPlugin} from './PopmotionPlugin' | import {PopmotionPlugin} from './PopmotionPlugin' | ||||
| import {IObject3D} from '../../core' | |||||
| import {IObject3D, IScene, ISceneEventMap} from '../../core' | |||||
| // todo make a serializable object like CameraView for proper ui state management | // todo make a serializable object like CameraView for proper ui state management | ||||
| export interface TSavedTransform { | export interface TSavedTransform { | ||||
| viewer.scene.removeEventListener('addSceneObject', this._addSceneObject) | viewer.scene.removeEventListener('addSceneObject', this._addSceneObject) | ||||
| return super.onRemove(viewer) | return super.onRemove(viewer) | ||||
| } | } | ||||
| private _addSceneObject = (e: any)=>{ | |||||
| const object = e.object as IObject3D | |||||
| private _addSceneObject: EventListener2<'addSceneObject', ISceneEventMap, IScene> = (e)=>{ | |||||
| const object = e.object | |||||
| object?.traverse && object.traverse((o: IObject3D)=>{ | object?.traverse && object.traverse((o: IObject3D)=>{ | ||||
| if (!o.userData[TransformAnimationPlugin.PluginType]) { | if (!o.userData[TransformAnimationPlugin.PluginType]) { | ||||
| o.userData[TransformAnimationPlugin.PluginType] = { | o.userData[TransformAnimationPlugin.PluginType] = { |
| import {UiObjectConfig} from 'uiconfig.js' | import {UiObjectConfig} from 'uiconfig.js' | ||||
| import {IWidget} from '../../core' | |||||
| import {IScene, ISceneEventMap, IWidget} from '../../core' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | import {AViewerPluginSync, ThreeViewer} from '../../viewer' | ||||
| import {IEvent, onChange} from 'ts-browser-helpers' | import {IEvent, onChange} from 'ts-browser-helpers' | ||||
| import {Object3D} from 'three' | |||||
| import {EventListener2, Object3D} from 'three' | |||||
| import {CameraHelper2, DirectionalLightHelper2, PointLightHelper2, SpotLightHelper2} from '../../three' | import {CameraHelper2, DirectionalLightHelper2, PointLightHelper2, SpotLightHelper2} from '../../three' | ||||
| export interface IObject3DHelper<T extends Object3D&IWidget = Object3D&IWidget>{ | export interface IObject3DHelper<T extends Object3D&IWidget = Object3D&IWidget>{ | ||||
| this._widgetRoot.clear() | this._widgetRoot.clear() | ||||
| super.onRemove(viewer) | super.onRemove(viewer) | ||||
| } | } | ||||
| private _addSceneObject = (e: any)=>{ | |||||
| private _addSceneObject: EventListener2<'addSceneObject', ISceneEventMap, IScene> = (e)=>{ | |||||
| this._createWidgets(e.object) | this._createWidgets(e.object) | ||||
| } | } | ||||
| import {Spherical, Vector3} from 'three' | |||||
| import {EventListener2, Spherical, Vector3} from 'three' | |||||
| import {IEvent, now, objectHasOwn, onChange, serialize} from 'ts-browser-helpers' | import {IEvent, now, objectHasOwn, onChange, serialize} from 'ts-browser-helpers' | ||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | import {AViewerPluginSync, ThreeViewer} from '../../viewer' | ||||
| import {uiButton, uiFolderContainer, uiInput, uiMonitor, uiToggle} from 'uiconfig.js' | import {uiButton, uiFolderContainer, uiInput, uiMonitor, uiToggle} from 'uiconfig.js' | ||||
| import {OrbitControls3} from '../../three' | import {OrbitControls3} from '../../three' | ||||
| import {IScene, ISceneEventMap} from '../../core' | |||||
| /** | /** | ||||
| * Interaction Prompt Plugin | * Interaction Prompt Plugin | ||||
| this.lastActionTime = now() | this.lastActionTime = now() | ||||
| } | } | ||||
| } | } | ||||
| private _addSceneObject = ()=>{ | |||||
| private _addSceneObject: EventListener2<'addSceneObject', ISceneEventMap, IScene> = ()=>{ | |||||
| if (this.autoStartOnObjectLoad) { | if (this.autoStartOnObjectLoad) { | ||||
| this.lastActionTime = now() - this.autoStartDelay + this.autoStartOnObjectLoadDelay | this.lastActionTime = now() - this.autoStartDelay + this.autoStartOnObjectLoadDelay | ||||
| } | } |
| import {OrthographicCamera, PerspectiveCamera} from 'three' | |||||
| import {EventListener2, OrthographicCamera, PerspectiveCamera} from 'three' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | import {AViewerPluginSync, ThreeViewer} from '../../viewer' | ||||
| import {uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js' | import {uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js' | ||||
| import {IEvent, onChange, serialize} from 'ts-browser-helpers' | |||||
| import {ICamera, ILight} from '../../core' | |||||
| import {onChange, serialize} from 'ts-browser-helpers' | |||||
| import {ICamera, ILight, IScene, ISceneEventMap} from '../../core' | |||||
| import {ProgressivePlugin} from './ProgressivePlugin' | import {ProgressivePlugin} from './ProgressivePlugin' | ||||
| export type TCamera = ICamera & (PerspectiveCamera|OrthographicCamera) | export type TCamera = ICamera & (PerspectiveCamera|OrthographicCamera) | ||||
| this.uiConfig?.uiRefresh?.(true, 'postFrame') | this.uiConfig?.uiRefresh?.(true, 'postFrame') | ||||
| } | } | ||||
| private _addSceneObject = (event: IEvent<string>)=> { | |||||
| private _addSceneObject: EventListener2<'addSceneObject', ISceneEventMap, IScene> = (event)=>{ | |||||
| event.object?.traverse((o: ILight)=>{ | event.object?.traverse((o: ILight)=>{ | ||||
| if (o && o.shadow && o.shadow.camera && o.shadow.mapSize) { | if (o && o.shadow && o.shadow.camera && o.shadow.mapSize) { | ||||
| this.trackedJitterCameras.add([o.shadow.camera as TCamera, o.shadow.mapSize]) | this.trackedJitterCameras.add([o.shadow.camera as TCamera, o.shadow.mapSize]) |
| if (pass.enabled && pass.beforeRender) pass.beforeRender(scene, scene.renderCamera, this) | if (pass.enabled && pass.beforeRender) pass.beforeRender(scene, scene.renderCamera, this) | ||||
| } | } | ||||
| this._composer.renderToScreen = renderToScreen ?? this.defaultRenderToScreen | this._composer.renderToScreen = renderToScreen ?? this.defaultRenderToScreen | ||||
| this.dispatchEvent({type: 'preRender', scene, renderToScreen: this._composer.renderToScreen}) | |||||
| this._composer.render() | this._composer.render() | ||||
| this.dispatchEvent({type: 'postRender', scene, renderToScreen: this._composer.renderToScreen}) | |||||
| this._composer.renderToScreen = true | this._composer.renderToScreen = true | ||||
| if (renderToScreen) { | if (renderToScreen) { | ||||
| this._frameCount += 1 | this._frameCount += 1 | ||||
| // required for uiconfig.js. see UiConfigMethods.getValue | // required for uiconfig.js. see UiConfigMethods.getValue | ||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | // eslint-disable-next-line @typescript-eslint/naming-convention | ||||
| _ui_isPrimitive = true | _ui_isPrimitive = true | ||||
| }(this, ...size, options) | }(this, ...size, options) | ||||
| } | } | ||||
| object.updateWorldMatrix(false, false) | object.updateWorldMatrix(false, false) | ||||
| // InstancedMesh has boundingBox = null, so it can be computed | // InstancedMesh has boundingBox = null, so it can be computed | ||||
| if ((object as any).boundingBox !== undefined) { | |||||
| if ((object as IObject3D).boundingBox !== undefined) { | |||||
| if ((object as any).boundingBox === null) { | |||||
| if ((object as IObject3D).boundingBox === null && typeof (object as IObject3D).computeBoundingBox === 'function') { | |||||
| (object as any).computeBoundingBox() | |||||
| (object as IObject3D).computeBoundingBox!() | |||||
| } | } | ||||
| Box3B._box.copy((object as any).boundingBox) | |||||
| Box3B._box.applyMatrix4(object.matrixWorld) | |||||
| if ((object as IObject3D).boundingBox !== null) { | |||||
| this.union(Box3B._box) | |||||
| Box3B._box.copy((object as IObject3D).boundingBox!) | |||||
| Box3B._box.applyMatrix4(object.matrixWorld) | |||||
| this.union(Box3B._box) | |||||
| } else { | |||||
| console.warn('Box3B - Unable to compute bounds for', object) | |||||
| } | |||||
| } else { | } else { | ||||
| Color, | Color, | ||||
| Event, | Event, | ||||
| EventDispatcher, | EventDispatcher, | ||||
| EventListener2, | |||||
| LinearSRGBColorSpace, | LinearSRGBColorSpace, | ||||
| Object3D, | Object3D, | ||||
| Quaternion, | Quaternion, | ||||
| Scene, | Scene, | ||||
| Vector2, | Vector2, | ||||
| Vector3, | Vector3, | ||||
| EventListener2, | |||||
| } from 'three' | } from 'three' | ||||
| import {Class, createCanvasElement, downloadBlob, onChange, serialize, timeout, ValOrArr} from 'ts-browser-helpers' | import {Class, createCanvasElement, downloadBlob, onChange, serialize, timeout, ValOrArr} from 'ts-browser-helpers' | ||||
| import {TViewerScreenShader} from '../postprocessing' | import {TViewerScreenShader} from '../postprocessing' | ||||
| * Use rendered gbuffer as depth-prepass / z-prepass. (Requires DepthBufferPlugin/GBufferPlugin). | * Use rendered gbuffer as depth-prepass / z-prepass. (Requires DepthBufferPlugin/GBufferPlugin). | ||||
| * Set it to true if you only have opaque objects in the scene to get better performance. | * Set it to true if you only have opaque objects in the scene to get better performance. | ||||
| * | * | ||||
| * @default false | |||||
| * | |||||
| * todo fix: It should be disabled when there are any transparent/transmissive objects with render to depth buffer enabled, see forceZPrepass | * todo fix: It should be disabled when there are any transparent/transmissive objects with render to depth buffer enabled, see forceZPrepass | ||||
| */ | */ | ||||
| zPrepass?: boolean | zPrepass?: boolean | ||||
| */ | */ | ||||
| maxRenderScale?: number | maxRenderScale?: number | ||||
| /** | |||||
| * Model Root Scale | |||||
| * @default 1 | |||||
| */ | |||||
| modelRootScale?: number | |||||
| debug?: boolean | debug?: boolean | ||||
| /** | /** | ||||
| }) | }) | ||||
| // if camera position or target changed in last frame, call setDirty on camera | // if camera position or target changed in last frame, call setDirty on camera | ||||
| this.addEventListener('preFrame', () => { // todo: move inside RootScene. | |||||
| this.addEventListener('preFrame', () => { // todo: move inside RootScene. and maybe check the world matrix and target vector change | |||||
| const cam = this._scene.mainCamera | const cam = this._scene.mainCamera | ||||
| if ( | if ( | ||||
| cam.getWorldPosition(this._tempVec).sub(this._lastCameraPosition).lengthSq() // position is in local space | cam.getWorldPosition(this._tempVec).sub(this._lastCameraPosition).lengthSq() // position is in local space | ||||
| this._lastCameraTarget.copy(this._scene.mainCamera.target) | this._lastCameraTarget.copy(this._scene.mainCamera.target) | ||||
| this._scene.mainCamera.getWorldQuaternion(this._lastCameraQuat) | this._scene.mainCamera.getWorldQuaternion(this._lastCameraQuat) | ||||
| }) | }) | ||||
| this._scene.modelRoot.scale.setScalar(options.modelRootScale ?? 1) | |||||
| // render manager | // render manager | ||||
| this.resize() // this is also required in case the browwser doesnt support/fire observer | this.resize() // this is also required in case the browwser doesnt support/fire observer | ||||
| } | } | ||||
| // todo make an example for this. | |||||
| // todo make a constructor parameter for renderSize | // todo make a constructor parameter for renderSize | ||||
| // todo make getRenderSize or get renderSize | // todo make getRenderSize or get renderSize | ||||
| /** | /** | ||||
| * 'fill': The canvas is stretched to completely fill the container, ignoring its aspect ratio. | * 'fill': The canvas is stretched to completely fill the container, ignoring its aspect ratio. | ||||
| * 'scale-down': The canvas is scaled down to fit within the container while maintaining its aspect ratio, but it won't be scaled up if it's smaller than the container. | * 'scale-down': The canvas is scaled down to fit within the container while maintaining its aspect ratio, but it won't be scaled up if it's smaller than the container. | ||||
| * 'none': container size is ignored, but devicePixelRatio is used | * 'none': container size is ignored, but devicePixelRatio is used | ||||
| * | |||||
| * Check the example for more details - https://threepipe.org/examples/#viewer-render-size/ | |||||
| * @param size - The size to set the render to. The canvas will render to this size. | * @param size - The size to set the render to. The canvas will render to this size. | ||||
| * @param mode - 'contain', 'cover', 'fill', 'scale-down' or 'none'. Default is 'contain'. | * @param mode - 'contain', 'cover', 'fill', 'scale-down' or 'none'. Default is 'contain'. | ||||
| * @param devicePixelRatio - typically set to `window.devicePixelRatio`, or `Math.min(1.5, window.devicePixelRatio)` for performance. Use this only when size is derived from dom elements. | * @param devicePixelRatio - typically set to `window.devicePixelRatio`, or `Math.min(1.5, window.devicePixelRatio)` for performance. Use this only when size is derived from dom elements. | ||||
| this.enabled = false | this.enabled = false | ||||
| } | } | ||||
| // private _addSceneObject = (e: IEvent<any>) => { | |||||
| // private _addSceneObject: EventListener2<'addSceneObject', ISceneEventMap, IScene> = (e)=>{ | |||||
| // if (!e || !e.object) return | // if (!e || !e.object) return | ||||
| // const config = e.object.__importedViewerConfig // this is set in gltf.ts when gltf file is imported. This is done here so that scene settings are applied whenever the imported object is added to scene. | // const config = e.object.__importedViewerConfig // this is set in gltf.ts when gltf file is imported. This is done here so that scene settings are applied whenever the imported object is added to scene. | ||||
| // if (!config) return | // if (!config) return |
| Another example with Vue SFC(Single file component): [Example: vue-sfc-sample](https://threepipe.org/examples/#vue-sfc-sample/) | Another example with Vue SFC(Single file component): [Example: vue-sfc-sample](https://threepipe.org/examples/#vue-sfc-sample/) | ||||
| ### Svelte | |||||
| ### Svelte 4 | |||||
| A sample [svelte](https://svelte.dev/) component in js to render a model with an environment map. | |||||
| A sample [svelte](https://svelte.dev/) 4 component in js to render a model with an environment map. | |||||
| ```html | ```html | ||||
| <script> | <script> |
| - [@threepipe/plugin-gltf-transform](../package/plugin-gltf-transform) - Plugin to transform gltf models like adding draco compression while exporting gltf files. | - [@threepipe/plugin-gltf-transform](../package/plugin-gltf-transform) - Plugin to transform gltf models like adding draco compression while exporting gltf files. | ||||
| - [@threepipe/plugins-extra-importers](../package/plugins-extra-importers) - Plugin for loading more file types supported by various types of loaders in three.js. | - [@threepipe/plugins-extra-importers](../package/plugins-extra-importers) - Plugin for loading more file types supported by various types of loaders in three.js. | ||||
| - [@threepipe/plugin-network](../package/plugin-network) - Network/Cloud related plugin implementations for Threepipe - `AWSClientPlugin` and `TransfrSharePlugin`. | - [@threepipe/plugin-network](../package/plugin-network) - Network/Cloud related plugin implementations for Threepipe - `AWSClientPlugin` and `TransfrSharePlugin`. | ||||
| - [@threepipe/plugin-blend-importer](../package/plugin-blend-importer) - Blender to add support for loading .blend file (WIP) | |||||
| - [@threepipe/plugin-gaussian-splatting](../package/plugin-gaussian-splatting) - Gaussian Splatting plugin for loading and rendering splat files (WIP) | |||||
| - [@threepipe/plugin-blend-importer](../package/plugin-blend-importer) - Add support for loading .blend file. (Partial/WIP) ([Blender](https://www.blender.org/)) | |||||
| - [@threepipe/plugin-gaussian-splatting](../package/plugin-gaussian-splatting) - [3D Gaussian Splatting](https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/) plugin for loading and rendering splat files | |||||
| - [@threepipe/plugin-svg-renderer](../package/plugin-svg-renderer) - Add support for exporting 3d scene as SVG (WIP) using [three-svg-renderer](https://www.npmjs.com/package/three-svg-renderer). | - [@threepipe/plugin-svg-renderer](../package/plugin-svg-renderer) - Add support for exporting 3d scene as SVG (WIP) using [three-svg-renderer](https://www.npmjs.com/package/three-svg-renderer). |