UNPKG

threepipe

Version:

A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.

698 lines 29.1 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Color, EquirectangularReflectionMapping, Object3D, Scene, UVMapping, Vector3, } from 'three'; import { autoGPUInstanceMeshes, bindToValue, Box3B } from '../../three'; import { onChange2, onChange3, serialize } from 'ts-browser-helpers'; import { PerspectiveCamera2 } from '../camera/PerspectiveCamera2'; import { ThreeSerialization } from '../../utils'; import { iObjectCommons } from './iObjectCommons'; import { uiButton, uiColor, uiConfig, uiFolderContainer, uiImage, uiSlider, uiToggle } from 'uiconfig.js'; import { getFittingDistance } from '../../three/utils/camera'; let RootScene = class RootScene extends Scene { get mainCamera() { return this._mainCamera || this._dummyCam; } set mainCamera(camera) { const cam = this.mainCamera; if (!camera) camera = this.defaultCamera; if (cam === camera) return; if (cam) { cam.deactivateMain(undefined, true); cam.removeEventListener('cameraUpdate', this._mainCameraUpdate); } if (camera) { this._mainCamera = camera; camera.addEventListener('cameraUpdate', this._mainCameraUpdate); camera.activateMain(undefined, true); } else { this._mainCamera = null; } this.dispatchEvent({ type: 'activeCameraChange', lastCamera: cam, camera }); // deprecated this.dispatchEvent({ type: 'mainCameraChange', lastCamera: cam, camera }); this.setDirty(); } get renderCamera() { return this._renderCamera ?? this.mainCamera; } set renderCamera(camera) { const cam = this._renderCamera; this._renderCamera = camera; this.dispatchEvent({ type: 'renderCameraChange', lastCamera: cam, camera }); } /** * Create a scene instance. This is done automatically in the {@link ThreeViewer} and must not be created separately. * @param camera * @param objectProcessor */ constructor(camera, objectProcessor) { super(); this.isRootScene = true; this.assetType = 'model'; // private _processors = new ObjectProcessorMap<'environment' | 'background'>() // private _sceneObjects: ISceneObject[] = [] this._mainCamera = null; this.backgroundColor = null; // read in three.js WebGLBackground this.background = null; /** * The intensity for the environment light. */ this.backgroundIntensity = 1; /** * The default environment map used when rendering materials in the scene */ this.environment = null; /** * The intensity for the environment light. */ this.envMapIntensity = 1; /** * Rotation in radians of the default environment map. * Same as {@link environment}.rotation. * * Note - this is not serialized here, but inside the texture. */ this.envMapRotation = 0; /** * Extra textures/envmaps that can be used by objects/materials/plugins and will be serialized. */ this.textureSlots = {}; /** * Fixed direction environment reflections irrespective of camera position. */ this.fixedEnvMapDirection = false; // private _environmentLight?: IEnvironmentLight // required just because we don't want activeCamera to be null. this._dummyCam = new PerspectiveCamera2(''); this._mainCameraUpdate = (e) => { this.setDirty({ refreshScene: false }); this.refreshActiveCameraNearFar(); if (e.key === 'fov') this.dollyActiveCameraFov(); this.dispatchEvent({ ...e, type: 'mainCameraUpdate' }); this.dispatchEvent({ ...e, type: 'activeCameraUpdate' }); // deprecated }; // cached values this._sceneBounds = new Box3B; this._sceneBoundingRadius = 0; this.refreshUi = iObjectCommons.refreshUi.bind(this); this._v1 = new Vector3(); this._v2 = new Vector3(); /** * For Programmatically toggling autoNearFar. This property is not supposed to be in the UI or serialized. * Use camera.userData.autoNearFar for UI and serialization * This is used in PickingPlugin * autoNearFar will still be disabled if this is true and camera.userData.autoNearFar is false */ this.autoNearFarEnabled = true; this.setDirty = this.setDirty.bind(this); iObjectCommons.upgradeObject3D.call(this, undefined, objectProcessor); // this is called from parentDispatch since scene is a parent. this.addEventListener('materialUpdate', (e) => this.dispatchEvent({ ...e, type: 'sceneMaterialUpdate' })); this.addEventListener('objectUpdate', this.refreshScene); this.addEventListener('geometryUpdate', this.refreshScene); this.addEventListener('geometryChanged', this.refreshScene); this.defaultCamera = camera; this.modelRoot = new Object3D(); this.modelRoot.userData.rootSceneModelRoot = true; this.modelRoot.name = 'Scene'; // for the UI // this.modelRoot.addEventListener('update', this.setDirty) // todo: where was this dispatched from/used ? // eslint-disable-next-line deprecation/deprecation this.add(this.modelRoot); // this.addSceneObject(this.modelRoot as any, {addToRoot: true, autoScale: false}) // this.addSceneObject(this.defaultCamera, {addToRoot: true}) // eslint-disable-next-line deprecation/deprecation this.add(this.defaultCamera); this.mainCamera = this.defaultCamera; // this.boxHelper = new Box3Helper(this.getBounds()) // this.boxHelper.userData.bboxVisible = false // this.boxHelper.visible = false // this.add(this.boxHelper) } /** * Add a widget (non-physical/interactive) object to the scene. like gizmos, ui components etc. * @param model * @param options */ // addWidget(model: IWidget, options: AnyOptions = {}): void { // if (model.assetType !== 'widget') { // console.warn('Invalid asset type for ', model, ', adding anyway') // } // this.add(model.modelObject) // // // todo: dispatch event, add event listeners, etc // } /** * Add any object to the scene. * @param imported * @param options */ addObject(imported, options) { if (options?.clearSceneObjects || options?.disposeSceneObjects) { this.clearSceneModels(options.disposeSceneObjects); } if (!imported) return imported; if (!imported.isObject3D) { console.error('Invalid object, cannot add to scene.', imported); return imported; } this._addObject3D(imported, options); this.dispatchEvent({ type: 'addSceneObject', object: imported, options }); return imported; } /** * Load model root scene exported to GLTF format. Used internally by {@link ThreeViewer.addSceneObject}. * @param obj * @param options */ loadModelRoot(obj, options) { if (options?.clearSceneObjects || options?.disposeSceneObjects) { this.clearSceneModels(options.disposeSceneObjects); } if (!obj.userData?.rootSceneModelRoot) { console.error('Invalid model root scene object. Trying to add anyway.', obj); } if (obj.userData) { // todo deep merge all userdata? if (obj.userData.__importData) this.modelRoot.userData.__importData = { ...this.modelRoot.userData.__importData, ...obj.userData.__importData, }; if (obj.userData.gltfAsset) { this.modelRoot.userData.__gltfAsset = { ...this.modelRoot.userData.__gltfAsset, ...obj.userData.gltfAsset, }; } if (obj.userData.gltfExtras) this.modelRoot.userData.__gltfExtras = { ...this.modelRoot.userData.__gltfExtras, ...obj.userData.gltfExtras, }; } if (obj.userData?.gltfAsset?.copyright) obj.children.forEach(c => !c.userData.license && (c.userData.license = obj.userData.gltfAsset?.copyright)); if (obj.animations) { if (!this.modelRoot.animations) this.modelRoot.animations = []; for (const animation of obj.animations) { if (this.modelRoot.animations.includes(animation)) continue; this.modelRoot.animations.push(animation); } } return [...obj.children] // need to clone .map(c => this.addObject(c, { ...options, clearSceneObjects: false, disposeSceneObjects: false })); } _addObject3D(model, { autoCenter = false, centerGeometries = false, centerGeometriesKeepPosition = true, autoScale = false, autoScaleRadius = 2., addToRoot = false, license } = {}) { const obj = model; if (!obj) { console.error('Invalid object, cannot add to scene.'); return; } // eslint-disable-next-line deprecation/deprecation if (addToRoot) this.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. } if (license) obj.userData.license = [obj.userData.license, license].filter(v => v).join(', '); this.setDirty({ refreshScene: true }); } centerAllGeometries(keepPosition = true, obj) { const geoms = new Set(); (obj ?? this.modelRoot).traverse((o) => o.geometry && geoms.add(o.geometry)); const undos = []; geoms.forEach(g => undos.push(g.center2(undefined, keepPosition))); return () => undos.forEach(u => u()); } clearSceneModels(dispose = false, setDirty = true) { if (dispose) return this.disposeSceneModels(setDirty); this.modelRoot.clear(); this.modelRoot.children = []; setDirty && this.setDirty({ refreshScene: true }); } disposeSceneModels(setDirty = true, clear = true) { if (clear) { [...this.modelRoot.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent()); this.modelRoot.clear(); if (setDirty) this.setDirty({ refreshScene: true }); } else { this.modelRoot.children.forEach(child => child.dispose && child.dispose()); } } _onEnvironmentChange() { // console.warn('environment changed') if (this.environment?.mapping === UVMapping) { this.environment.mapping = EquirectangularReflectionMapping; // for PMREMGenerator this.environment.needsUpdate = true; } this.dispatchEvent({ type: 'environmentChanged', environment: this.environment }); this.setDirty({ refreshScene: true, geometryChanged: false }); this.refreshUi?.(); } onBackgroundChange() { this.dispatchEvent({ type: 'backgroundChanged', background: this.background, backgroundColor: this.backgroundColor }); this.setDirty({ refreshScene: true, geometryChanged: false }); this.refreshUi?.(); } /** * @deprecated Use {@link addObject} */ add(...object) { super.add(...object); // this._onSceneUpdate() // this is not needed, since it will be bubbled up from the object3d and we will get event objectUpdate return this; } /** * Sets the backgroundColor property from a string, number or Color, and updates the scene. * @param color */ setBackgroundColor(color) { this.backgroundColor = color ? new Color(color) : null; } /** * Mark the scene dirty, and force render in the next frame. * @param options - set `refreshScene` to true to mark that any object transformations have changed. It might trigger effects like frame fade depening on plugins. * @returns {this} */ setDirty(options) { // todo: for onChange calls -> check options.key for specific key that's changed and use it to determine refreshScene if (options?.sceneUpdate) { console.warn('sceneUpdate is deprecated, use refreshScene instead.'); options.refreshScene = true; } if (options?.refreshScene) { this.refreshScene(options); } else { this.dispatchEvent({ type: 'update' }); // todo remove iObjectCommons.setDirty.call(this, { ...options, scene: this }); } // this sets dirty in the viewer return this; } /** * For visualizing the scene bounds. API incomplete. * @type {Box3Helper} */ // readonly boxHelper: Box3Helper refreshScene(event) { if (event && event.type === 'objectUpdate' && event.object === this) return this; // ignore self // todo test the isCamera here. this is for animation object plugin if (event?.sceneUpdate === false || event?.refreshScene === false || event?.object?.isCamera) return this.setDirty(event); // so that it doesn't trigger frame fade, shadow refresh etc // console.warn(event) this.refreshActiveCameraNearFar(); // this.dollyActiveCameraFov() this._sceneBounds = this.getBounds(false, true); // this.boxHelper?.boxHelper?.copy?.(this._sceneBounds) this._sceneBoundingRadius = this._sceneBounds.getSize(new Vector3()).length() / 2.; this.dispatchEvent({ ...event, type: 'sceneUpdate', hierarchyChanged: ['addedToParent', 'removedFromParent'].includes(event?.change || '') }); iObjectCommons.setDirty.call(this, event); return this; } /** * Dispose the scene and clear all resources. * @warn Not fully implemented yet, just clears the scene. */ dispose(clear = true) { this.disposeSceneModels(false, clear); if (clear) { [...this.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent()); this.clear(); } // todo: dispose more stuff? this.environment?.dispose(); if (this.background?.isTexture) this.background?.dispose?.(); if (clear) { this.environment = null; this.background = null; } return; } /** * Returns the bounding box of the whole scene (model root and other meta objects). * To get the bounds of just the objects added by the user(not by plugins) use `new Box3B().expandByObject(scene.modelRoot)` * @param precise * @param ignoreInvisible * @param ignoreWidgets * @param ignoreObject * @returns {Box3B} */ getBounds(precise = false, ignoreInvisible = true, ignoreWidgets = true, ignoreObject) { // See bboxVisible in userdata in Box3B return new Box3B().expandByObject(this, precise, ignoreInvisible, (o) => { if (ignoreWidgets && (o.isWidget || o.assetType === 'widget')) return true; return ignoreObject?.(o) ?? false; }); } /** * Similar to {@link getBounds}, but returns the bounding box of just the {@link modelRoot}. * @param precise * @param ignoreInvisible * @param ignoreWidgets * @param ignoreObject * @returns {Box3B} */ getModelBounds(precise = false, ignoreInvisible = true, ignoreWidgets = true, ignoreObject) { if (this.modelRoot == undefined) return new Box3B(); return new Box3B().expandByObject(this.modelRoot, precise, ignoreInvisible, (o) => { if (ignoreWidgets && o.assetType === 'widget') return true; return ignoreObject?.(o) ?? false; }); } autoGPUInstanceMeshes() { const geoms = new Set(); this.modelRoot.traverse((o) => o.geometry && geoms.add(o.geometry)); geoms.forEach((g) => autoGPUInstanceMeshes(g)); } /** * Refreshes the scene active camera near far values, based on the scene bounding box. * This is called automatically every time the camera is updated. */ refreshActiveCameraNearFar() { const camera = this.mainCamera; if (!camera) return; if (!this.autoNearFarEnabled || camera.userData.autoNearFar === false) { camera.near = camera.userData.minNearPlane ?? 0.5; camera.far = camera.userData.maxFarPlane ?? 1000; return; } // 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? camera.getWorldPosition(this._v1).sub(bbox.getCenter(this._v2)); const radius = 1.5 * bbox.getSize(this._v2).length() / 2.; const dist = this._v1.length(); // new way 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 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 // 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) camera.near = near; camera.far = far; // todo try using minimum of all 6 endpoints of bbox. // camera.near = 3 // camera.far = 20 } /** * Refreshes the scene active camera near far values, based on the scene bounding box. * This is called automatically every time the camera fov is updated. */ dollyActiveCameraFov() { const camera = this.mainCamera; if (!camera) return; if (!camera.userData.dollyFov) { return; } const bbox = this.getModelBounds(false, true, true); // todo this is not exact because of 1.5, this needs to be calculated based on current position and last fov const cameraZ = getFittingDistance(camera, bbox) * 1.5; const direction = new Vector3().subVectors(camera.target, camera.position).normalize(); camera.position.copy(direction.multiplyScalar(-cameraZ).add(camera.target)); camera.setDirty(); } updateShaderProperties(material) { if (material.uniforms.sceneBoundingRadius) material.uniforms.sceneBoundingRadius.value = this._sceneBoundingRadius; else console.warn('RootScene: no uniform: sceneBoundingRadius'); return this; } /** * Serialize the scene properties * @param meta * @returns {any} */ toJSON(meta) { const o = ThreeSerialization.Serialize(this, meta, true); // console.log(o) return o; } /** * Deserialize the scene properties * @param json - object from {@link toJSON} * @param meta * @returns {this<TCamera>} */ fromJSON(json, meta) { const env = json.environment; if (env !== undefined) { this.environment = ThreeSerialization.Deserialize(env, this.environment, meta, false); delete json.environment; } ThreeSerialization.Deserialize(json, this, meta, true); json.environment = env; return this; } addEventListener(type, listener) { if (type === 'activeCameraChange') console.error('activeCameraChange is deprecated. Use mainCameraChange instead.'); if (type === 'activeCameraUpdate') console.error('activeCameraUpdate is deprecated. Use mainCameraUpdate instead.'); if (type === 'sceneMaterialUpdate') console.error('sceneMaterialUpdate is deprecated. Use materialUpdate instead.'); if (type === 'update') console.error('update is deprecated. Use sceneUpdate instead.'); super.addEventListener(type, listener); } // endregion // region deprecated // /** // * Set the scene environment map, this will be processed with PMREM automatically later. // * @param asset // * @returns {void} // */ // public setEnvironment(asset: ITexture|null|undefined): void { // if (!asset) { // // eslint-disable-next-line deprecation/deprecation // this.environment = null // this._onEnvironmentChange() // return // } // if (!asset.isTexture) { // console.error('Unknown Environment type', asset) // return // } // if (asset.mapping === UVMapping) { // asset.mapping = EquirectangularReflectionMapping // for PMREMGenerator // asset.needsUpdate = true // } // // eslint-disable-next-line deprecation/deprecation // this.environment = asset // // eslint-disable-next-line deprecation/deprecation // // this.background = texture // for testing. // this._onEnvironmentChange() // } // // /** // * Get the current scene environment map // * @returns {ITexture<Texture>} // */ // getEnvironment(): ITexture | null { // return this.environment || null // } /** * Find objects by name exact match in the complete hierarchy. * @deprecated Use {@link getObjectByName} instead. * @param name - name * @param parent - optional root node to start search from * @returns Array of found objects */ findObjectsByName(name, parent) { const o = []; (parent ?? this).traverse(object => { if (object.name === name) o.push(object); }); return o; } /** * @deprecated * Sets the camera pointing towards the object at a specific distance. * @param rootObject - The object to point at. * @param centerOffset - The distance offset from the object to point at. * @param targetOffset - The distance offset for the target from the center of object to point at. * @param options - Not used yet. */ resetCamera(rootObject = undefined, centerOffset = new Vector3(1, 1, 1), targetOffset = new Vector3(0, 0, 0)) { if (this._mainCamera) { this.matrixWorldNeedsUpdate = true; this.updateMatrixWorld(true); const bounds = rootObject ? new Box3B().expandByObject(rootObject, true, true) : this.getBounds(true); const center = bounds.getCenter(new Vector3()); const radius = bounds.getSize(new Vector3()).length() * 0.5; center.add(targetOffset.clone().multiplyScalar(radius)); this._mainCamera.position = new Vector3(// todo: for nested cameras? center.x + centerOffset.x * radius, center.y + centerOffset.y * radius, center.z + centerOffset.z * radius); this._mainCamera.target = center; // this.scene.mainCamera.controls?.targetOffset.set(0, 0, 0) this.setDirty(); } } /** * Minimum Camera near plane * @deprecated - use camera.minNearPlane instead */ get minNearDistance() { console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead'); return this.mainCamera.userData.minNearPlane ?? 0.02; } /** * @deprecated - use camera.minNearPlane instead */ set minNearDistance(value) { console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead'); if (this.mainCamera) this.mainCamera.userData.minNearPlane = value; } /** * @deprecated */ get activeCamera() { console.error('activeCamera is deprecated. Use mainCamera instead.'); return this.mainCamera; } /** * @deprecated */ set activeCamera(camera) { console.error('activeCamera is deprecated. Use mainCamera instead.'); this.mainCamera = camera; } /** * Get the threejs scene object * @deprecated */ get modelObject() { return this; } /** * @deprecated use {@link envMapIntensity} instead */ get environmentIntensity() { return this.envMapIntensity; } /** * @deprecated use {@link envMapIntensity} instead */ set environmentIntensity(value) { this.envMapIntensity = value; } /** * Add any processed scene object to the scene. * @deprecated renamed to {@link addObject} * @param imported * @param options */ addSceneObject(imported, options) { return this.addObject(imported, options); } /** * Equivalent to setDirty({refreshScene: true}), dispatches 'sceneUpdate' event with the specified options. * @deprecated use refreshScene * @param options */ updateScene(options) { console.warn('updateScene is deprecated. Use refreshScene instead'); return this.refreshScene(options || {}); } /** * @deprecated renamed to {@link clearSceneModels} */ removeSceneModels() { this.clearSceneModels(); } }; __decorate([ uiColor('Background Color', (s) => ({ onChange: () => s?.onBackgroundChange(), })), serialize(), onChange2(RootScene.prototype.onBackgroundChange) ], RootScene.prototype, "backgroundColor", void 0); __decorate([ onChange2(RootScene.prototype.onBackgroundChange), serialize(), uiImage('Background Image') ], RootScene.prototype, "background", void 0); __decorate([ serialize(), onChange3(RootScene.prototype.setDirty), uiSlider('Background Intensity', [0, 10], 0.01) ], RootScene.prototype, "backgroundIntensity", void 0); __decorate([ uiImage('Environment'), serialize(), onChange2(RootScene.prototype._onEnvironmentChange) ], RootScene.prototype, "environment", void 0); __decorate([ uiSlider('Environment Intensity', [0, 10], 0.01), serialize(), onChange3(RootScene.prototype.setDirty) ], RootScene.prototype, "envMapIntensity", void 0); __decorate([ uiSlider('Environment Rotation', [-Math.PI, Math.PI], 0.01), bindToValue({ obj: 'environment', key: 'rotation', onChange: RootScene.prototype.setDirty, onChangeParams: false }) ], RootScene.prototype, "envMapRotation", void 0); __decorate([ serialize() ], RootScene.prototype, "textureSlots", void 0); __decorate([ uiToggle('Fixed Env Direction'), serialize(), onChange3(RootScene.prototype.setDirty) ], RootScene.prototype, "fixedEnvMapDirection", void 0); __decorate([ uiConfig(), serialize() ], RootScene.prototype, "defaultCamera", void 0); __decorate([ uiButton('Center All Geometries', { sendArgs: false }) ], RootScene.prototype, "centerAllGeometries", null); __decorate([ uiButton('Auto GPU Instance Meshes') ], RootScene.prototype, "autoGPUInstanceMeshes", null); RootScene = __decorate([ uiFolderContainer('Root Scene') ], RootScene); export { RootScene }; //# sourceMappingURL=RootScene.js.map