threepipe
Version:
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.
924 lines (810 loc) • 37.4 kB
text/typescript
import {
BufferGeometry,
Color,
EquirectangularReflectionMapping,
Euler,
EventListener,
EventListener2,
IUniform,
Object3D,
Scene,
UVMapping,
Vector3,
} from 'three'
import type {IObject3D, IObject3DEventMap, IObjectProcessor} from '../IObject'
import {type ICamera} from '../ICamera'
import {autoGPUInstanceMeshes, Box3B} from '../../three'
import {AnyOptions, onChange2, onChange3, serialize} from 'ts-browser-helpers'
import {PerspectiveCamera2} from '../camera/PerspectiveCamera2'
import {addModelProcess, centerAllGeometries, ThreeSerialization} from '../../utils'
import {ITexture} from '../ITexture'
import {AddObjectOptions, IScene, ISceneEventMap, ISceneSetDirtyOptions, IWidget} from '../IScene'
import {iObjectCommons} from './iObjectCommons'
import {RootSceneImportResult} from '../../assetmanager'
import {
uiButton,
uiColor,
uiConfig,
uiFolderContainer,
uiImage,
UiObjectConfig,
uiSlider,
uiToggle,
uiVector,
} from 'uiconfig.js'
import {getFittingDistance} from '../../three/utils/camera'
import {iCameraCommons} from './iCameraCommons'
export class RootScene<TE extends ISceneEventMap = ISceneEventMap> extends Scene<TE&ISceneEventMap> implements IScene<TE> {
readonly isRootScene = true
assetType = 'model' as const
declare uiConfig: UiObjectConfig
// private _processors = new ObjectProcessorMap<'environment' | 'background'>()
// private _sceneObjects: ISceneObject[] = []
private _mainCamera: ICamera | null = null
/**
* The root object where all imported objects are added.
*/
readonly modelRoot: IObject3D
// readonly lightsRoot: IObject3D // todo this can be added before modelRoot to add extra lights before the model root.
backgroundColor: Color | null = null // read in three.js WebGLBackground
<RootScene>('Background Color', (s)=>({
hidden: ()=>s.backgroundColor === null || s.backgroundColor === undefined || s.background === 'environment',
}))
protected get _backgroundColorUi() {
return '#' + (this.backgroundColor?.getHexString() ?? '000000')
}
protected set _backgroundColorUi(v) {
this.setBackgroundColor(v)
}
<RootScene>('Background Image', (s)=>({
hidden: ()=>s.backgroundColor === null || s.backgroundColor === undefined || s.background === 'environment',
}))
background: null | Color | ITexture | 'environment' = null
/**
* Toggle the background between color and transparent.
*/
<RootScene>(undefined, (s)=>({
label: ()=>!s.backgroundColor ? 'Set Color Background' : 'Set Transparent BG',
tags: ['context-menu'],
}))
toggleTransparentBackground() {
if (!this.backgroundColor) {
this.backgroundColor = new Color(0xffffff) // todo save last color and image?
} else {
this.background = null
this.backgroundColor = null
}
this.refreshUi?.()
this.setDirty()
}
/**
* Toggle the background between texture and environment map.
*/
<RootScene>(undefined, (s)=>({
label: ()=>s.background === 'environment' ? 'Remove Env Background' : 'Set Env Background',
disabled: ()=>!s.environment, tags: ['context-menu'],
}))
toggleEnvironmentBackground() {
if (this.background === 'environment') {
this.background = null
} else {
this.background = 'environment'
}
}
/**
* The intensity for the background color and map.
*/
backgroundIntensity = 1
/**
* Enable/Disable tonemapping selectively for the background.
* Note - This requires both TonemapPlugin and GBufferPlugin or DepthBufferPlugin to be in the viewer to work.
*/
// todo let scene access the viewer
backgroundTonemap = true
private _environment: ITexture | null = null
/**
* The default environment map used when rendering materials in the scene
*/
// Note: getter/setter defined in constructor using Object.defineProperty
declare environment: ITexture | null
/**
* The intensity for the environment light.
*/
environmentIntensity = 1
<RootScene>('Environment Rotation', undefined, undefined, (t)=>({disabled: ()=>t.fixedEnvMapDirection || !t.environment}))
declare environmentRotation: Euler
<RootScene>('Background Rotation', undefined, undefined, (t)=>({hidden: ()=>!t.background || t.background === 'environment' || !(t.background as any).isTexture || (t.background as any).mapping !== EquirectangularReflectionMapping}))
declare backgroundRotation: Euler
// @uiSlider('Environment Rotation', [-Math.PI, Math.PI], 0.01)
// @bindToValue({obj: 'environment', key: 'rotation', onChange: RootScene.prototype.setDirty, onChangeParams: false})
// envMapRotation = 0
/**
* Extra textures/envmaps that can be used by objects/materials/plugins and will be serialized.
*/
public textureSlots: Record<string, ITexture> = {}
/**
* Fixed direction environment reflections irrespective of camera position.
*/
fixedEnvMapDirection = false
/**
* The default camera in the scene. This camera is always in the scene and used by default if no camera is set as main.
* It is also saved along with the scene JSON and shown in the UI. This is added to the scene root, hence not saved in the glTF when a scene glb is exported.
*/
readonly defaultCamera: ICamera
/**
* Calls dispose on current old environment map, background map when it is changed.
* Runtime only (not serialized)
*/
autoDisposeSceneMaps = true
// private _environmentLight?: IEnvironmentLight
// required just because we don't want activeCamera to be null.
private _dummyCam = new PerspectiveCamera2('') as ICamera
get mainCamera(): ICamera {
return this._mainCamera || this._dummyCam
}
set mainCamera(camera: ICamera | undefined) {
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)
if (!camera._canvas && camera !== this.defaultCamera) {
console.warn('RootScene: mainCamera does not have a canvas set, some controls might not work properly.')
}
} else {
this._mainCamera = null
}
this.dispatchEvent({type: 'activeCameraChange', lastCamera: cam, camera}) // deprecated
this.dispatchEvent({type: 'mainCameraChange', lastCamera: cam, camera})
this.setDirty()
}
private _renderCamera: ICamera | undefined
get renderCamera() {
return this._renderCamera ?? this.mainCamera
}
set renderCamera(camera: ICamera) {
const cam = this._renderCamera
this._renderCamera = camera
this.dispatchEvent({type: 'renderCameraChange', lastCamera: cam, camera})
}
objectProcessor?: IObjectProcessor
/**
* Create a scene instance. This is done automatically in the {@link ThreeViewer} and must not be created separately.
* @param camera
* @param objectProcessor
*/
constructor(camera: ICamera, objectProcessor?: IObjectProcessor) {
super()
this.setDirty = this.setDirty.bind(this)
this.name = 'RootScene'
this.objectProcessor = objectProcessor
iObjectCommons.upgradeObject3D.call(this)
this.objectProcessor?.processObject(this)
// this is called from parentDispatch since scene is a parent.
this.addEventListener('materialUpdate', (e: any)=>this.dispatchEvent({...e, type: 'sceneMaterialUpdate'}))
this.addEventListener('objectUpdate', this.refreshScene)
this.addEventListener('geometryUpdate', this.refreshScene)
this.addEventListener('geometryChanged', this.refreshScene)
this.environmentRotation?._onChange(()=>{
this.setDirty({key: 'environmentRotation', value: this.environmentRotation})
})
this.backgroundRotation?._onChange(()=>{
this.setDirty({key: 'backgroundRotation', value: this.backgroundRotation})
})
this.defaultCamera = camera
this.modelRoot = new Object3D() as IObject3D
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 as any)
// this.addSceneObject(this.modelRoot as any, {addToRoot: true, autoScale: false})
// eslint-disable-next-line deprecation/deprecation
this.add(this.defaultCamera)
this.mainCamera = this.defaultCamera
Object.defineProperty(this, 'environment', {
configurable: true,
enumerable: true,
get: () => {
// Return override environment if we're in render step and override is set
if (this._isMainRendering && this.overrideRenderEnvironment !== null) {
return this.overrideRenderEnvironment
}
return this._environment
},
set: (value: ITexture | null) => {
const oldValue = this._environment
this._environment = value
this._onEnvironmentChange({key: 'environment', value, oldValue, target: this})
},
})
}
/**
* Add any object to the scene.
* @param imported
* @param options
*/
addObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T&IObject3D {
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 as T&IObject3D
}
this._addObject3D(<IObject3D>imported, options)
this.dispatchEvent({type: 'addSceneObject', object: <IObject3D>imported, options})
return imported as T&IObject3D
}
/**
* Load model root scene exported to GLTF format. Used internally by {@link ThreeViewer.addSceneObject}.
* @param obj
* @param options
*/
loadModelRoot(obj: RootSceneImportResult, options?: AddObjectOptions) {
if (options?.clearSceneObjects || options?.disposeSceneObjects) {
this.clearSceneModels(options.disposeSceneObjects)
}
if (!obj.userData?.rootSceneModelRoot) {
console.error('RootScene: Invalid model root scene object. Trying to add anyway.', obj)
}
if (obj.userData) {
// todo deep merge all userdata?
if (obj.userData.__importData) // this is with `__` as it is not automatically serialized, but it can be read in gltf exporter extensions and serialized manually
this.modelRoot.userData.__importData = {
...this.modelRoot.userData.__importData,
...obj.userData.__importData,
}
if (obj.userData.gltfAsset) {
this.modelRoot.userData.gltfAsset = { // todo: why are we merging values?
...this.modelRoot.userData.gltfAsset,
...obj.userData.gltfAsset,
extras: {
...this.modelRoot.userData.gltfAsset?.extras,
...obj.userData.gltfAsset.extras,
},
}
}
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)
}
}
if (obj._loadingPromise) {
if (this.modelRoot._loadingPromise) {
this.modelRoot._loadingPromise = Promise.allSettled([this.modelRoot._loadingPromise, obj._loadingPromise])
} else {
this.modelRoot._loadingPromise = obj._loadingPromise
}
}
const children = obj._childrenCopy || [...obj.children]
return children.map(c=>this.addObject(c, {...options, clearSceneObjects: false, disposeSceneObjects: false}))
}
private _addObject3D(model: IObject3D|null, {addToRoot = false, ...options}: AddObjectOptions = {}): void {
const obj = model
if (!obj || !obj.isObject3D) {
console.error('RootScene: Invalid object, cannot add to scene.')
return
}
const target = addToRoot ? this : this.modelRoot
target.add(obj)
if (options.indexInParent !== undefined) {
const newIndex = options.indexInParent
const newIndex2 = target.children.indexOf(obj)
if (newIndex >= 0 && newIndex2 >= 0 && newIndex !== newIndex2 && newIndex < target.children.length) {
target.children.splice(newIndex2, 1)
target.children.splice(newIndex, 0, obj) // add at new index
}
}
addModelProcess(obj, options)
this.setDirty({refreshScene: true})
}
centerAllGeometries(keepPosition = true, obj?: IObject3D) {
return centerAllGeometries(obj ?? this.modelRoot, keepPosition)
}
clearSceneModels(dispose = false, setDirty = true): void {
if (dispose) return this.disposeSceneModels(setDirty)
this.modelRoot.clear()
this.modelRoot.children = []
setDirty && this.setDirty({refreshScene: true})
}
disposeSceneModels(setDirty = true, clear = true) {
if (clear) {
for (const child of [...this.modelRoot.children]) {
child.dispose ? child.dispose() : child.removeFromParent()
}
this.modelRoot.clear()
if (setDirty) this.setDirty({refreshScene: true})
} else {
for (const child of this.modelRoot.children) {
child.dispose && child.dispose(false)
}
}
}
private _onEnvironmentChange(ev?: {value: ITexture|null, oldValue: ITexture|null, key?: string, target?: any}) {
if (ev?.oldValue && ev.oldValue !== ev.value) {
if (this.autoDisposeSceneMaps && typeof ev.oldValue.dispose === 'function') ev.oldValue.dispose()
}
// console.warn('environment changed')
if (this.environment?.mapping === UVMapping) {
this.environment.mapping = EquirectangularReflectionMapping // for PMREMGenerator
this.environment.needsUpdate = true
}
// todo dispatch texturesChanged also
this.dispatchEvent({
type: 'environmentChanged',
oldTexture: ev?.oldValue?.isTexture ? ev.oldValue : null,
texture: this.environment?.isTexture ? this.environment : null,
environment: this.environment,
})
this.setDirty({refreshScene: true, geometryChanged: false})
this.refreshUi?.()
}
onBackgroundChange(ev?: {value: ITexture|null, oldValue: ITexture|null}) {
if (ev?.oldValue && ev.oldValue !== ev.value) {
if (this.autoDisposeSceneMaps && typeof ev.oldValue.dispose === 'function') ev.oldValue.dispose()
}
// todo dispatch texturesChanged also
this.dispatchEvent({
type: 'backgroundChanged',
oldTexture: ev?.oldValue && ev.oldValue.isTexture ? ev.oldValue : null,
texture:(this.background as ITexture)?.isTexture ? (this.background as ITexture) : null,
background: this.background,
backgroundColor: this.backgroundColor,
})
this.setDirty({refreshScene: true, geometryChanged: false})
this.refreshUi?.()
}
/**
* @deprecated Use {@link addObject}
*/
add(...object: Object3D[]): this {
const filter = object.filter(o=>o.parent !== this)
filter.length && super.add(...filter) // to prevent multiple event dispatch
// 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.
* Note that when setting a `Color` object, it will be cloned.
* @param color
*/
setBackgroundColor(color: string | number | Color | null) {
const col = color || typeof color === 'number' ? new Color(color) : null
if (col && this.backgroundColor && !col.equals(this.backgroundColor) ||
(!col || !this.backgroundColor) && col !== this.backgroundColor
) this.backgroundColor = col
}
/**
* 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?: ISceneSetDirtyOptions): this {
// 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
}
this.dispatchEvent({type: 'update', bubbleToParent: false, object: this}) // todo remove
iObjectCommons.setDirty.call(this, {...options, scene: this})
return this
}
private _mainCameraUpdate: EventListener2<'cameraUpdate', IObject3DEventMap, ICamera> = (e) => {
if (!this._mainCamera?.parent) this.setDirty({refreshScene: false})
this.dispatchEvent({...e, type: 'mainCameraUpdate'})
this.dispatchEvent({...e, type: 'activeCameraUpdate'}) // deprecated
if (e.source !== 'RootScene') {
if (e.key === 'fov' && this.dollyActiveCameraFov()) return
if (this.refreshActiveCameraNearFar(!e.projectionUpdated)) {
// it will call mainCameraUpdate twice, that's fine first without projectionUpdated, then with projectionUpdated
return
}
}
}
// cached values
private _sceneBounds: Box3B = new Box3B
private _sceneBoundingRadius = 0
refreshScene(event?: Partial<(ISceneEventMap['objectUpdate']|ISceneEventMap['geometryUpdate']|ISceneEventMap['geometryChanged'])> & ISceneSetDirtyOptions & {type?: keyof ISceneEventMap}): this {
const fromSelf = event && event.type === 'objectUpdate' && (event.object === this || (event as any).target === this)
// todo test the isCamera here. this is for animation object plugin
if (event?.sceneUpdate === false || event?.refreshScene === false || event?.object?.isCamera) return fromSelf ? this : 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._sceneBoundingRadius = this._sceneBounds.getSize(new Vector3()).length() / 2.
this.dispatchEvent({...event, type: 'sceneUpdate', hierarchyChanged: ['addedToParent', 'removedFromParent'].includes(event?.change || '')})
if (!fromSelf) iObjectCommons.setDirty.call(this, {...event, scene: this})
return this
}
refreshUi = iObjectCommons.refreshUi.bind(this)
traverseModels = iObjectCommons.traverseModels.bind(this)
/**
* Dispose the scene and clear all resources.
*/
dispose(clear = true): void {
this.disposeSceneModels(false, clear)
if (clear) {
[...this.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent())
this.clear()
}
// todo: dispose more stuff?
this.disposeTextures(clear)
return
}
/**
* Dispose and optionally remove all textures set directly on this scene.
* @param clear
*/
disposeTextures(clear = true) {
this.environment?.dispose()
if ((this.background as ITexture)?.isTexture) (this.background as ITexture)?.dispose?.()
if (clear) {
this.environment = null
this.background = null
}
}
/**
* 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?: (obj: Object3D)=>boolean): Box3B {
// See bboxVisible in userdata in Box3B
return new Box3B().expandByObject(this, precise, ignoreInvisible, (o: any)=>{
if (ignoreWidgets && ((o as IWidget).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?: (obj: Object3D)=>boolean): Box3B {
if (this.modelRoot == undefined)
return new Box3B()
return new Box3B().expandByObject(this.modelRoot, precise, ignoreInvisible, (o: any)=>{
if (ignoreWidgets && o.assetType === 'widget') return true
return ignoreObject?.(o) ?? false
})
}
autoGPUInstanceMeshes() {
const geoms = new Set<BufferGeometry>()
this.modelRoot.traverseModels!((o) => {o.geometry && geoms.add(o.geometry)}, {visible: false, widgets: false})
geoms.forEach((g: any) => autoGPUInstanceMeshes(g))
}
private _v1 = new Vector3()
private _v2 = new Vector3()
private _autoNearFarDisabled = new Set<string>()
/**
* 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, editor plugins
* autoNearFar will still be disabled if this is true and camera.userData.autoNearFar is false
*/
disableAutoNearFar(id = 'default') {
const enabled = this._autoNearFarDisabled.size === 0
this._autoNearFarDisabled.add(id)
const camera = this.mainCamera as ICamera
if (enabled && camera.userData.autoNearFar !== false) {
let near = camera.near, far = camera.far
near = camera.userData.minNearPlane ?? iCameraCommons.defaultMinNear
far = camera.userData.maxFarPlane ?? iCameraCommons.defaultMaxFar
iCameraCommons.setNearFar(camera, near, far, true, 'RootScene')
}
}
enableAutoNearFar(id = 'default') {
if (!this._autoNearFarDisabled.has(id)) return
this._autoNearFarDisabled.delete(id)
const camera = this.mainCamera as ICamera
if (this._autoNearFarDisabled.size === 0 && camera) {
this.setDirty()
}
}
/**
* 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(setDirty = true): boolean {
const camera = this.mainCamera as ICamera
if (!camera) return false
let near = camera.near, far = camera.far
if (camera.userData.minNearPlane !== undefined) {
near = camera.userData.minNearPlane
}
if (camera.userData.maxFarPlane !== undefined) {
far = camera.userData.maxFarPlane
}
// console.log(this.autoNearFarEnabled, camera.userData.autoNearFar, camera.userData.maxFarPlane, camera.far)
if (this._autoNearFarDisabled.size !== 0 || camera.userData.autoNearFar === false) {
return iCameraCommons.setNearFar(camera, near, far, setDirty, 'RootScene')
}
// 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 size = bbox.getSize(this._v2).length()
if (size < 0.001) {
return iCameraCommons.setNearFar(camera, near, far, setDirty, 'RootScene')
}
camera.getWorldPosition(this._v1).sub(bbox.getCenter(this._v2))
const radius = 1.5 * Math.max(0.25, size) / 2.
const dist = this._v1.length()
// new way
const dist1 = Math.max(0.1, -this._v1.normalize().dot(camera.getWorldDirection(new Vector3())))
near = Math.max(Math.max(camera.userData.minNearPlane ?? iCameraCommons.defaultMinNear, 0.001), dist1 * (dist - radius))
far = Math.min(Math.max(near + radius, dist1 * (dist + radius)), camera.userData.maxFarPlane ?? iCameraCommons.defaultMaxFar)
// 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)
if (far < near || far - near < 0.1) {
far = near + 0.1
}
return iCameraCommons.setNearFar(camera, near, far, setDirty, 'RootScene')
// 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(): boolean {
const camera = this.mainCamera as ICamera
if (!camera) return false
if (!camera.userData.dollyFov) {
return false
}
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({change: 'position', source: 'RootScene'})
return true
}
updateShaderProperties(material: {defines: Record<string, string|number|undefined>, uniforms: {[name: string]: IUniform}}): this {
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?: any): any {
const o = ThreeSerialization.Serialize(this, meta, true)
o.envMapIntensity = o.environmentIntensity // for backward compatibility, remove later
return o
}
/**
* Deserialize the scene properties
* @param json - object from {@link toJSON}
* @param meta
* @returns {this<ICamera>}
*/
fromJSON(json: any, meta?: any): this {
const env = json.environment
if (env !== undefined) {
this.environment = ThreeSerialization.Deserialize(env, this.environment, meta, false)
delete json.environment
if (meta?._configMetadata && meta._configMetadata.version < 2) {
// legacy - files saved pre three.js < r162, threepipe < v0.4.0
if (this.environment?.rotation) {
// old files used to save y rotation inside the texture.
this.environmentRotation.y = this.environment.rotation
this.environment.rotation = 0 // for next save
}
}
}
// some files have both for backwards compatibility, prefer environmentIntensity
if (json.environmentIntensity !== undefined && json.envMapIntensity !== undefined) {
json = {...json}
delete json.envMapIntensity
}
ThreeSerialization.Deserialize(json, this, meta, true)
json.environment = env
return this
}
addEventListener<T extends keyof ISceneEventMap>(type: T, listener: EventListener<ISceneEventMap[T], T, this>): void {
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)
}
/**
* Override environment map to use during rendering.
* When set and _isMainRendering is true, this will be returned instead of the normal environment.
*/
overrideRenderEnvironment: ITexture | null = null;
/**
* Flag to indicate if we're currently in the render step.
* Set by ViewerApp during rendering.
* @internal
*/
['_isMainRendering'] = false
// region inherited type fixes
// re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936
declare traverse: (callback: (object: IObject3D) => void) => void
declare traverseVisible: (callback: (object: IObject3D) => void) => void
declare traverseAncestors: (callback: (object: IObject3D) => void) => void
declare getObjectById: (id: number) => IObject3D | undefined
declare getObjectByName: (name: string) => IObject3D | undefined
declare getObjectByProperty: (name: string, value: string) => IObject3D | undefined
// dispatchEvent: (event: ISceneEvent) => void
declare parent: IObject3D | null
declare children: IObject3D[]
// endregion
// region deprecated
/**
* 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
*/
public findObjectsByName(name: string, parent?: IObject3D, upgradedOnly = false): IObject3D[] {
const o: IObject3D[] = []
const fn = (object: IObject3D) => {
if (object.name === name) o.push(object)
}
const obj: IObject3D = parent ?? this
if (upgradedOnly && obj.traverseModels) obj.traverseModels(fn, {visible: false, widgets: true})
else obj.traverse(fn)
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.
*/
resetCamera(rootObject:Object3D|undefined = undefined, centerOffset = new Vector3(1, 1, 1), targetOffset = new Vector3(0, 0, 0)): void {
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(): number {
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: number) {
console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead')
if (this.mainCamera)
this.mainCamera.userData.minNearPlane = value
}
/**
* @deprecated
*/
get activeCamera(): ICamera {
console.error('activeCamera is deprecated. Use mainCamera instead.')
return this.mainCamera
}
/**
* @deprecated
*/
set activeCamera(camera: ICamera | undefined) {
console.error('activeCamera is deprecated. Use mainCamera instead.')
this.mainCamera = camera
}
/**
* Get the threejs scene object
* @deprecated
*/
get modelObject(): this {
return this as any
}
/**
* Add any processed scene object to the scene.
* @deprecated renamed to {@link addObject}
* @param imported
* @param options
*/
addSceneObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T {
return this.addObject(imported, options)
}
/**
* Equivalent to setDirty({refreshScene: true}), dispatches 'sceneUpdate' event with the specified options.
* @deprecated use refreshScene
* @param options
*/
updateScene(options?: AnyOptions): this {
console.warn('updateScene is deprecated. Use refreshScene instead')
return this.refreshScene(options || {})
}
/**
* @deprecated renamed to {@link clearSceneModels}
*/
removeSceneModels() {
this.clearSceneModels()
}
/**
* @deprecated use {@link enableAutoNearFar} and {@link disableAutoNearFar} instead.
*/
get autoNearFarEnabled() {
return this._autoNearFarDisabled.size === 0
}
/**
* @deprecated use {@link enableAutoNearFar} and {@link disableAutoNearFar} instead.
*/
set autoNearFarEnabled(v) {
if (v) this.enableAutoNearFar('default')
else this.disableAutoNearFar('default')
}
/**
* @deprecated Use environmentIntensity instead.
*/
get envMapIntensity() {
console.warn('RootScene.envMapIntensity is deprecated, use environmentIntensity instead.')
return this.environmentIntensity
}
set envMapIntensity(value: number) {
console.warn('RootScene.envMapIntensity is deprecated, use environmentIntensity instead.')
this.environmentIntensity = value
}
// endregion
}