| - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | ||||
| - [KTX2LoadPlugin](#ktx2loadplugin) - Add support for loading .ktx2 files | - [KTX2LoadPlugin](#ktx2loadplugin) - Add support for loading .ktx2 files | ||||
| - [KTXLoadPlugin](#ktxloadplugin) - Add support for loading .ktx files | - [KTXLoadPlugin](#ktxloadplugin) - Add support for loading .ktx files | ||||
| - [SimplifyModifierPlugin](#simplifymodifierplugin) - Boilerplate for plugin to simplify geometries | |||||
| - [MeshOptSimplifyModifierPlugin](#meshoptsimplifymodifierplugin) - Simplify geometries using meshoptimizer library | |||||
| - [Packages](#threepipe-packages) | - [Packages](#threepipe-packages) | ||||
| - [@threepipe/plugin-tweakpane](#threepipeplugin-tweakpane) Tweakpane UI Plugin | - [@threepipe/plugin-tweakpane](#threepipeplugin-tweakpane) Tweakpane UI Plugin | ||||
| - [@threepipe/plugin-blueprintjs](#threepipeplugin-blueprintjs) BlueprintJs UI Plugin | - [@threepipe/plugin-blueprintjs](#threepipeplugin-blueprintjs) BlueprintJs UI Plugin | ||||
| const texture = await viewer.load('file.ktx') | const texture = await viewer.load('file.ktx') | ||||
| ``` | ``` | ||||
| ## SimplifyModifierPlugin | |||||
| [Example](https://threepipe.org/examples/#simplify-modifier-plugin/) — | |||||
| [Source Code](./src/plugins/extras/SimplifyModifierPlugin.ts) — | |||||
| [API Reference](https://threepipe.org/docs/classes/SimplifyModifierPlugin.html) | |||||
| Boilerplate for implementing a plugin for simplifying geometries. | |||||
| This is a base class and cannot be used directly. | |||||
| A sample to use it: | |||||
| ```typescript | |||||
| class SimplifyModifierPluginImpl extends SimplifyModifierPlugin { | |||||
| protected _simplify(geometry: IGeometry, count: number) { | |||||
| return new SimplifyModifier().modify(geometry, count) as IGeometry | |||||
| } | |||||
| } | |||||
| const plugin = viewer.addPluginSync(new SimplifyModifierPluginImpl()) | |||||
| const root = await viewer.load('file.glb') | |||||
| plugin.simplifyAll(root, {factor: 0.75}) | |||||
| ``` | |||||
| Check the [example](https://threepipe.org/examples/#simplify-modifier-plugin/) for full implementation. | |||||
| ## MeshOptSimplifyModifierPlugin | |||||
| [Example](https://threepipe.org/examples/#meshopt-simplify-modifier-plugin/) — | |||||
| [Source Code](./src/plugins/extras/MeshOptSimplifyModifierPlugin.ts) — | |||||
| [API Reference](https://threepipe.org/docs/classes/MeshOptSimplifyModifierPlugin.html) | |||||
| Simplify modifier using [meshoptimizer](https://github.com/zeux/meshoptimizer) library. It Loads the library at runtime from a customisable CDN URL. | |||||
| Note: It does not guarantee that the geometry will be simplified to the exact target count. | |||||
| ```typescript | |||||
| const simplifyModifier = viewer.addPluginSync(new MeshOptSimplifyModifierPlugin()) | |||||
| const root = await viewer.load('file.glb') | |||||
| simplifyModifier.simplifyAll(root, {factor: 0.75}) | |||||
| ``` | |||||
| # @threepipe Packages | # @threepipe Packages | ||||
| Additional plugins can be found in the [plugins](plugins/) directory. | Additional plugins can be found in the [plugins](plugins/) directory. |
| <li><a href="./device-orientation-controls-plugin/">Device Orientation Controls Plugin (Gyroscope) </a></li> | <li><a href="./device-orientation-controls-plugin/">Device Orientation Controls Plugin (Gyroscope) </a></li> | ||||
| <li><a href="./pointer-lock-controls-plugin/">Pointer Lock(FPS) Controls Plugin </a></li> | <li><a href="./pointer-lock-controls-plugin/">Pointer Lock(FPS) Controls Plugin </a></li> | ||||
| <li><a href="./three-first-person-controls-plugin/">Three First Person(look around) Controls Plugin </a></li> | <li><a href="./three-first-person-controls-plugin/">Three First Person(look around) Controls Plugin </a></li> | ||||
| <li><a href="./simplify-modifier-plugin/">Simplify Modifier Plugin </a></li> | |||||
| <li><a href="./meshopt-simplify-modifier-plugin/">MeshOpt Simplify Modifier Plugin </a></li> | |||||
| </ul> | </ul> | ||||
| <h2 class="category">Import</h2> | <h2 class="category">Import</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./image-snapshot-export/">PNG, JPEG, WEBP Export<br/>(Image Snapshot) </a></li> | <li><a href="./image-snapshot-export/">PNG, JPEG, WEBP Export<br/>(Image Snapshot) </a></li> | ||||
| <li><a href="./render-target-export/">EXR, PNG, JPEG, WEBP Export<br/>(Render Target Export) </a></li> | <li><a href="./render-target-export/">EXR, PNG, JPEG, WEBP Export<br/>(Render Target Export) </a></li> | ||||
| <li><a href="./glb-export/">GLB Export </a></li> | <li><a href="./glb-export/">GLB Export </a></li> | ||||
| <li><a href="./pmat-material-export/">PMAT Material export </a></li> | |||||
| <li><a href="./pmat-material-export/">PMAT Material Export </a></li> | |||||
| <li><a href="./svg-geometry-playground/">SVG Geometry Playground </a></li> | <li><a href="./svg-geometry-playground/">SVG Geometry Playground </a></li> | ||||
| </ul> | </ul> | ||||
| <h2 class="category">UI Config</h2> | <h2 class="category">UI Config</h2> |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>MeshOpt Simplify Modifier 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, IObject3D, MeshOptSimplifyModifierPlugin, PickingPlugin, ThreeViewer} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| plugins: [PickingPlugin], | |||||
| }) | |||||
| const simplify = viewer.addPluginSync(MeshOptSimplifyModifierPlugin) | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', { | |||||
| setBackground: true, | |||||
| }) | |||||
| const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| result?.traverse((obj) => { | |||||
| obj.materials?.map(m=>m.wireframe = true) | |||||
| }) | |||||
| createSimpleButtons({ | |||||
| ['Simplify']: async(_: HTMLButtonElement) => { | |||||
| await simplify.simplifyAll(result, {factor: 0.5}) | |||||
| }, | |||||
| }) | |||||
| ui.setupPluginUi(MeshOptSimplifyModifierPlugin) | |||||
| ui.setupPluginUi(PickingPlugin) | |||||
| } | |||||
| init().finally(_testFinish) |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Simplify Modifier 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": { | |||||
| "three": "./../../dist/index.mjs", | |||||
| "three/examples/jsm/modifiers/SimplifyModifier.js": "https://cdn.jsdelivr.net/gh/repalash/three.js-modded/examples/jsm/modifiers/SimplifyModifier.js", | |||||
| "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, | |||||
| generateUiFolder, | |||||
| IGeometry, | |||||
| IMaterial, | |||||
| IObject3D, | |||||
| PickingPlugin, | |||||
| SimplifyModifierPlugin, | |||||
| ThreeViewer, | |||||
| } from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| import {SimplifyModifier} from 'three/examples/jsm/modifiers/SimplifyModifier.js' | |||||
| import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | |||||
| class SimplifyModifierPluginImpl extends SimplifyModifierPlugin { | |||||
| protected _simplify(geometry: IGeometry, count: number) { | |||||
| const res = new SimplifyModifier().modify(geometry, count) as IGeometry | |||||
| res.computeVertexNormals() | |||||
| return res | |||||
| } | |||||
| uiConfig = generateUiFolder('Simplify Modifier', this) | |||||
| } | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| plugins: [PickingPlugin], | |||||
| }) | |||||
| const simplify = viewer.addPluginSync(SimplifyModifierPluginImpl) | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', { | |||||
| setBackground: true, | |||||
| }) | |||||
| const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| const mats: IMaterial[] = [] | |||||
| result?.traverse((obj) => { | |||||
| obj.materials?.map(m=>{ | |||||
| if (!m) return | |||||
| m.wireframe = true | |||||
| mats.push(m) | |||||
| }) | |||||
| }) | |||||
| createSimpleButtons({ | |||||
| ['Simplify']: async(_: HTMLButtonElement) => { | |||||
| await simplify.simplifyAll(result, {factor: 0.5}) | |||||
| }, | |||||
| }) | |||||
| ui.setupPluginUi(SimplifyModifierPluginImpl) | |||||
| mats.forEach(m=>ui.appendChild(m.uiConfig, {expanded: false})) | |||||
| } | |||||
| init().finally(_testFinish) |
| HemisphereLight, | HemisphereLight, | ||||
| KTX2LoadPlugin, | KTX2LoadPlugin, | ||||
| KTXLoadPlugin, | KTXLoadPlugin, | ||||
| MeshOptSimplifyModifierPlugin, | |||||
| NoiseBumpMaterialPlugin, | NoiseBumpMaterialPlugin, | ||||
| NormalBufferPlugin, | NormalBufferPlugin, | ||||
| Object3DGeneratorPlugin, | Object3DGeneratorPlugin, | ||||
| RenderTargetPreviewPlugin, | RenderTargetPreviewPlugin, | ||||
| Rhino3dmLoadPlugin, | Rhino3dmLoadPlugin, | ||||
| SceneUiConfigPlugin, | SceneUiConfigPlugin, | ||||
| SSAOPlugin, | |||||
| STLLoadPlugin, | STLLoadPlugin, | ||||
| ThreeFirstPersonControlsPlugin, | ThreeFirstPersonControlsPlugin, | ||||
| ThreeViewer, | ThreeViewer, | ||||
| TonemapPlugin, | TonemapPlugin, | ||||
| TransformControlsPlugin, | TransformControlsPlugin, | ||||
| UnsignedByteType, | |||||
| USDZLoadPlugin, | USDZLoadPlugin, | ||||
| ViewerUiConfigPlugin, | ViewerUiConfigPlugin, | ||||
| VignettePlugin, | VignettePlugin, | ||||
| new VignettePlugin(false), | new VignettePlugin(false), | ||||
| new ChromaticAberrationPlugin(false), | new ChromaticAberrationPlugin(false), | ||||
| new FilmicGrainPlugin(false), | new FilmicGrainPlugin(false), | ||||
| new SSAOPlugin(UnsignedByteType, 1), | |||||
| KTX2LoadPlugin, | KTX2LoadPlugin, | ||||
| KTXLoadPlugin, | KTXLoadPlugin, | ||||
| PLYLoadPlugin, | PLYLoadPlugin, | ||||
| DeviceOrientationControlsPlugin, | DeviceOrientationControlsPlugin, | ||||
| PointerLockControlsPlugin, | PointerLockControlsPlugin, | ||||
| ThreeFirstPersonControlsPlugin, | ThreeFirstPersonControlsPlugin, | ||||
| new MeshOptSimplifyModifierPlugin(false), // will auto-initialize on first use. | |||||
| // new BasicSVGRendererPlugin(false, true), | |||||
| ...extraImportPlugins, | ...extraImportPlugins, | ||||
| ]) | ]) | ||||
| editor.loadPlugins({ | editor.loadPlugins({ | ||||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin, TweakpaneUiPlugin], | ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin, TweakpaneUiPlugin], | ||||
| ['Scene']: [ContactShadowGroundPlugin], | ['Scene']: [ContactShadowGroundPlugin], | ||||
| ['Interaction']: [HierarchyUiPlugin, TransformControlsPlugin, PickingPlugin, Object3DGeneratorPlugin, GeometryGeneratorPlugin, EditorViewWidgetPlugin, Object3DWidgetsPlugin], | |||||
| ['Interaction']: [HierarchyUiPlugin, TransformControlsPlugin, PickingPlugin, Object3DGeneratorPlugin, GeometryGeneratorPlugin, EditorViewWidgetPlugin, Object3DWidgetsPlugin, MeshOptSimplifyModifierPlugin], | |||||
| ['GBuffer']: [GBufferPlugin, DepthBufferPlugin, NormalBufferPlugin], | ['GBuffer']: [GBufferPlugin, DepthBufferPlugin, NormalBufferPlugin], | ||||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin, ChromaticAberrationPlugin, FilmicGrainPlugin], | |||||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, SSAOPlugin, FrameFadePlugin, VignettePlugin, ChromaticAberrationPlugin, FilmicGrainPlugin], | |||||
| ['Export']: [CanvasSnapshotPlugin], | ['Export']: [CanvasSnapshotPlugin], | ||||
| ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | ||||
| ['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin], | ['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin], |
| import {ThreeViewer} from '../../viewer' | |||||
| import {BufferAttribute, BufferGeometry} from 'three' | |||||
| import {IGeometry, iGeometryCommons} from '../../core' | |||||
| import {toIndexedGeometry} from '../../three' | |||||
| import {SimplifyModifierPlugin} from './SimplifyModifierPlugin' | |||||
| import {uiFolderContainer, uiNumber, uiToggle} from 'uiconfig.js' | |||||
| /** | |||||
| * Simplify modifier using [meshoptimizer](https://github.com/zeux/meshoptimizer) library. | |||||
| * Loads the library at runtime from a customisable cdn url. | |||||
| */ | |||||
| @uiFolderContainer('Simplify Modifier (meshopt)') | |||||
| export class MeshOptSimplifyModifierPlugin extends SimplifyModifierPlugin { | |||||
| public static readonly PluginType = 'MeshOptSimplifyModifierPlugin' | |||||
| constructor(initialize = true) { | |||||
| super() | |||||
| // todo: check if compatible? | |||||
| if (initialize) this.initialize() | |||||
| } | |||||
| get initialized() { | |||||
| return !!window.MeshoptSimplifier | |||||
| } | |||||
| // static SIMPLIFIER_URL = 'https://cdn.jsdelivr.net/gh/zeux/meshoptimizer@master/js/meshopt_simplifier.module.js' | |||||
| static SIMPLIFIER_URL = 'https://unpkg.com/meshoptimizer@0.20.0/meshopt_simplifier.module.js' | |||||
| onAdded(viewer: ThreeViewer) { | |||||
| super.onAdded(viewer) | |||||
| } | |||||
| protected _initializing?: Promise<void> = undefined | |||||
| async initialize() { | |||||
| if (this.initialized) return | |||||
| if (this._initializing) return await this._initializing | |||||
| const s = document.createElement('script') | |||||
| s.type = 'module' | |||||
| const ev = Math.random().toString(36).substring(7) | |||||
| s.innerHTML = ` | |||||
| import { MeshoptSimplifier } from '${MeshOptSimplifyModifierPlugin.SIMPLIFIER_URL}'; | |||||
| MeshoptSimplifier.ready.then(() => { | |||||
| window.MeshoptSimplifier = MeshoptSimplifier; | |||||
| window.dispatchEvent(new CustomEvent('${ev}')) | |||||
| }); | |||||
| ` | |||||
| this._initializing = new Promise<void>((res) => { | |||||
| const l = () => { | |||||
| window.removeEventListener(ev, l) | |||||
| res() // todo timeout? | |||||
| } | |||||
| window.addEventListener(ev, l) | |||||
| document.head.appendChild(s) // todo remove later? | |||||
| // this._script = s | |||||
| }) | |||||
| } | |||||
| @uiNumber() | |||||
| errorThreshold = 0.5 | |||||
| @uiToggle() | |||||
| lockBorder = false | |||||
| protected _simplify(geometry: BufferGeometry, count: number): IGeometry { | |||||
| if (!this.initialized) throw new Error('MeshOptSimplifyModifierPlugin not initialized') | |||||
| if (!geometry.index) { | |||||
| geometry = toIndexedGeometry(geometry) | |||||
| } else { | |||||
| geometry = geometry.clone() | |||||
| } | |||||
| const srcIndexArray = geometry.index!.array | |||||
| const srcPositionArray = geometry.attributes.position.array | |||||
| const factor = count / geometry.attributes.position.count | |||||
| // console.log(factor) | |||||
| // const targetCount = count * 3 | |||||
| const targetCount = 3 * Math.floor(factor * srcIndexArray.length / 3) | |||||
| // console.log('srcCount', srcIndexArray.length / 3, 'targetCount', targetCount / 3) | |||||
| // const errorThresh = 1e-2 | |||||
| const [dstIndexArray, error] = window.MeshoptSimplifier.simplify( | |||||
| srcIndexArray, | |||||
| srcPositionArray, | |||||
| 3, | |||||
| targetCount, | |||||
| this.errorThreshold, | |||||
| this.lockBorder ? ['LockBorder'] : [], | |||||
| ) | |||||
| console.log('srcCount', srcIndexArray.length / 3, 'destCount', dstIndexArray.length / 3) | |||||
| if (error) { | |||||
| console.error('Simplify error', error) | |||||
| // return geometry // todo | |||||
| } | |||||
| // (geometry.index!.array as Uint32Array).set(dstIndexArray) | |||||
| geometry.setIndex(new BufferAttribute(new Uint32Array(dstIndexArray), 1)) | |||||
| // geometry.index!.needsUpdate = true | |||||
| // geometry.setDrawRange(0, dstIndexArray.length) | |||||
| return iGeometryCommons.upgradeGeometry.call(geometry.toNonIndexed()) | |||||
| } | |||||
| } | |||||
| declare global{ | |||||
| interface Window{ | |||||
| MeshoptSimplifier?: any | |||||
| } | |||||
| } |
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {PickingPlugin} from '../interaction/PickingPlugin' | |||||
| import {uiButton, uiSlider} from 'uiconfig.js' | |||||
| import {IGeometry, IObject3D} from '../../core' | |||||
| import {ValOrArr} from 'ts-browser-helpers' | |||||
| import {Vector3} from 'three' | |||||
| export interface SimplifyOptions{ | |||||
| /** | |||||
| * Number of vertices to remove. | |||||
| * Factor is not used when count is set. | |||||
| */ | |||||
| count?: number | |||||
| /** | |||||
| * Factor of vertices to remove. eg 0.5 will remove half of the vertices. | |||||
| */ | |||||
| factor?: number | |||||
| /** | |||||
| * Replace the geometry with the simplified version in all meshes that use it. | |||||
| */ | |||||
| replace?: boolean | |||||
| /** | |||||
| * Displace the simplified geometry in the scene. Only used when replace is true | |||||
| * If set to true, the geometry will be disposed when replaced. | |||||
| * Default is false. | |||||
| * This will automatically be done when disposeOnIdle is not false in the geometry.userData. | |||||
| */ | |||||
| disposeOnReplace?: boolean | |||||
| } | |||||
| /** | |||||
| * Boilerplate for implementing a plugin for simplifying geometries. | |||||
| * This is a base class and cannot be used directly. | |||||
| * See {@link MeshOptSimplifyModifierPlugin} the [simplify-modifier-plugin](https://threepipe.org/examples/#simplify-modifier-plugin) example for a sample implementation. | |||||
| */ | |||||
| export abstract class SimplifyModifierPlugin extends AViewerPluginSync<''> { | |||||
| public static readonly PluginType: string = 'SimplifyModifierPlugin' | |||||
| enabled = true | |||||
| toJSON: any = undefined | |||||
| constructor() { | |||||
| super() | |||||
| } | |||||
| get initialized() { return true } | |||||
| async initialize() {return} | |||||
| private _pickingPlugin?: PickingPlugin | |||||
| onAdded(viewer: ThreeViewer) { | |||||
| super.onAdded(viewer) | |||||
| this._pickingPlugin = viewer.getPlugin(PickingPlugin) | |||||
| } | |||||
| /** | |||||
| * Factor of vertices to remove. eg 0.5 will remove half of the vertices. | |||||
| * Default is 0.5 | |||||
| * This is used when no factor or count is provided in the options to simplifyGeometry or simplifyGeometries. | |||||
| */ | |||||
| @uiSlider('Simplify Factor', [0, 1]) | |||||
| simplifyFactor = 0.5 | |||||
| simplifyGeometries(geometry?: ValOrArr<IGeometry>, options?: SimplifyOptions) { | |||||
| if (!geometry) { | |||||
| const selected = this._pickingPlugin?.getSelectedObject() | |||||
| const geom: IGeometry[] = [] | |||||
| selected?.traverse((o) => { | |||||
| if (o.geometry && !geom.includes(o.geometry)) geom.push(o.geometry) | |||||
| }) | |||||
| geometry = geom | |||||
| if (!geometry || !geometry.length) return | |||||
| } | |||||
| if (!Array.isArray(geometry)) geometry = [geometry] | |||||
| const res: IGeometry[] = [] | |||||
| for (const g of geometry) { | |||||
| res.push(this.simplifyGeometry(g, options)!) | |||||
| } | |||||
| return res | |||||
| } | |||||
| simplifyGeometry(geometry?: IGeometry, { | |||||
| factor, | |||||
| count, | |||||
| replace = true, | |||||
| disposeOnReplace = false, | |||||
| }: SimplifyOptions = {}): IGeometry|undefined { | |||||
| if (!geometry) { | |||||
| const selected = this._pickingPlugin?.getSelectedObject() | |||||
| geometry = selected?.geometry | |||||
| if (!geometry) return undefined | |||||
| } | |||||
| if (!geometry.attributes.position) { | |||||
| this._viewer?.console.error('SimplifyModifierPlugin: Geometry does not have position attribute', geometry) | |||||
| return geometry | |||||
| } | |||||
| factor = factor || this.simplifyFactor | |||||
| count = count || geometry.attributes.position.count * factor | |||||
| if (!geometry.boundingBox) geometry.computeBoundingBox() | |||||
| const simplified = this._simplify(geometry, count) | |||||
| simplified.computeBoundingBox() | |||||
| simplified.computeBoundingSphere() | |||||
| simplified.computeVertexNormals() | |||||
| const bbox = simplified.boundingBox | |||||
| const size = bbox!.getSize(new Vector3()) | |||||
| if (!isFinite(size.x) || !isFinite(size.y) || !isFinite(size.z)) { | |||||
| this._viewer?.console.error('SimplifyModifierPlugin: Unable to simplify', geometry, simplified, size) | |||||
| return geometry | |||||
| } | |||||
| const oldBB = geometry.boundingBox | |||||
| const oldSize = oldBB!.getSize(new Vector3()) | |||||
| const diff = size.clone().sub(oldSize) | |||||
| const diffPerc = diff.clone().divide(oldSize) | |||||
| if (diffPerc.lengthSq() > 0.001) { | |||||
| // todo: add option to skip this | |||||
| console.warn('Simplify', geometry, simplified, bbox, oldBB, size, oldSize, diff, diffPerc) | |||||
| } | |||||
| // simplified.setDirty() | |||||
| if (!replace) return simplified | |||||
| // not working? | |||||
| // geometry.copy(simplified) | |||||
| // geometry.setDirty() | |||||
| // simplified.dispose() | |||||
| const meshes = geometry.appliedMeshes | |||||
| if (!meshes) { | |||||
| console.error('No meshes found for geometry, cannot replace', geometry) | |||||
| return simplified | |||||
| } | |||||
| for (const mesh of meshes) { | |||||
| mesh.geometry = simplified | |||||
| } | |||||
| if (disposeOnReplace) { | |||||
| geometry.dispose(true) | |||||
| } | |||||
| return simplified | |||||
| } | |||||
| /** | |||||
| * Sample for three.js addons SimplifyModifier: | |||||
| * ` | |||||
| * import {SimplifyModifier} from 'three/examples/jsm/modifiers/SimplifyModifier' | |||||
| * protected _simplify(geometry: IGeometry, count: number): IGeometry { | |||||
| * const modifier = new SimplifyModifier() | |||||
| * return modifier.modify(geometry, count) as IGeometry | |||||
| * } | |||||
| * ` | |||||
| * @param geometry | |||||
| * @param count | |||||
| */ | |||||
| protected abstract _simplify(geometry: IGeometry, count: number): IGeometry | |||||
| @uiButton('Simplify All') | |||||
| async simplifyAll(root?: IObject3D, options?: SimplifyOptions) { | |||||
| if (!root && this._viewer) root = this._viewer.scene.modelRoot | |||||
| if (!root) { | |||||
| console.error('SimplifyModifierPlugin: No root found') | |||||
| return | |||||
| } | |||||
| if (!this.initialized) { | |||||
| await this.initialize() | |||||
| if (!this.initialized) { | |||||
| this._viewer?.console.error('SimplifyModifierPlugin cannot be initialized') | |||||
| return | |||||
| } | |||||
| } | |||||
| const geometries: IGeometry[] = [] | |||||
| root.traverse((o) => { | |||||
| if (o.geometry && !geometries.includes(o.geometry)) geometries.push(o.geometry) | |||||
| }) | |||||
| if (!geometries.length) { | |||||
| console.error('SimplifyModifierPlugin: No geometries found') | |||||
| return | |||||
| } | |||||
| return this.simplifyGeometries(geometries, options) | |||||
| } | |||||
| @uiButton('Simplify Selected') | |||||
| async simplifySelected() { | |||||
| if (!this._viewer) return | |||||
| if (!this.initialized) { | |||||
| await this.initialize() | |||||
| if (!this.initialized) { | |||||
| await this._viewer.dialog.alert('SimplifyModifierPlugin cannot be initialized') | |||||
| return | |||||
| } | |||||
| } | |||||
| const selected = this._pickingPlugin?.getSelectedObject() | |||||
| if (!selected) { | |||||
| await this._viewer.dialog.alert('Nothing Selected') | |||||
| return | |||||
| } | |||||
| let doAll = false | |||||
| if (!selected.geometry) doAll = true | |||||
| else if (selected.children.length === 0) doAll = true | |||||
| if (!doAll) { | |||||
| const resp = await this._viewer.dialog.confirm('Simplify all in hierarchy?') | |||||
| if (resp) doAll = true | |||||
| } | |||||
| if (doAll) { | |||||
| this.simplifyGeometries() | |||||
| } else { | |||||
| this.simplifyGeometry(selected.geometry) | |||||
| } | |||||
| } | |||||
| } |
| export {Object3DWidgetsPlugin} from './extras/Object3DWidgetsPlugin' | export {Object3DWidgetsPlugin} from './extras/Object3DWidgetsPlugin' | ||||
| export {Object3DGeneratorPlugin} from './extras/Object3DGeneratorPlugin' | export {Object3DGeneratorPlugin} from './extras/Object3DGeneratorPlugin' | ||||
| export {ContactShadowGroundPlugin} from './extras/ContactShadowGroundPlugin' | export {ContactShadowGroundPlugin} from './extras/ContactShadowGroundPlugin' | ||||
| export {SimplifyModifierPlugin} from './extras/SimplifyModifierPlugin' | |||||
| export {MeshOptSimplifyModifierPlugin} from './extras/MeshOptSimplifyModifierPlugin' |