diff --git a/editor/index.html b/editor/index.html index 8c45220c640e59..866b956d1f1757 100644 --- a/editor/index.html +++ b/editor/index.html @@ -195,6 +195,7 @@ const signals = editor.signals; + signals.cameraResetted.add( saveState ); signals.geometryChanged.add( saveState ); signals.objectAdded.add( saveState ); signals.objectChanged.add( saveState ); diff --git a/editor/js/Config.js b/editor/js/Config.js index 63d4bf4425bdb3..620596d1dd62bd 100644 --- a/editor/js/Config.js +++ b/editor/js/Config.js @@ -15,6 +15,8 @@ function Config() { 'project/editable': false, 'project/vr': false, + 'project/camera': 'perspective', + 'project/renderer/type': 'WebGLRenderer', 'project/renderer/antialias': true, 'project/renderer/shadows': true, @@ -28,7 +30,9 @@ function Config() { 'settings/shortcuts/rotate': 'e', 'settings/shortcuts/scale': 'r', 'settings/shortcuts/undo': 'z', - 'settings/shortcuts/focus': 'f' + 'settings/shortcuts/focus': 'f', + 'settings/shortcuts/perspective': 'p', + 'settings/shortcuts/orthographic': 'o' }; if ( window.localStorage[ name ] === undefined ) { diff --git a/editor/js/Editor.js b/editor/js/Editor.js index 98873d47289b2e..baa5077221e7f9 100644 --- a/editor/js/Editor.js +++ b/editor/js/Editor.js @@ -11,6 +11,7 @@ var _DEFAULT_CAMERA = new THREE.PerspectiveCamera( 50, 1, 0.001, 1e10 ); _DEFAULT_CAMERA.name = 'Camera'; _DEFAULT_CAMERA.position.set( 0, 5, 10 ); _DEFAULT_CAMERA.lookAt( new THREE.Vector3() ); +const _ORTHOGRAPHIC_FRUSTUM_SIZE = 100; function Editor() { @@ -551,6 +552,68 @@ Editor.prototype = { }, + setCameraType: function ( type ) { + + const oldCamera = this.camera; + + const isOrthographic = oldCamera.isOrthographicCamera === true; + + if ( ( type === 'orthographic' && isOrthographic ) || ( type === 'perspective' && ! isOrthographic ) ) return; + + // the orbit point the framing should be preserved around + + const center = this.controls ? this.controls.center : new THREE.Vector3(); + const distance = oldCamera.position.distanceTo( center ); + + let newCamera; + + if ( type === 'orthographic' ) { + + const halfSize = _ORTHOGRAPHIC_FRUSTUM_SIZE / 2; + newCamera = new THREE.OrthographicCamera( - halfSize, halfSize, halfSize, - halfSize, 0, 10000 ); + newCamera.position.copy( oldCamera.position ); + newCamera.quaternion.copy( oldCamera.quaternion ); + + // derive the zoom so the orthographic framing matches the perspective view at the orbit center + + const halfFOV = THREE.MathUtils.DEG2RAD * oldCamera.fov / 2; + newCamera.zoom = ( newCamera.top - newCamera.bottom ) / ( 2 * Math.max( distance, 0.0001 ) * Math.tan( halfFOV ) ); + + } else { + + newCamera = new THREE.PerspectiveCamera( 50, 1, 0.001, 1e10 ); + newCamera.quaternion.copy( oldCamera.quaternion ); + + // reposition along the view direction so the perspective framing matches the orthographic view + + const halfFOV = THREE.MathUtils.DEG2RAD * newCamera.fov / 2; + const targetDistance = ( oldCamera.top - oldCamera.bottom ) / ( 2 * oldCamera.zoom * Math.tan( halfFOV ) ); + + const offset = new THREE.Vector3().subVectors( oldCamera.position, center ); + if ( offset.lengthSq() === 0 ) offset.set( 0, 0, 1 ).applyQuaternion( oldCamera.quaternion ); + offset.normalize().multiplyScalar( targetDistance ); + + newCamera.position.copy( center ).add( offset ); + + } + + newCamera.name = oldCamera.name; + newCamera.uuid = oldCamera.uuid; + newCamera.updateProjectionMatrix(); + + this.camera = newCamera; + this.cameras[ newCamera.uuid ] = newCamera; + + if ( this.viewportCamera === oldCamera ) this.viewportCamera = newCamera; + + this.signals.cameraResetted.dispatch(); + + // keep the selection (and thus the sidebar) in sync with the new camera instance + + if ( this.selected === oldCamera ) this.select( newCamera ); + + }, + setViewportCamera: function ( uuid ) { this.viewportCamera = this.cameras[ uuid ] || this.camera; @@ -629,6 +692,7 @@ Editor.prototype = { this.history.clear(); this.storage.clear(); + this.setCameraType( 'perspective' ); this.camera.copy( _DEFAULT_CAMERA ); this.signals.cameraResetted.dispatch(); @@ -676,6 +740,8 @@ Editor.prototype = { var loader = new THREE.ObjectLoader(); var camera = await loader.parseAsync( json.camera ); + this.setCameraType( camera.isOrthographicCamera ? 'orthographic' : 'perspective' ); + const existingUuid = this.camera.uuid; const incomingUuid = camera.uuid; diff --git a/editor/js/EditorControls.js b/editor/js/EditorControls.js index c8a66add08a0cb..7ad14feb1fc3fb 100644 --- a/editor/js/EditorControls.js +++ b/editor/js/EditorControls.js @@ -40,6 +40,12 @@ class EditorControls extends THREE.EventDispatcher { var changeEvent = { type: 'change' }; + this.setCamera = function ( camera ) { + + object = camera; + + }; + this.focus = function ( target ) { var distance; @@ -66,13 +72,22 @@ class EditorControls extends THREE.EventDispatcher { object.position.copy( center ).add( delta ); + if ( object.isOrthographicCamera ) { + + object.zoom = ( object.top - object.bottom ) / ( distance * 2 ); + object.updateProjectionMatrix(); + + } + scope.dispatchEvent( changeEvent ); }; this.pan = function ( delta ) { - var distance = object.position.distanceTo( center ); + var distance = object.isOrthographicCamera + ? ( object.top - object.bottom ) / object.zoom + : object.position.distanceTo( center ); delta.multiplyScalar( distance * scope.panSpeed ); delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) ); @@ -86,15 +101,24 @@ class EditorControls extends THREE.EventDispatcher { this.zoom = function ( delta ) { - var distance = object.position.distanceTo( center ); + if ( object.isOrthographicCamera ) { - delta.multiplyScalar( distance * scope.zoomSpeed ); + object.zoom = Math.max( 0.0001, object.zoom * Math.pow( 0.95, delta.z ) ); + object.updateProjectionMatrix(); - if ( delta.length() > distance ) return; + } else { - delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) ); + var distance = object.position.distanceTo( center ); - object.position.add( delta ); + delta.multiplyScalar( distance * scope.zoomSpeed ); + + if ( delta.length() > distance ) return; + + delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) ); + + object.position.add( delta ); + + } scope.dispatchEvent( changeEvent ); diff --git a/editor/js/Menubar.Add.js b/editor/js/Menubar.Add.js index b097e502416d9e..f9e9ba6d347686 100644 --- a/editor/js/Menubar.Add.js +++ b/editor/js/Menubar.Add.js @@ -526,7 +526,9 @@ function MenubarAdd( editor ) { option.setTextContent( strings.getKey( 'menubar/add/camera/orthographic' ) ); option.onClick( function () { - const aspect = editor.camera.aspect; + const aspect = editor.camera.isPerspectiveCamera + ? editor.camera.aspect + : ( editor.camera.right - editor.camera.left ) / ( editor.camera.top - editor.camera.bottom ); const camera = new THREE.OrthographicCamera( - aspect, aspect ); camera.name = 'OrthographicCamera'; diff --git a/editor/js/Menubar.Render.js b/editor/js/Menubar.Render.js index 0902f7c8232410..3fef5d1a698fe5 100644 --- a/editor/js/Menubar.Render.js +++ b/editor/js/Menubar.Render.js @@ -202,13 +202,28 @@ class RenderImageDialog { const loader = new THREE.ObjectLoader(); const camera = await loader.parseAsync( json.camera ); - camera.aspect = imageWidth.getValue() / imageHeight.getValue(); + + const aspect = imageWidth.getValue() / imageHeight.getValue(); + + if ( camera.isPerspectiveCamera ) { + + camera.aspect = aspect; + + } else { + + const frustumHeight = camera.top - camera.bottom; + + camera.left = - frustumHeight * aspect / 2; + camera.right = frustumHeight * aspect / 2; + + } + camera.updateProjectionMatrix(); camera.updateMatrixWorld(); const scene = await loader.parseAsync( json.scene ); - const renderer = new THREE.WebGLRenderer( { antialias: true, logarithmicDepthBuffer: true } ); + const renderer = new THREE.WebGLRenderer( { antialias: true, reversedDepthBuffer: true } ); renderer.setSize( imageWidth.getValue(), imageHeight.getValue() ); renderer.setClearColor( editor.viewportColor ); diff --git a/editor/js/Sidebar.Project.Renderer.js b/editor/js/Sidebar.Project.Renderer.js index 8961ed04846aa7..6f6a834601e4d5 100644 --- a/editor/js/Sidebar.Project.Renderer.js +++ b/editor/js/Sidebar.Project.Renderer.js @@ -15,6 +15,30 @@ function SidebarProjectRenderer( editor ) { const container = new UIPanel(); container.setBorderTop( '0px' ); + // Camera + + const cameraRow = new UIRow(); + container.add( cameraRow ); + + cameraRow.add( new UIText( strings.getKey( 'sidebar/project/camera' ) ).setClass( 'Label' ) ); + + const cameraTypeSelect = new UISelect().setOptions( { + 'perspective': 'Perspective', + 'orthographic': 'Orthographic' + } ).setWidth( '150px' ).onChange( function () { + + editor.setCameraType( this.getValue() ); + + } ); + cameraTypeSelect.setValue( config.getKey( 'project/camera' ) ); + cameraRow.add( cameraTypeSelect ); + + if ( config.getKey( 'project/camera' ) === 'orthographic' ) { + + editor.setCameraType( 'orthographic' ); + + } + // Renderer const rendererRow = new UIRow(); @@ -111,12 +135,12 @@ function SidebarProjectRenderer( editor ) { if ( rendererType === 'WebGPURenderer' ) { - currentRenderer = new WebGPURenderer( { antialias: antialias, logarithmicDepthBuffer: true } ); + currentRenderer = new WebGPURenderer( { antialias: antialias, reversedDepthBuffer: true } ); await currentRenderer.init(); } else { - currentRenderer = new THREE.WebGLRenderer( { antialias: antialias, logarithmicDepthBuffer: true } ); + currentRenderer = new THREE.WebGLRenderer( { antialias: antialias, reversedDepthBuffer: true } ); } @@ -135,6 +159,15 @@ function SidebarProjectRenderer( editor ) { // Signals + signals.cameraResetted.add( function () { + + const type = editor.camera.isOrthographicCamera ? 'orthographic' : 'perspective'; + + cameraTypeSelect.setValue( type ); + config.setKey( 'project/camera', type ); + + } ); + signals.editorCleared.add( function () { currentRenderer.shadowMap.enabled = true; diff --git a/editor/js/Sidebar.Scene.js b/editor/js/Sidebar.Scene.js index efdad601e95a95..d0d31973666c22 100644 --- a/editor/js/Sidebar.Scene.js +++ b/editor/js/Sidebar.Scene.js @@ -503,6 +503,8 @@ function SidebarScene( editor ) { signals.sceneGraphChanged.add( refreshUI ); + signals.cameraResetted.add( refreshUI ); + signals.objectChanged.add( function ( object ) { const options = outliner.options; diff --git a/editor/js/Sidebar.Settings.Shortcuts.js b/editor/js/Sidebar.Settings.Shortcuts.js index f09e0a9e9ee486..20b92a5ba0a513 100644 --- a/editor/js/Sidebar.Settings.Shortcuts.js +++ b/editor/js/Sidebar.Settings.Shortcuts.js @@ -24,7 +24,7 @@ function SidebarSettingsShortcuts( editor ) { headerRow.add( new UIText( strings.getKey( 'sidebar/settings/shortcuts' ).toUpperCase() ) ); container.add( headerRow ); - const shortcuts = [ 'translate', 'rotate', 'scale', 'undo', 'focus' ]; + const shortcuts = [ 'translate', 'rotate', 'scale', 'undo', 'focus', 'perspective', 'orthographic' ]; function createShortcutInput( name ) { @@ -175,6 +175,18 @@ function SidebarSettingsShortcuts( editor ) { break; + case config.getKey( 'settings/shortcuts/perspective' ): + + editor.setCameraType( 'perspective' ); + + break; + + case config.getKey( 'settings/shortcuts/orthographic' ): + + editor.setCameraType( 'orthographic' ); + + break; + } } ); diff --git a/editor/js/Strings.js b/editor/js/Strings.js index 3246f8cb513fe5..7627143f21731a 100644 --- a/editor/js/Strings.js +++ b/editor/js/Strings.js @@ -815,6 +815,7 @@ function Strings( config ) { 'sidebar/script/remove': 'Remove', 'sidebar/project': 'Project', + 'sidebar/project/camera': 'Camera', 'sidebar/project/renderer': 'Renderer', 'sidebar/project/antialias': 'Antialias', 'sidebar/project/shadows': 'Shadows', @@ -849,6 +850,8 @@ function Strings( config ) { 'sidebar/settings/shortcuts/scale': 'Scale', 'sidebar/settings/shortcuts/undo': 'Undo', 'sidebar/settings/shortcuts/focus': 'Focus', + 'sidebar/settings/shortcuts/perspective': 'Perspective', + 'sidebar/settings/shortcuts/orthographic': 'Orthographic', 'sidebar/history': 'History', 'sidebar/history/clear': 'Clear', diff --git a/editor/js/Viewport.XR.js b/editor/js/Viewport.XR.js index 83b59b7cbc8f5b..3aea6446d2d381 100644 --- a/editor/js/Viewport.XR.js +++ b/editor/js/Viewport.XR.js @@ -20,7 +20,18 @@ class XR { const onSessionStarted = async ( session ) => { - camera.copy( editor.camera ); + if ( editor.camera.isPerspectiveCamera ) { + + camera.copy( editor.camera ); + + } else { + + // an orthographic default camera can't be mirrored into a perspective XR camera + + camera.position.copy( editor.camera.position ); + camera.quaternion.copy( editor.camera.quaternion ); + + } const sidebar = document.getElementById( 'sidebar' ); sidebar.style.width = '350px'; diff --git a/editor/js/Viewport.js b/editor/js/Viewport.js index a4b0eb12cc047d..f399da7235bf74 100644 --- a/editor/js/Viewport.js +++ b/editor/js/Viewport.js @@ -39,7 +39,7 @@ function Viewport( editor ) { let pmremGenerator = null; let pathtracer = null; - const camera = editor.camera; + let camera = editor.camera; const scene = editor.scene; const sceneHelpers = editor.sceneHelpers; @@ -166,8 +166,10 @@ function Viewport( editor ) { } else { - camera.left = - aspect; - camera.right = aspect; + const frustumHeight = camera.top - camera.bottom; + + camera.left = - frustumHeight * aspect / 2; + camera.right = frustumHeight * aspect / 2; } @@ -812,7 +814,22 @@ function Viewport( editor ) { } ); - signals.cameraResetted.add( updateAspectRatio ); + signals.cameraResetted.add( function () { + + if ( camera !== editor.camera ) { + + camera = editor.camera; + + controls.setCamera( camera ); + transformControls.camera = camera; + viewHelper.camera = camera; + + } + + updateAspectRatio(); + render(); + + } ); // animations diff --git a/editor/js/libs/app.js b/editor/js/libs/app.js index 7f26dd0afaeeb5..50ed374818080e 100644 --- a/editor/js/libs/app.js +++ b/editor/js/libs/app.js @@ -34,12 +34,12 @@ const APP = { if ( project.renderer === 'WebGPURenderer' ) { const { WebGPURenderer } = await import( 'three/webgpu' ); - renderer = new WebGPURenderer( { antialias: true, logarithmicDepthBuffer: true } ); + renderer = new WebGPURenderer( { antialias: true, reversedDepthBuffer: true } ); await renderer.init(); } else { - renderer = new THREE.WebGLRenderer( { antialias: true, logarithmicDepthBuffer: true } ); + renderer = new THREE.WebGLRenderer( { antialias: true, reversedDepthBuffer: true } ); } @@ -125,8 +125,7 @@ const APP = { this.setCamera = function ( value ) { camera = value; - camera.aspect = this.width / this.height; - camera.updateProjectionMatrix(); + setCameraAspect( camera, this.width / this.height ); }; @@ -155,8 +154,7 @@ const APP = { if ( camera ) { - camera.aspect = this.width / this.height; - camera.updateProjectionMatrix(); + setCameraAspect( camera, this.width / this.height ); } @@ -168,6 +166,25 @@ const APP = { }; + function setCameraAspect( camera, aspect ) { + + if ( camera.isPerspectiveCamera ) { + + camera.aspect = aspect; + + } else { + + const frustumHeight = camera.top - camera.bottom; + + camera.left = - frustumHeight * aspect / 2; + camera.right = frustumHeight * aspect / 2; + + } + + camera.updateProjectionMatrix(); + + } + function dispatch( array, event ) { for ( let i = 0, l = array.length; i < l; i ++ ) { diff --git a/examples/jsm/helpers/ViewHelper.js b/examples/jsm/helpers/ViewHelper.js index ef8b89482b3800..c4e6dbaf773c1a 100644 --- a/examples/jsm/helpers/ViewHelper.js +++ b/examples/jsm/helpers/ViewHelper.js @@ -49,6 +49,14 @@ class ViewHelper extends Object3D { */ this.isViewHelper = true; + /** + * The camera whose transformation is visualized. It can be reassigned at + * any time to rebind the helper to a different camera. + * + * @type {Camera} + */ + this.camera = camera; + /** * Whether the helper is currently animating or not. * @@ -86,6 +94,8 @@ class ViewHelper extends Object3D { const options = {}; + const scope = this; + const interactiveObjects = []; const raycaster = new Raycaster(); const mouse = new Vector2(); @@ -163,11 +173,11 @@ class ViewHelper extends Object3D { */ this.render = function ( renderer ) { - this.quaternion.copy( camera.quaternion ).invert(); + this.quaternion.copy( this.camera.quaternion ).invert(); this.updateMatrixWorld(); point.set( 0, 0, 1 ); - point.applyQuaternion( camera.quaternion ); + point.applyQuaternion( this.camera.quaternion ); // @@ -325,11 +335,11 @@ class ViewHelper extends Object3D { // animate position by doing a slerp and then scaling the position on the unit sphere q1.rotateTowards( q2, step ); - camera.position.set( 0, 0, 1 ).applyQuaternion( q1 ).multiplyScalar( radius ).add( this.center ); + this.camera.position.set( 0, 0, 1 ).applyQuaternion( q1 ).multiplyScalar( radius ).add( this.center ); // animate orientation - camera.quaternion.rotateTowards( targetQuaternion, step ); + this.camera.quaternion.rotateTowards( targetQuaternion, step ); if ( q1.angleTo( q2 ) === 0 ) { @@ -408,12 +418,12 @@ class ViewHelper extends Object3D { // - radius = camera.position.distanceTo( focusPoint ); + radius = scope.camera.position.distanceTo( focusPoint ); targetPosition.multiplyScalar( radius ).add( focusPoint ); dummy.position.copy( focusPoint ); - dummy.lookAt( camera.position ); + dummy.lookAt( scope.camera.position ); q1.copy( dummy.quaternion ); dummy.lookAt( targetPosition ); diff --git a/examples/webgl_gpgpu_water.html b/examples/webgl_gpgpu_water.html index c89b4402df30be..89f19de21e9630 100644 --- a/examples/webgl_gpgpu_water.html +++ b/examples/webgl_gpgpu_water.html @@ -151,7 +151,7 @@ import { SimplexNoise } from 'three/addons/math/SimplexNoise.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { HDRLoader } from 'three/addons/loaders/HDRLoader.js'; - import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; + import { DRACOLoader, DRACO_GLTF_CONFIG } from 'three/addons/loaders/DRACOLoader.js'; // Texture width for simulation const WIDTH = 128; @@ -242,7 +242,7 @@ const hdrLoader = new HDRLoader().setPath( './textures/equirectangular/' ); const glbloader = new GLTFLoader().setPath( 'models/gltf/' ); - glbloader.setDRACOLoader( new DRACOLoader().setDecoderPath( 'jsm/libs/draco/gltf/' ) ); + glbloader.setDRACOLoader( new DRACOLoader().setDecoderPath( DRACO_GLTF_CONFIG ) ); const [ env, model ] = await Promise.all( [ hdrLoader.loadAsync( 'blouberg_sunrise_2_1k.hdr' ), glbloader.loadAsync( 'duck.glb' ) ] ); env.mapping = THREE.EquirectangularReflectionMapping; diff --git a/examples/webgl_loader_3dtiles.html b/examples/webgl_loader_3dtiles.html index e168c6aa2918d6..74d4d09867ee9a 100644 --- a/examples/webgl_loader_3dtiles.html +++ b/examples/webgl_loader_3dtiles.html @@ -73,7 +73,7 @@