utils/cameraFootprint.tstypescript
import { Ellipsoid } from "../ellipsoid/Ellipsoid";
import type { PlanetCamera } from "../camera/PlanetCamera";
import { Vec3 } from "../math/Vec3";
import { EPS8 } from "../math";
const FOOTPRINT_SCREEN_EDGE_SEARCH_STEPS = 4;
const _terrainPoint = new Vec3();
/**
* Points under the screen corners, as [leftTop, rightTop, leftBottom, rightBottom].
*/
export type CameraFootprintCorners = [Vec3 | undefined, Vec3 | undefined, Vec3 | undefined, Vec3 | undefined];
export interface ICameraFootprintParams {
screenMargin?: number;
terrainHeightStep?: number;
drop?: number;
}
function getEllipsoidHit(camera: PlanetCamera, ellipsoid: Ellipsoid, x: number, y: number): Vec3 | undefined {
let ray = camera.getRay(x, y);
return ellipsoid.hitRay(ray.origin, ray.direction);
}
/**
* Returns the horizon point in the given direction: the tangency point of the line drawn from the
* camera to the sphere of radius `radius`. Seen from the sphere center, that point sits `r^2 / d`
* along the camera up axis and `r * sqrt(d^2 - r^2) / d` along the horizontal direction.
*/
function getHorizonPointByDirection(camera: PlanetCamera, direction: Vec3, radius: number): Vec3 | undefined {
let up = camera.eye.getNormal();
let horizonDirection = Vec3.proj_b_to_plane(direction, up);
if (horizonDirection.length2() < EPS8) {
return;
}
horizonDirection.normalize();
let distanceToCamera = camera.eye.length();
if (distanceToCamera <= radius) {
return;
}
let tangentDistance = Math.sqrt(distanceToCamera * distanceToCamera - radius * radius);
let upDistance = (radius * radius) / distanceToCamera;
let horizonDistance = (radius * tangentDistance) / distanceToCamera;
return up.scaleTo(upDistance).addA(horizonDirection.scaleTo(horizonDistance));
}
/**
* Search for the boundary point between a hit and a miss along a screen row.
*/
function getFootprintBoundaryOnScreenSegment(
camera: PlanetCamera,
ellipsoid: Ellipsoid,
hitX: number,
missX: number,
y: number
): Vec3 | undefined {
let x0 = hitX;
let x1 = missX;
let hit = getEllipsoidHit(camera, ellipsoid, x0, y);
if (!hit) return;
for (let i = 0; i < FOOTPRINT_SCREEN_EDGE_SEARCH_STEPS; i++) {
let x = (x0 + x1) * 0.5;
let midHit = getEllipsoidHit(camera, ellipsoid, x, y);
if (midHit) {
x0 = x;
hit = midHit;
} else {
x1 = x;
}
}
return hit;
}
function getGroundPoint(camera: PlanetCamera, point: Vec3): Vec3 {
_terrainPoint.copy(point);
let distance = camera.planet.getCartesianTerrainPoint(point, _terrainPoint);
return distance != undefined ? _terrainPoint.clone() : point.clone();
}
/**
* Reference surface points under the four screen corners, as [leftTop, rightTop, leftBottom, rightBottom].
*
* @param {PlanetCamera} camera - Camera to take the footprint of.
* @param {number} [screenMargin=100] - Border of the screen, in screen pixels, the corners are sampled inside of.
* @param {Ellipsoid} [ellipsoid] - Reference surface, the planet ellipsoid by default. See CameraFootprint
* for one raised to the terrain under the camera.
*/
export function getCameraFootprint(
camera: PlanetCamera,
screenMargin: number = 100,
ellipsoid: Ellipsoid = camera.planet.ellipsoid
): CameraFootprintCorners {
let screenLeft = screenMargin;
let screenRight = camera.width - screenMargin;
let screenTop = screenMargin;
let screenBottom = camera.height - screenMargin;
let rayLt = camera.getRay(screenLeft, screenTop);
let rayRt = camera.getRay(screenRight, screenTop);
let rayLb = camera.getRay(screenLeft, screenBottom);
let rayRb = camera.getRay(screenRight, screenBottom);
let rawHitLt = ellipsoid.hitRay(rayLt.origin, rayLt.direction);
let rawHitRt = ellipsoid.hitRay(rayRt.origin, rayRt.direction);
let rawHitLb = ellipsoid.hitRay(rayLb.origin, rayLb.direction);
let rawHitRb = ellipsoid.hitRay(rayRb.origin, rayRb.direction);
let hitLt = rawHitLt;
let hitRt = rawHitRt;
let hitLb = rawHitLb;
let hitRb = rawHitRb;
//
// @todo: if it works fine, then remove radius parameters
// and replace to the getHorizonPointByDirection
//
//let radius = ellipsoid.projToSurface(camera.eye).length();
let radius = ellipsoid.equatorialSize;
if (!hitLt && (rawHitLb || rawHitRt)) {
hitLt = getHorizonPointByDirection(camera, rayLt.direction, radius);
}
if (!hitRt && (rawHitRb || rawHitLt)) {
hitRt = getHorizonPointByDirection(camera, rayRt.direction, radius);
}
if (!hitLb && (rawHitLt || rawHitRb)) {
hitLb = getHorizonPointByDirection(camera, rayLb.direction, radius);
}
if (!hitRb && (rawHitRt || rawHitLb)) {
hitRb = getHorizonPointByDirection(camera, rayRb.direction, radius);
}
let isLeftColumnOnly = rawHitLt && rawHitLb && !rawHitRt && !rawHitRb;
let isRightColumnOnly = rawHitRt && rawHitRb && !rawHitLt && !rawHitLb;
if (isLeftColumnOnly) {
hitRt = getFootprintBoundaryOnScreenSegment(camera, ellipsoid, screenLeft, screenRight, screenTop) || hitRt;
hitRb = getFootprintBoundaryOnScreenSegment(camera, ellipsoid, screenLeft, screenRight, screenBottom) || hitRb;
} else if (isRightColumnOnly) {
hitLt = getFootprintBoundaryOnScreenSegment(camera, ellipsoid, screenRight, screenLeft, screenTop) || hitLt;
hitLb = getFootprintBoundaryOnScreenSegment(camera, ellipsoid, screenRight, screenLeft, screenBottom) || hitLb;
}
return [hitLt, hitRt, hitLb, hitRb];
}
/**
* The ground area a camera sees: the four screen corners cast onto a reference surface and dropped onto the
* rendered terrain, plus the center, surface normals and radius derived from them.
*
* The reference surface is the planet ellipsoid raised to the terrain right under the camera, so that a
* camera on a mountain does not measure its footprint against sea level. Only the height under the camera
* is used, which keeps the footprint independent of how far the surrounding tiles have loaded.
*/
export class CameraFootprint {
/** Border of the screen, in screen pixels, the corners are sampled inside of. */
public screenMargin: number;
/**
* Terrain height under the camera is quantized to this step, in world units, before the reference
* ellipsoid is rebuilt, so that the footprint does not jitter while flying over rough terrain.
*/
public terrainHeightStep: number;
/** Lowers the reference ellipsoid by this many world units. */
public drop: number;
/** Camera the current footprint was taken of. */
public camera: PlanetCamera | null;
/** Reference surface, the planet ellipsoid raised to the terrain under the camera. */
public ellipsoid: Ellipsoid | null;
/** Height the reference ellipsoid is raised by, in world units. */
public ellipsoidHeight: number;
/** Whether the last update found all four corners. */
public isValid: boolean;
/** Reference surface corners in screen order, as [leftTop, rightTop, leftBottom, rightBottom]. */
public corners: CameraFootprintCorners;
/** Reference surface corners in contour order. */
public surfacePoints: Vec3[];
/** Corners dropped onto the rendered terrain, in contour order. */
public points: Vec3[];
/** Reference surface normals under the ground points. */
public normals: Vec3[];
public center: Vec3;
/** Distance from the center to the farthest ground point. */
public radius: number;
constructor(params: ICameraFootprintParams = {}) {
this.screenMargin = params.screenMargin ?? 100;
this.terrainHeightStep = params.terrainHeightStep ?? 1.0;
this.drop = params.drop ?? 0.0;
this.camera = null;
this.ellipsoid = null;
this.ellipsoidHeight = 0.0;
this.isValid = false;
this.corners = [undefined, undefined, undefined, undefined];
this.surfacePoints = [];
this.points = [];
this.normals = [];
this.center = new Vec3();
this.radius = 0.0;
}
/**
* Takes the footprint of the camera.
*
* @param {PlanetCamera} camera - Camera to take the footprint of.
* @returns {boolean} - False when the screen misses the planet, which leaves the previous points in place.
*/
public update(camera: PlanetCamera): boolean {
this.camera = camera;
this._updateEllipsoid(camera);
let corners = getCameraFootprint(camera, this.screenMargin, this.ellipsoid!);
this.corners = corners;
if (!corners[0] || !corners[1] || !corners[2] || !corners[3]) {
this.isValid = false;
return false;
}
// [leftTop, rightTop, rightBottom, leftBottom], so that the points can be walked
let surfacePoints = [corners[0], corners[1], corners[3], corners[2]];
let points: Vec3[] = [];
let normals: Vec3[] = [];
let center = new Vec3();
for (let i = 0; i < surfacePoints.length; i++) {
points.push(getGroundPoint(camera, surfacePoints[i]));
center.addA(points[i]);
}
center.scale(1.0 / points.length);
let radius = 0.0;
for (let i = 0; i < points.length; i++) {
normals.push(this.ellipsoid!.getSurfaceNormal3v(points[i]));
radius = Math.max(radius, points[i].distance(center));
}
this.surfacePoints = surfacePoints;
this.points = points;
this.normals = normals;
this.center = center;
this.radius = radius;
this.isValid = true;
return true;
}
/**
* Level the reference ellipsoid is raised to: the terrain right under the camera, and nothing else.
*/
protected _getTerrainHeight(camera: PlanetCamera): number {
return camera.getHeight() - camera.getAltitude() - this.drop;
}
protected _updateEllipsoid(camera: PlanetCamera): void {
let baseEllipsoid = camera.planet.ellipsoid;
let terrainHeight =
Math.round(this._getTerrainHeight(camera) / this.terrainHeightStep) * this.terrainHeightStep;
if (this.ellipsoid && terrainHeight === this.ellipsoidHeight) {
return;
}
this.ellipsoidHeight = terrainHeight;
this.ellipsoid =
terrainHeight === 0.0
? baseEllipsoid
: new Ellipsoid(baseEllipsoid.equatorialSize + terrainHeight, baseEllipsoid.polarSize + terrainHeight);
}
}