Sfoglia il codice sorgente

Add OrthographicCamera2, fix jitter when changing position

master
Palash Bansal 1 anno fa
parent
commit
48c42f37b4
Nessun account collegato all'indirizzo email del committer

+ 36
- 0
examples/camera-ortho-uiconfig/index.html Vedi File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Camera UiConfig</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 28
- 0
examples/camera-ortho-uiconfig/script.ts Vedi File

@@ -0,0 +1,28 @@
import {_testFinish, IObject3D, LoadingScreenPlugin, ThreeViewer} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
plugins: [LoadingScreenPlugin],
camera: {
type: 'orthographic',
},
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))

ui.appendChild(viewer.scene.mainCamera.uiConfig)

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
autoCenter: true,
autoScale: true,
})


}

init().finally(_testFinish)

+ 1
- 0
examples/index.html Vedi File

@@ -505,6 +505,7 @@
<li><a href="./material-uiconfig/">Material UI </a></li>
<li><a href="./object-uiconfig/">Object UI </a></li>
<li><a href="./camera-uiconfig/">Camera UI </a></li>
<li><a href="./camera-ortho-uiconfig/">Camera (Ortho) UI </a></li>
<li><a href="./scene-uiconfig/">Scene UI </a></li>
<li><a href="./viewer-uiconfig/">Viewer UI </a></li>
</ul>

+ 568
- 0
src/core/camera/OrthographicCamera2.ts Vedi File

@@ -0,0 +1,568 @@
import {Camera, Event, IUniform, Object3D, OrthographicCamera, Vector3} from 'three'
import {generateUiConfig, uiInput, uiNumber, UiObjectConfig, uiToggle, uiVector} from 'uiconfig.js'
import {onChange, onChange2, onChange3, serialize} from 'ts-browser-helpers'
import type {ICamera, ICameraEvent, ICameraUserData, TCameraControlsMode} from '../ICamera'
import {ICameraSetDirtyOptions} from '../ICamera'
import type {ICameraControls, TControlsCtor} from './ICameraControls'
import {OrbitControls3} from '../../three/controls/OrbitControls3'
import {IObject3D} from '../IObject'
import {ThreeSerialization} from '../../utils'
import {iCameraCommons} from '../object/iCameraCommons'
import {bindToValue} from '../../three/utils/decorators'
import {makeICameraCommonUiConfig} from '../object/IObjectUi'
import {CameraView, ICameraView} from './CameraView'

// todo: extract out common functions with perspective camera into iCameraCommons
// todo: maybe change domElement to some wrapper/base class of viewer
export class OrthographicCamera2 extends OrthographicCamera implements ICamera {
assetType = 'camera' as const
get controls(): ICameraControls | undefined {
return this._controls
}

@uiInput('Name') declare name: string

@serialize('camControls')
private _controls?: ICameraControls
private _currentControlsMode: TCameraControlsMode = ''
@onChange2(OrthographicCamera2.prototype.refreshCameraControls)
controlsMode: TCameraControlsMode
/**
* It should be the canvas actually
* @private
*/
private _canvas?: HTMLCanvasElement
get isMainCamera(): boolean {
return this.userData ? this.userData.__isMainCamera || false : false
}

@serialize()
userData: ICameraUserData = {}

@onChange3(OrthographicCamera2.prototype.setDirty)
@uiNumber('Zoom')
@serialize() declare zoom: number

@onChange3(OrthographicCamera2.prototype.setDirty)
@uiNumber<OrthographicCamera2>('Left', (t)=>({hidden: ()=>t._frustumSize !== undefined}))
@serialize() declare left: number

@onChange3(OrthographicCamera2.prototype.setDirty)
@uiNumber<OrthographicCamera2>('Right', (t)=>({hidden: ()=>t._frustumSize !== undefined}))
@serialize() declare right: number

@onChange3(OrthographicCamera2.prototype.setDirty)
@uiNumber<OrthographicCamera2>('Top', (t)=>({hidden: ()=>t._frustumSize !== undefined}))
@serialize() declare top: number

@onChange3(OrthographicCamera2.prototype.setDirty)
@uiNumber<OrthographicCamera2>('Bottom', (t)=>({hidden: ()=>t._frustumSize !== undefined}))
@serialize() declare bottom: number

private _frustumSize: number | undefined = undefined

/**
* Frustum size of the camera. This is used to calculate bounds (left, right, top, bottom) based on aspect ratio.
* Set to 0 (or negative) value to disable automatic, and to set the bounds manually.
*/
@uiInput<OrthographicCamera2>('Frustum Size'/* , (t)=>({hidden: ()=>t.frustumSize === undefined})*/)
get frustumSize(): number {
return this._frustumSize ?? 0
}

set frustumSize(value: number) {
this._frustumSize = value <= 0 ? undefined : value
this.refreshFrustum(false)
this.setDirty()
}

// @onChange3(OrthographicCamera2.prototype.setDirty)
// @serialize() declare focus: number

// @onChange3(OrthographicCamera2.prototype.setDirty)
// @uiSlider('FoV Zoom', [0.001, 10], 0.001)
// @serialize() declare zoom: number

@uiVector('Position', undefined, undefined, (that:OrthographicCamera2)=>({onChange: ()=>that.setDirty()}))
@serialize() declare readonly position: Vector3

/**
* The target position of the camera (where the camera looks at). Also syncs with the controls.target, so it's not required to set that separately.
* Note: this is always in world-space
* Note: {@link autoLookAtTarget} must be set to `true` to make the camera look at the target when no controls are enabled
*/
@uiVector('Target', undefined, undefined, (that:OrthographicCamera2)=>({onChange: ()=>that.setDirty()}))
@serialize() readonly target: Vector3 = new Vector3(0, 0, 0)

/**
* Automatically manage aspect ratio based on window/canvas size.
* Defaults to `true` if {@link domElement}(canvas) is set.
*/
@serialize()
@onChange2(OrthographicCamera2.prototype.refreshAspect)
@uiToggle('Auto Aspect')
autoAspect: boolean

/**
* Aspect ratio to use when {@link frustumSize} is defined
*/
@serialize()
@onChange2(OrthographicCamera2.prototype.refreshAspect)
@uiToggle<OrthographicCamera2>('Aspect Ratio', (t)=>({disabled: ()=>t.autoAspect}))
aspect: number

/**
* Near clipping plane.
* This is managed by RootScene for active cameras
* To change the minimum that's possible set {@link minNearPlane}
* To use a fixed value set {@link autoNearFar} to false and set {@link minNearPlane}
*/
@onChange2(OrthographicCamera2.prototype._nearFarChanged)
near = 0.01

/**
* Far clipping plane.
* This is managed by RootScene for active cameras
* To change the maximum that's possible set {@link maxFarPlane}
* To use a fixed value set {@link autoNearFar} to false and set {@link maxFarPlane}
*/
@onChange2(OrthographicCamera2.prototype._nearFarChanged)
far = 50

/**
* Automatically make the camera look at the {@link target} on {@link setDirty} call
* Defaults to false. Note that this must be set to true to make the camera look at the target without any controls
*/
@bindToValue({obj: 'userData', onChange: 'setDirty'})
autoLookAtTarget = false // bound to userData so that it's saved in the glb.

/**
* Automatically manage near and far clipping planes based on scene size.
*/
@bindToValue({obj: 'userData', onChange: 'setDirty'})
autoNearFar = true // bound to userData so that it's saved in the glb.

/**
* Minimum near clipping plane allowed. (Distance from camera)
* Used in RootScene when {@link autoNearFar} is true.
* @default 0.2
*/
@bindToValue({obj: 'userData', onChange: 'setDirty'})
minNearPlane = 0.5

/**
* Maximum far clipping plane allowed. (Distance from camera)
* Used in RootScene when {@link autoNearFar} is true.
*/
@bindToValue({obj: 'userData', onChange: 'setDirty'})
maxFarPlane = 1000

constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, frustumSize?: number, left?: number, right?: number, top?: number, bottom?: number, near?: number, far?: number, aspect?: number) {
super(left, right, top, bottom, near, far)
this._canvas = domElement
this.aspect = aspect || 1
this._frustumSize = frustumSize ?? 4
this.autoAspect = autoAspect ?? !!domElement

iCameraCommons.upgradeCamera.call(this) // todo: test if autoUpgrade = false works as expected if we call upgradeObject3D externally after constructor, because we have setDirty, refreshTarget below.

this.controlsMode = controlsMode || ''

this.refreshTarget(undefined, false)
this.refreshFrustum(false)

// if (!camera)
// this.targetUpdated(false)
this.setDirty()


// if (domElement)
// domElement.style.touchAction = 'none' // this is done in orbit controls anyway

// this.refreshCameraControls() // this is done on set controlsMode
// const target = this.target

}

private _interactionsDisabledBy = new Set<string>()

/**
* If interactions are enabled for this camera. It can be disabled by some code or plugin.
* see also {@link setInteractions}
* @deprecated use {@link canUserInteract} to check if the user can interact with this camera
* @readonly
*/
get interactionsEnabled(): boolean {
return this._interactionsDisabledBy.size === 0
}

setInteractions(enabled: boolean, by: string): void {
const size = this._interactionsDisabledBy.size
if (enabled) {
this._interactionsDisabledBy.delete(by)
} else {
this._interactionsDisabledBy.add(by)
}
if (size !== this._interactionsDisabledBy.size) this.refreshCameraControls(true)
}

get canUserInteract() {
return this._interactionsDisabledBy.size === 0 && this.isMainCamera && this.controlsMode !== ''
}

// endregion

// region refreshing

setDirty(options?: ICameraSetDirtyOptions|Event): void {
if (!this._positionWorld) return // class not initialized

if (!options?.key || ['zoom', 'left', 'right', 'top', 'bottom', 'aspect', 'frustumSize'].includes(options.key)) {
this.updateProjectionMatrix()
}

this.getWorldPosition(this._positionWorld)

iCameraCommons.setDirty.call(this, options)
if (options?.last !== false)
this._camUi.forEach(u=>u?.uiRefresh?.(false, 'postFrame', 1)) // because camera changes a lot. so we dont want to deep refresh ui on every change
}

/**
* when aspect ratio is set to auto it must be refreshed on resize, this is done by the viewer for the main camera.
* @param setDirty
*/
refreshAspect(setDirty = true): void {
if (this.autoAspect) {
if (!this._canvas) console.error('OrthographicCamera2: cannot calculate aspect ratio without canvas/container')
else {
let aspect = this._canvas.clientWidth / this._canvas.clientHeight
if (!isFinite(aspect)) aspect = 1
this.aspect = aspect
this.refreshFrustum(false)
}
}
if (setDirty) this.setDirty()
// console.log('refreshAspect', this._options.aspect)
}

protected _nearFarChanged() {
if (this.view === undefined) return // not initialized yet
this.updateProjectionMatrix && this.updateProjectionMatrix()
}

refreshUi = iCameraCommons.refreshUi
refreshTarget = iCameraCommons.refreshTarget
activateMain = iCameraCommons.activateMain
deactivateMain = iCameraCommons.deactivateMain

refreshFrustum(setDirty = true) {
if (this._frustumSize === undefined) return
this.top = this._frustumSize / 2
this.bottom = -this.top
this.left = this.bottom * this.aspect
this.right = this.top * this.aspect
setDirty && this.setDirty()
}

// endregion

// region controls

// todo: move orbit to a plugin maybe? so that its not forced
private _controlsCtors = new Map<string, TControlsCtor>([['orbit', (object, domElement)=>{
const elem = domElement ? !domElement.ownerDocument ? domElement.documentElement : domElement : document.body
const controls = new OrbitControls3(object, elem)
// this._controls.enabled = false

// set tab index so that we get keyboard events
if (elem.tabIndex === -1) {
elem.tabIndex = 1000
// disable focus outline
elem.style.outline = 'none'
}

controls.listenToKeyEvents(elem) // optional // todo: make option for this
// controls.enableKeys = true
controls.screenSpacePanning = true
controls.enableZoom = false
return controls
}]])
setControlsCtor(key: string, ctor: TControlsCtor, replace = false): void {
if (!replace && this._controlsCtors.has(key)) {
console.error('OrthographicCamera2: ' + key + ' already exists.')
return
}
this._controlsCtors.set(key, ctor)
}
removeControlsCtor(key: string): void {
this._controlsCtors.delete(key)
}

private _controlsChanged = ()=>{
if (this._controls && this._controls.target) this.refreshTarget(undefined, false)
this.setDirty({change: 'controls'})
}
private _initCameraControls() {
const mode = this.controlsMode
this._controls = this._controlsCtors.get(mode)?.(this, this._canvas) ?? undefined
if (!this._controls && mode !== '') console.error('OrthographicCamera2 - Unable to create controls with mode ' + mode + '. Are you missing a plugin?')
this._controls?.addEventListener('change', this._controlsChanged)
this._currentControlsMode = this._controls ? mode : ''
// todo maybe set target like this:
// if (this._controls) this._controls.target = this.target
}

private _disposeCameraControls() {
if (this._controls) {
if (this._controls.target === this.target) this._controls.target = new Vector3() // just in case
this._controls?.removeEventListener('change', this._controlsChanged)
this._controls?.dispose()
}
this._currentControlsMode = ''
this._controls = undefined
}

refreshCameraControls(setDirty = true): void {
if (!this._controlsCtors) return // class not initialized
if (this._controls) {
if (this._currentControlsMode !== this.controlsMode || this !== this._controls.object) { // in-case camera changed or mode changed
this._disposeCameraControls()
this._initCameraControls()
}
} else {
this._initCameraControls()
}

// todo: only for orbit control like controls?
if (this._controls) {
const ce = this.canUserInteract
this._controls.enabled = ce
if (ce) this.up.copy(Object3D.DEFAULT_UP)
}

if (setDirty) this.setDirty()
this.refreshUi()
}

// endregion

// region serialization

/**
* Serializes this camera with controls to JSON.
* @param meta - metadata for serialization
* @param baseOnly - Calls only super.toJSON, does internal three.js serialization. Set it to true only if you know what you are doing.
*/
toJSON(meta?: any, baseOnly = false): any {
if (baseOnly) return super.toJSON(meta)
return ThreeSerialization.Serialize(this, meta, true)
}

fromJSON(data: any, meta?: any): this | null {
ThreeSerialization.Deserialize(data, this, meta, true)
this.setDirty({change: 'deserialize'})
return this
}

// endregion

// region camera views

getView<T extends ICameraView = CameraView>(worldSpace = true, _view?: T) {
const up = new Vector3()
this.updateWorldMatrix(true, false)
const matrix = this.matrixWorld
up.x = matrix.elements[4]
up.y = matrix.elements[5]
up.z = matrix.elements[6]
up.normalize()
const view = _view || new CameraView()
view.name = this.name
view.position.copy(this.position)
view.target.copy(this.target)
view.quaternion.copy(this.quaternion)
view.zoom = this.zoom
// view.up.copy(up)
const parent = this.parent
if (parent) {
if (worldSpace) {
view.position.applyMatrix4(parent.matrixWorld)
this.getWorldQuaternion(view.quaternion)
// target, up is already in world space
} else {
up.transformDirection(parent.matrixWorld.clone().invert())
// pos is already in local space
// target should always be in world space
}
}
view.isWorldSpace = worldSpace
view.uiConfig?.uiRefresh?.(true, 'postFrame')
return view as T
}

setView(view: ICameraView) {
this.position.copy(view.position)
this.target.copy(view.target)
// this.up.copy(view.up)
this.quaternion.copy(view.quaternion)
this.zoom = view.zoom
this.setDirty()
}

setViewFromCamera(camera: Camera|ICamera, distanceFromTarget?: number, worldSpace = true) {
// todo: getView, setView can also be used, do we need copy? as that will copy all the properties
this.copy(camera, undefined, distanceFromTarget, worldSpace)
}

setViewToMain(eventOptions: Partial<ICameraEvent>) {
this.dispatchEvent({type: 'setView', ...eventOptions, camera: this, bubbleToParent: true})
}

// endregion

// region utils/others

// for shader prop updater
private _positionWorld = new Vector3()

/**
* See also cameraHelpers.glsl
* @param material
*/
updateShaderProperties(material: {defines: Record<string, string | number | undefined>; uniforms: {[p: string]: IUniform}}): this {
material.uniforms.cameraPositionWorld?.value?.copy(this._positionWorld)
material.uniforms.cameraNearFar?.value?.set(this.near, this.far)
if (material.uniforms.projection) material.uniforms.projection.value = this.projectionMatrix // todo: rename to projectionMatrix2?
material.defines.PERSPECTIVE_CAMERA = this.type === 'OrthographicCamera' ? '1' : '0'
material.defines.ORTHOGRAPHIC_CAMERA = this.type === 'OrthographicCamera' ? '1' : '0'
return this
}


dispose(): void {
this._disposeCameraControls()
// todo: anything else?
// iObjectCommons.dispose and dispatch event dispose is called automatically because of updateObject3d
}

// endregion

// region ui

private _camUi: UiObjectConfig[] = [
...generateUiConfig(this) || [],
{
type: 'input',
label: ()=>(this.autoNearFar ? 'Min' : '') + ' Near',
property: [this, 'minNearPlane'],
},
{
type: 'input',
label: ()=>(this.autoNearFar ? 'Max' : '') + ' Far',
property: [this, 'maxFarPlane'],
},
()=>({ // because _controlsCtors can change
type: 'dropdown',
label: 'Controls Mode',
property: [this, 'controlsMode'],
children: ['', 'orbit', ...this._controlsCtors.keys()].map(v=>({label: v === '' ? 'none' : v, value:v})),
onChange: () => this.refreshCameraControls(),
}),
()=>makeICameraCommonUiConfig.call(this, this.uiConfig),
]

uiConfig: UiObjectConfig = {
type: 'folder',
label: ()=>this.name || 'Camera',
children: [
...this._camUi,
()=>this._controls?.uiConfig,
],
}

// endregion

// region deprecated/old

@onChange((k: string, v: boolean)=>{
if (!v) console.warn('Setting camera invisible is not supported', k, v)
})
declare visible: boolean

get isActiveCamera(): boolean {
return this.isMainCamera
}
/**
* @deprecated use `<T>camera.controls` instead
*/
getControls<T extends ICameraControls>(): T|undefined {
return this._controls as any as T
}

/**
* @deprecated use `this` instead
*/
get cameraObject(): this {
return this
}

/**
* @deprecated use `this` instead
*/
get modelObject(): this {
return this
}

/**
* @deprecated - use setDirty directly
* @param setDirty
*/
targetUpdated(setDirty = true): void {
if (setDirty) this.setDirty()
}

// endregion

// region inherited type fixes
// re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936

traverse: (callback: (object: IObject3D) => void) => void
traverseVisible: (callback: (object: IObject3D) => void) => void
traverseAncestors: (callback: (object: IObject3D) => void) => void
getObjectById: <T extends IObject3D = IObject3D>(id: number) => T | undefined
getObjectByName: <T extends IObject3D = IObject3D>(name: string) => T | undefined
getObjectByProperty: <T extends IObject3D = IObject3D>(name: string, value: string) => T | undefined
copy: (source: ICamera|Camera|IObject3D, recursive?: boolean, distanceFromTarget?: number, worldSpace?: boolean) => this
clone: (recursive?: boolean) => this
add: (...object: IObject3D[]) => this
remove: (...object: IObject3D[]) => this
dispatchEvent: (event: ICameraEvent) => void
declare parent: IObject3D | null
declare children: IObject3D[]

// endregion

}

/**
* Empty class with the constructor same as OrthographicCamera in three.js.
* This can be used to remain compatible with three.js construct signature.
*/
export class OrthographicCamera0 extends OrthographicCamera2 {
constructor(left?: number, right?: number, top?: number, bottom?: number, near?: number, far?: number) {
super(undefined, undefined, undefined, undefined, left, right, top, bottom, near, far, 1)
if (near !== undefined || far) {
this.autoNearFar = false
if (near) {
this.near = near
this.minNearPlane = near
}
if (far) {
this.far = far
this.maxFarPlane = far
}
}
}
}

+ 18
- 27
src/core/camera/PerspectiveCamera2.ts Vedi File

@@ -55,7 +55,7 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
/**
* The target position of the camera (where the camera looks at). Also syncs with the controls.target, so it's not required to set that separately.
* Note: this is always in world-space
* Note: {@link autoLookAtTarget} must be set to trye to make the camera look at the target when no controls are enabled
* Note: {@link autoLookAtTarget} must be set to `true` to make the camera look at the target when no controls are enabled
*/
@uiVector('Target', undefined, undefined, (that:PerspectiveCamera2)=>({onChange: ()=>that.setDirty()}))
@serialize() readonly target: Vector3 = new Vector3(0, 0, 0)
@@ -110,14 +110,14 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {

/**
* 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'})
maxFarPlane = 1000

/**
* Automatically move the camera(dolly) when the field of view(fov) changes.
* Works when controls are enabled or autoLookAtTarget is true.
* Works when controls are enabled or `autoLookAtTarget` is `true`.
*
* Note - this is not exact
*/
@@ -230,7 +230,7 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
let aspect = this._canvas.clientWidth / this._canvas.clientHeight
if (!isFinite(aspect)) aspect = 1
this.aspect = aspect
this.updateProjectionMatrix?.()
this.updateProjectionMatrix && this.updateProjectionMatrix()
}
}
if (setDirty) this.setDirty()
@@ -239,7 +239,7 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {

protected _nearFarChanged() {
if (this.view === undefined) return // not initialized yet
this.updateProjectionMatrix?.()
this.updateProjectionMatrix && this.updateProjectionMatrix()
}

refreshUi = iCameraCommons.refreshUi
@@ -253,17 +253,26 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {

// todo: move orbit to a plugin maybe? so that its not forced
private _controlsCtors = new Map<string, TControlsCtor>([['orbit', (object, domElement)=>{
const controls = new OrbitControls3(object, domElement ? !domElement.ownerDocument ? domElement.documentElement : domElement : document.body)
const elem = domElement ? !domElement.ownerDocument ? domElement.documentElement : domElement : document.body
const controls = new OrbitControls3(object, elem)
// this._controls.enabled = false

// this._controls.listenToKeyEvents(window as any) // optional // todo: this breaks keyboard events in UI like cursor left/right, make option for this
// this._controls.enableKeys = true
// set tab index so that we get keyboard events
if (elem.tabIndex === -1) {
elem.tabIndex = 1000
// disable focus outline
elem.style.outline = 'none'
}

controls.listenToKeyEvents(elem) // optional // todo: make option for this
// controls.enableKeys = true
controls.screenSpacePanning = true

return controls
}]])
setControlsCtor(key: string, ctor: TControlsCtor, replace = false): void {
if (!replace && this._controlsCtors.has(key)) {
console.error(key + ' already exists.')
console.error('PerspectiveCamera2: ' + key + ' already exists.')
return
}
this._controlsCtors.set(key, ctor)
@@ -587,24 +596,6 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
// return this._camera
// }

// for ortho
// private _frustumSize: number | undefined = undefined
//
// get frustumSize(): number | undefined {
// return this._frustumSize
// }
//
// set frustumSize(value: number | undefined) {
// this._frustumSize = value
// if (value !== undefined) {
// cam.top = value / 2
// cam.bottom = -value / 2
// cam.left = aspect * value / 2
// cam.right = -aspect * value / 2
// }
// this.setDirty()
// }

// endregion

// region inherited type fixes

+ 1
- 0
src/core/index.ts Vedi File

@@ -1,4 +1,5 @@
export {PerspectiveCamera2, PerspectiveCamera0} from './camera/PerspectiveCamera2'
export {OrthographicCamera2, OrthographicCamera0} from './camera/OrthographicCamera2'
export {CameraView, type ICameraView} from './camera/CameraView'
export {ExtendedShaderMaterial} from './material/ExtendedShaderMaterial'
export {PhysicalMaterial, type PhysicalMaterialEventTypes, MeshStandardMaterial2} from './material/PhysicalMaterial'

+ 4
- 4
src/core/object/iCameraCommons.ts Vedi File

@@ -9,11 +9,11 @@ export const iCameraCommons = {
this.controls.target.copy(this.target)
// this.controls.update() // this should be done automatically postFrame
}
if (!this.controls || !this.controls.enabled) {
if (this.userData.autoLookAtTarget) {
this.lookAt(this.target)
}
// if (!this.controls || !this.controls.enabled) {
if (this.userData.autoLookAtTarget) {
this.lookAt(this.target)
}
// }
this.dispatchEvent({...options, type: 'update'}) // does not bubble
this.dispatchEvent({...options, type: 'cameraUpdate', bubbleToParent: true}) // this sets dirty in the viewer
iObjectCommons.setDirty.call(this, {refreshScene: false, ...options})

+ 7
- 3
src/viewer/ThreeViewer.ts Vedi File

@@ -20,6 +20,7 @@ import {
IObject3D,
IObjectProcessor,
ITexture,
OrthographicCamera2,
PerspectiveCamera2,
RootScene,
TCameraControlsMode,
@@ -188,10 +189,10 @@ export interface ThreeViewerOptions {
tonemap?: boolean

camera?: {
type?: 'perspective'|'orthographic',
controlsMode?: TCameraControlsMode,
position?: Vector3,
target?: Vector3,

}

// values above this might be clamped in post processing
@@ -373,8 +374,11 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes

// camera

const camera = new PerspectiveCamera2(options.camera?.controlsMode ?? 'orbit', this._canvas)
camera.name = 'Default Camera'
const camera =
options.camera?.type === 'orthographic' ?
new OrthographicCamera2(options.camera?.controlsMode ?? 'orbit', this._canvas) :
new PerspectiveCamera2(options.camera?.controlsMode ?? 'orbit', this._canvas)
camera.name = 'Default Camera' + (camera.type === 'OrthographicCamera' ? ' (Ortho)' : '')
options.camera?.position ? camera.position.copy(options.camera.position) : camera.position.set(0, 0, 5)
options.camera?.target ? camera.target.copy(options.camera.target) : camera.target.set(0, 0, 0)
camera.setDirty()

+ 1
- 1
src/viewer/version.ts Vedi File

@@ -1 +1 @@
export const VERSION = '0.0.37'
export const VERSION = '0.0.38-dev'

+ 33
- 2
website/guide/viewer-api.md Vedi File

@@ -677,9 +677,9 @@ Check [IObject3DEventTypes](https://threepipe.org/docs/interfaces/IObject3DEvent

## ICamera

Source Code: [src/core/camera/PerspectiveCamera2.ts](https://github.com/repalash/threepipe/blob/master/src/core/camera/PerspectiveCamera2.ts), [src/core/ICamera.ts](https://github.com/repalash/threepipe/blob/master/src/core/ICamera.ts)
Source Code: [src/core/camera/PerspectiveCamera2.ts](https://github.com/repalash/threepipe/blob/master/src/core/camera/PerspectiveCamera2.ts), [src/core/ICamera.ts](https://github.com/repalash/threepipe/blob/master/src/core/ICamera.ts), [src/core/camera/OrthographicCamera2.ts](https://github.com/repalash/threepipe/blob/master/src/core/camera/OrthographicCamera2.ts)

API Reference: [PerspectiveCamera2](https://threepipe.org/docs/classes/PerspectiveCamera2.html), [ICamera](https://threepipe.org/docs/interfaces/ICamera.html)
API Reference: [PerspectiveCamera2](https://threepipe.org/docs/classes/PerspectiveCamera2.html), [ICamera](https://threepipe.org/docs/interfaces/ICamera.html), [OrthographicCamera2](https://threepipe.org/docs/classes/OrthographicCamera2.html)

ICamera is an interface for a camera that extends the three.js [Camera](https://threejs.org/docs/#api/en/cameras/Camera).
PerspectiveCamera2 implements the interface,
@@ -781,6 +781,8 @@ camera.deactivateMain()

[`camera.deactivateMain`](https://threepipe.org/docs/classes/PerspectiveCamera2.html#deactivateMain) - Deactivate the camera as the main camera.

[`OrbitControls3`](https://threepipe.org/docs/classes/OrbitControls3.html) - An extension of three.js orbit controls with several new features like scroll damping, room bounds, dolly zoom and more.

See also [CameraViewPlugin](../plugin/CameraViewPlugin) for camera focus animation.

::: info Note
@@ -788,6 +790,35 @@ The constructor signature of `PerspectiveCamera2` is different `PerspectiveCamer
Because of this `PerspectiveCamera0` is provided with the same signature as `PerspectiveCamera` for compatibility, in case the controls functionality is not required.
:::

::: tip Orthographic Projection
Threepipe provides a way to specify the type of the main scene camera when initializing. This is `perspective` by default but can be used as `orthographic` to set the main camera as an orthographic camera.
For most 3d applications, using `orthographic` main camera is not recommended, as it provides a different API and serialized differently.

To use orthographic projection, it is possible to use the Perspective Camera with a field of view of 1 degree for an approximation, which is good enough for most applications.
This has several additional advantages including the ability to move the camera with scroll in orbit controls, animate between perspective and orthographic(by animating FoV), compatibility with gltf and other formats, compatibility with all post-processing plugins, and use the same API for both cameras.
:::

::: details Orthographic Main Camera
`OrthographicCamera2` is an extension of three.js [OrthographicCamera](https://threejs.org/docs/#api/en/cameras/OrthographicCamera) with extra features like target, automatic frustum management(with aspect), automatic near far management(from RootScene), camera control attachment and hooks(like OrbitControls) and the ability to set as the main camera in the root scene.
It can be used as the main camera by passing the type when creating the `ThreeViewer` instance -
```typescript
const viewer = new ThreeViewer({
camera: {
type: 'orthographic',
}
})
const camera = viewer.scene.mainCamera as OrthographicCamera2
```

A `OrthographicCamera0` is also provided similar to `PerspectiveCamera0` for compatibility.

As mentioned above, its possible to use perspective camera with a field of view of 1 degree for an approximation -
```typescript
const viewer = new ThreeViewer({...})
viewer.scene.mainCamera.fov = 1
```
:::

## AssetManager

Source Code: [src/assetmanager/AssetManager.ts](https://github.com/repalash/threepipe/blob/master/src/assetmanager/AssetManager.ts)

Loading…
Annulla
Salva