Browse Source

Editor: Add support for orthographic cameras. (#33701)

Michael Herzog 1 week ago
parent
commit
0d711cd79b

+ 1 - 0
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 );

+ 5 - 1
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 ) {

+ 66 - 0
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;
 

+ 30 - 6
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 );
 

+ 3 - 1
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';
 

+ 17 - 2
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 );
 

+ 35 - 2
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;

+ 2 - 0
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;

+ 13 - 1
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;
+
 		}
 
 	} );

+ 3 - 0
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',

+ 12 - 1
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';

+ 21 - 4
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
 

+ 23 - 6
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 ++ ) {

+ 16 - 6
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 );

粤ICP备19079148号