UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

178 lines (154 loc) 9.03 kB
import {type mat4, vec3, vec4} from 'gl-matrix'; import {Aabb} from './aabb'; import {pointPlaneSignedDistance, rayPlaneIntersection} from '../util'; export class Frustum { constructor(public points: vec4[], public planes: vec4[], public aabb: Aabb) { } public static fromInvProjectionMatrix(invProj: mat4, worldSize: number = 1, zoom: number = 0, horizonPlane?: vec4, flippedNearFar?: 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] ]; // Globe and mercator projection matrices have different Y directions, hence we need different sets of indices. // This should be fixed in the future. const frustumPlanePointIndices = flippedNearFar ? [ [6, 5, 4], // near [0, 1, 2], // far [0, 3, 7], // left [2, 1, 5], // right [3, 2, 6], // bottom [0, 4, 5] // top ] : [ [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 scale = Math.pow(2, zoom); // Transform frustum corner points from clip space to tile space, Z to meters const frustumCoords = clipSpaceCorners.map(v => unprojectClipSpacePoint(v, invProj, worldSize, scale)); if (horizonPlane) { // A horizon clipping plane was supplied. adjustFarPlaneByHorizonPlane(frustumCoords, frustumPlanePointIndices[0], horizonPlane, flippedNearFar); } const frustumPlanes = frustumPlanePointIndices.map((p: number[]) => { const a = vec3.sub([] as any, frustumCoords[p[0]] as vec3, frustumCoords[p[1]] as vec3); const b = vec3.sub([] as any, frustumCoords[p[2]] as vec3, frustumCoords[p[1]] as vec3); const n = vec3.normalize([] as any, vec3.cross([] as any, a, b)) as any; const d = -vec3.dot(n, frustumCoords[p[1]] as vec3); return n.concat(d); }); const min: vec3 = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]; const max: vec3 = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY]; for (const p of frustumCoords) { for (let i = 0; i < 3; i++) { min[i] = Math.min(min[i], p[i]); max[i] = Math.max(max[i], p[i]); } } return new Frustum(frustumCoords, frustumPlanes, new Aabb(min, max)); } } function unprojectClipSpacePoint(point: vec4 | number[], invProj: mat4, worldSize: number, scale: number): vec4 { const v = vec4.transformMat4([] as any, point as any, invProj) as any; const s = 1.0 / v[3] / worldSize * scale; return vec4.mul(v as any, v as any, [s, s, 1.0 / v[3], s] as vec4); } /** * Modifies points in the supplied `frustumCoords` array so that the frustum's far plane only lies as far as the horizon, * which improves frustum culling effectiveness. * @param frustumCoords - Points of the frustum. * @param nearPlanePointsIndices - Which indices in the `frustumCoords` form the near plane. * @param horizonPlane - The horizon plane. */ function adjustFarPlaneByHorizonPlane(frustumCoords: vec4[], nearPlanePointsIndices: number[], horizonPlane: vec4, flippedNearFar: boolean): void { // For each of the 4 edges from near to far plane, // we find at which distance these edges intersect the given clipping plane, // select the maximal value from these distances and then we move // the frustum's far plane so that it is at most as far away from the near plane // as this maximal distance. const nearPlanePointsOffset = flippedNearFar ? 4 : 0; const farPlanePointsOffset = flippedNearFar ? 0 : 4; let maxDist = 0; const cornerRayLengths: number[] = []; const cornerRayNormalizedDirections: vec3[] = []; for (let i = 0; i < 4; i++) { const dir = vec3.sub([] as any, frustumCoords[i + farPlanePointsOffset] as vec3, frustumCoords[i + nearPlanePointsOffset] as vec3); const len = vec3.length(dir); vec3.scale(dir, dir, 1.0 / len); // normalize cornerRayLengths.push(len); cornerRayNormalizedDirections.push(dir); } for (let i = 0; i < 4; i++) { const dist = rayPlaneIntersection(frustumCoords[i + nearPlanePointsOffset] as vec3, cornerRayNormalizedDirections[i], horizonPlane); if (dist !== null && dist >= 0) { maxDist = Math.max(maxDist, dist); } else { // Use the original ray length for rays parallel to the horizon plane, or for rays pointing away from it. maxDist = Math.max(maxDist, cornerRayLengths[i]); } } // Compute the near plane. // We use its normal as the "view vector" - direction in which the camera is looking. const nearPlaneNormalized = getNormalizedNearPlane(frustumCoords, nearPlanePointsIndices); // We also try to adjust the far plane position so that it exactly intersects the point on the horizon // that is most distant from the near plane. const idealFarPlaneDistanceFromNearPlane = getIdealNearFarPlaneDistance(horizonPlane, nearPlaneNormalized); if (idealFarPlaneDistanceFromNearPlane !== null) { const idealCornerRayLength = idealFarPlaneDistanceFromNearPlane / vec3.dot(cornerRayNormalizedDirections[0], nearPlaneNormalized as vec3); // dot(near plane, ray dir) is the same for all 4 corners maxDist = Math.min(maxDist, idealCornerRayLength); } for (let i = 0; i < 4; i++) { const targetLength = Math.min(maxDist, cornerRayLengths[i]); const newPoint = [ frustumCoords[i + nearPlanePointsOffset][0] + cornerRayNormalizedDirections[i][0] * targetLength, frustumCoords[i + nearPlanePointsOffset][1] + cornerRayNormalizedDirections[i][1] * targetLength, frustumCoords[i + nearPlanePointsOffset][2] + cornerRayNormalizedDirections[i][2] * targetLength, 1, ] as vec4; frustumCoords[i + farPlanePointsOffset] = newPoint; } } /** * Returns the near plane equation with unit length direction. * @param frustumCoords - Points of the frustum. * @param nearPlanePointsIndices - Which indices in the `frustumCoords` form the near plane. */ function getNormalizedNearPlane(frustumCoords: vec4[], nearPlanePointsIndices: number[]): vec4 { const nearPlaneA = vec3.sub([] as any, frustumCoords[nearPlanePointsIndices[0]] as vec3, frustumCoords[nearPlanePointsIndices[1]] as vec3); const nearPlaneB = vec3.sub([] as any, frustumCoords[nearPlanePointsIndices[2]] as vec3, frustumCoords[nearPlanePointsIndices[1]] as vec3); const nearPlaneNormalized = [0, 0, 0, 0] as vec4; vec3.normalize(nearPlaneNormalized as vec3, vec3.cross([] as any, nearPlaneA, nearPlaneB)) as any; nearPlaneNormalized[3] = -vec3.dot(nearPlaneNormalized as vec3, frustumCoords[nearPlanePointsIndices[0]] as vec3); return nearPlaneNormalized; } /** * Returns the ideal distance between the frustum's near and far plane so that the far plane only lies as far as the horizon. */ function getIdealNearFarPlaneDistance(horizonPlane: vec4, nearPlaneNormalized: vec4): number | null { // Normalize the horizon plane to unit direction const horizonPlaneLen = vec3.len(horizonPlane as vec3); const normalizedHorizonPlane = vec4.scale([] as any, horizonPlane, 1 / horizonPlaneLen); // Project the view vector onto the horizon plane const projectedViewDirection = vec3.sub([] as any, nearPlaneNormalized as vec3, vec3.scale([] as any, normalizedHorizonPlane as vec3, vec3.dot(nearPlaneNormalized as vec3, normalizedHorizonPlane as vec3))); const projectedViewLength = vec3.len(projectedViewDirection); // projectedViewLength will be 0 if the camera is looking straight down if (projectedViewLength > 0) { // Find the radius and center of the horizon circle (the horizon circle is the intersection of the planet's sphere and the horizon plane). const horizonCircleRadius = Math.sqrt(1 - normalizedHorizonPlane[3] * normalizedHorizonPlane[3]); const horizonCircleCenter = vec3.scale([] as any, normalizedHorizonPlane as vec3, -normalizedHorizonPlane[3]); // The horizon plane normal always points towards the camera. // Find the furthest point on the horizon circle from the near plane. const pointFurthestOnHorizonCircle = vec3.add([] as any, horizonCircleCenter, vec3.scale([] as any, projectedViewDirection, horizonCircleRadius / projectedViewLength)); // Compute this point's distance from the near plane. return pointPlaneSignedDistance(nearPlaneNormalized, pointFurthestOnHorizonCircle); } else { return null; } }