Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add r3f component for CameraTransitionManager #832

Merged
merged 12 commits into from
Oct 30, 2024
2 changes: 1 addition & 1 deletion example/fadingTiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function init() {
transition.camera.lookAt( 0, 0, 0 );
transition.autoSync = false;

transition.addEventListener( 'camera-changed', ( { camera, prevCamera } ) => {
transition.addEventListener( 'camera-change', ( { camera, prevCamera } ) => {

skyTiles.deleteCamera( prevCamera );
groundTiles.deleteCamera( prevCamera );
Expand Down
2 changes: 1 addition & 1 deletion example/googleMapsExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function init() {
transition.perspectiveCamera.lookAt( 0, 0, 0 );
transition.autoSync = false;

transition.addEventListener( 'camera-changed', ( { camera, prevCamera } ) => {
transition.addEventListener( 'camera-change', ( { camera, prevCamera } ) => {

tiles.deleteCamera( prevCamera );
tiles.setCamera( camera );
Expand Down
177 changes: 177 additions & 0 deletions example/r3f/components/CameraTransition.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { forwardRef, useEffect, useMemo } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { CameraTransitionManager } from '../../src/camera/CameraTransitionManager.js';

export const CameraTransition = forwardRef( function CameraTransition( props, ref ) {

const { mode, onTransitionStart, onTransitionEnd, perspectiveCamera, orthographicCamera } = props;
const [ set, invalidate, controls, camera, size ] = useThree( state => [ state.set, state.invalidate, state.controls, state.camera, state.size ] );

// create the manager
const manager = useMemo( () => {

const manager = new CameraTransitionManager();
manager.autoSync = false;

if ( camera.isOrthographicCamera ) {

manager.orthographicCamera.copy( camera );
manager.mode = 'orthographic';

} else {

manager.perspectiveCamera.copy( camera );

}

manager.syncCameras();
manager.mode = mode;

return manager;

// only respect the camera initially so the default camera settings are automatically used

}, [] );

useEffect( () => {

const { perspectiveCamera, orthographicCamera } = manager;
const aspect = size.width / size.height;
perspectiveCamera.aspect = aspect;
perspectiveCamera.updateProjectionMatrix();

orthographicCamera.left = - orthographicCamera.top * aspect;
orthographicCamera.right = - orthographicCamera.left;
perspectiveCamera.updateProjectionMatrix();

}, [ manager, size ] );

// assign ref
useEffect( () => {

if ( ref ) {

if ( ref instanceof Function ) {

ref( manager );

} else {

ref.current = manager;

}

}

}, [ manager, ref ] );

// set the camera
useEffect( () => {

const cameraCallback = ( { camera } ) => {

set( () => ( { camera } ) );

};

set( () => ( { camera: manager.camera } ) );
manager.addEventListener( 'camera-change', cameraCallback );
return () => {

manager.removeEventListener( 'camera-change', cameraCallback );

};

}, [ manager, set ] );

// register for events
useEffect( () => {

if ( onTransitionEnd ) {

manager.addEventListener( 'transition-end', onTransitionEnd );
return () => manager.removeEventListener( 'transition-end', onTransitionEnd );

}

}, [ onTransitionEnd, manager ] );

useEffect( () => {

if ( onTransitionStart ) {

manager.addEventListener( 'transition-start', onTransitionStart );
return () => manager.removeEventListener( 'transition-start', onTransitionStart );

}

}, [ onTransitionStart, manager ] );

// assign cameras
useEffect( () => {

const oldPerspectiveCamera = manager.perspectiveCamera;
const oldOrthographicCamera = manager.orthographicCamera;
manager.perspectiveCamera = perspectiveCamera || oldPerspectiveCamera;
manager.orthographicCamera = orthographicCamera || oldOrthographicCamera;

set( () => ( { camera: manager.camera } ) );

return () => {

manager.perspectiveCamera = oldPerspectiveCamera;
manager.orthographicCamera = oldOrthographicCamera;

};

}, [ perspectiveCamera, orthographicCamera, manager, set ] );

// toggle
useEffect( () => {

if ( mode !== manager.mode ) {

if ( controls && controls.isEnvironmentControls ) {

controls.getPivotPoint( manager.fixedPoint );
manager.syncCameras();
controls.adjustCamera( manager.perspectiveCamera );
controls.adjustCamera( manager.orthographicCamera );

} else {

manager.fixedPoint
.set( 0, 0, - 1 )
.transformDirection( manager.camera.matrixWorld )
.multiplyScalar( 50 )
.add( manager.camera.position );
manager.syncCameras();

}

manager.toggle();
invalidate();

}

}, [ mode, manager, invalidate, controls ] );

// update animation
useFrame( () => {

manager.update();
if ( controls ) {

controls.enabled = ! manager.animating;

}

if ( manager.animating ) {

invalidate();

}

}, - 1 );

} );
22 changes: 18 additions & 4 deletions example/r3f/globe.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
EastNorthUpFrame,
CompassGizmo,
} from '3d-tiles-renderer/r3f';
import { CameraTransition } from './components/CameraTransition.jsx';

// Plugins
import { GoogleCloudAuthPlugin } from '3d-tiles-renderer';
Expand Down Expand Up @@ -41,8 +42,18 @@ function Pointer() {
pointer.updateMatrixWorld();
vec2.setFromMatrixPosition( pointer.matrixWorld );

const distance = vec1.distanceTo( vec2 );
const scale = Math.max( 0.05 * distance, 25 );
let scale;
if ( camera.isPerspectiveCamera ) {

const distance = vec1.distanceTo( vec2 );
scale = Math.max( 0.05 * distance * Math.atan( camera.fov * MathUtils.DEG2RAD ), 25 );

} else {

scale = Math.max( ( camera.top - camera.bottom ) * 0.05 / camera.zoom, 25 );

}

pointer.scale.setScalar( scale );
pointer.position.z = scale * 0.5;

Expand All @@ -64,10 +75,11 @@ function App() {
value: localStorage.getItem( 'google-token' ) || 'put-your-api-key-here',
onChange: ( value ) => localStorage.setItem( 'google-token', value ),
transient: false,
}
},
ortho: false,
};

const { apiToken } = useControls( levaParams );
const { apiToken, ortho } = useControls( levaParams );
return (
<Canvas
camera={ {
Expand Down Expand Up @@ -117,6 +129,8 @@ function App() {
backgroundBlurriness={ 0.9 }
environmentIntensity={ 1 }
/>

<CameraTransition mode={ ortho ? 'orthographic' : 'perspective' }/>
</Canvas>
);

Expand Down
45 changes: 44 additions & 1 deletion example/src/camera/CameraTransitionManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,37 @@ export class CameraTransitionManager extends EventDispatcher {

}

get mode() {

return this._target === 0 ? 'perspective' : 'orthographic';

}

set mode( v ) {

if ( v === this.mode ) {

return;

}

const prevCamera = this.camera;
if ( v === 'perspective' ) {

this._target = 0;
this._alpha = 0;

} else {

this._target = 1;
this._alpha = 1;

}

this.dispatchEvent( { type: 'camera-change', camera: this.camera, prevCamera: prevCamera } );

}

constructor( perspectiveCamera = new PerspectiveCamera(), orthographicCamera = new OrthographicCamera() ) {

super();
Expand Down Expand Up @@ -92,7 +123,19 @@ export class CameraTransitionManager extends EventDispatcher {

if ( prevCamera !== newCamera ) {

this.dispatchEvent( { type: 'camera-changed', camera: newCamera, prevCamera: prevCamera } );
if ( newCamera === transitionCamera ) {

this.dispatchEvent( { type: 'transition-start' } );

}

this.dispatchEvent( { type: 'camera-change', camera: newCamera, prevCamera: prevCamera } );

if ( prevCamera === transitionCamera ) {

this.dispatchEvent( { type: 'transition-end' } );

}

}

Expand Down
37 changes: 34 additions & 3 deletions src/r3f/components/CompassGizmo.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,46 @@
import { createPortal, useFrame, useThree } from '@react-three/fiber';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { BackSide, Matrix4, OrthographicCamera, Scene, Vector3 } from 'three';
import { BackSide, Matrix4, OrthographicCamera, Ray, Scene, Vector3 } from 'three';
import { TilesRendererContext } from './TilesRenderer';
import { closestRayEllipsoidSurfacePointEstimate } from '../../three/controls/utils';

// Based in part on @pmndrs/drei's Gizmo component

const _vec = /*@__PURE__*/ new Vector3();
const _axis = /*@__PURE__*/ new Vector3();
const _pos = /*@__PURE__*/ new Vector3();
const _matrix = /*@__PURE__*/ new Matrix4();
const _invMatrix = /*@__PURE__*/ new Matrix4();
const _enuMatrix = /*@__PURE__*/ new Matrix4();
const _ray = /*@__PURE__*/ new Ray();
const _cart = {};

// Returns the "focus" point that the camera is facing based on the closest point to the ellipsoid.
// Used for determining the compass orientation.
function getCameraFocusPoint( camera, ellipsoid, tilesGroup, target ) {

_invMatrix.copy( tilesGroup.matrixWorld ).invert();

// get ray in globe coordinate frame
_ray.origin.copy( camera.position );
_ray.direction.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );
_ray.applyMatrix4( _invMatrix );

// get the closest point to the ray on the globe in the global coordinate frame
closestRayEllipsoidSurfacePointEstimate( _ray, ellipsoid, _pos );
_pos.applyMatrix4( tilesGroup.matrixWorld );

// get ortho camera info
_axis.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );

// ensure we move the camera exactly along the forward vector to avoid shifting
// the camera in other directions due to floating point error
const dist = _pos.sub( camera.position ).dot( _axis );
target.copy( camera.position ).addScaledVector( _axis, dist );
return target;

}

// Renders the portal with an orthographic camera
function RenderPortal( props ) {

Expand Down Expand Up @@ -147,8 +177,9 @@ export function CompassGizmo( { children, overrideRenderLoop, mode = '3d', margi

// get the ENU frame in world space
_matrix.copy( tiles.group.matrixWorld ).invert();
_vec.setFromMatrixPosition( defaultCamera.matrixWorld ).applyMatrix4( _matrix );
ellipsoid.getPositionToCartographic( _vec, _cart );
getCameraFocusPoint( defaultCamera, ellipsoid, tiles.group, _pos ).applyMatrix4( _matrix );
ellipsoid.getPositionToCartographic( _pos, _cart );

ellipsoid
.getRotationMatrixFromAzElRoll( _cart.lat, _cart.lon, 0, 0, 0, _enuMatrix )
.premultiply( tiles.group.matrixWorld );
Expand Down
2 changes: 2 additions & 0 deletions src/three/controls/EnvironmentControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export class EnvironmentControls extends EventDispatcher {

super();

this.isEnvironmentControls = true;

this.domElement = null;
this.camera = null;
this.scene = null;
Expand Down
3 changes: 3 additions & 0 deletions src/three/controls/GlobeControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export class GlobeControls extends EnvironmentControls {

// store which mode the drag stats are in
super( scene, camera, domElement );

this.isGlobeControls = true;

this._dragMode = 0;
this._rotationMode = 0;
this.maxZoom = 0.01;
Expand Down
Loading