3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
792 lines (540 loc) • 23.1 kB
JavaScript
import {
Matrix4,
Quaternion,
Vector2,
Vector3,
MathUtils,
Ray,
Group,
} from 'three';
import { DRAG, ZOOM, EnvironmentControls, NONE } from './EnvironmentControls.js';
import { makeRotateAroundPoint, mouseToCoords, setRaycasterFromCamera } from './utils.js';
import { Ellipsoid } from '../math/Ellipsoid.js';
import { WGS84_ELLIPSOID } from '../math/GeoConstants.js';
const _invMatrix = /* @__PURE__ */ new Matrix4();
const _rotMatrix = /* @__PURE__ */ new Matrix4();
const _pos = /* @__PURE__ */ new Vector3();
const _vec = /* @__PURE__ */ new Vector3();
const _center = /* @__PURE__ */ new Vector3();
const _forward = /* @__PURE__ */ new Vector3();
const _targetRight = /* @__PURE__ */ new Vector3();
const _globalUp = /* @__PURE__ */ new Vector3();
const _quaternion = /* @__PURE__ */ new Quaternion();
const _zoomPointUp = /* @__PURE__ */ new Vector3();
const _toCenter = /* @__PURE__ */ new Vector3();
const _ray = /* @__PURE__ */ new Ray();
const _ellipsoid = /* @__PURE__ */ new Ellipsoid();
const _pointer = /* @__PURE__ */ new Vector2();
const _latLon = {};
// hand picked minimum elevation to tune far plane near surface
const MIN_ELEVATION = 2550;
export class GlobeControls extends EnvironmentControls {
get tilesGroup() {
console.warn( 'GlobeControls: "tilesGroup" has been deprecated. Use "ellipsoidGroup", instead.' );
return this.ellipsoidFrame;
}
get ellipsoidFrame() {
return this.ellipsoidGroup.matrixWorld;
}
get ellipsoidFrameInverse() {
const { ellipsoidGroup, ellipsoidFrame, _ellipsoidFrameInverse } = this;
return ellipsoidGroup.matrixWorldInverse ?
ellipsoidGroup.matrixWorldInverse :
_ellipsoidFrameInverse.copy( ellipsoidFrame ).invert();
}
constructor( scene = null, camera = null, domElement = null, tilesRenderer = null ) {
// 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;
this.nearMargin = 0.25;
this.farMargin = 0;
this.useFallbackPlane = false;
this.autoAdjustCameraRotation = false;
this.globeInertia = new Quaternion();
this.globeInertiaFactor = 0;
this.ellipsoid = WGS84_ELLIPSOID.clone();
this.ellipsoidGroup = new Group();
this._ellipsoidFrameInverse = new Matrix4();
if ( tilesRenderer !== null ) {
this.setTilesRenderer( tilesRenderer );
}
}
setTilesRenderer( tilesRenderer ) {
super.setTilesRenderer( tilesRenderer );
if ( tilesRenderer !== null ) {
this.setEllipsoid( tilesRenderer.ellipsoid, tilesRenderer.group );
}
}
setEllipsoid( ellipsoid, ellipsoidGroup ) {
this.ellipsoid = ellipsoid || WGS84_ELLIPSOID.clone();
this.ellipsoidGroup = ellipsoidGroup || new Group();
}
getPivotPoint( target ) {
const { camera, ellipsoidFrame, ellipsoidFrameInverse, ellipsoid } = this;
// get camera values
_forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );
// set a ray in the local ellipsoid frame
_ray.origin.copy( camera.position );
_ray.direction.copy( _forward );
_ray.applyMatrix4( ellipsoidFrameInverse );
// get the estimated closest point
ellipsoid
.closestPointToRayEstimate( _ray, _vec )
.applyMatrix4( ellipsoidFrame );
// use the closest point if no pivot was provided or it's closer
if (
super.getPivotPoint( target ) === null ||
_pos.subVectors( target, _ray.origin ).dot( _ray.direction ) > _pos.subVectors( _vec, _ray.origin ).dot( _ray.direction )
) {
target.copy( _vec );
}
return target;
}
// get the vector to the center of the provided globe
getVectorToCenter( target ) {
const { ellipsoidFrame, camera } = this;
return target
.setFromMatrixPosition( ellipsoidFrame )
.sub( camera.position );
}
// get the distance to the center of the globe
getDistanceToCenter() {
return this
.getVectorToCenter( _vec )
.length();
}
getUpDirection( point, target ) {
// get the "up" direction based on the wgs84 ellipsoid
const { ellipsoidFrame, ellipsoidFrameInverse, ellipsoid } = this;
_vec.copy( point ).applyMatrix4( ellipsoidFrameInverse );
ellipsoid.getPositionToNormal( _vec, target );
target.transformDirection( ellipsoidFrame );
}
getCameraUpDirection( target ) {
const { ellipsoidFrame, ellipsoidFrameInverse, ellipsoid, camera } = this;
if ( camera.isOrthographicCamera ) {
this._getVirtualOrthoCameraPosition( _vec );
_vec.applyMatrix4( ellipsoidFrameInverse );
ellipsoid.getPositionToNormal( _vec, target );
target.transformDirection( ellipsoidFrame );
} else {
this.getUpDirection( camera.position, target );
}
}
update( deltaTime = Math.min( this.clock.getDelta(), 64 / 1000 ) ) {
if ( ! this.enabled || ! this.camera || deltaTime === 0 ) {
return;
}
const { camera, pivotMesh } = this;
// if we're outside the transition threshold then we toggle some reorientation behavior
// when adjusting the up frame while moving the camera
if ( this._isNearControls() ) {
this.scaleZoomOrientationAtEdges = this.zoomDelta < 0;
} else {
if ( this.state !== NONE && this._dragMode !== 1 && this._rotationMode !== 1 ) {
pivotMesh.visible = false;
}
this.scaleZoomOrientationAtEdges = false;
}
const adjustCameraRotation = this.needsUpdate || this._inertiaNeedsUpdate();
// fire basic controls update
super.update( deltaTime );
// update the camera planes and the ortho camera position
this.adjustCamera( camera );
// align the camera up vector if the camera as updated
if ( adjustCameraRotation && this._isNearControls() ) {
this.getCameraUpDirection( _globalUp );
this._alignCameraUp( _globalUp, 1 );
this.getCameraUpDirection( _globalUp );
this._clampRotation( _globalUp );
}
}
// Updates the passed camera near and far clip planes to encapsulate the ellipsoid from the
// current position in addition to adjusting the height.
adjustCamera( camera ) {
super.adjustCamera( camera );
const { ellipsoidFrame, ellipsoidFrameInverse, ellipsoid, nearMargin, farMargin } = this;
const maxRadius = Math.max( ...ellipsoid.radius );
if ( camera.isPerspectiveCamera ) {
// adjust the clip planes
const distanceToCenter = _vec
.setFromMatrixPosition( ellipsoidFrame )
.sub( camera.position ).length();
// update the projection matrix
// interpolate from the 25% radius margin around the globe down to the surface
// so we can avoid z fighting when near value is too far at a high altitude
const margin = nearMargin * maxRadius;
const alpha = MathUtils.clamp( ( distanceToCenter - maxRadius ) / margin, 0, 1 );
const minNear = MathUtils.lerp( 1, 1000, alpha );
camera.near = Math.max( minNear, distanceToCenter - maxRadius - margin );
// update the far plane to the horizon distance
_pos.copy( camera.position ).applyMatrix4( ellipsoidFrameInverse );
ellipsoid.getPositionToCartographic( _pos, _latLon );
// use a minimum elevation for computing the horizon distance to avoid the far clip
// plane approaching zero or clipping mountains over the horizon in the distance as
// the camera goes to or below sea level.
const elevation = Math.max( ellipsoid.getPositionElevation( _pos ), MIN_ELEVATION );
const horizonDistance = ellipsoid.calculateHorizonDistance( _latLon.lat, elevation );
camera.far = horizonDistance + 0.1 + maxRadius * farMargin;
camera.updateProjectionMatrix();
} else {
this._getVirtualOrthoCameraPosition( camera.position, camera );
camera.updateMatrixWorld();
_invMatrix.copy( camera.matrixWorld ).invert();
_vec.setFromMatrixPosition( ellipsoidFrame ).applyMatrix4( _invMatrix );
const distanceToCenter = - _vec.z;
camera.near = distanceToCenter - maxRadius * ( 1 + nearMargin );
camera.far = distanceToCenter + 0.1 + maxRadius * farMargin;
// adjust the position of the ortho camera such that the near value is 0
camera.position.addScaledVector( _forward, camera.near );
camera.far -= camera.near;
camera.near = 0;
camera.updateProjectionMatrix();
camera.updateMatrixWorld();
}
}
// resets the "stuck" drag modes
setState( ...args ) {
super.setState( ...args );
this._dragMode = 0;
this._rotationMode = 0;
}
_updateInertia( deltaTime ) {
super._updateInertia( deltaTime );
const {
globeInertia,
enableDamping,
dampingFactor,
camera,
cameraRadius,
minDistance,
inertiaTargetDistance,
ellipsoidFrame,
} = this;
if ( ! this.enableDamping || this.inertiaStableFrames > 1 ) {
this.globeInertiaFactor = 0;
this.globeInertia.identity();
return;
}
const factor = Math.pow( 2, - deltaTime / dampingFactor );
const stableDistance = Math.max( camera.near, cameraRadius, minDistance, inertiaTargetDistance );
const resolution = 2 * 1e3;
const pixelWidth = 2 / resolution;
const pixelThreshold = 0.25 * pixelWidth;
_center.setFromMatrixPosition( ellipsoidFrame );
if ( this.globeInertiaFactor !== 0 ) {
// calculate two screen points at 1 pixel apart in our notional resolution so we can stop when the delta is ~ 1 pixel
// projected into world space
setRaycasterFromCamera( _ray, _vec.set( 0, 0, - 1 ), camera );
_ray.applyMatrix4( camera.matrixWorldInverse );
_ray.direction.normalize();
_ray.recast( - _ray.direction.dot( _ray.origin ) ).at( stableDistance / _ray.direction.z, _vec );
_vec.applyMatrix4( camera.matrixWorld );
setRaycasterFromCamera( _ray, _pos.set( pixelThreshold, pixelThreshold, - 1 ), camera );
_ray.applyMatrix4( camera.matrixWorldInverse );
_ray.direction.normalize();
_ray.recast( - _ray.direction.dot( _ray.origin ) ).at( stableDistance / _ray.direction.z, _pos );
_pos.applyMatrix4( camera.matrixWorld );
// get implied angle
_vec.sub( _center ).normalize();
_pos.sub( _center ).normalize();
this.globeInertiaFactor *= factor;
const threshold = _vec.angleTo( _pos ) / deltaTime;
const globeAngle = 2 * Math.acos( globeInertia.w ) * this.globeInertiaFactor;
if ( globeAngle < threshold || ! enableDamping ) {
this.globeInertiaFactor = 0;
globeInertia.identity();
}
}
if ( this.globeInertiaFactor !== 0 ) {
// ensure our w component is non-one if the xyz values are
// non zero to ensure we can animate
if (
globeInertia.w === 1 && (
globeInertia.x !== 0 ||
globeInertia.y !== 0 ||
globeInertia.z !== 0
)
) {
globeInertia.w = Math.min( globeInertia.w, 1 - 1e-9 );
}
// construct the rotation matrix
_center.setFromMatrixPosition( ellipsoidFrame );
_quaternion.identity().slerp( globeInertia, this.globeInertiaFactor * deltaTime );
makeRotateAroundPoint( _center, _quaternion, _rotMatrix );
// apply the rotation
camera.matrixWorld.premultiply( _rotMatrix );
camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec );
}
}
_inertiaNeedsUpdate() {
return super._inertiaNeedsUpdate() || this.globeInertiaFactor !== 0;
}
_updatePosition( deltaTime ) {
if ( this.state === DRAG ) {
// save the drag mode state so we can update the pivot mesh visuals in "update"
if ( this._dragMode === 0 ) {
this._dragMode = this._isNearControls() ? 1 : - 1;
}
const {
raycaster,
camera,
pivotPoint,
pointerTracker,
domElement,
ellipsoidFrame,
ellipsoidFrameInverse,
} = this;
// reuse cache variables
const pivotDir = _pos;
const newPivotDir = _targetRight;
// get the pointer and ray
pointerTracker.getCenterPoint( _pointer );
mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer );
setRaycasterFromCamera( raycaster, _pointer, camera );
// transform to ellipsoid frame
raycaster.ray.applyMatrix4( ellipsoidFrameInverse );
// construct an ellipsoid that matches a sphere with the radius of the globe so
// the drag position matches where the initial click was
const pivotRadius = _vec.copy( pivotPoint ).applyMatrix4( ellipsoidFrameInverse ).length();
_ellipsoid.radius.setScalar( pivotRadius );
// if we drag off the sphere then end the operation and follow through on the inertia
if ( ! _ellipsoid.intersectRay( raycaster.ray, _vec ) ) {
this.resetState();
this._updateInertia( deltaTime );
return;
}
_vec.applyMatrix4( ellipsoidFrame );
// get the point directions
_center.setFromMatrixPosition( ellipsoidFrame );
pivotDir.subVectors( pivotPoint, _center ).normalize();
newPivotDir.subVectors( _vec, _center ).normalize();
// construct the rotation
_quaternion.setFromUnitVectors( newPivotDir, pivotDir );
makeRotateAroundPoint( _center, _quaternion, _rotMatrix );
// apply the rotation
camera.matrixWorld.premultiply( _rotMatrix );
camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec );
if ( pointerTracker.getMoveDistance() / deltaTime < 2 * window.devicePixelRatio ) {
this.inertiaStableFrames ++;
} else {
this.globeInertia.copy( _quaternion );
this.globeInertiaFactor = 1 / deltaTime;
this.inertiaStableFrames = 0;
}
}
}
// disable rotation once we're outside the control transition
_updateRotation( ...args ) {
if ( this._rotationMode === 1 || this._isNearControls() ) {
this._rotationMode = 1;
super._updateRotation( ...args );
} else {
this.pivotMesh.visible = false;
this._rotationMode = - 1;
}
}
_updateZoom() {
const { zoomDelta, ellipsoid, zoomSpeed, zoomPoint, camera, maxZoom, state } = this;
if ( state !== ZOOM && zoomDelta === 0 ) {
return;
}
// reset momentum
this.rotationInertia.set( 0, 0 );
this.dragInertia.set( 0, 0, 0 );
this.globeInertia.identity();
this.globeInertiaFactor = 0;
// used to scale the tilt transitions based on zoom intensity
const deltaAlpha = MathUtils.clamp( MathUtils.mapLinear( Math.abs( zoomDelta ), 0, 20, 0, 1 ), 0, 1 );
if ( this._isNearControls() || zoomDelta > 0 ) {
this._updateZoomDirection();
// When zooming try to tilt the camera towards the center of the planet to avoid the globe
// spinning as you zoom out from the horizon
if ( zoomDelta < 0 && ( this.zoomPointSet || this._updateZoomPoint() ) ) {
// get the forward vector and vector toward the center of the ellipsoid
_forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).normalize();
_toCenter.copy( this.up ).multiplyScalar( - 1 );
// Calculate alpha values to use to scale the amount of tilt that occurs as the camera moves.
// Scales based on mouse position near the horizon and current tilt.
this.getUpDirection( zoomPoint, _zoomPointUp );
const upAlpha = MathUtils.clamp( MathUtils.mapLinear( - _zoomPointUp.dot( _toCenter ), 1, 0.95, 0, 1 ), 0, 1 );
const forwardAlpha = 1 - _forward.dot( _toCenter );
const cameraAlpha = camera.isOrthographicCamera ? 0.05 : 1;
const adjustedDeltaAlpha = MathUtils.clamp( deltaAlpha * 3, 0, 1 );
// apply scale
const alpha = Math.min( upAlpha * forwardAlpha * cameraAlpha * adjustedDeltaAlpha, 0.1 );
_toCenter.lerpVectors( _forward, _toCenter, alpha ).normalize();
// perform rotation
_quaternion.setFromUnitVectors( _forward, _toCenter );
makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix );
camera.matrixWorld.premultiply( _rotMatrix );
camera.matrixWorld.decompose( camera.position, camera.quaternion, _toCenter );
// update zoom direction
this.zoomDirection.subVectors( zoomPoint, camera.position ).normalize();
}
super._updateZoom();
} else if ( camera.isPerspectiveCamera ) {
// orient the camera to focus on the earth during the zoom
const transitionDistance = this._getPerspectiveTransitionDistance();
const maxDistance = this._getMaxPerspectiveDistance();
const distanceAlpha = MathUtils.mapLinear( this.getDistanceToCenter(), transitionDistance, maxDistance, 0, 1 );
this._tiltTowardsCenter( MathUtils.lerp( 0, 0.4, distanceAlpha * deltaAlpha ) );
this._alignCameraUpToNorth( MathUtils.lerp( 0, 0.2, distanceAlpha * deltaAlpha ) );
// calculate zoom in a similar way to environment controls so
// the zoom speeds are comparable
const dist = this.getDistanceToCenter() - ellipsoid.radius.x;
const scale = zoomDelta * dist * zoomSpeed * 0.0025;
const clampedScale = Math.max( scale, Math.min( this.getDistanceToCenter() - maxDistance, 0 ) );
// zoom out directly from the globe center
this.getVectorToCenter( _vec ).normalize();
this.camera.position.addScaledVector( _vec, clampedScale );
this.camera.updateMatrixWorld();
this.zoomDelta = 0;
} else {
const transitionZoom = this._getOrthographicTransitionZoom();
const minZoom = this._getMinOrthographicZoom();
const distanceAlpha = MathUtils.mapLinear( camera.zoom, transitionZoom, minZoom, 0, 1 );
this._tiltTowardsCenter( MathUtils.lerp( 0, 0.4, distanceAlpha * deltaAlpha ) );
this._alignCameraUpToNorth( MathUtils.lerp( 0, 0.2, distanceAlpha * deltaAlpha ) );
const scale = this.zoomDelta;
const normalizedDelta = Math.pow( 0.95, Math.abs( scale * 0.05 ) );
const scaleFactor = scale > 0 ? 1 / Math.abs( normalizedDelta ) : normalizedDelta;
const maxScaleFactor = minZoom / camera.zoom;
const clampedScaleFactor = Math.max( scaleFactor * zoomSpeed, Math.min( maxScaleFactor, 1 ) );
camera.zoom = Math.min( maxZoom, camera.zoom * clampedScaleFactor );
camera.updateProjectionMatrix();
this.zoomDelta = 0;
this.zoomDirectionSet = false;
}
}
// tilt the camera to align with north
_alignCameraUpToNorth( alpha ) {
const { ellipsoidFrame } = this;
_globalUp.set( 0, 0, 1 ).transformDirection( ellipsoidFrame );
this._alignCameraUp( _globalUp, alpha );
}
// tilt the camera to look at the center of the globe
_tiltTowardsCenter( alpha ) {
const {
camera,
ellipsoidFrame,
} = this;
_forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).normalize();
_vec.setFromMatrixPosition( ellipsoidFrame ).sub( camera.position ).normalize();
_vec.lerp( _forward, 1 - alpha ).normalize();
_quaternion.setFromUnitVectors( _forward, _vec );
camera.quaternion.premultiply( _quaternion );
camera.updateMatrixWorld();
}
// returns the perspective camera transition distance can move to based on globe size and fov
_getPerspectiveTransitionDistance() {
const { camera, ellipsoid } = this;
if ( ! camera.isPerspectiveCamera ) {
throw new Error();
}
// When the smallest fov spans 65% of the ellipsoid then we use the near controls
const ellipsoidRadius = Math.max( ...ellipsoid.radius );
const fovHoriz = 2 * Math.atan( Math.tan( MathUtils.DEG2RAD * camera.fov * 0.5 ) * camera.aspect );
const distVert = ellipsoidRadius / Math.tan( MathUtils.DEG2RAD * camera.fov * 0.5 );
const distHoriz = ellipsoidRadius / Math.tan( fovHoriz * 0.5 );
const dist = Math.max( distVert, distHoriz );
return dist;
}
// returns the max distance the perspective camera can move to based on globe size and fov
_getMaxPerspectiveDistance() {
const { camera, ellipsoid } = this;
if ( ! camera.isPerspectiveCamera ) {
throw new Error();
}
// allow for zooming out such that the ellipsoid is half the size of the largest fov
const ellipsoidRadius = Math.max( ...ellipsoid.radius );
const fovHoriz = 2 * Math.atan( Math.tan( MathUtils.DEG2RAD * camera.fov * 0.5 ) * camera.aspect );
const distVert = ellipsoidRadius / Math.tan( MathUtils.DEG2RAD * camera.fov * 0.5 );
const distHoriz = ellipsoidRadius / Math.tan( fovHoriz * 0.5 );
const dist = 2 * Math.max( distVert, distHoriz );
return dist;
}
// returns the transition threshold for orthographic zoom based on the globe size and camera settings
_getOrthographicTransitionZoom() {
const { camera, ellipsoid } = this;
if ( ! camera.isOrthographicCamera ) {
throw new Error();
}
const orthoHeight = ( camera.top - camera.bottom );
const orthoWidth = ( camera.right - camera.left );
const orthoSize = Math.max( orthoHeight, orthoWidth );
const ellipsoidRadius = Math.max( ...ellipsoid.radius );
const ellipsoidDiameter = 2 * ellipsoidRadius;
return 2 * orthoSize / ellipsoidDiameter;
}
// returns the minimum allowed orthographic zoom based on the globe size and camera settings
_getMinOrthographicZoom() {
const { camera, ellipsoid } = this;
if ( ! camera.isOrthographicCamera ) {
throw new Error();
}
const orthoHeight = ( camera.top - camera.bottom );
const orthoWidth = ( camera.right - camera.left );
const orthoSize = Math.min( orthoHeight, orthoWidth );
const ellipsoidRadius = Math.max( ...ellipsoid.radius );
const ellipsoidDiameter = 2 * ellipsoidRadius;
return 0.7 * orthoSize / ellipsoidDiameter;
}
// returns the "virtual position" of the orthographic based on where it is and
// where it's looking primarily so we can reasonably position the camera object
// in space and derive a reasonable "up" value.
_getVirtualOrthoCameraPosition( target, camera = this.camera ) {
const { ellipsoidFrame, ellipsoidFrameInverse, ellipsoid } = this;
if ( ! camera.isOrthographicCamera ) {
throw new Error();
}
// get ray in globe coordinate frame
_ray.origin.copy( camera.position );
_ray.direction.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );
_ray.applyMatrix4( ellipsoidFrameInverse );
// get the closest point to the ray on the globe in the global coordinate frame
ellipsoid
.closestPointToRayEstimate( _ray, _pos )
.applyMatrix4( ellipsoidFrame );
// get ortho camera info
const orthoHeight = ( camera.top - camera.bottom );
const orthoWidth = ( camera.right - camera.left );
const orthoSize = Math.max( orthoHeight, orthoWidth ) / camera.zoom;
_forward.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( _forward );
target.copy( camera.position ).addScaledVector( _forward, dist - orthoSize * 4 );
}
_isNearControls() {
const { camera } = this;
if ( camera.isPerspectiveCamera ) {
return this.getDistanceToCenter() < this._getPerspectiveTransitionDistance();
} else {
return camera.zoom > this._getOrthographicTransitionZoom();
}
}
_raycast( raycaster ) {
const result = super._raycast( raycaster );
if ( result === null ) {
// if there was no hit then fallback to intersecting the ellipsoid.
const { ellipsoid, ellipsoidFrame, ellipsoidFrameInverse } = this;
_ray.copy( raycaster.ray ).applyMatrix4( ellipsoidFrameInverse );
const point = ellipsoid.intersectRay( _ray, _vec );
if ( point !== null ) {
point.applyMatrix4( ellipsoidFrame );
return {
point: point.clone(),
distance: point.distanceTo( raycaster.ray.origin ),
};
} else {
return null;
}
} else {
return result;
}
}
}