mapbox-gl
Version:
A WebGL interactive maps library
245 lines (199 loc) • 7.57 kB
JavaScript
// @flow
import {vec3, vec4} from 'gl-matrix';
import assert from 'assert';
import type {Vec3} from 'gl-matrix';
class Ray {
pos: Vec3;
dir: Vec3;
constructor(pos_: Vec3, dir_: Vec3) {
this.pos = pos_;
this.dir = dir_;
}
intersectsPlane(pt: Vec3, normal: Vec3, out: Vec3): boolean {
const D = vec3.dot(normal, this.dir);
// ray is parallel to plane, so it misses
if (Math.abs(D) < 1e-6) { return false; }
const t = (
(pt[0] - this.pos[0]) * normal[0] +
(pt[1] - this.pos[1]) * normal[1] +
(pt[2] - this.pos[2]) * normal[2]) / D;
out[0] = this.pos[0] + this.dir[0] * t;
out[1] = this.pos[1] + this.dir[1] * t;
out[2] = this.pos[2] + this.dir[2] * t;
return true;
}
closestPointOnSphere(center: Vec3, r: number, out: Vec3): boolean {
assert(vec3.squaredLength(this.dir) > 0.0 && r >= 0.0);
if (vec3.equals(this.pos, center) || r === 0.0) {
out[0] = out[1] = out[2] = 0;
return false;
}
const [dx, dy, dz] = this.dir;
const px = this.pos[0] - center[0];
const py = this.pos[1] - center[1];
const pz = this.pos[2] - center[2];
const a = dx * dx + dy * dy + dz * dz;
const b = 2.0 * (px * dx + py * dy + pz * dz);
const c = (px * px + py * py + pz * pz) - r * r;
const d = b * b - 4 * a * c;
if (d < 0.0) {
// No intersection, find distance between closest points
const t = Math.max(-b / 2, 0.0);
const gx = px + dx * t; // point to globe
const gy = py + dy * t;
const gz = pz + dz * t;
const glen = Math.hypot(gx, gy, gz);
out[0] = gx * r / glen;
out[1] = gy * r / glen;
out[2] = gz * r / glen;
return false;
} else {
assert(a > 0.0);
const t = (-b - Math.sqrt(d)) / (2.0 * a);
if (t < 0.0) {
// Ray is pointing away from the sphere
const plen = Math.hypot(px, py, pz);
out[0] = px * r / plen;
out[1] = py * r / plen;
out[2] = pz * r / plen;
return false;
} else {
out[0] = px + dx * t;
out[1] = py + dy * t;
out[2] = pz + dz * t;
return true;
}
}
}
}
class Frustum {
points: Array<Array<number>>;
planes: Array<Array<number>>;
constructor(points_: Array<Array<number>>, planes_: Array<Array<number>>) {
this.points = points_;
this.planes = planes_;
}
static fromInvProjectionMatrix(invProj: Float64Array, worldSize: number, zoom: number, zInMeters: boolean): Frustum {
const clipSpaceCorners = [
[-1, 1, -1, 1],
[ 1, 1, -1, 1],
[ 1, -1, -1, 1],
[-1, -1, -1, 1],
[-1, 1, 1, 1],
[ 1, 1, 1, 1],
[ 1, -1, 1, 1],
[-1, -1, 1, 1]
];
const scale = Math.pow(2, zoom);
// Transform frustum corner points from clip space to tile space
const frustumCoords = clipSpaceCorners
.map(v => {
const s = vec4.transformMat4([], v, invProj);
const k = 1.0 / s[3] / worldSize * scale;
// Z scale in meters.
return vec4.mul(s, s, [k, k, zInMeters ? 1.0 / s[3] : k, k]);
});
const frustumPlanePointIndices = [
[0, 1, 2], // near
[6, 5, 4], // far
[0, 3, 7], // left
[2, 1, 5], // right
[3, 2, 6], // bottom
[0, 4, 5] // top
];
const frustumPlanes = frustumPlanePointIndices.map((p: Array<number>) => {
const a = vec3.sub([], frustumCoords[p[0]], frustumCoords[p[1]]);
const b = vec3.sub([], frustumCoords[p[2]], frustumCoords[p[1]]);
const n = vec3.normalize([], vec3.cross([], a, b));
const d = -vec3.dot(n, frustumCoords[p[1]]);
return n.concat(d);
});
return new Frustum(frustumCoords, frustumPlanes);
}
}
class Aabb {
min: Vec3;
max: Vec3;
center: Vec3;
constructor(min_: Vec3, max_: Vec3) {
this.min = min_;
this.max = max_;
this.center = vec3.scale([], vec3.add([], this.min, this.max), 0.5);
}
quadrant(index: number): Aabb {
const split = [(index % 2) === 0, index < 2];
const qMin = vec3.clone(this.min);
const qMax = vec3.clone(this.max);
for (let axis = 0; axis < split.length; axis++) {
qMin[axis] = split[axis] ? this.min[axis] : this.center[axis];
qMax[axis] = split[axis] ? this.center[axis] : this.max[axis];
}
// Temporarily, elevation is constant, hence quadrant.max.z = this.max.z
qMax[2] = this.max[2];
return new Aabb(qMin, qMax);
}
distanceX(point: Array<number>): number {
const pointOnAabb = Math.max(Math.min(this.max[0], point[0]), this.min[0]);
return pointOnAabb - point[0];
}
distanceY(point: Array<number>): number {
const pointOnAabb = Math.max(Math.min(this.max[1], point[1]), this.min[1]);
return pointOnAabb - point[1];
}
distanceZ(point: Array<number>): number {
const pointOnAabb = Math.max(Math.min(this.max[2], point[2]), this.min[2]);
return pointOnAabb - point[2];
}
getCorners(): Array<Array<number>> {
const mn = this.min;
const mx = this.max;
return [
[mn[0], mn[1], mn[2]],
[mx[0], mn[1], mn[2]],
[mx[0], mx[1], mn[2]],
[mn[0], mx[1], mn[2]],
[mn[0], mn[1], mx[2]],
[mx[0], mn[1], mx[2]],
[mx[0], mx[1], mx[2]],
[mn[0], mx[1], mx[2]],
];
}
// Performs a frustum-aabb intersection test. Returns 0 if there's no intersection,
// 1 if shapes are intersecting and 2 if the aabb if fully inside the frustum.
intersects(frustum: Frustum): number {
// Execute separating axis test between two convex objects to find intersections
// Each frustum plane together with 3 major axes define the separating axes
const aabbPoints = this.getCorners();
let fullyInside = true;
for (let p = 0; p < frustum.planes.length; p++) {
const plane = frustum.planes[p];
let pointsInside = 0;
for (let i = 0; i < aabbPoints.length; i++) {
pointsInside += vec3.dot(plane, aabbPoints[i]) + plane[3] >= 0;
}
if (pointsInside === 0)
return 0;
if (pointsInside !== aabbPoints.length)
fullyInside = false;
}
if (fullyInside)
return 2;
for (let axis = 0; axis < 3; axis++) {
let projMin = Number.MAX_VALUE;
let projMax = -Number.MAX_VALUE;
for (let p = 0; p < frustum.points.length; p++) {
const projectedPoint = frustum.points[p][axis] - this.min[axis];
projMin = Math.min(projMin, projectedPoint);
projMax = Math.max(projMax, projectedPoint);
}
if (projMax < 0 || projMin > this.max[axis] - this.min[axis])
return 0;
}
return 1;
}
}
export {
Aabb,
Frustum,
Ray
};