| rgbm: false, // The pass from three.js doesn't support RGBM encoded render targets | rgbm: false, // The pass from three.js doesn't support RGBM encoded render targets | ||||
| zPrepass: false, | zPrepass: false, | ||||
| renderScale: 1, | renderScale: 1, | ||||
| maxHDRIntensity: 8, | |||||
| maxHDRIntensity: 100, | |||||
| dropzone: { | dropzone: { | ||||
| addOptions: { | addOptions: { | ||||
| disposeSceneObjects: true, | disposeSceneObjects: true, |
| "exports": { | "exports": { | ||||
| ".": { | ".": { | ||||
| "import": "./dist/index.mjs", | "import": "./dist/index.mjs", | ||||
| "types": "./dist/index.d.ts", | |||||
| "require": "./dist/index.js" | "require": "./dist/index.js" | ||||
| }, | }, | ||||
| "./dist/": { | "./dist/": { |
| * @default false | * @default false | ||||
| */ | */ | ||||
| autoLookAtTarget?: boolean | autoLookAtTarget?: boolean | ||||
| /** | |||||
| * Automatically move the camera(dolly) based on the scene size when the field of view(fov) changes. | |||||
| * Works when controls are enabled or autoLookAtTarget is true. | |||||
| * | |||||
| * Note - The camera must be added to RootScene for this to work | |||||
| */ | |||||
| dollyFov?: boolean | |||||
| /** | /** | ||||
| * Disable jitter for this camera. (for {@link SSAAPlugin}) | * Disable jitter for this camera. (for {@link SSAAPlugin}) |
| refreshUi(): void; | refreshUi(): void; | ||||
| uiConfig?: UiObjectConfig | uiConfig?: UiObjectConfig | ||||
| appliedMeshes: Set<IObject3D> | appliedMeshes: Set<IObject3D> | ||||
| center(offset?: Vector3, keepWorldPosition?: boolean): this | |||||
| /** | |||||
| * Centers the geometry. | |||||
| * @param offset - returns the offset applied to the geometry | |||||
| * @param keepWorldPosition - Updates the attached meshes, so that the world position of the geometry remains the same. | |||||
| * @param setDirty | |||||
| */ | |||||
| center(offset?: Vector3, keepWorldPosition?: boolean, setDirty?: boolean): this | |||||
| /** | |||||
| * Same as center but returns a function to undo the centering | |||||
| * @param offset | |||||
| * @param keepWorldPosition | |||||
| */ | |||||
| center2(offset?: Vector3, keepWorldPosition?: boolean): ()=>void | |||||
| // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation | // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation | ||||
| userData: IGeometryUserData | userData: IGeometryUserData |
| import {IDisposable} from 'ts-browser-helpers' | import {IDisposable} from 'ts-browser-helpers' | ||||
| import {IMaterial} from './IMaterial' | import {IMaterial} from './IMaterial' | ||||
| import {Event, Object3D} from 'three' | |||||
| import {Event, Object3D, Vector3} from 'three' | |||||
| import {ChangeEvent, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | import {ChangeEvent, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | ||||
| import {IGeometry, IGeometryEvent} from './IGeometry' | import {IGeometry, IGeometryEvent} from './IGeometry' | ||||
| import {IImportResultUserData} from '../assetmanager' | import {IImportResultUserData} from '../assetmanager' | ||||
| userData: IObject3DUserData | userData: IObject3DUserData | ||||
| /** | /** | ||||
| * Scales the object to fit the given radius. | |||||
| * | * | ||||
| * @param autoScaleRadius - optional (taken from userData.autoScaleRadius by default) | * @param autoScaleRadius - optional (taken from userData.autoScaleRadius by default) | ||||
| * @param isCentered - optional (taken from userData.isCentered by default) | * @param isCentered - optional (taken from userData.isCentered by default) | ||||
| autoScale?(autoScaleRadius?: number, isCentered?: boolean, setDirty?: boolean, undo?: boolean): this | autoScale?(autoScaleRadius?: number, isCentered?: boolean, setDirty?: boolean, undo?: boolean): this | ||||
| /** | /** | ||||
| * Moves the bounding box center of the object to the center of the world | |||||
| * | * | ||||
| * @param setDirty - calls {@link setDirty} @default true | * @param setDirty - calls {@link setDirty} @default true | ||||
| * @param undo - undo any previous autoCenter operation | * @param undo - undo any previous autoCenter operation | ||||
| */ | */ | ||||
| autoCenter?(setDirty?: boolean, undo?: boolean): this | autoCenter?(setDirty?: boolean, undo?: boolean): this | ||||
| /** | |||||
| * Moves the object pivot to the center of the bounding box. | |||||
| * | |||||
| * The object will rotate around the new pivot. | |||||
| * | |||||
| * @param setDirty - calls {@link setDirty} @default true | |||||
| * @returns undo function | |||||
| */ | |||||
| pivotToBoundsCenter?(setDirty?: boolean): () => void | |||||
| /** | |||||
| * Moves the object pivot to the given point | |||||
| * | |||||
| * The object will rotate around the new pivot. | |||||
| * | |||||
| * @param point - point to move the pivot to | |||||
| * @param setDirty - calls {@link setDirty} @default true | |||||
| * @returns undo function | |||||
| */ | |||||
| pivotToPoint?(point: Vector3, setDirty?: boolean): this | |||||
| /** | /** | ||||
| * @deprecated use object directly | * @deprecated use object directly | ||||
| */ | */ |
| */ | */ | ||||
| @bindToValue({obj: 'userData', onChange: 'setDirty'}) | @bindToValue({obj: 'userData', onChange: 'setDirty'}) | ||||
| minNearPlane = 0.5 | minNearPlane = 0.5 | ||||
| /** | /** | ||||
| * Maximum far clipping plane allowed. (Distance from camera) | * Maximum far clipping plane allowed. (Distance from camera) | ||||
| * Used in RootScene when {@link autoNearFar} is true. | * Used in RootScene when {@link autoNearFar} is true. | ||||
| @bindToValue({obj: 'userData', onChange: 'setDirty'}) | @bindToValue({obj: 'userData', onChange: 'setDirty'}) | ||||
| maxFarPlane = 1000 | maxFarPlane = 1000 | ||||
| /** | |||||
| * Automatically move the camera(dolly) when the field of view(fov) changes. | |||||
| * Works when controls are enabled or autoLookAtTarget is true. | |||||
| * | |||||
| * Note - this is not exact | |||||
| */ | |||||
| @bindToValue({obj: 'userData'}) | |||||
| dollyFov = false // bound to userData so that it's saved in the glb. | |||||
| constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, fov?: number, aspect?: number) { | constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, fov?: number, aspect?: number) { | ||||
| super(fov, aspect) | super(fov, aspect) | ||||
| this._canvas = domElement | this._canvas = domElement | ||||
| } | } | ||||
| // endregion | // endregion | ||||
| // region utils/others | // region utils/others | ||||
| // for shader prop updater | // for shader prop updater | ||||
| return this | return this | ||||
| } | } | ||||
| dispose(): void { | dispose(): void { | ||||
| this._disposeCameraControls() | this._disposeCameraControls() | ||||
| // todo: anything else? | // todo: anything else? | ||||
| label: 'Auto Near Far', | label: 'Auto Near Far', | ||||
| property: [this, 'autoNearFar'], | property: [this, 'autoNearFar'], | ||||
| }, | }, | ||||
| { | |||||
| type: 'input', | |||||
| label: 'Dolly FoV', | |||||
| property: [this, 'dollyFov'], | |||||
| }, | |||||
| ()=>({ // because _controlsCtors can change | ()=>({ // because _controlsCtors can change | ||||
| type: 'dropdown', | type: 'dropdown', | ||||
| label: 'Controls Mode', | label: 'Controls Mode', | ||||
| export class PerspectiveCamera0 extends PerspectiveCamera2 { | export class PerspectiveCamera0 extends PerspectiveCamera2 { | ||||
| constructor(fov?: number, aspect?: number, near?: number, far?: number) { | constructor(fov?: number, aspect?: number, near?: number, far?: number) { | ||||
| super(undefined, undefined, undefined, fov, aspect || 1) | super(undefined, undefined, undefined, fov, aspect || 1) | ||||
| this.dollyFov = false | |||||
| if (near || far) { | if (near || far) { | ||||
| this.autoNearFar = false | this.autoNearFar = false | ||||
| if (near) { | if (near) { |
| import {autoGPUInstanceMeshes, isInScene, toIndexedGeometry} from '../../three/utils' | import {autoGPUInstanceMeshes, isInScene, toIndexedGeometry} from '../../three/utils' | ||||
| import {BufferGeometry, Vector3} from 'three' | import {BufferGeometry, Vector3} from 'three' | ||||
| import {ThreeViewer} from '../../viewer' | import {ThreeViewer} from '../../viewer' | ||||
| import {IObject3D} from '../IObject' | |||||
| export const iGeometryCommons = { | export const iGeometryCommons = { | ||||
| setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void { | setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void { | ||||
| }, | }, | ||||
| upgradeGeometry: upgradeGeometry, | upgradeGeometry: upgradeGeometry, | ||||
| center: (superCenter: BufferGeometry['center']): IGeometry['center'] => | center: (superCenter: BufferGeometry['center']): IGeometry['center'] => | ||||
| function(this: IGeometry, offset?: Vector3, keepWorldPosition = false): IGeometry { | |||||
| function(this: IGeometry, offset?: Vector3, keepWorldPosition = false, setDirty = true): IGeometry { | |||||
| if (keepWorldPosition) { | if (keepWorldPosition) { | ||||
| offset = offset ? offset.clone() : new Vector3() | offset = offset ? offset.clone() : new Vector3() | ||||
| superCenter.call(this, offset) | superCenter.call(this, offset) | ||||
| for (const m of meshes) { | for (const m of meshes) { | ||||
| m.updateMatrix() | m.updateMatrix() | ||||
| m.position.copy(offset).applyMatrix4(m.matrix) | m.position.copy(offset).applyMatrix4(m.matrix) | ||||
| m.setDirty() | |||||
| if (setDirty) m.setDirty() | |||||
| } | } | ||||
| } else { | } else { | ||||
| superCenter.call(this, offset) | superCenter.call(this, offset) | ||||
| } | } | ||||
| this.setDirty() | |||||
| if (setDirty) this.setDirty() | |||||
| return this | return this | ||||
| }, | }, | ||||
| center2: function(this: IGeometry, offset?: Vector3, keepWorldPosition = false, setDirty = true): ()=>void { | |||||
| const offset1 = offset ? offset : new Vector3() | |||||
| if (keepWorldPosition) { | |||||
| this.center(offset1, false, false) | |||||
| const meshes = this.appliedMeshes | |||||
| const positions = new WeakMap<IObject3D, Vector3>() | |||||
| for (const m of meshes) { | |||||
| m.updateMatrix() | |||||
| positions.set(m, m.position.clone()) | |||||
| m.position.set(-offset1.x, -offset1.y, -offset1.z).applyMatrix4(m.matrix) | |||||
| if (setDirty) m.setDirty() | |||||
| } | |||||
| if (setDirty) this.setDirty() | |||||
| return ()=>{ | |||||
| // undo | |||||
| for (const m of meshes) { | |||||
| const pos = positions.get(m) | |||||
| if (!pos) { | |||||
| console.warn('GeometryCommons: No position found for mesh', m) | |||||
| continue | |||||
| } | |||||
| m.position.copy(pos) | |||||
| if (setDirty) m.setDirty() | |||||
| } | |||||
| if (setDirty) this.setDirty() | |||||
| } | |||||
| } else { | |||||
| this.center(offset1, false, false) | |||||
| if (setDirty) this.setDirty() | |||||
| return ()=>{ | |||||
| // undo | |||||
| this.translate(-offset1.x, -offset1.y, -offset1.z) | |||||
| if (setDirty) this.setDirty() | |||||
| } | |||||
| } | |||||
| }, | |||||
| makeUiConfig: function(this: IGeometry): UiObjectConfig { | makeUiConfig: function(this: IGeometry): UiObjectConfig { | ||||
| if (this.uiConfig) return this.uiConfig | if (this.uiConfig) return this.uiConfig | ||||
| return { | return { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Center Geometry', | label: 'Center Geometry', | ||||
| value: async() => { | value: async() => { | ||||
| if (!await ThreeViewer.Dialog.confirm('This will move the objects based on the geometry center, do you want to continue?\nThis action cannot be undone.')) return | |||||
| this.center() | |||||
| if (!await ThreeViewer.Dialog.confirm('This will move the objects based on the geometry center, do you want to continue?')) return | |||||
| return this.center2() | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Center Geometry (keep position)', | label: 'Center Geometry (keep position)', | ||||
| value: async() => { | value: async() => { | ||||
| if (!await ThreeViewer.Dialog.confirm('This will move the geometry center keeping the object position, do you want to continue?\nThis action cannot be undone.')) return | |||||
| this.center(undefined, true) | |||||
| if (!await ThreeViewer.Dialog.confirm('This will move the geometry center keeping the object position, do you want to continue?')) return | |||||
| return this.center2(undefined, true) | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| this.dispose = iGeometryCommons.dispose(this.dispose) | this.dispose = iGeometryCommons.dispose(this.dispose) | ||||
| this.center = iGeometryCommons.center(this.center) | this.center = iGeometryCommons.center(this.center) | ||||
| this.clone = iGeometryCommons.clone(this.clone) | this.clone = iGeometryCommons.clone(this.clone) | ||||
| if (!this.center2) this.center2 = iGeometryCommons.center2 | |||||
| if (!this.setDirty) this.setDirty = iGeometryCommons.setDirty | if (!this.setDirty) this.setDirty = iGeometryCommons.setDirty | ||||
| if (!this.refreshUi) this.refreshUi = iGeometryCommons.refreshUi | if (!this.refreshUi) this.refreshUi = iGeometryCommons.refreshUi |
| } | } | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | |||||
| type: 'button', | |||||
| label: 'Pivot to Node Center', | |||||
| value: async()=>{ | |||||
| const res = await ThreeViewer.Dialog.confirm('Pivot to Center: Adjust the pivot to bounding box center. The object will rotate around the new pivot, are you sure you want to proceed?') | |||||
| if (!res) return | |||||
| return this.pivotToBoundsCenter?.(true) // return value is the undo function | |||||
| }, | |||||
| }, | |||||
| { | { | ||||
| type: 'folder', | type: 'folder', | ||||
| label: 'Rotate model', | label: 'Rotate model', |
| import {RootSceneImportResult} from '../../assetmanager' | import {RootSceneImportResult} from '../../assetmanager' | ||||
| import {uiButton, uiColor, uiConfig, uiFolderContainer, uiImage, UiObjectConfig, uiSlider, uiToggle} from 'uiconfig.js' | import {uiButton, uiColor, uiConfig, uiFolderContainer, uiImage, UiObjectConfig, uiSlider, uiToggle} from 'uiconfig.js' | ||||
| import {IGeometry} from '../IGeometry' | import {IGeometry} from '../IGeometry' | ||||
| import {getFittingDistance} from '../../three/utils/camera' | |||||
| export type TCamera = ICamera | export type TCamera = ICamera | ||||
| centerAllGeometries(keepPosition = true, obj?: IObject3D) { | centerAllGeometries(keepPosition = true, obj?: IObject3D) { | ||||
| const geoms = new Set<IGeometry>() | const geoms = new Set<IGeometry>() | ||||
| ;(obj ?? this.modelRoot).traverse((o) => o.geometry && geoms.add(o.geometry)) | ;(obj ?? this.modelRoot).traverse((o) => o.geometry && geoms.add(o.geometry)) | ||||
| geoms.forEach(g => g.center(undefined, keepPosition)) | |||||
| const undos: (()=>void)[] = [] | |||||
| geoms.forEach(g => undos.push(g.center2(undefined, keepPosition))) | |||||
| return ()=>undos.forEach(u=>u()) | |||||
| } | } | ||||
| clearSceneModels(dispose = false, setDirty = true): void { | clearSceneModels(dispose = false, setDirty = true): void { | ||||
| private _mainCameraUpdate = (e: any) => { | private _mainCameraUpdate = (e: any) => { | ||||
| this.setDirty({refreshScene: false}) | this.setDirty({refreshScene: false}) | ||||
| this.refreshActiveCameraNearFar() | this.refreshActiveCameraNearFar() | ||||
| if (e.key === 'fov') this.dollyActiveCameraFov() | |||||
| this.dispatchEvent({...e, type: 'mainCameraUpdate'}) | this.dispatchEvent({...e, type: 'mainCameraUpdate'}) | ||||
| this.dispatchEvent({...e, type: 'activeCameraUpdate'}) // deprecated | this.dispatchEvent({...e, type: 'activeCameraUpdate'}) // deprecated | ||||
| } | } | ||||
| 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 | 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) | // console.warn(event) | ||||
| this.refreshActiveCameraNearFar() | this.refreshActiveCameraNearFar() | ||||
| // this.dollyActiveCameraFov() | |||||
| this._sceneBounds = this.getBounds(false, true) | this._sceneBounds = this.getBounds(false, true) | ||||
| // this.boxHelper?.boxHelper?.copy?.(this._sceneBounds) | // this.boxHelper?.boxHelper?.copy?.(this._sceneBounds) | ||||
| this._sceneBoundingRadius = this._sceneBounds.getSize(new Vector3()).length() / 2. | this._sceneBoundingRadius = this._sceneBounds.getSize(new Vector3()).length() / 2. | ||||
| // camera.far = 20 | // 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(): void { | |||||
| const camera = this.mainCamera as TCamera | |||||
| 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: {defines: Record<string, string|number|undefined>, uniforms: {[name: string]: IUniform}}): this { | updateShaderProperties(material: {defines: Record<string, string|number|undefined>, uniforms: {[name: string]: IUniform}}): this { | ||||
| if (material.uniforms.sceneBoundingRadius) material.uniforms.sceneBoundingRadius.value = this._sceneBoundingRadius | if (material.uniforms.sceneBoundingRadius) material.uniforms.sceneBoundingRadius.value = this._sceneBoundingRadius | ||||
| else console.warn('RootScene: no uniform: sceneBoundingRadius') | else console.warn('RootScene: no uniform: sceneBoundingRadius') |
| import {Event, Mesh, Vector3} from 'three' | |||||
| import {Event, Matrix4, Mesh, Vector3} from 'three' | |||||
| import {IMaterial} from '../IMaterial' | import {IMaterial} from '../IMaterial' | ||||
| import {objectHasOwn} from 'ts-browser-helpers' | import {objectHasOwn} from 'ts-browser-helpers' | ||||
| import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject' | import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject' | ||||
| if (setDirty) this.setDirty({change: 'autoCenter', undo}) | if (setDirty) this.setDirty({change: 'autoCenter', undo}) | ||||
| return this | return this | ||||
| }, | }, | ||||
| autoScale: function<T extends IObject3D>(this: T, autoScaleRadius?: number, isCentered?: boolean, setDirty = true, undo = false): T { | autoScale: function<T extends IObject3D>(this: T, autoScaleRadius?: number, isCentered?: boolean, setDirty = true, undo = false): T { | ||||
| let scale = 1 | let scale = 1 | ||||
| if (undo) { // Note - undo only works for quick undo, not for multiple times | if (undo) { // Note - undo only works for quick undo, not for multiple times | ||||
| return this | return this | ||||
| }, | }, | ||||
| pivotToBoundsCenter: function<T extends IObject3D>(this: T, setDirty = true): ()=>void { | |||||
| const bb = new Box3B().expandByObject(this, true, true) | |||||
| const center = bb.getCenter(new Vector3()) | |||||
| return iObjectCommons.pivotToPoint.call(this, center, setDirty) | |||||
| }, | |||||
| pivotToPoint: function<T extends IObject3D>(this: T, point: Vector3, setDirty = true): ()=>void { | |||||
| const worldCenter = new Vector3().copy(point) | |||||
| const localCenter = new Vector3().copy(worldCenter) | |||||
| const worldMatrixInv = new Matrix4().copy(this.matrixWorld).invert() | |||||
| const m = this.parent?.matrixWorld | |||||
| const parentWorldMatrixInv = new Matrix4() | |||||
| if (m !== undefined) | |||||
| parentWorldMatrixInv.copy(m).invert() | |||||
| // Get the center with respect to the parent | |||||
| worldCenter.applyMatrix4(parentWorldMatrixInv) | |||||
| const lastPosition = this.position.clone() | |||||
| // Apply the new position | |||||
| this.position.copy(worldCenter) | |||||
| // local center | |||||
| localCenter.applyMatrix4(worldMatrixInv).negate() | |||||
| // Shift the geometry | |||||
| if (this.geometry) { | |||||
| this.geometry.translate(localCenter.x, localCenter.y, localCenter.z) | |||||
| } | |||||
| // Add offsets | |||||
| this.children.forEach((object)=> { | |||||
| object.position.add(localCenter) | |||||
| }) | |||||
| if (setDirty) this.setDirty({change: 'pivotToPoint', undo: false}) | |||||
| return ()=>{ | |||||
| // undo | |||||
| this.position.copy(lastPosition) | |||||
| if (this.geometry) { | |||||
| this.geometry.translate(-localCenter.x, -localCenter.y, -localCenter.z) | |||||
| } | |||||
| this.children.forEach((object)=> { | |||||
| object.position.sub(localCenter) | |||||
| }) | |||||
| if (setDirty) this.setDirty({change: 'pivotToPoint', undo: true}) | |||||
| } | |||||
| }, | |||||
| eventCallbacks: { | eventCallbacks: { | ||||
| onAddedToParent: function(this: IObject3D, e: Event): void { | onAddedToParent: function(this: IObject3D, e: Event): void { | ||||
| // added to some parent | // added to some parent | ||||
| if (!this.refreshUi) this.refreshUi = iObjectCommons.refreshUi | if (!this.refreshUi) this.refreshUi = iObjectCommons.refreshUi | ||||
| if (!this.autoScale) this.autoScale = iObjectCommons.autoScale.bind(this) | if (!this.autoScale) this.autoScale = iObjectCommons.autoScale.bind(this) | ||||
| if (!this.autoCenter) this.autoCenter = iObjectCommons.autoCenter.bind(this) | if (!this.autoCenter) this.autoCenter = iObjectCommons.autoCenter.bind(this) | ||||
| if (!this.pivotToBoundsCenter) this.pivotToBoundsCenter = iObjectCommons.pivotToBoundsCenter.bind(this) | |||||
| if (!this.pivotToPoint) this.pivotToPoint = iObjectCommons.pivotToPoint.bind(this) | |||||
| // fired from Object3D.js | // fired from Object3D.js | ||||
| this.addEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent) | this.addEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent) |
| import {onChange, serialize, timeout} from 'ts-browser-helpers' | import {onChange, serialize, timeout} from 'ts-browser-helpers' | ||||
| import {generateUiConfig, uiButton, uiDropdown, uiInput, UiObjectConfig, uiSlider, uiToggle} from 'uiconfig.js' | import {generateUiConfig, uiButton, uiDropdown, uiInput, UiObjectConfig, uiSlider, uiToggle} from 'uiconfig.js' | ||||
| import {EasingFunctions, EasingFunctionType} from '../../utils' | import {EasingFunctions, EasingFunctionType} from '../../utils' | ||||
| import {CameraView, ICamera, ICameraView, PerspectiveCamera2} from '../../core' | |||||
| import {CameraView, ICamera, ICameraView} from '../../core' | |||||
| import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin' | import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin' | ||||
| import {InteractionPromptPlugin} from '../interaction/InteractionPromptPlugin' | import {InteractionPromptPlugin} from '../interaction/InteractionPromptPlugin' | ||||
| import {getFittingDistance} from '../../three/utils/camera' | |||||
| export interface CameraViewPluginOptions{duration?: number, ease?: EasingFunctionType, interpolateMode?: 'spherical'|'linear'} | export interface CameraViewPluginOptions{duration?: number, ease?: EasingFunctionType, interpolateMode?: 'spherical'|'linear'} | ||||
| public async animateToFitObject(selected?: Object3D, distanceMultiplier = 1.5, duration = 1000, ease?: Easing|EasingFunctionType, distanceBounds = {min: 0.5, max: 50.0}) { | public async animateToFitObject(selected?: Object3D, distanceMultiplier = 1.5, duration = 1000, ease?: Easing|EasingFunctionType, distanceBounds = {min: 0.5, max: 50.0}) { | ||||
| if (!this._viewer) return | if (!this._viewer) return | ||||
| const bbox = new Box3B().expandByObject(selected || this._viewer.scene.modelRoot, false, true) | const bbox = new Box3B().expandByObject(selected || this._viewer.scene.modelRoot, false, true) | ||||
| const cameraZ = getFittingDistance(this._viewer.scene.mainCamera, bbox) | |||||
| const center = bbox.getCenter(new Vector3()) // world position | const center = bbox.getCenter(new Vector3()) // world position | ||||
| const size = bbox.getSize(new Vector3()) | |||||
| const cam = this._viewer.scene.mainCamera | |||||
| let cameraZ = 1 | |||||
| if (cam.isPerspectiveCamera && size.length() > 0.0001) { | |||||
| const aspect = isFinite(cam.aspect) ? cam.aspect : 1 | |||||
| // get the max side of the bounding box (fits to width OR height as needed ) | |||||
| const fov = Math.max(1, (cam as PerspectiveCamera2).fov) * (Math.PI / 180) | |||||
| const fovh = 2 * Math.atan(Math.tan(fov / 2) * aspect) | |||||
| const dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)) | |||||
| const dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)) | |||||
| cameraZ = Math.max(dx, dy) | |||||
| } | |||||
| await this.animateToTarget(Math.min(distanceBounds.max, Math.max(distanceBounds.min, cameraZ * distanceMultiplier)), center, duration, ease) | await this.animateToTarget(Math.min(distanceBounds.max, Math.max(distanceBounds.min, cameraZ * distanceMultiplier)), center, duration, ease) | ||||
| } | } | ||||
| @uiButton('Center All Meshes') | @uiButton('Center All Meshes') | ||||
| centerAllMeshes() { | centerAllMeshes() { | ||||
| this._viewer?.scene.centerAllGeometries(true) | |||||
| return this._viewer?.scene.centerAllGeometries(true) | |||||
| } | } | ||||
| } | } |
| import {IPassID, IPipelinePass} from './Pass' | import {IPassID, IPipelinePass} from './Pass' | ||||
| import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../core' | import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../core' | ||||
| import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js' | import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js' | ||||
| import {getOrCall, ValOrFunc} from 'ts-browser-helpers' | |||||
| @uiFolderContainer<GBufferRenderPass>((c)=>c.passId + ' Render Pass') | @uiFolderContainer<GBufferRenderPass>((c)=>c.passId + ' Render Pass') | ||||
| export class GBufferRenderPass<TP extends IPassID=IPassID, T extends WebGLMultipleRenderTargets | WebGLRenderTarget=WebGLMultipleRenderTargets | WebGLRenderTarget> extends RenderPass implements IPipelinePass<TP> { // todo: extend from jittered? | export class GBufferRenderPass<TP extends IPassID=IPassID, T extends WebGLMultipleRenderTargets | WebGLRenderTarget=WebGLMultipleRenderTargets | WebGLRenderTarget> extends RenderPass implements IPipelinePass<TP> { // todo: extend from jittered? | ||||
| after?: IPassID[] | after?: IPassID[] | ||||
| required?: IPassID[] | required?: IPassID[] | ||||
| constructor(public readonly passId: TP, public target: T, material: Material, clearColor: Color = new Color(1, 1, 1), clearAlpha = 1) { | |||||
| constructor(public readonly passId: TP, public target: ValOrFunc<T>, material: Material, clearColor: Color = new Color(1, 1, 1), clearAlpha = 1) { | |||||
| super(undefined, undefined, material, clearColor, clearAlpha) | super(undefined, undefined, material, clearColor, clearAlpha) | ||||
| } | } | ||||
| transparentRender: false, | transparentRender: false, | ||||
| transmissionRender: false, | transmissionRender: false, | ||||
| mainRenderPass: false, | mainRenderPass: false, | ||||
| }, ()=> super.render(renderer, null, this.target, deltaTime as any, maskActive as any)) // here this.target is the write-buffer, variable writeBuffer is ignored | |||||
| }, ()=> super.render(renderer, null, getOrCall(this.target), deltaTime as any, maskActive as any)) // here this.target is the write-buffer, variable writeBuffer is ignored | |||||
| this._transparentMats.forEach(m => m.transparent = !m.transparent) | this._transparentMats.forEach(m => m.transparent = !m.transparent) | ||||
| this._transparentMats.clear() | this._transparentMats.clear() |
| import {Vector3} from 'three' | |||||
| import {Box3B} from '../math/Box3B' | |||||
| import {ICamera, PerspectiveCamera2} from '../../core' | |||||
| /** | |||||
| * Find distance of camera at which the camera's fov fits the given bounding box dimensions | |||||
| * @param cam | |||||
| * @param box | |||||
| */ | |||||
| export function getFittingDistance(cam: ICamera, box: Box3B): number { | |||||
| const size = box.getSize(new Vector3()) | |||||
| let cameraZ = 1 | |||||
| if (cam.isPerspectiveCamera && size.length() > 0.0001) { | |||||
| const aspect = isFinite(cam.aspect) ? cam.aspect : 1 | |||||
| // get the max side of the bounding box (fits to width OR height as needed ) | |||||
| const fov = Math.max(1, (cam as PerspectiveCamera2).fov) * (Math.PI / 180) | |||||
| const fovh = 2 * Math.atan(Math.tan(fov / 2) * aspect) | |||||
| const dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)) | |||||
| const dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)) | |||||
| cameraZ = Math.max(dx, dy) | |||||
| } | |||||
| return cameraZ | |||||
| } |
| // noinspection ES6PreferShortImport | // noinspection ES6PreferShortImport | ||||
| import {TonemapPlugin} from '../plugins/postprocessing/TonemapPlugin' | import {TonemapPlugin} from '../plugins/postprocessing/TonemapPlugin' | ||||
| import {VERSION} from './version' | import {VERSION} from './version' | ||||
| import {Easing} from 'popmotion' | |||||
| import {OrbitControls3} from '../three' | import {OrbitControls3} from '../three' | ||||
| export interface IViewerEvent extends BaseEvent, Partial<IAnimationLoopEvent> { | export interface IViewerEvent extends BaseEvent, Partial<IAnimationLoopEvent> { | ||||
| */ | */ | ||||
| rgbm?: boolean | rgbm?: boolean | ||||
| /** | /** | ||||
| * Use rendered gbuffer as depth-prepass / z-prepass. (Requires DepthBufferPlugin/GBufferPlugin) | |||||
| * todo: It will be disabled when there are any transparent/transmissive objects with render to depth buffer enabled. | |||||
| * Use rendered gbuffer as depth-prepass / z-prepass. (Requires DepthBufferPlugin/GBufferPlugin). | |||||
| * Set it to true if you only have opaque objects in the scene to get better performance. | |||||
| * | |||||
| * todo fix: It will be disabled when there are any transparent/transmissive objects with render to depth buffer enabled, see forceZPrepass | |||||
| */ | */ | ||||
| zPrepass?: boolean | zPrepass?: boolean | ||||
| /** | /** | ||||
| target?: Vector3, | target?: Vector3, | ||||
| } | } | ||||
| // values above this might be clamped in post processing | // values above this might be clamped in post processing | ||||
| maxHDRIntensity?: number | maxHDRIntensity?: number | ||||
| // this.fromJSON(config, config.resources) | // this.fromJSON(config, config.resources) | ||||
| // } | // } | ||||
| public async fitToView(selected?: Object3D, distanceMultiplier = 1.5, duration?: number, ease?: Easing|EasingFunctionType) { | |||||
| public async fitToView(selected?: Object3D, distanceMultiplier = 1.5, duration?: number, ease?: ((v: number) => number)|EasingFunctionType) { | |||||
| const camViews = this.getPlugin<CameraViewPlugin>('CameraViews') | const camViews = this.getPlugin<CameraViewPlugin>('CameraViews') | ||||
| if (!camViews) { | if (!camViews) { | ||||
| this.console.error('ThreeViewer: CameraViewPlugin (CameraViews) is required for fitToView to work') | this.console.error('ThreeViewer: CameraViewPlugin (CameraViews) is required for fitToView to work') |