| @@ -2,7 +2,7 @@ | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Extra importer plugins</title> | |||
| <title>Geometry Generator Plugin</title> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <!-- Import maps polyfill --> | |||
| <!-- Remove this when import maps will be widely supported --> | |||
| @@ -1,4 +1,4 @@ | |||
| import {_testFinish, CameraViewPlugin, PickingPlugin, ThreeViewer} from 'threepipe' | |||
| import {_testFinish, CameraViewPlugin, Object3DGeneratorPlugin, PickingPlugin, ThreeViewer} from 'threepipe' | |||
| import {GeometryGeneratorPlugin} from '@threepipe/plugin-geometry-generator' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| @@ -7,7 +7,7 @@ async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: true, | |||
| plugins: [PickingPlugin, CameraViewPlugin], | |||
| plugins: [PickingPlugin, CameraViewPlugin, Object3DGeneratorPlugin], | |||
| }) | |||
| const generator = viewer.addPluginSync(GeometryGeneratorPlugin) | |||
| @@ -16,12 +16,14 @@ async function init() { | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| console.log(generator.generators) | |||
| const sphere = generator.generateObject('sphere', {radius: 0.5, widthSegments: 32, heightSegments: 32}) | |||
| viewer.scene.addObject(sphere) | |||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| ui.setupPluginUi(GeometryGeneratorPlugin) | |||
| ui.setupPluginUi(PickingPlugin) | |||
| ui.setupPluginUi(Object3DGeneratorPlugin) | |||
| } | |||
| @@ -0,0 +1,38 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Object3D Generator 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", | |||
| "threepipe": "./../../dist/index.mjs", | |||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs", | |||
| "@threepipe/plugin-tweakpane-editor": "./../../plugins/tweakpane-editor/dist/index.mjs" | |||
| } | |||
| } | |||
| </script> | |||
| <style id="example-style"> | |||
| html, body, #canvas-container, #mcanvas { | |||
| width: 100%; | |||
| height: 100%; | |||
| margin: 0; | |||
| overflow: hidden; | |||
| } | |||
| </style> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,44 @@ | |||
| import { | |||
| _testFinish, | |||
| CameraViewPlugin, | |||
| Object3DGeneratorPlugin, | |||
| Object3DWidgetsPlugin, | |||
| PickingPlugin, | |||
| ThreeViewer, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| import {HierarchyUiPlugin} from '@threepipe/plugin-tweakpane-editor' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: true, | |||
| plugins: [PickingPlugin, CameraViewPlugin, Object3DWidgetsPlugin, HierarchyUiPlugin], | |||
| }) | |||
| const generator = viewer.addPluginSync(Object3DGeneratorPlugin) | |||
| viewer.scene.setBackgroundColor('#444466') | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| console.log(generator.generators) | |||
| const object = generator.generate('light-directional', {color: 0x00ff00}) | |||
| console.log(object) | |||
| viewer.getPlugin(PickingPlugin)?.setSelectedObject(object) | |||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| ui.setupPluginUi(Object3DGeneratorPlugin)!.expanded = true | |||
| ui.setupPluginUi(HierarchyUiPlugin) | |||
| ui.setupPluginUi(PickingPlugin) | |||
| } | |||
| init().then(_testFinish) | |||
| @@ -0,0 +1,37 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Object3D Widgets 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", | |||
| "threepipe": "./../../dist/index.mjs", | |||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||
| } | |||
| } | |||
| </script> | |||
| <style id="example-style"> | |||
| html, body, #canvas-container, #mcanvas { | |||
| width: 100%; | |||
| height: 100%; | |||
| margin: 0; | |||
| overflow: hidden; | |||
| } | |||
| </style> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,59 @@ | |||
| import { | |||
| _testFinish, | |||
| CameraViewPlugin, | |||
| Object3DGeneratorPlugin, | |||
| Object3DWidgetsPlugin, | |||
| ThreeViewer, | |||
| Vector3, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: true, | |||
| plugins: [CameraViewPlugin, Object3DGeneratorPlugin], | |||
| }) | |||
| const widgets = viewer.addPluginSync(Object3DWidgetsPlugin) | |||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| ui.setupPluginUi(Object3DWidgetsPlugin) | |||
| viewer.scene.setBackgroundColor('#444466') | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| console.log(widgets.helpers) | |||
| const generator = viewer.getPlugin(Object3DGeneratorPlugin)! | |||
| let object | |||
| object = generator.generate('camera-perspective', { | |||
| position: new Vector3(5, 5, 0), | |||
| }) | |||
| ui.appendChild(object?.uiConfig) | |||
| object = generator.generate('light-directional', { | |||
| position: new Vector3(5, 0, 5), | |||
| }) | |||
| ui.appendChild(object?.uiConfig) | |||
| object = generator.generate('light-spot', { | |||
| position: new Vector3(-5, 0, 5), | |||
| }) | |||
| ui.appendChild(object?.uiConfig) | |||
| object = generator.generate('light-point', { | |||
| position: new Vector3(-5, 5, -5), | |||
| }) | |||
| ui.appendChild(object?.uiConfig) | |||
| viewer.scene.mainCamera.position.z += 10 | |||
| viewer.scene.mainCamera.setDirty() | |||
| } | |||
| init().then(_testFinish) | |||
| @@ -19,6 +19,8 @@ import { | |||
| KTXLoadPlugin, | |||
| NoiseBumpMaterialPlugin, | |||
| NormalBufferPlugin, | |||
| Object3DGeneratorPlugin, | |||
| Object3DWidgetsPlugin, | |||
| PickingPlugin, | |||
| PLYLoadPlugin, | |||
| ProgressivePlugin, | |||
| @@ -89,6 +91,8 @@ async function init() { | |||
| BlendLoadPlugin, | |||
| HierarchyUiPlugin, | |||
| GeometryGeneratorPlugin, | |||
| Object3DWidgetsPlugin, | |||
| Object3DGeneratorPlugin, | |||
| ...extraImportPlugins, | |||
| ]) | |||
| @@ -98,7 +102,7 @@ async function init() { | |||
| editor.loadPlugins({ | |||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | |||
| ['Interaction']: [HierarchyUiPlugin, TransformControlsPlugin, PickingPlugin, GeometryGeneratorPlugin, EditorViewWidgetPlugin], | |||
| ['Interaction']: [HierarchyUiPlugin, TransformControlsPlugin, PickingPlugin, Object3DGeneratorPlugin, GeometryGeneratorPlugin, EditorViewWidgetPlugin, Object3DWidgetsPlugin], | |||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | |||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin, ChromaticAberrationPlugin, FilmicGrainPlugin], | |||
| ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | |||
| @@ -0,0 +1,138 @@ | |||
| import {AViewerPluginSync} from '../../viewer' | |||
| import { | |||
| AmbientLight2, | |||
| DirectionalLight2, | |||
| HemisphereLight2, | |||
| IObject3D, | |||
| PerspectiveCamera2, | |||
| PointLight2, | |||
| RectAreaLight2, | |||
| SpotLight2, | |||
| } from '../../core' | |||
| import {uiButton, uiDropdown, uiPanelContainer} from 'uiconfig.js' | |||
| import {Vector3} from 'three' | |||
| /** | |||
| * Adds support for generating different types of lights and camera objects in the viewer. | |||
| * @category Plugin | |||
| */ | |||
| @uiPanelContainer('Generate Scene Objects') | |||
| export class Object3DGeneratorPlugin extends AViewerPluginSync<''> { | |||
| public static readonly PluginType = 'Object3DGeneratorPlugin' | |||
| enabled = true | |||
| toJSON: any = undefined | |||
| @uiDropdown('Type', undefined, (that)=>({ | |||
| children: [()=>Object.keys(that.generators).map(label=>({label}))], | |||
| })) | |||
| protected _selectedType = '' | |||
| @uiButton('Generate') | |||
| generate(type?: string, params?: any, addToScene = true) { | |||
| if (!this._viewer) throw new Error('No viewer') | |||
| const obj = this.generators[type ?? this._selectedType]?.(params) | |||
| addToScene && obj && this._viewer.scene.addObject(obj) | |||
| return obj | |||
| } | |||
| generators: Record<string, (params?: any)=>IObject3D> = { | |||
| ['camera-perspective']: (params: { | |||
| controlsMode?: string, | |||
| autoAspect?: boolean, | |||
| fov?: number, | |||
| aspect?: number, | |||
| position?: Vector3, | |||
| target?: Vector3, | |||
| autoLookAtTarget?: boolean, | |||
| name?: string, | |||
| } = {})=>{ | |||
| const camera = new PerspectiveCamera2( | |||
| params.controlsMode ?? '', | |||
| this._viewer?.canvas, | |||
| params.autoAspect, | |||
| params.fov, | |||
| params.aspect, | |||
| ) | |||
| params.position ? camera.position.copy(params.position) : camera.position.set(0, 0, 5) | |||
| params.target ? camera.target.copy(params.target) : camera.target.set(0, 0, 0) | |||
| camera.autoLookAtTarget = params.autoLookAtTarget ?? true | |||
| camera.setDirty() | |||
| camera.name = params.name ?? 'Perspective Camera' | |||
| return camera | |||
| }, | |||
| ['light-directional']: (params: { | |||
| color?: number, | |||
| intensity?: number, | |||
| position?: Vector3, | |||
| target?: Vector3, | |||
| name?: string, | |||
| } = {})=>{ | |||
| const light = new DirectionalLight2(params.color ?? 0xff0000, params.intensity ?? 3) | |||
| params.position ? light.position.copy(params.position) : light.position.set(5, 5, 5) | |||
| light.lookAt(params.target ?? new Vector3(0, 0, 0)) | |||
| light.name = 'Directional Light' | |||
| return light | |||
| }, | |||
| ['light-ambient']: (params: { | |||
| color?: number, | |||
| intensity?: number, | |||
| name?: string, | |||
| } = {})=>{ | |||
| const light = new AmbientLight2(params.color ?? 0xffffff, params.intensity ?? 1) | |||
| light.name = 'Ambient Light' | |||
| return light | |||
| }, | |||
| ['light-point']: (params: { | |||
| color?: number, | |||
| intensity?: number, | |||
| position?: Vector3, | |||
| name?: string, | |||
| } = {})=>{ | |||
| const light = new PointLight2(params.color ?? 0xff0000, params.intensity ?? 3) | |||
| params.position ? light.position.copy(params.position) : light.position.set(5, 5, 5) | |||
| light.name = 'Point Light' | |||
| return light | |||
| }, | |||
| ['light-spot']: (params: { | |||
| color?: number, | |||
| intensity?: number, | |||
| position?: Vector3, | |||
| target?: Vector3, | |||
| name?: string, | |||
| } = {})=>{ | |||
| const light = new SpotLight2(params.color ?? 0xff0000, params.intensity ?? 3) | |||
| params.position ? light.position.copy(params.position) : light.position.set(5, 5, 5) | |||
| light.lookAt(params.target ?? new Vector3(0, 0, 0)) | |||
| light.name = 'Spot Light' | |||
| return light | |||
| }, | |||
| ['light-hemisphere']: (params: { | |||
| color?: number, | |||
| intensity?: number, | |||
| name?: string, | |||
| } = {})=>{ | |||
| const light = new HemisphereLight2(params.color ?? 0xaaaaff, 0x555443, params.intensity ?? 1) | |||
| light.name = 'Hemisphere Light' | |||
| return light | |||
| }, | |||
| ['light-rect-area']: (params: { | |||
| color?: number, | |||
| intensity?: number, | |||
| position?: Vector3, | |||
| target?: Vector3, | |||
| name?: string, | |||
| } = {})=>{ | |||
| const light = new RectAreaLight2(params.color ?? 0x000ff, params.intensity ?? 3, 2, 2) | |||
| params.position ? light.position.copy(params.position) : light.position.set(5, 5, 5) | |||
| light.lookAt(params.target ?? new Vector3(0, 0, 0)) | |||
| light.name = 'Rect Area Light' | |||
| return light | |||
| }, | |||
| } | |||
| constructor() { | |||
| super() | |||
| this._selectedType = Object.keys(this.generators)[0] | |||
| } | |||
| } | |||
| @@ -0,0 +1,112 @@ | |||
| import {UiObjectConfig} from 'uiconfig.js' | |||
| import {IWidget} from '../../core' | |||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||
| import {IEvent, onChange} from 'ts-browser-helpers' | |||
| import {Object3D} from 'three' | |||
| import {CameraHelper2, DirectionalLightHelper2, PointLightHelper2, SpotLightHelper2} from '../../three' | |||
| export interface IObject3DHelper<T extends Object3D&IWidget = Object3D&IWidget>{ | |||
| Create: (o: Object3D)=>T, | |||
| Check: (o: Object3D)=>boolean, | |||
| } | |||
| /** | |||
| * Adds light and camera helpers/gizmos in the viewer. | |||
| * A helper is automatically created when any supported light or camera is added to the scene. | |||
| * @category Plugin | |||
| */ | |||
| export class Object3DWidgetsPlugin extends AViewerPluginSync<''> { | |||
| @onChange(Object3DWidgetsPlugin.prototype.setDirty) | |||
| enabled = true | |||
| public static readonly PluginType = 'Object3DWidgetsPlugin' | |||
| helpers: IObject3DHelper[] = [ | |||
| DirectionalLightHelper2, | |||
| SpotLightHelper2, | |||
| PointLightHelper2, | |||
| CameraHelper2, | |||
| ] | |||
| setDirty() { | |||
| this.widgets?.forEach(w => w.visible = !this.isDisabled()) | |||
| this._viewer?.setDirty() | |||
| } | |||
| toJSON: any = null | |||
| constructor(enabled = true) { | |||
| super() | |||
| this.enabled = enabled | |||
| } | |||
| private _widgetRoot = new Object3D() | |||
| onAdded(viewer: ThreeViewer) { | |||
| super.onAdded(viewer) | |||
| viewer.scene.addEventListener('addSceneObject', this._addSceneObject) | |||
| viewer.scene.addObject(this._widgetRoot) | |||
| } | |||
| onRemove(viewer: ThreeViewer) { | |||
| viewer.scene.removeEventListener('addSceneObject', this._addSceneObject) | |||
| this.widgets.forEach(w => w.dispose && w.dispose()) | |||
| this.widgets = [] | |||
| this._widgetRoot.removeFromParent() | |||
| this._widgetRoot.clear() | |||
| super.onRemove(viewer) | |||
| } | |||
| private _addSceneObject = (e: any)=>{ | |||
| this._createWidgets(e.object) | |||
| } | |||
| refresh() { | |||
| this._createWidgets(this._viewer?.scene.modelRoot) | |||
| } | |||
| widgets: (IWidget&Object3D)[] = [] | |||
| private _widgetDisposed = (e: IEvent<any>)=> this._unregisterWidget(e.target) | |||
| private _registerWidget(w: IWidget&Object3D) { | |||
| this.widgets.push(w) | |||
| w.addEventListener('dispose', this._widgetDisposed) // todo: maybe unregister when removed from parent, dispose makes little sense. | |||
| } | |||
| private _unregisterWidget(w: IWidget&Object3D) { | |||
| w.removeEventListener('dispose', this._widgetDisposed) | |||
| const i = this.widgets.indexOf(w) | |||
| if (i >= 0) this.widgets.splice(i, 1) | |||
| } | |||
| private _createWidgets(o?: Object3D) { | |||
| o?.traverse((l: any) => { | |||
| const widget = this.widgets.find(w => w.object === l) | |||
| if (widget) { | |||
| widget.update && widget.update() | |||
| return | |||
| } | |||
| const helpers = this.helpers.filter(h => h.Check(l)) | |||
| helpers.forEach(h => { | |||
| const w = h.Create(l) | |||
| w.visible = !this.isDisabled() | |||
| this._widgetRoot.add(w) | |||
| this._registerWidget(w) | |||
| }) | |||
| }) | |||
| } | |||
| uiConfig: UiObjectConfig = { | |||
| type: 'folder', | |||
| label: 'Widgets', | |||
| children: [ | |||
| { | |||
| type: 'checkbox', | |||
| label: 'Enabled', | |||
| property: [this, 'enabled'], | |||
| }, | |||
| { | |||
| type: 'button', | |||
| label: 'Refresh', | |||
| value: ()=>this.refresh(), | |||
| }, | |||
| ], | |||
| } | |||
| } | |||
| @@ -50,8 +50,10 @@ export {NoiseBumpMaterialPlugin} from './material/NoiseBumpMaterialPlugin' | |||
| export {CustomBumpMapPlugin} from './material/CustomBumpMapPlugin' | |||
| export {FragmentClippingExtensionPlugin, FragmentClippingMode} from './material/FragmentClippingExtensionPlugin' | |||
| // extras | |||
| // rendering | |||
| export {VirtualCamerasPlugin} from './rendering/VirtualCamerasPlugin' | |||
| // extras | |||
| export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin' | |||
| export {Object3DWidgetsPlugin} from './extras/Object3DWidgetsPlugin' | |||
| export {Object3DGeneratorPlugin} from './extras/Object3DGeneratorPlugin' | |||