UNPKG

@cesium/engine

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

1,480 lines (1,331 loc) 62.9 kB
import { MeshoptDecoder } from "meshoptimizer"; import AttributeCompression from "./AttributeCompression.js"; import Axis from "../Scene/Axis.js"; import AxisAlignedBoundingBox from "./AxisAlignedBoundingBox.js"; import binarySearch from "./binarySearch.js"; import BoundingSphere from "./BoundingSphere.js"; import Cartesian2 from "./Cartesian2.js"; import Cartesian3 from "./Cartesian3.js"; import Cartographic from "./Cartographic.js"; import CesiumMath from "./Math.js"; import Check from "./Check.js"; import ComponentDatatype from "./ComponentDatatype.js"; import Ellipsoid from "./Ellipsoid.js"; import EllipsoidalOccluder from "./EllipsoidalOccluder.js"; import Frozen from "./Frozen.js"; import Matrix4 from "./Matrix4.js"; import OrientedBoundingBox from "./OrientedBoundingBox.js"; import Rectangle from "./Rectangle.js"; import TerrainEncoding from "./TerrainEncoding.js"; import TerrainMesh from "./TerrainMesh.js"; import TerrainProvider from "./TerrainProvider.js"; import Transforms from "./Transforms.js"; import WebMercatorProjection from "./WebMercatorProjection.js"; /** * Contains functions to create a mesh from 3D Tiles terrain data. * * @namespace Cesium3DTilesTerrainGeometryProcessor * * @private */ const Cesium3DTilesTerrainGeometryProcessor = {}; /** * Contains information about geometry-related vertex arrays in a glTF asset. * @private * @typedef GltfInfo * * @property {Float32Array} positions The decoded position attributes. * @property {Float32Array|undefined} normals The decoded normal attributes, or <code>undefined</code> if the glTF has no normals. * @property {Uint16Array|Uint32Array} indices The decoded indices. * @property {Uint16Array|Uint32Array} edgeIndicesWest The edge indices along the West side of the tile. * @property {Uint16Array|Uint32Array} edgeIndicesSouth The edge indices along the South side of the tile. * @property {Uint16Array|Uint32Array} edgeIndicesEast The edge indices along the East side of the tile. * @property {Uint16Array|Uint32Array} edgeIndicesNorth The edge indices along the North side of the tile. */ const scratchGltfInfo = { positions: undefined, normals: undefined, indices: undefined, edgeIndicesWest: undefined, edgeIndicesSouth: undefined, edgeIndicesEast: undefined, edgeIndicesNorth: undefined, }; const scratchCenterCartographic = new Cartographic(); const scratchCenterCartesian = new Cartesian3(); const scratchEnuToEcef = new Matrix4(); const scratchEcefToEnu = new Matrix4(); const scratchTilesetTransform = new Matrix4(); const scratchMinimumPositionENU = new Cartesian3(); const scratchMaximumPositionENU = new Cartesian3(); const scratchPosLocal = new Cartesian3(); const scratchPosEcef = new Cartesian3(); const scratchCartographic = new Cartographic(); const scratchUV = new Cartesian2(); const scratchNormal = new Cartesian3(); const scratchNormalOct = new Cartesian2(); const scratchGeodeticSurfaceNormal = new Cartesian3(); const scratchPosEnu = new Cartesian3(); /** * Compares two edge indices for sorting. * @private * @param {number} a The first edge index. * @param {number} b The second edge index. * @returns {number} A negative number if <code>a</code> is less than <code>b</code>, a positive number if <code>a</code> is greater than <code>b</code>, or zero if they are equal. */ const sortedEdgeCompare = function (a, b) { return a - b; }; /** * @typedef {object} Cesium3DTilesTerrainGeometryProcessor.CreateMeshOptions * @property {Ellipsoid} ellipsoid The ellipsoid. * @property {Rectangle} rectangle The rectangle covered by the tile. * @property {boolean} hasVertexNormals <code>true</code> if the tile has vertex normals. * @property {boolean} hasWebMercatorT <code>true</code> if the tile has Web Mercator T coordinates. * @property {Object.<string,*>} gltf The glTF JSON of the tile. * @property {number} minimumHeight The minimum height of the tile. * @property {number} maximumHeight The maximum height of the tile. * @property {BoundingSphere} boundingSphere The bounding sphere of the tile. * @property {OrientedBoundingBox} orientedBoundingBox The oriented bounding box of the tile. * @property {Cartesian3} horizonOcclusionPoint The horizon occlusion point of the tile. * @property {number} skirtHeight The height of the skirts. * @property {number} [exaggeration=1.0] The scale used to exaggerate the terrain. * @property {number} [exaggerationRelativeHeight=0.0] The height relative to which terrain is exaggerated. */ /** * Creates a {@link TerrainMesh} from this terrain data. * @function * * @private * * @param {Cesium3DTilesTerrainGeometryProcessor.CreateMeshOptions} options An object describing options for mesh creation. * @returns {Promise.<TerrainMesh>} A promise to a terrain mesh. */ Cesium3DTilesTerrainGeometryProcessor.createMesh = async function (options) { options = options ?? Frozen.EMPTY_OBJECT; const { exaggeration = 1.0, exaggerationRelativeHeight = 0.0, hasVertexNormals, hasWebMercatorT, gltf, minimumHeight, maximumHeight, skirtHeight, } = options; //>>includeStart('debug', pragmas.debug); Check.typeOf.object("options.ellipsoid", options.ellipsoid); Check.typeOf.object("options.rectangle", options.rectangle); Check.typeOf.bool("options.hasVertexNormals", hasVertexNormals); Check.typeOf.bool("options.hasWebMercatorT", hasWebMercatorT); Check.typeOf.object("options.gltf", gltf); Check.typeOf.number("options.minimumHeight", minimumHeight); Check.typeOf.number("options.maximumHeight", maximumHeight); Check.typeOf.object("options.boundingSphere", options.boundingSphere); Check.typeOf.object( "options.orientedBoundingBox", options.orientedBoundingBox, ); Check.typeOf.object( "options.horizonOcclusionPoint", options.horizonOcclusionPoint, ); Check.typeOf.number("options.skirtHeight", skirtHeight); //>>includeEnd('debug'); const hasExaggeration = exaggeration !== 1.0; const hasGeodeticSurfaceNormals = hasExaggeration; const boundingSphere = BoundingSphere.clone( options.boundingSphere, new BoundingSphere(), ); const orientedBoundingBox = OrientedBoundingBox.clone( options.orientedBoundingBox, new OrientedBoundingBox(), ); const horizonOcclusionPoint = Cartesian3.clone( options.horizonOcclusionPoint, new Cartesian3(), ); const ellipsoid = Ellipsoid.clone(options.ellipsoid, new Ellipsoid()); const rectangle = Rectangle.clone(options.rectangle, new Rectangle()); const hasMeshOptCompression = gltf.extensionsRequired !== undefined && gltf.extensionsRequired.indexOf("EXT_meshopt_compression") !== -1; const decoderPromise = hasMeshOptCompression ? MeshoptDecoder.ready : Promise.resolve(undefined); await decoderPromise; const tileMinLongitude = rectangle.west; const tileMinLatitude = rectangle.south; const tileMaxLatitude = rectangle.north; const tileLengthLongitude = rectangle.width; const tileLengthLatitude = rectangle.height; const approximateCenterCartographic = Rectangle.center( rectangle, scratchCenterCartographic, ); approximateCenterCartographic.height = 0.5 * (minimumHeight + maximumHeight); const approximateCenterPosition = Cartographic.toCartesian( approximateCenterCartographic, ellipsoid, scratchCenterCartesian, ); const enuToEcef = Transforms.eastNorthUpToFixedFrame( approximateCenterPosition, ellipsoid, scratchEnuToEcef, ); const ecefToEnu = Matrix4.inverseTransformation(enuToEcef, scratchEcefToEnu); let tilesetTransform = Matrix4.unpack( gltf.nodes[0].matrix, 0, scratchTilesetTransform, ); tilesetTransform = Matrix4.multiply( Axis.Y_UP_TO_Z_UP, tilesetTransform, tilesetTransform, ); const gltfInfo = decodeGltf(gltf, hasVertexNormals, scratchGltfInfo); const skirtVertexCount = TerrainProvider.getSkirtVertexCount( gltfInfo.edgeIndicesWest, gltfInfo.edgeIndicesSouth, gltfInfo.edgeIndicesEast, gltfInfo.edgeIndicesNorth, ); const positionsLocalWithoutSkirts = gltfInfo.positions; const normalsWithoutSkirts = gltfInfo.normals; const indicesWithoutSkirts = gltfInfo.indices; const vertexCountWithoutSkirts = positionsLocalWithoutSkirts.length / 3; const vertexCountWithSkirts = vertexCountWithoutSkirts + skirtVertexCount; const indexCountWithoutSkirts = indicesWithoutSkirts.length; const skirtIndexCount = TerrainProvider.getSkirtIndexCountWithFilledCorners(skirtVertexCount); // For consistency with glTF spec, 16 bit index buffer can't contain 65535 const SizedIndexTypeWithSkirts = vertexCountWithSkirts <= 65535 ? Uint16Array : Uint32Array; // Make the index buffer large enough that we can add in the skirt indices later const indexBufferWithSkirts = new SizedIndexTypeWithSkirts( indexCountWithoutSkirts + skirtIndexCount, ); indexBufferWithSkirts.set(indicesWithoutSkirts); const westIndices = new SizedIndexTypeWithSkirts(gltfInfo.edgeIndicesWest); const southIndices = new SizedIndexTypeWithSkirts(gltfInfo.edgeIndicesSouth); const eastIndices = new SizedIndexTypeWithSkirts(gltfInfo.edgeIndicesEast); const northIndices = new SizedIndexTypeWithSkirts(gltfInfo.edgeIndicesNorth); const sortedWestIndices = new SizedIndexTypeWithSkirts(westIndices).sort(); const sortedSouthIndices = new SizedIndexTypeWithSkirts(southIndices).sort(); const sortedEastIndices = new SizedIndexTypeWithSkirts(eastIndices).sort(); const sortedNorthIndices = new SizedIndexTypeWithSkirts(northIndices).sort(); const southMercatorAngle = WebMercatorProjection.geodeticLatitudeToMercatorAngle(tileMinLatitude); const northMercatorAngle = WebMercatorProjection.geodeticLatitudeToMercatorAngle(tileMaxLatitude); const oneOverMercatorHeight = 1.0 / (northMercatorAngle - southMercatorAngle); // Use a terrain encoding without quantization. // This is just an easier way to save intermediate state let minPosEnu = Cartesian3.fromElements( Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, scratchMinimumPositionENU, ); let maxPosEnu = Cartesian3.fromElements( Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, scratchMaximumPositionENU, ); const tempTerrainEncoding = new TerrainEncoding( boundingSphere.center, undefined, undefined, undefined, undefined, hasVertexNormals, hasWebMercatorT, hasGeodeticSurfaceNormals, exaggeration, exaggerationRelativeHeight, ); const tempBufferStride = tempTerrainEncoding.stride; const tempBuffer = new Float32Array(vertexCountWithSkirts * tempBufferStride); let tempBufferOffset = 0; for (let i = 0; i < vertexCountWithoutSkirts; i++) { const posLocal = Cartesian3.unpack( positionsLocalWithoutSkirts, i * 3, scratchPosLocal, ); const posECEF = Matrix4.multiplyByPoint( tilesetTransform, posLocal, scratchPosEcef, ); const cartographic = Cartographic.fromCartesian( posECEF, ellipsoid, scratchCartographic, ); const { longitude, latitude, height } = cartographic; // If a vertex is an edge vertex we already know its exact UV and don't need to derive it from the position (which can have accuracy issues). let u = (longitude - tileMinLongitude) / tileLengthLongitude; let v = (latitude - tileMinLatitude) / tileLengthLatitude; // Clamp the UVs to the valid range // This should only happen when the cartesian to cartographic conversion introduces error on a point that is already very close the edge u = CesiumMath.clamp(u, 0.0, 1.0); v = CesiumMath.clamp(v, 0.0, 1.0); if (binarySearch(sortedWestIndices, i, sortedEdgeCompare) >= 0) { u = 0.0; } else if (binarySearch(sortedEastIndices, i, sortedEdgeCompare) >= 0) { u = 1.0; } if (binarySearch(sortedSouthIndices, i, sortedEdgeCompare) >= 0) { v = 0.0; } else if (binarySearch(sortedNorthIndices, i, sortedEdgeCompare) >= 0) { v = 1.0; } const uv = Cartesian2.fromElements(u, v, scratchUV); let normalOct; if (hasVertexNormals) { let normal = Cartesian3.unpack( normalsWithoutSkirts, i * 3, scratchNormal, ); normal = Matrix4.multiplyByPointAsVector( tilesetTransform, normal, scratchNormal, ); normal = Cartesian3.normalize(normal, scratchNormal); normalOct = AttributeCompression.octEncode(normal, scratchNormalOct); } let webMercatorT; if (hasWebMercatorT) { const mercatorAngle = WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude); webMercatorT = (mercatorAngle - southMercatorAngle) * oneOverMercatorHeight; } let geodeticSurfaceNormal; if (hasGeodeticSurfaceNormals) { geodeticSurfaceNormal = ellipsoid.geodeticSurfaceNormal( posECEF, scratchGeodeticSurfaceNormal, ); } tempBufferOffset = tempTerrainEncoding.encode( tempBuffer, tempBufferOffset, posECEF, uv, height, normalOct, webMercatorT, geodeticSurfaceNormal, ); const posEnu = Matrix4.multiplyByPoint(ecefToEnu, posECEF, scratchPosEnu); minPosEnu = Cartesian3.minimumByComponent(posEnu, minPosEnu, minPosEnu); maxPosEnu = Cartesian3.maximumByComponent(posEnu, maxPosEnu, maxPosEnu); } const mesh = new TerrainMesh( Cartesian3.clone(tempTerrainEncoding.center, new Cartesian3()), tempBuffer, indexBufferWithSkirts, indexCountWithoutSkirts, vertexCountWithoutSkirts, minimumHeight, maximumHeight, BoundingSphere.clone(boundingSphere, new BoundingSphere()), Cartesian3.clone(horizonOcclusionPoint, new Cartesian3()), tempBufferStride, OrientedBoundingBox.clone(orientedBoundingBox, new OrientedBoundingBox()), tempTerrainEncoding, westIndices, southIndices, eastIndices, northIndices, ); addSkirtsToMesh( mesh, rectangle, ellipsoid, minPosEnu, maxPosEnu, enuToEcef, ecefToEnu, skirtHeight, ); return Promise.resolve(mesh); }; const scratchMinUV = new Cartesian2(); const scratchMaxUV = new Cartesian2(); const scratchPolygonIndices = new Array(6); const scratchUvA = new Cartesian2(); const scratchUvB = new Cartesian2(); const scratchUvC = new Cartesian2(); const scratchNormalA = new Cartesian3(); const scratchNormalB = new Cartesian3(); const scratchNormalC = new Cartesian3(); const scratchCenterCartographicUpsample = new Cartographic(); const scratchCenterCartesianUpsample = new Cartesian3(); const scratchCartographicSkirt = new Cartographic(); const scratchCartographicUpsample = new Cartographic(); const scratchPosEcefSkirt = new Cartesian3(); const scratchPosEcefUpsample = new Cartesian3(); const scratchPosEnuSkirt = new Cartesian3(); const scratchPosEnuUpsample = new Cartesian3(); const scratchMinimumPositionENUSkirt = new Cartesian3(); const scratchMaximumPositionENUSkirt = new Cartesian3(); const scratchMinimumPositionENUUpsample = new Cartesian3(); const scratchMaximumPositionENUUpsample = new Cartesian3(); const scratchEnuToEcefUpsample = new Matrix4(); const scratchEcefToEnuUpsample = new Matrix4(); const scratchUVSkirt = new Cartesian2(); const scratchUVUpsample = new Cartesian2(); const scratchHorizonOcclusionPoint = new Cartesian3(); const scratchBoundingSphere = new BoundingSphere(); const scratchOrientedBoundingBox = new OrientedBoundingBox(); const scratchAABBEnuSkirt = new AxisAlignedBoundingBox(); const scratchNormalUpsample = new Cartesian3(); const scratchNormalOctSkirt = new Cartesian2(); const scratchNormalOctUpsample = new Cartesian2(); const scratchGeodeticSurfaceNormalSkirt = new Cartesian3(); const scratchGeodeticSurfaceNormalUpsample = new Cartesian3(); /** * Decode the position attributes from a glTF object. * @private * @param {Object.<string,*>} gltf The glTF JSON. * @returns {Float32Array} The decoded positions, as a flattened array of x, y, z values. */ function decodePositions(gltf) { const primitive = gltf.meshes[0].primitives[0]; const accessor = gltf.accessors[primitive.attributes["POSITION"]]; const bufferView = gltf.bufferViews[accessor.bufferView]; const positionCount = accessor.count; const bufferViewMeshOpt = bufferView.extensions ? bufferView.extensions["EXT_meshopt_compression"] : undefined; if (bufferViewMeshOpt === undefined) { const buffer = gltf.buffers[bufferView.buffer].extras._pipeline.source; return new Float32Array( buffer.buffer, buffer.byteOffset + // offset from the start of the glb (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0), positionCount * 3, ); } const buffer = gltf.buffers[bufferViewMeshOpt.buffer].extras._pipeline.source; const compressedBuffer = new Uint8Array( buffer.buffer, buffer.byteOffset + // offset from the start of the glb (bufferViewMeshOpt.byteOffset ?? 0) + (accessor.byteOffset ?? 0), bufferViewMeshOpt.byteLength, ); const positionByteLength = bufferViewMeshOpt.byteStride; const PositionType = positionByteLength === 4 ? Uint8Array : Uint16Array; const positionsResult = new PositionType(positionCount * 4); MeshoptDecoder.decodeVertexBuffer( new Uint8Array(positionsResult.buffer), positionCount, positionByteLength, compressedBuffer, ); const positionStorageValueMax = (1 << (positionsResult.BYTES_PER_ELEMENT * 8)) - 1; const positions = new Float32Array(positionCount * 3); for (let p = 0; p < positionCount; p++) { // only the first 3 components are used positions[p * 3 + 0] = positionsResult[p * 4 + 0] / positionStorageValueMax; positions[p * 3 + 1] = positionsResult[p * 4 + 1] / positionStorageValueMax; positions[p * 3 + 2] = positionsResult[p * 4 + 2] / positionStorageValueMax; // fourth component is not used } return positions; } /** * Decode the normal attributes from a glTF object. * @private * @param {Object.<string,*>} gltf The glTF JSON. * @returns {Float32Array} The decoded normals, as a flattened array of x, y, z values. */ function decodeNormals(gltf) { const primitive = gltf.meshes[0].primitives[0]; const accessor = gltf.accessors[primitive.attributes["NORMAL"]]; const bufferView = gltf.bufferViews[accessor.bufferView]; const normalCount = accessor.count; const bufferViewMeshOpt = bufferView.extensions ? bufferView.extensions["EXT_meshopt_compression"] : undefined; if (bufferViewMeshOpt === undefined) { const buffer = gltf.buffers[bufferView.buffer].extras._pipeline.source; return new Float32Array( buffer.buffer, buffer.byteOffset + // offset from the start of the glb (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0), normalCount * 3, ); } const buffer = gltf.buffers[bufferViewMeshOpt.buffer].extras._pipeline.source; const compressedBuffer = new Uint8Array( buffer.buffer, buffer.byteOffset + // offset from the start of the glb (bufferViewMeshOpt.byteOffset ?? 0) + (accessor.byteOffset ?? 0), bufferViewMeshOpt.byteLength, ); const normalByteLength = bufferViewMeshOpt.byteStride; const normalsResult = new Int8Array(normalCount * normalByteLength); MeshoptDecoder.decodeVertexBuffer( new Uint8Array(normalsResult.buffer), normalCount, normalByteLength, compressedBuffer, ); const normals = new Float32Array(normalCount * 3); for (let i = 0; i < normalCount; i++) { // AttributeCompression.octDecodeInRange is not compatible with KHR_mesh_quantization, so do the oct decode manually // The quantization puts values between -127 and +127, but clamp in case it has -128 // The third component is unused until normals support non-8-bit quantization // The fourth component is always unused let octX = Math.max(normalsResult[i * 4 + 0] / 127.0, -1.0); let octY = Math.max(normalsResult[i * 4 + 1] / 127.0, -1.0); const octZ = 1.0 - (Math.abs(octX) + Math.abs(octY)); if (octZ < 0.0) { const oldX = octX; const oldY = octY; octX = (1.0 - Math.abs(oldY)) * CesiumMath.signNotZero(oldX); octY = (1.0 - Math.abs(oldX)) * CesiumMath.signNotZero(oldY); } let normal = scratchNormal; normal.x = octX; normal.y = octY; normal.z = octZ; normal = Cartesian3.normalize(normal, scratchNormal); normals[i * 3 + 0] = normal.x; normals[i * 3 + 1] = normal.y; normals[i * 3 + 2] = normal.z; } return normals; } /** * Decode the index attributes from a glTF object. * @private * @param {Object.<string,*>} gltf The glTF JSON. * @returns {Uint16Array|Uint32Array} An array of indices. */ function decodeIndices(gltf) { const primitive = gltf.meshes[0].primitives[0]; const accessor = gltf.accessors[primitive.indices]; const bufferView = gltf.bufferViews[accessor.bufferView]; const indexCount = accessor.count; const SizedIndexType = accessor.componentType === ComponentDatatype.UNSIGNED_SHORT ? Uint16Array : Uint32Array; const bufferViewMeshOpt = bufferView.extensions ? bufferView.extensions["EXT_meshopt_compression"] : undefined; if (bufferViewMeshOpt === undefined) { const buffer = gltf.buffers[bufferView.buffer].extras._pipeline.source; return new SizedIndexType( buffer.buffer, buffer.byteOffset + // offset from the glb (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0), indexCount, ); } const buffer = gltf.buffers[bufferViewMeshOpt.buffer].extras._pipeline.source; const compressedBuffer = new Uint8Array( buffer.buffer, buffer.byteOffset + // offset from the start of the glb (bufferViewMeshOpt.byteOffset ?? 0) + (accessor.byteOffset ?? 0), bufferViewMeshOpt.byteLength, ); const indices = new SizedIndexType(indexCount); MeshoptDecoder.decodeIndexBuffer( new Uint8Array(indices.buffer), indexCount, bufferViewMeshOpt.byteStride, compressedBuffer, ); return indices; } /** * Decode the edge index attributes from a glTF object. * @private * @param {Object.<string,*>} gltf The glTF JSON. * @param {string} name The name of the edge indices to decode. * @returns {Uint16Array|Uint32Array} An array of edge indices. */ function decodeEdgeIndices(gltf, name) { const primitive = gltf.meshes[0].primitives[0]; const accessor = gltf.accessors[primitive.extensions.CESIUM_tile_edges[name]]; const bufferView = gltf.bufferViews[accessor.bufferView]; const indexCount = accessor.count; const SizedIndexType = accessor.componentType === ComponentDatatype.UNSIGNED_SHORT ? Uint16Array : Uint32Array; const bufferViewMeshOpt = bufferView.extensions ? bufferView.extensions["EXT_meshopt_compression"] : undefined; if (bufferViewMeshOpt === undefined) { const buffer = gltf.buffers[bufferView.buffer].extras._pipeline.source; return new SizedIndexType( buffer.buffer, buffer.byteOffset + // offset from the glb (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0), indexCount, ); } const buffer = gltf.buffers[bufferViewMeshOpt.buffer].extras._pipeline.source; const compressedBuffer = new Uint8Array( buffer.buffer, buffer.byteOffset + // offset from the start of the glb (bufferViewMeshOpt.byteOffset ?? 0) + (accessor.byteOffset ?? 0), bufferViewMeshOpt.byteLength, ); const indices = new SizedIndexType(indexCount); const indexByteLength = bufferViewMeshOpt.byteStride; MeshoptDecoder.decodeIndexSequence( new Uint8Array(indices.buffer), indexCount, indexByteLength, compressedBuffer, ); return indices; } /** * Decodes geometry-related vertex arrays from a glTF asset. * @private * @param {Object.<string,*>} gltf The glTF JSON. * @param {boolean} hasNormals <code>true</code> if the glTF has normal attributes. * @param {GltfInfo} result The object to store the decoded arrays. * @returns {GltfInfo} The decoded geometry info. */ function decodeGltf(gltf, hasNormals, result) { result.positions = decodePositions(gltf); result.normals = hasNormals ? decodeNormals(gltf) : undefined; result.indices = decodeIndices(gltf); result.edgeIndicesWest = decodeEdgeIndices(gltf, "left"); result.edgeIndicesSouth = decodeEdgeIndices(gltf, "bottom"); result.edgeIndicesEast = decodeEdgeIndices(gltf, "right"); result.edgeIndicesNorth = decodeEdgeIndices(gltf, "top"); return result; } /** * @typedef {object} Cesium3DTilesTerrainGeometryProcessor.UpsampleMeshOptions * @property {boolean} isEastChild <code>true</code> if the tile is the east child of its parent. * @property {boolean} isNorthChild <code>true</code> if the tile is the north child of its parent. * @property {Rectangle} rectangle The rectangle covered by the tile. * @property {Ellipsoid} ellipsoid The ellipsoid. * @property {number} skirtHeight The height of the skirts. * @property {Float32Array} parentVertices The parent tile's vertex buffer. * @property {Uint8Array|Uint16Array|Uint32Array} parentIndices The parent tile's index buffer. * @property {number} parentVertexCountWithoutSkirts The number of vertices in the parent tile excluding skirts. * @property {number} parentIndexCountWithoutSkirts The number of indices in the parent tile excluding skirts. * @property {number} parentMinimumHeight The minimum height of the parent tile. * @property {number} parentMaximumHeight The maximum height of the parent tile. * @property {TerrainEncoding} parentEncoding The parent tile's terrain encoding. */ /** * Upsamples a parent tile's mesh to create a higher-detail child tile's mesh. * * Overview: Only include triangles that are inside the UV clipping region. * If a triangle is partly outside, it will be clipped at the border. * The clipping function returns a polygon where each point is a barycentric coordinate of the input triangle. * Most of the time the triangle will not be clipped, so the polygon will be the three barycentric coordinates of the input triangle. * If the triangle is completely outside the clipping region, the polygon will have no points and will be ignored. * If the triangle is clipped, the polygon will have between four and six points and needs to be triangulated. * Vertex data for points that fall inside the triangle will be interpolated using the barycentric coordinates. * Each vertex in the polygon is added to the new vertex list, with some special handling to avoid duplicate points between triangles. * * @private * @param {Cesium3DTilesTerrainGeometryProcessor.UpsampleMeshOptions} options An object describing options for mesh upsampling. * @returns {TerrainMesh} The upsampled terrain mesh. */ Cesium3DTilesTerrainGeometryProcessor.upsampleMesh = function (options) { options = options ?? Frozen.EMPTY_OBJECT; const { isEastChild, isNorthChild, parentMinimumHeight, parentMaximumHeight, skirtHeight, } = options; //>>includeStart('debug', pragmas.debug) Check.typeOf.bool("options.isEastChild", isEastChild); Check.typeOf.bool("options.isNorthChild", isNorthChild); Check.typeOf.object("options.parentVertices", options.parentVertices); Check.typeOf.object("options.parentIndices", options.parentIndices); Check.typeOf.number( "options.parentVertexCountWithoutSkirts", options.parentVertexCountWithoutSkirts, ); Check.typeOf.number( "options.parentIndexCountWithoutSkirts", options.parentIndexCountWithoutSkirts, ); Check.typeOf.number("options.parentMinimumHeight", parentMinimumHeight); Check.typeOf.number("options.parentMaximumHeight", parentMaximumHeight); Check.typeOf.object("options.parentEncoding", options.parentEncoding); Check.typeOf.object("options.rectangle", options.rectangle); Check.typeOf.number("options.skirtHeight", skirtHeight); Check.typeOf.object("options.ellipsoid", options.ellipsoid); //>>includeEnd('debug'); const indexCount = options.parentIndexCountWithoutSkirts; const indices = options.parentIndices; const vertexCount = options.parentVertexCountWithoutSkirts; const vertexBuffer = options.parentVertices; const encoding = TerrainEncoding.clone( options.parentEncoding, new TerrainEncoding(), ); const hasVertexNormals = encoding.hasVertexNormals; const hasWebMercatorT = encoding.hasWebMercatorT; const exaggeration = encoding.exaggeration; const exaggerationRelativeHeight = encoding.exaggerationRelativeHeight; const hasExaggeration = exaggeration !== 1.0; const hasGeodeticSurfaceNormals = hasExaggeration; const upsampleRectangle = Rectangle.clone(options.rectangle, new Rectangle()); const ellipsoid = Ellipsoid.clone(options.ellipsoid); const upsampledTriIDs = []; const upsampledUVs = []; const upsampledBarys = []; const upsampledIndices = []; const upsampledWestIndices = []; const upsampledSouthIndices = []; const upsampledEastIndices = []; const upsampledNorthIndices = []; clipTileFromQuadrant( isEastChild, isNorthChild, indexCount, indices, vertexCount, vertexBuffer, encoding, upsampledIndices, upsampledWestIndices, upsampledSouthIndices, upsampledEastIndices, upsampledNorthIndices, upsampledTriIDs, upsampledBarys, upsampledUVs, ); // Don't know the min and max height of the upsampled positions yet, // so calculate a center point from the parent's min and max height const approximateCenterCartographic = Rectangle.center( upsampleRectangle, scratchCenterCartographicUpsample, ); approximateCenterCartographic.height = 0.5 * (parentMinimumHeight + parentMaximumHeight); const approximateCenterPosition = Cartographic.toCartesian( approximateCenterCartographic, ellipsoid, scratchCenterCartesianUpsample, ); const upsampledVertexCountWithoutSkirts = upsampledTriIDs.length; const upsampledTerrainEncoding = new TerrainEncoding( approximateCenterPosition, undefined, undefined, undefined, undefined, hasVertexNormals, hasWebMercatorT, hasGeodeticSurfaceNormals, exaggeration, exaggerationRelativeHeight, ); const upsampledVertexBufferStride = upsampledTerrainEncoding.stride; const upsampledSkirtVertexCount = TerrainProvider.getSkirtVertexCount( upsampledWestIndices, upsampledSouthIndices, upsampledEastIndices, upsampledNorthIndices, ); const upsampledVertexCountWithSkirts = upsampledVertexCountWithoutSkirts + upsampledSkirtVertexCount; const upsampledIndexCountWithoutSkirts = upsampledIndices.length; const upsampledSkirtIndexCount = TerrainProvider.getSkirtIndexCountWithFilledCorners( upsampledSkirtVertexCount, ); const upsampledIndexCountWithSkirts = upsampledIndexCountWithoutSkirts + upsampledSkirtIndexCount; // For consistency with glTF spec, 16 bit index buffer can't contain 65535 const SizedIndexTypeWithSkirts = upsampledVertexCountWithSkirts <= 65535 ? Uint16Array : Uint32Array; const upsampledIndexBuffer = new SizedIndexTypeWithSkirts( upsampledIndexCountWithSkirts, ); upsampledIndexBuffer.set(upsampledIndices); const upsampledWestIndicesBuffer = new SizedIndexTypeWithSkirts( upsampledWestIndices, ); const upsampledSouthIndicesBuffer = new SizedIndexTypeWithSkirts( upsampledSouthIndices, ); const upsampledEastIndicesBuffer = new SizedIndexTypeWithSkirts( upsampledEastIndices, ); const upsampledNorthIndicesBuffer = new SizedIndexTypeWithSkirts( upsampledNorthIndices, ); const upsampledVertexBuffer = new Float32Array( upsampledVertexCountWithSkirts * upsampledVertexBufferStride, ); let upsampledVertexBufferOffset = 0; const enuToEcef = Transforms.eastNorthUpToFixedFrame( approximateCenterPosition, ellipsoid, scratchEnuToEcefUpsample, ); const ecefToEnu = Matrix4.inverseTransformation( enuToEcef, scratchEcefToEnuUpsample, ); const minimumLongitude = upsampleRectangle.west; const maximumLongitude = upsampleRectangle.east; const minimumLatitude = upsampleRectangle.south; const maximumLatitude = upsampleRectangle.north; const southMercatorAngle = WebMercatorProjection.geodeticLatitudeToMercatorAngle(minimumLatitude); const northMercatorAngle = WebMercatorProjection.geodeticLatitudeToMercatorAngle(maximumLatitude); const oneOverMercatorHeight = 1.0 / (northMercatorAngle - southMercatorAngle); let minimumHeight = Number.POSITIVE_INFINITY; let maximumHeight = Number.NEGATIVE_INFINITY; let minPosEnu = Cartesian3.fromElements( Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, scratchMinimumPositionENUUpsample, ); let maxPosEnu = Cartesian3.fromElements( Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, scratchMaximumPositionENUUpsample, ); for (let i = 0; i < upsampledVertexCountWithoutSkirts; i++) { const triId = upsampledTriIDs[i]; const indexA = indices[triId * 3 + 0]; const indexB = indices[triId * 3 + 1]; const indexC = indices[triId * 3 + 2]; const uv = scratchUVUpsample; uv.x = upsampledUVs[i * 2 + 0]; uv.y = upsampledUVs[i * 2 + 1]; const u = uv.x; const v = uv.y; const baryA = upsampledBarys[i * 2 + 0]; const baryB = upsampledBarys[i * 2 + 1]; const baryC = 1.0 - baryA - baryB; const heightA = encoding.decodeHeight(vertexBuffer, indexA); const heightB = encoding.decodeHeight(vertexBuffer, indexB); const heightC = encoding.decodeHeight(vertexBuffer, indexC); const height = heightA * baryA + heightB * baryB + heightC * baryC; minimumHeight = Math.min(height, minimumHeight); maximumHeight = Math.max(height, maximumHeight); const lon = CesiumMath.lerp(minimumLongitude, maximumLongitude, u); const lat = CesiumMath.lerp(minimumLatitude, maximumLatitude, v); const carto = Cartographic.fromRadians( lon, lat, height, scratchCartographicUpsample, ); const position = Cartographic.toCartesian( carto, ellipsoid, scratchPosEcefUpsample, ); const posEnu = Matrix4.multiplyByPoint( ecefToEnu, position, scratchPosEnuUpsample, ); minPosEnu = Cartesian3.minimumByComponent(posEnu, minPosEnu, minPosEnu); maxPosEnu = Cartesian3.maximumByComponent(posEnu, maxPosEnu, maxPosEnu); let normalOct; if (hasVertexNormals) { const normalA = encoding.decodeNormal( vertexBuffer, indexA, scratchNormalA, ); const normalB = encoding.decodeNormal( vertexBuffer, indexB, scratchNormalB, ); const normalC = encoding.decodeNormal( vertexBuffer, indexC, scratchNormalC, ); let normal = Cartesian3.fromElements( normalA.x * baryA + normalB.x * baryB + normalC.x * baryC, normalA.y * baryA + normalB.y * baryB + normalC.y * baryC, normalA.z * baryA + normalB.z * baryB + normalC.z * baryC, scratchNormalUpsample, ); normal = Cartesian3.normalize(normal, scratchNormalUpsample); normalOct = AttributeCompression.octEncode( normal, scratchNormalOctUpsample, ); } let webMercatorT; if (hasWebMercatorT) { const mercatorAngle = WebMercatorProjection.geodeticLatitudeToMercatorAngle(lat); webMercatorT = (mercatorAngle - southMercatorAngle) * oneOverMercatorHeight; } let geodeticSurfaceNormal; if (hasGeodeticSurfaceNormals) { geodeticSurfaceNormal = ellipsoid.geodeticSurfaceNormal( position, scratchGeodeticSurfaceNormalUpsample, ); } upsampledVertexBufferOffset = upsampledTerrainEncoding.encode( upsampledVertexBuffer, upsampledVertexBufferOffset, position, uv, height, normalOct, webMercatorT, geodeticSurfaceNormal, ); } // Now generate the more tight-fitting bounding volumes that are used for culling and other things const orientedBoundingBox = OrientedBoundingBox.fromRectangle( upsampleRectangle, minimumHeight, maximumHeight, ellipsoid, scratchOrientedBoundingBox, ); const boundingSphere = BoundingSphere.fromVertices( upsampledVertexBuffer, upsampledTerrainEncoding.center, upsampledVertexBufferStride, scratchBoundingSphere, ); const occluder = new EllipsoidalOccluder(ellipsoid); const horizonOcclusionPoint = occluder.computeHorizonCullingPointFromVerticesPossiblyUnderEllipsoid( upsampledTerrainEncoding.center, // vector from ellipsoid center to horizon occlusion point upsampledVertexBuffer, upsampledVertexBufferStride, upsampledTerrainEncoding.center, minimumHeight, scratchHorizonOcclusionPoint, ); const upsampledMesh = new TerrainMesh( Cartesian3.clone(upsampledTerrainEncoding.center, new Cartesian3()), upsampledVertexBuffer, upsampledIndexBuffer, upsampledIndexCountWithoutSkirts, upsampledVertexCountWithoutSkirts, minimumHeight, maximumHeight, BoundingSphere.clone(boundingSphere), Cartesian3.clone(horizonOcclusionPoint), upsampledVertexBufferStride, OrientedBoundingBox.clone(orientedBoundingBox), upsampledTerrainEncoding, upsampledWestIndicesBuffer, upsampledSouthIndicesBuffer, upsampledEastIndicesBuffer, upsampledNorthIndicesBuffer, ); addSkirtsToMesh( upsampledMesh, upsampleRectangle, ellipsoid, minPosEnu, maxPosEnu, enuToEcef, ecefToEnu, skirtHeight, ); return upsampledMesh; }; /** * Helper function that adds skirts to a TerrainMesh. The mesh's vertex and index * buffers are expected to be pre-allocated to fit the skirts. * The mesh's vertex buffer must have quantization disabled. * If the final quantization changes, a new vertex buffer will be allocated using the new quantization. * Currently skirts do not affect the tile's bounding volume. * @private * @param {TerrainMesh} mesh * @param {Rectangle} rectangle * @param {Ellipsoid} ellipsoid * @param {Cartesian3} enuMinimum * @param {Cartesian3} enuMaximum * @param {Matrix4} enuToEcef * @param {Matrix4} ecefToEnu * @param {number} skirtHeight */ function addSkirtsToMesh( mesh, rectangle, ellipsoid, enuMinimum, enuMaximum, enuToEcef, ecefToEnu, skirtHeight, ) { const { encoding } = mesh; const vertexStride = encoding.stride; const vertexBuffer = mesh.vertices; const { hasVertexNormals, hasWebMercatorT, exaggeration, exaggerationRelativeHeight, } = encoding; const hasExaggeration = exaggeration !== 1.0; const hasGeodeticSurfaceNormals = hasExaggeration; const vertexCountWithoutSkirts = mesh.vertexCountWithoutSkirts; let vertexBufferOffset = vertexCountWithoutSkirts * vertexStride; const vertexCountWithSkirts = vertexBuffer.length / vertexStride; const skirtVertexCount = vertexCountWithSkirts - vertexCountWithoutSkirts; const indices = mesh.indices; const indexCountWithoutSkirts = mesh.indexCountWithoutSkirts; const westIndices = mesh.westIndicesSouthToNorth; const southIndices = mesh.southIndicesEastToWest; const eastIndices = mesh.eastIndicesNorthToSouth; const northIndices = mesh.northIndicesWestToEast; TerrainProvider.addSkirtIndicesWithFilledCorners( westIndices, southIndices, eastIndices, northIndices, vertexCountWithoutSkirts, indices, indexCountWithoutSkirts, ); const westOffset = 0; const southOffset = westOffset + westIndices.length; const eastOffset = southOffset + southIndices.length; const northOffset = eastOffset + eastIndices.length; const edges = [westIndices, southIndices, eastIndices, northIndices]; const edgeIndexOffset = [westOffset, southOffset, eastOffset, northOffset]; const edgeLongitudeSign = [-1.0, 0.0, +1.0, 0.0]; const edgeLatitudeSign = [0.0, -1.0, 0.0, +1.0]; const minimumPositionENUWithSkirts = Cartesian3.clone( enuMinimum, scratchMinimumPositionENUSkirt, ); const maximumPositionENUWithSkirts = Cartesian3.clone( enuMaximum, scratchMaximumPositionENUSkirt, ); const maximumHeight = mesh.maximumHeight; const minimumHeightWithSkirts = mesh.minimumHeight - skirtHeight; for (let skirtId = 0; skirtId < skirtVertexCount; skirtId++) { let side = 0; for (side = 0; side < 3; side++) { if (skirtId < edgeIndexOffset[side + 1]) { break; } } const vertexIndex = edges[side][skirtId - edgeIndexOffset[side]]; const uv = encoding.decodeTextureCoordinates( vertexBuffer, vertexIndex, scratchUVSkirt, ); const skirtLonLatOffsetPercent = 0.0001; const longitudeT = uv.x + edgeLongitudeSign[side] * skirtLonLatOffsetPercent; const latitudeT = uv.y + edgeLatitudeSign[side] * skirtLonLatOffsetPercent; const longitude = CesiumMath.lerp( rectangle.west, rectangle.east, longitudeT, ); // Don't offset the skirt past the poles, it will screw up the cartographic -> cartesian const latitude = CesiumMath.clamp( CesiumMath.lerp(rectangle.south, rectangle.north, latitudeT), -CesiumMath.PI_OVER_TWO, +CesiumMath.PI_OVER_TWO, ); const vertHeight = encoding.decodeHeight(vertexBuffer, vertexIndex); const height = vertHeight - skirtHeight; const cartographic = Cartographic.fromRadians( longitude, latitude, height, scratchCartographicSkirt, ); const positionEcef = Cartographic.toCartesian( cartographic, ellipsoid, scratchPosEcefSkirt, ); let normalOct; if (hasVertexNormals) { normalOct = encoding.getOctEncodedNormal( vertexBuffer, vertexIndex, scratchNormalOctSkirt, ); } let webMercatorT; if (hasWebMercatorT) { webMercatorT = encoding.decodeWebMercatorT(vertexBuffer, vertexIndex); } let geodeticSurfaceNormal; if (hasGeodeticSurfaceNormals) { geodeticSurfaceNormal = ellipsoid.geodeticSurfaceNormal( positionEcef, scratchGeodeticSurfaceNormalSkirt, ); } vertexBufferOffset = encoding.encode( vertexBuffer, vertexBufferOffset, positionEcef, uv, height, normalOct, webMercatorT, geodeticSurfaceNormal, ); const positionENU = Matrix4.multiplyByPoint( ecefToEnu, positionEcef, scratchPosEnuSkirt, ); Cartesian3.minimumByComponent( positionENU, minimumPositionENUWithSkirts, minimumPositionENUWithSkirts, ); Cartesian3.maximumByComponent( positionENU, maximumPositionENUWithSkirts, maximumPositionENUWithSkirts, ); } const aabbEnuWithSkirts = AxisAlignedBoundingBox.fromCorners( minimumPositionENUWithSkirts, maximumPositionENUWithSkirts, scratchAABBEnuSkirt, ); // Check if the final terrain encoding has a different quantization. If so, // the vertices need to be re-encoded with the new quantization. Otherwise, // use the vertex buffer as-is. const encodingWithSkirts = new TerrainEncoding( encoding.center, aabbEnuWithSkirts, minimumHeightWithSkirts, maximumHeight, enuToEcef, encoding.hasVertexNormals, encoding.hasWebMercatorT, hasGeodeticSurfaceNormals, exaggeration, exaggerationRelativeHeight, ); if (encoding.quantization !== encodingWithSkirts.quantization) { const finalEncoding = encodingWithSkirts; const finalVertexStride = finalEncoding.stride; const finalVertexBuffer = new Float32Array( vertexCountWithSkirts * finalVertexStride, ); let finalVertexBufferOffset = 0; for (let i = 0; i < vertexCountWithSkirts; i++) { finalVertexBufferOffset = finalEncoding.encode( finalVertexBuffer, finalVertexBufferOffset, encoding.decodePosition(vertexBuffer, i, scratchPosEcefSkirt), encoding.decodeTextureCoordinates(vertexBuffer, i, scratchUVSkirt), encoding.decodeHeight(vertexBuffer, i), encoding.hasVertexNormals ? encoding.getOctEncodedNormal(vertexBuffer, i, scratchNormalOctSkirt) : undefined, encoding.hasWebMercatorT ? encoding.decodeWebMercatorT(vertexBuffer, i) : undefined, encoding.hasGeodeticSurfaceNormals ? encoding.decodeGeodeticSurfaceNormal( vertexBuffer, i, scratchGeodeticSurfaceNormalSkirt, ) : undefined, ); } mesh.vertices = finalVertexBuffer; mesh.stride = finalVertexStride; mesh.encoding = finalEncoding; } return mesh; } const EDGE_ID_LEFT = 0; const EDGE_ID_TOP = 1; const EDGE_ID_RIGHT = 2; const EDGE_ID_BOTTOM = 3; const EDGE_COUNT = 4; const scratchIntersection = new Cartesian3(); const scratchInBarys = [ new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), ]; const scratchInPoints = [ new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2(), ]; const scratchOutBarys = [ new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), ]; const scratchOutPoints = [ new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2(), ]; /** * Check if a given point is inside the limits of a box. * @private * @param {Cartesian2} boxMinimum The lower left corner of the box. * @param {Cartesian2} boxMaximum The upper right corner of the box. * @param {number} edgeId The ID of the edge to test against. * @param {Cartesian2} p The point to test. * @returns {number} Positive if inside, negative if outside, zero if on the edge. */ function inside(boxMinimum, boxMaximum, edgeId, p) { switch (edgeId) { case EDGE_ID_LEFT: return CesiumMath.sign(p.x - boxMinimum.x); case EDGE_ID_RIGHT: return CesiumMath.sign(boxMaximum.x - p.x); case EDGE_ID_BOTTOM: return CesiumMath.sign(p.y - boxMinimum.y); default: // EDGE_ID_TOP return CesiumMath.sign(boxMaximum.y - p.y); } } /** * Compute the intersection of a line segment against one edge of a box. * @private * @param {Cartesian2} boxMinimum The lower left corner of the box. * @param {Cartesian2} boxMaximum The upper right corner of the box. * @param {number} edgeId The ID of the edge to intersect against. * @param {Cartesian2} a The beginning of the line segment. * @param {Cartesian2} b The end of the line segment. * @param {Cartesian3} result The object into which to copy the result. * @returns {Cartesian3} The intersection point in 2D coordinates and the interpolation factor t as the third component. */ function intersect(boxMinimum, boxMaximum, edgeId, a, b, result) { let t, intersectX, intersectY; switch (edgeId) { case EDGE_ID_LEFT: t = (boxMinimum.x - a.x) / (b.x - a.x); intersectX = boxMinimum.x; intersectY = a.y + (b.y - a.y) * t; break; case EDGE_ID_RIGHT: t = (boxMaximum.x - a.x) / (b.x - a.x); intersectX = boxMaximum.x; intersectY = a.y + (b.y - a.y) * t; break; case EDGE_ID_BOTTOM: t = (boxMinimum.y - a.y) / (b.y - a.y); intersectX = a.x + (b.x - a.x) * t; intersectY = boxMinimum.y; break; default: // EDGE_ID_TOP t = (boxMaximum.y - a.y) / (b.y - a.y); intersectX = a.x + (b.x - a.x) * t; intersectY = boxMaximum.y; break; } return Cartesian3.fromElements(intersectX, intersectY, t, result); } /** * Coordinates of a quadrilateral resulting from clipping a triangle against a box. * @private * @typedef PolygonResult * * @property {number} length * @property {Cartesian2[]} coordinates A pre-allocated array of six 2D coordinates. * @property {Cartesian3[]} barycentricCoordinates A pre-allocated array of six barycentric coordinates. */ /** * A scratch polygon result for use in clipping. * @private * @type {PolygonResult} */ const scratchPolygon = { length: 0, coordinates: [ new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2(), ], barycentricCoordinates: [ new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), ], }; /** * Clips a 2D triangle against axis-aligned edges of a box using the Sutherland-Hodgman * clipping algorithm. The resulting polygon will have between 0 and 6 vertices. * * @private * @param {number} edgeStart The first edge to clip against. * @param {number} edgeCount The number of edges to clip against, starting from edgeStart. * @param {Cartesian2} boxMinimum The bottom-left corner of the axis-aligned box. * @param {Cartesian2} boxMaximum The top-right corner of the axis-aligned box. * @param {Cartesian2} p0 The coordinates of the first vertex in the triangle, in counter-clockwise order. * @param {Cartesian2} p1 The coordinates of the second vertex in the triangle, in counter-clockwise order. * @param {Cartesian2} p2 The coordinates of the third vertex in the triangle, in counter-clockwise order. * @param {PolygonResult} result The object into which to copy the result. * @returns {PolygonResult} The polygon that results after the clip, specified as a list of coordinates in counter-clockwise order. */ function clipTriangleAgainstBoxEdgeRang