Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

Commit

Permalink
Support oblique perspective projection in ClipPlanesEvaluator (#2249)
Browse files Browse the repository at this point in the history
* MAPSJS-2660: Add clip planes IBCT.

Signed-off-by: Andres Mandado <andres.mandado-almajano@here.com>

* MAPSJS-2660: Support asymmetric perspective projection in ClipPlanesEvaluator.

Signed-off-by: Andres Mandado <andres.mandado-almajano@here.com>

* MAPSJS-2660: Address review comments.

Signed-off-by: Andres Mandado <andres.mandado-almajano@here.com>
  • Loading branch information
atomicsulfate committed Aug 23, 2021
1 parent 13b1c4b commit fbba18f
Show file tree
Hide file tree
Showing 9 changed files with 1,008 additions and 210 deletions.
380 changes: 348 additions & 32 deletions @here/harp-mapview/lib/CameraUtils.ts

Large diffs are not rendered by default.

161 changes: 97 additions & 64 deletions @here/harp-mapview/lib/ClipPlanesEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ namespace SphericalProj {
* Calculate distance to the nearest point where the near plane is tangent to the sphere,
* projected onto the camera forward vector.
* @param camera - The camera.
* @param halfVerticalFovAngle
* @param bottomFov - Angle from camera forward vector to frustum bottom plane.
* @param R - The sphere radius.
* @returns The tangent point distance if the point is visible, otherwise `undefined`.
*/
export function getProjNearPlaneTanDistance(
camera: THREE.Camera,
halfVerticalFovAngle: number,
bottomFov: number,
R: number
): number | undefined {
// ,^;;;;;;;;;;;;;;;;-
Expand Down Expand Up @@ -143,7 +143,7 @@ namespace SphericalProj {
const cosTanDirToFwdDir = near / camToTanVec.length();

// Tangent point visible if angle from fwdDir to tangent is less than to frustum bottom.
return cosTanDirToFwdDir > Math.cos(halfVerticalFovAngle) ? near : undefined;
return cosTanDirToFwdDir > Math.cos(bottomFov) ? near : undefined;
}

/**
Expand Down Expand Up @@ -477,7 +477,10 @@ export class TopViewClipPlanesEvaluator extends ElevationBasedClipPlanesEvaluato
let halfFovAngle = THREE.MathUtils.degToRad(camera.fov / 2);
// If width > height, then we have to compute the horizontal FOV.
if (camera.aspect > 1) {
halfFovAngle = CameraUtils.computeHorizontalFov(camera);
halfFovAngle = MapViewUtils.calculateHorizontalFovByVerticalFov(
halfFovAngle * 2,
camera.aspect
);
}

const maxR = r + this.maxElevation;
Expand Down Expand Up @@ -591,20 +594,18 @@ export class TopViewClipPlanesEvaluator extends ElevationBasedClipPlanesEvaluato
* angle (angle between look-at vector and ground surface normal).
*/
export class TiltViewClipPlanesEvaluator extends TopViewClipPlanesEvaluator {
private static readonly NDC_BOTTOM_DIR = new THREE.Vector2(0, -1);
private static readonly NDC_TOP_RIGHT_DIR = new THREE.Vector2(1, 1);
private readonly m_tmpV2 = new THREE.Vector2();

/**
* Calculate distances to top/bottom frustum plane intersections with the ground plane.
*
* @param camera - The perspective camera.
* @param projection - The geo-projection used to convert geographic to world coordinates.
*/
private getFrustumGroundIntersectionDist(
/** @override */
protected evaluateDistancePlanarProj(
camera: THREE.PerspectiveCamera,
projection: Projection
): { top: number; bottom: number } {
assert(projection.type !== ProjectionType.Spherical);
projection: Projection,
elevationProvider?: ElevationProvider
): ViewRanges {
// Find intersections of top/bottom frustum side's medians with the ground plane, taking
// into account min/max elevations.Top side intersection distance determines the far
// distance (it's further away than bottom intersection on tilted views), and bottom side
// intersection distance determines the near distance.
// 🎥
// C
// |\
Expand Down Expand Up @@ -636,51 +637,43 @@ export class TiltViewClipPlanesEvaluator extends TopViewClipPlanesEvaluator {
//
// The intersection distances to be found are |c1| (bottom plane) and |c2| (top plane).

assert(projection.type !== ProjectionType.Spherical);
const viewRanges = { ...this.minimumViewRange };
const halfPiLimit = Math.PI / 2 - epsilon;
const z = projection.groundDistance(camera.position);
const cameraTilt = MapViewUtils.extractCameraTilt(camera, projection);
// Angle between top/bottom plane and eye vector is half of the vertical fov.
const halfFov = THREE.MathUtils.degToRad(camera.fov / 2);

// Angles between top/bottom plane and eye vector. For centered projections both are equal
// to half of the vertical fov.
const topFov = CameraUtils.getTopFov(camera);
const bottomFov = CameraUtils.getBottomFov(camera);
// Angle between z and c2
const topAngle = THREE.MathUtils.clamp(cameraTilt + halfFov, -halfPiLimit, halfPiLimit);
const topAngle = THREE.MathUtils.clamp(cameraTilt + topFov, -halfPiLimit, halfPiLimit);
// Angle between z and c1
const bottomAngle = THREE.MathUtils.clamp(cameraTilt - halfFov, -halfPiLimit, halfPiLimit);
const bottomAngle = THREE.MathUtils.clamp(
cameraTilt - bottomFov,
-halfPiLimit,
halfPiLimit
);

// Compute |c2|. This will determine the far distance (top intersection is further away than
// bottom intersection on tilted views), so take the furthest distance possible, i.e.the
// distance to the min elevation.
// cos(topAngle) = (z2 - minElev) / |c2|
// |c2| = (z2 - minElev) / cos(topAngle)
const topDist = (z - this.minElevation) / Math.cos(topAngle);
const topDist = Math.max(0, (z - this.minElevation) / Math.cos(topAngle));
// Compute |c1|. This will determine the near distance, so take the nearest distance
// possible, i.e.the distance to the max elevation.
const bottomDist = (z - this.maxElevation) / Math.cos(bottomAngle);

return { top: Math.max(topDist, 0), bottom: Math.max(bottomDist, 0) };
}

/** @override */
protected evaluateDistancePlanarProj(
camera: THREE.PerspectiveCamera,
projection: Projection,
elevationProvider?: ElevationProvider
): ViewRanges {
assert(projection.type !== ProjectionType.Spherical);
const viewRanges = { ...this.minimumViewRange };

// Find the distances to the top/bottom frustum plane intersections with the ground plane.
const planesDist = this.getFrustumGroundIntersectionDist(camera, projection);
const bottomDist = Math.max(0, (z - this.maxElevation) / Math.cos(bottomAngle));

// Project intersection distances onto the eye vector.
// Angle between top/bottom plane and eye vector is half of the vertical fov.
const halfFov = THREE.MathUtils.degToRad(camera.fov / 2);
const cosHalfFov = Math.cos(halfFov);
// cos(halfFov) = near / bottomDist
// near = cos(halfFov) * bottomDist
viewRanges.near = planesDist.bottom * cosHalfFov;
viewRanges.near = bottomDist * Math.cos(bottomFov);
// cos(halfFov) = far / topDist
// far = cos(halfFov) * topDist
viewRanges.far = planesDist.top * cosHalfFov;
viewRanges.far = topDist * Math.cos(topFov);

return this.applyViewRangeConstraints(viewRanges, camera, projection, elevationProvider);
}

Expand All @@ -692,11 +685,9 @@ export class TiltViewClipPlanesEvaluator extends TopViewClipPlanesEvaluator {
): ViewRanges {
assert(projection.type === ProjectionType.Spherical);
const viewRanges = { ...this.minimumViewRange };
assert(camera instanceof THREE.PerspectiveCamera, "Unsupported camera type.");
const perspectiveCam = camera as THREE.PerspectiveCamera;

viewRanges.near = this.computeNearDistSphericalProj(perspectiveCam, projection);
viewRanges.far = this.computeFarDistSphericalProj(perspectiveCam, projection);
viewRanges.near = this.computeNearDistSphericalProj(camera, projection);
viewRanges.far = this.computeFarDistSphericalProj(camera, projection);

return this.applyViewRangeConstraints(viewRanges, camera, projection, elevationProvider);
}
Expand All @@ -715,17 +706,38 @@ export class TiltViewClipPlanesEvaluator extends TopViewClipPlanesEvaluator {
return 0;
}

const perspectiveCam = camera as THREE.PerspectiveCamera;
const halfVerticalFovAngle = THREE.MathUtils.degToRad(perspectiveCam.fov / 2);
const maxR = EarthConstants.EQUATORIAL_RADIUS + this.maxElevation;
const ndcBottomDir = TiltViewClipPlanesEvaluator.NDC_BOTTOM_DIR;

// Use as near the distance of the near plane's tangent point to the sphere. If not visible,
// use the distance to then bottom frustum side intersection.
const near =
SphericalProj.getProjNearPlaneTanDistance(camera, halfVerticalFovAngle, maxR) ??
SphericalProj.getProjSphereIntersectionDistance(camera, ndcBottomDir, maxR);
assert(near !== undefined, "No reference point for near distance found");

// Angles between bottom plane and eye vector. For centered projections it's equal to half
// of the vertical fov.
const bottomFov = CameraUtils.getBottomFov(camera);

// First, use the distance of the near plane's tangent point to the sphere.
const nearPlaneTanDist = SphericalProj.getProjNearPlaneTanDistance(camera, bottomFov, maxR);
if (nearPlaneTanDist !== undefined) {
return nearPlaneTanDist;
}
// If near plan tangent is not visible, use the distance to the closest frustum intersection
// with the sphere. If principal point has a y offset <= 0, bottom frustum intersection
// is at same distance or closer than top intersection, otherwise both need to be checked.
// At least one of the sides must intersect, if not the near plane tangent must have been
// visible.
CameraUtils.getPrincipalPoint(camera, this.m_tmpV2);
const checkTopIntersection = this.m_tmpV2.y > 0;
const bottomDist = SphericalProj.getProjSphereIntersectionDistance(
camera,
this.m_tmpV2.setComponent(1, -1),
maxR
);
const topDist = checkTopIntersection
? SphericalProj.getProjSphereIntersectionDistance(
camera,
this.m_tmpV2.setComponent(1, 1),
maxR
)
: Infinity;
const near = Math.min(bottomDist ?? Infinity, topDist ?? Infinity);
assert(near !== Infinity, "No reference point for near distance found");
return near ?? defaultNear;
}

Expand All @@ -738,15 +750,36 @@ export class TiltViewClipPlanesEvaluator extends TopViewClipPlanesEvaluator {
const minR = r + this.minElevation;
const maxR = r + this.maxElevation;
const d = camera.position.length();
const ndcTopRightDir = TiltViewClipPlanesEvaluator.NDC_TOP_RIGHT_DIR;

// If the top right frustum edge intersects the world, use as far distance the distance to
// the intersection projected on the eye vector. Otherwise, use the distance of the horizon
// at the maximum elevation.
return (
SphericalProj.getProjSphereIntersectionDistance(camera, ndcTopRightDir, minR) ??
SphericalProj.getFarDistanceFromElevatedHorizon(camera, d, r, maxR)

// If all frustum edges intersect the world, use as far distance the distance to the
// farthest intersection projected on eye vector. If principal point has a y offset <= 0,
// top frustum intersection is at same distance or farther than bottom intersection,
// otherwise both need to be checked.
CameraUtils.getPrincipalPoint(camera, this.m_tmpV2);
const isRightIntersectionFarther = this.m_tmpV2.x <= 0.0;
const ndcX = isRightIntersectionFarther ? 1 : -1;
const checkBottomIntersection = this.m_tmpV2.y > 0;

const topDist = SphericalProj.getProjSphereIntersectionDistance(
camera,
this.m_tmpV2.set(ndcX, 1),
minR
);
const bottomDist = checkBottomIntersection
? SphericalProj.getProjSphereIntersectionDistance(
camera,
this.m_tmpV2.set(ndcX, -1),
minR
)
: 0;
const largestDist = Math.max(topDist ?? Infinity, bottomDist ?? Infinity);
if (largestDist !== Infinity) {
return largestDist;
}

// If any frustum edge does not intersect (i.e horizon is visible in that viewport corner),
// use the horizon distance at the maximum elevation.
return SphericalProj.getFarDistanceFromElevatedHorizon(camera, d, r, maxR);
}

private applyViewRangeConstraints(
Expand Down
5 changes: 3 additions & 2 deletions @here/harp-mapview/lib/MapView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3868,10 +3868,10 @@ export class MapView extends EventDispatcher {
fov = THREE.MathUtils.degToRad(fovCalculation.fov);
} else {
assert(this.m_focalLength !== 0);
fov = CameraUtils.computeVerticalFov(this.m_focalLength, height);
fov = CameraUtils.computeVerticalFov(this.m_camera, this.m_focalLength, height);
}

CameraUtils.setVerticalFov(this.m_camera, fov);
CameraUtils.setVerticalFovAndFocalLength(this.m_camera, fov, this.m_focalLength, height);
}

/**
Expand All @@ -3885,6 +3885,7 @@ export class MapView extends EventDispatcher {
private updateFocalLength(height: number) {
assert(this.m_options.fovCalculation !== undefined);
this.m_focalLength = CameraUtils.computeFocalLength(
this.m_camera,
THREE.MathUtils.degToRad(this.m_options.fovCalculation!.fov),
height
);
Expand Down
5 changes: 3 additions & 2 deletions @here/harp-mapview/lib/SphereHorizon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,16 +366,17 @@ export class SphereHorizon {
return this.m_cameraPitch;
}

// TODO: Support off-center projections.
private get halfFovVertical(): number {
if (this.m_halfFovVertical === undefined) {
this.m_halfFovVertical = THREE.MathUtils.degToRad(this.m_camera.fov / 2);
this.m_halfFovVertical = CameraUtils.getVerticalFov(this.m_camera) / 2;
}
return this.m_halfFovVertical;
}

private get halfFovHorizontal(): number {
if (this.m_halfFovHorizontal === undefined) {
this.m_halfFovHorizontal = CameraUtils.computeHorizontalFov(this.m_camera) / 2;
this.m_halfFovHorizontal = CameraUtils.getHorizontalFov(this.m_camera) / 2;
}
return this.m_halfFovHorizontal;
}
Expand Down
25 changes: 17 additions & 8 deletions @here/harp-mapview/lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,8 @@ export namespace MapViewUtils {
cameraPos.copy(camera.position);

const halfVertFov = THREE.MathUtils.degToRad(camera.fov / 2);
const halfHorzFov = CameraUtils.computeHorizontalFov(camera) / 2;
// TODO: Support off-center projections.
const halfHorzFov = calculateHorizontalFovByVerticalFov(halfVertFov * 2, camera.aspect) / 2;

// tan(fov/2)
const halfVertFovTan = 1 / Math.tan(halfVertFov);
Expand Down Expand Up @@ -1622,32 +1623,40 @@ export namespace MapViewUtils {
* @deprecated
*/
export function calculateVerticalFovByHorizontalFov(hFov: number, aspect: number): number {
tmpCamera.aspect = aspect;
CameraUtils.setHorizontalFov(tmpCamera, hFov);
return THREE.MathUtils.degToRad(tmpCamera.fov);
return 2 * Math.atan(Math.tan(hFov / 2) / aspect);
}

/**
* @deprecated Use {@link CameraUtils.computeHorizontalFov}.
* @deprecated Use {@link CameraUtils.getHorizontalFov}.
*/
export function calculateHorizontalFovByVerticalFov(vFov: number, aspect: number): number {
tmpCamera.fov = THREE.MathUtils.radToDeg(vFov);
tmpCamera.aspect = aspect;
return CameraUtils.computeHorizontalFov(tmpCamera);
return CameraUtils.getHorizontalFov(tmpCamera);
}

/**
* @deprecated Use {@link CameraUtils.computeFocalLength}.
*/
export function calculateFocalLengthByVerticalFov(vFov: number, height: number): number {
return CameraUtils.computeFocalLength(vFov, height);
// computeVerticalFov takes into account the principal point position to support
// off-center projections. Keep previous behaviour by passing a camera with centered
// principal point.
CameraUtils.setPrincipalPoint(tmpCamera, new THREE.Vector2());
return CameraUtils.computeFocalLength(tmpCamera, vFov, height);
}

/**
* @deprecated Use {@link CameraUtils.computeVerticalFov}.
*/
export function calculateFovByFocalLength(focalLength: number, height: number): number {
return THREE.MathUtils.radToDeg(CameraUtils.computeVerticalFov(focalLength, height));
// computeVerticalFov takes into account the principal point position to support
// off-center projections. Keep previous behaviour by passing a camera with centered
// principal point.
CameraUtils.setPrincipalPoint(tmpCamera, new THREE.Vector2());
return THREE.MathUtils.radToDeg(
CameraUtils.computeVerticalFov(tmpCamera, focalLength, height)
);
}

/**
Expand Down
Loading

0 comments on commit fbba18f

Please sign in to comment.