UNPKG

@cesium/engine

Version:

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

1,060 lines (967 loc) 30.3 kB
// @ts-check import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import ComponentDatatype from "../Core/ComponentDatatype.js"; import PolygonPipeline from "../Core/PolygonPipeline.js"; import PrimitiveType from "../Core/PrimitiveType.js"; import Rectangle from "../Core/Rectangle.js"; import WebGLConstants from "../Core/WebGLConstants.js"; import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js"; import defined from "../Core/defined.js"; import oneTimeWarning from "../Core/oneTimeWarning.js"; import MetadataType from "./MetadataType.js"; /** @import { TypedArray } from "../Core/globalTypes.js"; */ /** @ignore */ const DEFAULT_HEIGHT = 0; const scratchWorld = new Cartesian3(); const scratchLocal = new Cartesian3(); const tilingScheme = new WebMercatorTilingScheme(); /** * @typedef {object} VectorTilePoint * @property {number} x Tile-local x (0–extent) * @property {number} y Tile-local y (0–extent) * @ignore */ /** * @typedef {object} VectorTileFeature * @property {"Point"|"LineString"|"Polygon"|"Unknown"} type * @property {Array<VectorTilePoint>|Array<Array<VectorTilePoint>>} geometry * @property {object} [properties] * @ignore */ /** * @typedef {object} VectorTileLayer * @property {string} [name] * @property {number} extent * @property {VectorTileFeature[]} features * @ignore */ /** * @typedef {object} DecodedVectorTile * @property {VectorTileLayer[]} layers * @ignore */ /** * @typedef {object} PolygonRingGroup * @property {Array.<VectorTilePoint>} outerRing * @property {Array.<Array.<VectorTilePoint>>} holes * @ignore */ /** * @typedef {object} BuildVectorGltfOptions * @property {string} [featureIdProperty] MVT property name to use as feature ID. * @ignore */ /** * Build a vector glTF payload from decoded tile-local vector geometry. * Each MVT layer produces a separate glTF node (named after the layer), * enabling layer-specific styling in the future. * * @param {DecodedVectorTile} decoded * @param {{tileX:number, tileY:number, tileZ:number}} tileCoordinates * @param {BuildVectorGltfOptions} [options] * @returns {Uint8Array|undefined} * * @ignore */ function buildVectorGltfFromMVT(decoded, tileCoordinates, options) { const tileX = tileCoordinates.tileX; const tileY = tileCoordinates.tileY; const tileZ = tileCoordinates.tileZ; const featureIdProperty = options?.featureIdProperty; const tileRect = tilingScheme.tileXYToRectangle(tileX, tileY, tileZ); const tileCenter = Rectangle.center(tileRect); const origin = Cartesian3.fromRadians( tileCenter.longitude, tileCenter.latitude, 0, ); // Maximum value of a Uint32; used as sentinel for null feature IDs and primitive restart indices. const MAX_INT_U32 = 0xffffffff; const nullFeatureId = MAX_INT_U32; const primitiveRestartIndex = MAX_INT_U32; // Maps a property value (or auto-increment key) to a compact integer feature ID. const featureIdLookup = new Map(); // Maps featureId -> properties object (first-seen wins for ID collisions). /** @type {Map<number, Object.<string, *>>} */ const featureProperties = new Map(); /** @type {object[]} */ const bufferViews = []; /** @type {object[]} */ const accessors = []; /** @type {Uint8Array[]} */ const chunks = []; let byteLength = 0; function addPadding() { const padding = (4 - (byteLength % 4)) % 4; if (padding > 0) { chunks.push(new Uint8Array(padding)); byteLength += padding; } } /** * @param {TypedArray} typedArray * @param {number} target * @returns {number} */ function addBufferView(typedArray, target) { addPadding(); const byteOffset = byteLength; chunks.push( new Uint8Array( typedArray.buffer, typedArray.byteOffset, typedArray.byteLength, ), ); byteLength += typedArray.byteLength; const bufferViewIndex = bufferViews.length; bufferViews.push({ buffer: 0, byteOffset: byteOffset, byteLength: typedArray.byteLength, target: target, }); return bufferViewIndex; } /** * @param {TypedArray} typedArray * @param {object} options * @param {string} options.type * @param {number} options.componentType * @param {number} options.target * @param {number[]} [options.min] * @param {number[]} [options.max] * @returns {number} */ function addAccessor(typedArray, options) { const bufferView = addBufferView(typedArray, options.target); const componentCount = /** @type {*} */ (MetadataType).getComponentCount( options.type, ); const accessor = /** @type {*} */ ({ bufferView: bufferView, byteOffset: 0, componentType: options.componentType, count: typedArray.length / componentCount, type: options.type, }); if (defined(options.min)) { accessor.min = options.min; } if (defined(options.max)) { accessor.max = options.max; } const accessorIndex = accessors.length; accessors.push(accessor); return accessorIndex; } /** * @param {Float32Array} positions * @returns {{min:number[],max:number[]}} */ function computeMinMax(positions) { let minX = Number.POSITIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let minZ = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; let maxZ = Number.NEGATIVE_INFINITY; for (let i = 0; i < positions.length; i += 3) { const x = positions[i]; const y = positions[i + 1]; const z = positions[i + 2]; if (x < minX) { minX = x; } if (y < minY) { minY = y; } if (z < minZ) { minZ = z; } if (x > maxX) { maxX = x; } if (y > maxY) { maxY = y; } if (z > maxZ) { maxZ = z; } } return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ], }; } /** * @param {*} attributes * @param {*} extensions * @param {number[]} featureIdValues */ function addFeatureIdsToPrimitive(attributes, extensions, featureIdValues) { if (featureIdValues.length === 0) { return; } const featureIds = new Uint32Array(featureIdValues); const featureAccessor = addAccessor(featureIds, { type: "SCALAR", componentType: ComponentDatatype.UNSIGNED_INT, target: WebGLConstants.ARRAY_BUFFER, }); attributes._FEATURE_ID_0 = featureAccessor; /** @type {*} */ const featureIdDef = { featureCount: featureIdLookup.size, nullFeatureId: nullFeatureId, attribute: 0, }; if (featureProperties.size > 0) { featureIdDef.propertyTable = 0; } extensions.EXT_mesh_features = { featureIds: [featureIdDef], }; } /** * Adds a raw buffer view for metadata (no accessor target). * @param {Uint8Array} bytes * @param {number} [alignment=4] Required byte alignment (e.g., 8 for Float64). * @returns {number} bufferView index */ function addMetadataBufferView(bytes, alignment) { alignment = alignment ?? 4; // Align to the required boundary. const pad = (alignment - (byteLength % alignment)) % alignment; if (pad > 0) { chunks.push(new Uint8Array(pad)); byteLength += pad; } const byteOffset = byteLength; chunks.push(bytes); byteLength += bytes.byteLength; const bufferViewIndex = bufferViews.length; bufferViews.push({ buffer: 0, byteOffset: byteOffset, byteLength: bytes.byteLength, }); return bufferViewIndex; } /** * Builds the EXT_structural_metadata extension object with schema and * property table from the collected feature properties. * @returns {object|undefined} */ function buildStructuralMetadata() { if (featureProperties.size === 0) { return undefined; } // 1. Determine union of all property names and infer types. // propertyName -> "STRING"|"SCALAR"|"BOOLEAN" /** @type {Map<string, string>} */ const propertyTypes = new Map(); for (const props of featureProperties.values()) { for (const [key, value] of Object.entries(props)) { if (!defined(value)) { continue; } const jsType = typeof value; let metaType; if (jsType === "string") { metaType = "STRING"; } else if (jsType === "number") { metaType = "SCALAR"; } else if (jsType === "boolean") { metaType = "BOOLEAN"; } else { // Objects/arrays: coerce to string metaType = "STRING"; } const existing = propertyTypes.get(key); if (!defined(existing)) { propertyTypes.set(key, metaType); } else if (existing !== metaType) { // Mixed types: coerce to STRING propertyTypes.set(key, "STRING"); } } } if (propertyTypes.size === 0) { return undefined; } // 2. Build schema class properties. /** @type {Object.<string, *>} */ const classProperties = {}; for (const [name, type] of propertyTypes) { if (type === "SCALAR") { classProperties[name] = { type: "SCALAR", componentType: "FLOAT64", }; } else if (type === "BOOLEAN") { classProperties[name] = { type: "BOOLEAN", }; } else { classProperties[name] = { type: "STRING", }; } } // 3. Encode property values into binary buffers. /** @type {Object.<string, *>} */ const tableProperties = {}; const count = featureIdLookup.size; for (const [name, type] of propertyTypes) { if (type === "SCALAR") { const values = new Float64Array(count); for (let i = 0; i < count; i++) { const props = featureProperties.get(i); const raw = props?.[name]; values[i] = typeof raw === "number" && Number.isFinite(raw) ? raw : NaN; } const bvIndex = addMetadataBufferView( new Uint8Array(values.buffer, values.byteOffset, values.byteLength), 8, ); tableProperties[name] = { values: bvIndex }; } else if (type === "BOOLEAN") { const byteCount = Math.ceil(count / 8); const values = new Uint8Array(byteCount); for (let i = 0; i < count; i++) { const props = featureProperties.get(i); const raw = props?.[name]; if (raw) { values[i >> 3] |= 1 << (i & 7); } } const bvIndex = addMetadataBufferView(values); tableProperties[name] = { values: bvIndex }; } else { // STRING encoding: values (UTF-8 bytes) + stringOffsets (Uint32) const encoder = new TextEncoder(); /** @type {Uint8Array[]} */ const stringParts = []; const offsets = new Uint32Array(count + 1); let totalBytes = 0; for (let i = 0; i < count; i++) { offsets[i] = totalBytes; const props = featureProperties.get(i); const raw = props?.[name]; let str; if (raw === null || raw === undefined) { str = ""; } else if (typeof raw === "string") { str = raw; } else { str = String(raw); } const encoded = encoder.encode(str); stringParts.push(encoded); totalBytes += encoded.byteLength; } offsets[count] = totalBytes; // Concatenate string bytes const valuesBuffer = new Uint8Array(totalBytes); let writeOffset = 0; for (const part of stringParts) { valuesBuffer.set(part, writeOffset); writeOffset += part.byteLength; } const valuesBv = addMetadataBufferView(valuesBuffer); const offsetsBv = addMetadataBufferView( new Uint8Array( offsets.buffer, offsets.byteOffset, offsets.byteLength, ), ); tableProperties[name] = { values: valuesBv, stringOffsets: offsetsBv, stringOffsetType: "UINT32", }; } } return { schema: { classes: { mvt_feature: { properties: classProperties, }, }, }, propertyTables: [ { class: "mvt_feature", count: count, properties: tableProperties, }, ], }; } /** @type {object[]} */ const meshes = []; /** @type {object[]} */ const nodes = []; const translation = [origin.x, origin.y, origin.z]; for (const layer of decoded.layers) { const extent = layer.extent; /** @type {number[]} */ const pointPositions = []; /** @type {number[]} */ const pointFeatureIds = []; /** @type {number[]} */ const linePositions = []; /** @type {number[]} */ const lineFeatureIds = []; /** @type {number[]} */ const lineIndices = []; let lineCount = 0; /** @type {number[]} */ const polygonPositions = []; /** @type {number[]} */ const polygonFeatureIds = []; /** @type {number[]} */ const polygonIndices = []; /** @type {number[]} */ const polygonAttributeOffsets = []; /** @type {number[]} */ const polygonIndicesOffsets = []; /** @type {number[]} */ const polygonHoleCounts = []; /** @type {number[]} */ const polygonHoleOffsets = []; let polygonCount = 0; for (const feature of layer.features) { const currentFeatureId = defined(featureIdProperty) ? (mapFeatureIdFromProperty( feature, featureIdProperty, featureIdLookup, ) ?? nullFeatureId) : getOrAssignAutoFeatureId(feature, featureIdLookup); // Collect properties for the property table (first-seen wins). if ( currentFeatureId !== nullFeatureId && !featureProperties.has(currentFeatureId) ) { /** @type {Object.<string, *>} */ const props = Object.assign({}, feature.properties); props["_layer"] = layer.name ?? ""; featureProperties.set(currentFeatureId, props); } if (feature.type === "Point") { const points = /** @type {VectorTilePoint[]} */ (feature.geometry); for (const point of points) { appendTilePointAsLocalPosition( point, tileX, tileY, tileZ, extent, DEFAULT_HEIGHT, origin, pointPositions, ); pointFeatureIds.push(currentFeatureId); } continue; } if (feature.type === "LineString") { const lines = /** @type {VectorTilePoint[][]} */ (feature.geometry); for (const line of lines) { const lineStart = linePositions.length / 3; for (const point of line) { appendTilePointAsLocalPosition( point, tileX, tileY, tileZ, extent, DEFAULT_HEIGHT, origin, linePositions, ); lineFeatureIds.push(currentFeatureId); } for (let i = 0; i < line.length; i++) { lineIndices.push(lineStart + i); } lineIndices.push(primitiveRestartIndex); lineCount++; } continue; } if (feature.type === "Polygon") { const rawRings = /** @type {VectorTilePoint[][]} */ (feature.geometry); const groups = groupPolygonRings(rawRings); for (const group of groups) { const rings = [group.outerRing, ...group.holes]; /** @type {Cartesian2[]} */ const positions2D = []; /** @type {number[]} */ const polygonPositionComponents = []; /** @type {number[]} */ const holeOffsets = []; let vertexOffset = 0; for (let ringIndex = 0; ringIndex < rings.length; ringIndex++) { const ring = rings[ringIndex]; if (ringIndex > 0) { holeOffsets.push(vertexOffset); } for (const point of ring) { positions2D.push(new Cartesian2(point.x, point.y)); appendTilePointAsLocalPosition( point, tileX, tileY, tileZ, extent, DEFAULT_HEIGHT, origin, polygonPositionComponents, ); vertexOffset++; } } if (positions2D.length < 3) { continue; } const triangles = PolygonPipeline.triangulate( positions2D, holeOffsets.length > 0 ? holeOffsets : undefined, ); if (!defined(triangles) || triangles.length === 0) { oneTimeWarning( "buildVectorGltfFromMVT-triangulation-failed", "Polygon triangulation failed; skipping polygon.", ); continue; } const globalVertexStart = polygonPositions.length / 3; const globalIndexStart = polygonIndices.length; polygonAttributeOffsets.push(globalVertexStart); polygonIndicesOffsets.push(globalIndexStart); polygonHoleCounts.push(holeOffsets.length); for (let i = 0; i < holeOffsets.length; i++) { polygonHoleOffsets.push(globalVertexStart + holeOffsets[i]); } for (let i = 0; i < polygonPositionComponents.length; i++) { polygonPositions.push(polygonPositionComponents[i]); } for (let i = 0; i < polygonPositionComponents.length / 3; i++) { polygonFeatureIds.push(currentFeatureId); } for (let i = 0; i < triangles.length; i++) { polygonIndices.push(triangles[i] + globalVertexStart); } polygonCount++; } } } // Skip layers with no geometry. if ( pointPositions.length === 0 && linePositions.length === 0 && polygonPositions.length === 0 ) { continue; } if ( lineIndices.length > 0 && lineIndices[lineIndices.length - 1] === primitiveRestartIndex ) { lineIndices.pop(); } /** @type {object[]} */ const primitives = []; if (pointPositions.length > 0) { const positions = new Float32Array(pointPositions); const minMax = computeMinMax(positions); const positionAccessor = addAccessor(positions, { type: "VEC3", componentType: ComponentDatatype.FLOAT, target: WebGLConstants.ARRAY_BUFFER, min: minMax.min, max: minMax.max, }); const attributes = /** @type {*} */ ({ POSITION: positionAccessor, }); const extensions = /** @type {*} */ ({ CESIUM_mesh_vector: { vector: true, count: positions.length / 3, }, }); addFeatureIdsToPrimitive(attributes, extensions, pointFeatureIds); primitives.push({ mode: PrimitiveType.POINTS, attributes: attributes, extensions: extensions, }); } if (linePositions.length > 0 && lineIndices.length > 1) { const positions = new Float32Array(linePositions); const indices = new Uint32Array(lineIndices); const minMax = computeMinMax(positions); const positionAccessor = addAccessor(positions, { type: "VEC3", componentType: ComponentDatatype.FLOAT, target: WebGLConstants.ARRAY_BUFFER, min: minMax.min, max: minMax.max, }); const indicesAccessor = addAccessor(indices, { type: "SCALAR", componentType: ComponentDatatype.UNSIGNED_INT, target: WebGLConstants.ELEMENT_ARRAY_BUFFER, }); const attributes = /** @type {*} */ ({ POSITION: positionAccessor, }); const extensions = /** @type {*} */ ({ CESIUM_mesh_vector: { vector: true, count: lineCount, }, }); addFeatureIdsToPrimitive(attributes, extensions, lineFeatureIds); primitives.push({ mode: PrimitiveType.LINE_STRIP, indices: indicesAccessor, attributes: attributes, extensions: extensions, }); } if (polygonPositions.length > 0 && polygonIndices.length >= 3) { const positions = new Float32Array(polygonPositions); const indices = new Uint32Array(polygonIndices); const attributeOffsets = new Uint32Array(polygonAttributeOffsets); const indicesOffsets = new Uint32Array(polygonIndicesOffsets); const hasPolygonHoles = polygonHoleOffsets.length > 0; const holeCounts = hasPolygonHoles ? new Uint32Array(polygonHoleCounts) : undefined; const holeOffsets = hasPolygonHoles ? new Uint32Array(polygonHoleOffsets) : undefined; const minMax = computeMinMax(positions); const positionAccessor = addAccessor(positions, { type: "VEC3", componentType: ComponentDatatype.FLOAT, target: WebGLConstants.ARRAY_BUFFER, min: minMax.min, max: minMax.max, }); const indicesAccessor = addAccessor(indices, { type: "SCALAR", componentType: ComponentDatatype.UNSIGNED_INT, target: WebGLConstants.ELEMENT_ARRAY_BUFFER, }); const attributeOffsetsAccessor = addAccessor(attributeOffsets, { type: "SCALAR", componentType: ComponentDatatype.UNSIGNED_INT, target: WebGLConstants.ARRAY_BUFFER, }); const indicesOffsetsAccessor = addAccessor(indicesOffsets, { type: "SCALAR", componentType: ComponentDatatype.UNSIGNED_INT, target: WebGLConstants.ARRAY_BUFFER, }); const holeCountsAccessor = defined(holeCounts) ? addAccessor(holeCounts, { type: "SCALAR", componentType: ComponentDatatype.UNSIGNED_INT, target: WebGLConstants.ARRAY_BUFFER, }) : undefined; const holeOffsetsAccessor = defined(holeOffsets) ? addAccessor(holeOffsets, { type: "SCALAR", componentType: ComponentDatatype.UNSIGNED_INT, target: WebGLConstants.ARRAY_BUFFER, }) : undefined; const attributes = /** @type {*} */ ({ POSITION: positionAccessor, }); const extensions = /** @type {*} */ ({ CESIUM_mesh_vector: { vector: true, count: polygonCount, polygonAttributeOffsets: attributeOffsetsAccessor, polygonIndicesOffsets: indicesOffsetsAccessor, }, }); if (hasPolygonHoles) { extensions.CESIUM_mesh_vector.polygonHoleCounts = holeCountsAccessor; extensions.CESIUM_mesh_vector.polygonHoleOffsets = holeOffsetsAccessor; } addFeatureIdsToPrimitive(attributes, extensions, polygonFeatureIds); primitives.push({ mode: PrimitiveType.TRIANGLES, indices: indicesAccessor, attributes: attributes, extensions: extensions, }); } if (primitives.length === 0) { continue; } const meshIndex = meshes.length; meshes.push({ primitives: primitives }); nodes.push({ name: layer.name, mesh: meshIndex, translation: translation, }); } if (nodes.length === 0) { return undefined; } // Build property table AFTER primitives (adds metadata buffer views). const structuralMetadata = buildStructuralMetadata(); const binaryChunk = concatChunks(chunks, byteLength); const extensionsUsed = ["CESIUM_mesh_vector"]; if (featureIdLookup.size > 0) { extensionsUsed.push("EXT_mesh_features"); } if (defined(structuralMetadata)) { extensionsUsed.push("EXT_structural_metadata"); } const nodeIndices = nodes.map((_, i) => i); const gltfJson = { asset: { version: "2.0", }, extensionsUsed: extensionsUsed, scene: 0, scenes: [ { nodes: nodeIndices, }, ], nodes: nodes, meshes: meshes, accessors: accessors, bufferViews: bufferViews, buffers: [ { byteLength: binaryChunk.byteLength, }, ], extensions: /** @type {Object|undefined} */ (undefined), }; if (defined(structuralMetadata)) { gltfJson.extensions = { EXT_structural_metadata: structuralMetadata, }; } return buildGlb(gltfJson, binaryChunk); } /** * Assigns a stable auto-incrementing integer ID to each unique feature. * * @param {VectorTileFeature} feature * @param {Map<VectorTileFeature, number>} featureIdLookup * @returns {number} * @ignore */ function getOrAssignAutoFeatureId(feature, featureIdLookup) { let id = featureIdLookup.get(feature); if (!defined(id)) { id = featureIdLookup.size; featureIdLookup.set(feature, id); } return id; } /** * @param {VectorTileFeature} feature * @param {string} featureIdProperty * @param {Map<string, number>} featureIdLookup * @returns {number|undefined} * @ignore */ function mapFeatureIdFromProperty(feature, featureIdProperty, featureIdLookup) { const properties = feature.properties; if (!defined(properties)) { return undefined; } const propertyValue = /** @type {*} */ (properties)[featureIdProperty]; if (!defined(propertyValue)) { return undefined; } if ( typeof propertyValue !== "string" && typeof propertyValue !== "number" && typeof propertyValue !== "boolean" ) { return undefined; } if (typeof propertyValue === "number" && !Number.isFinite(propertyValue)) { return undefined; } const mapKey = `${typeof propertyValue}:${propertyValue}`; let mappedFeatureId = featureIdLookup.get(mapKey); if (defined(mappedFeatureId)) { return mappedFeatureId; } mappedFeatureId = featureIdLookup.size; featureIdLookup.set(mapKey, mappedFeatureId); return mappedFeatureId; } /** * @param {VectorTilePoint[][]} rawRings * @returns {Array.<PolygonRingGroup>} * @ignore */ function groupPolygonRings(rawRings) { /** @type {Array.<PolygonRingGroup>} */ const groups = []; for (const rawRing of rawRings) { const ring = stripClosingVertex(rawRing); if (ring.length < 3) { continue; } const area = ringSignedArea(ring); if (area <= 0) { groups.push({ outerRing: ring, holes: [] }); } else if (groups.length > 0) { groups[groups.length - 1].holes.push(ring); } } return groups; } /** * @param {VectorTilePoint[]} ring * @returns {number} * @ignore */ function ringSignedArea(ring) { let area = 0; for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { area += (ring[j].x + ring[i].x) * (ring[j].y - ring[i].y); } return area / 2; } /** * @param {VectorTilePoint[]} ring * @returns {VectorTilePoint[]} * @ignore */ function stripClosingVertex(ring) { if ( ring.length > 1 && ring[0].x === ring[ring.length - 1].x && ring[0].y === ring[ring.length - 1].y ) { return ring.slice(0, ring.length - 1); } return ring; } /** * @param {VectorTilePoint} point * @param {number} tileX * @param {number} tileY * @param {number} tileZ * @param {number} extent * @param {number} height * @param {Cartesian3} origin * @param {number[]} out * @ignore */ function appendTilePointAsLocalPosition( point, tileX, tileY, tileZ, extent, height, origin, out, ) { const n = 1 << tileZ; const u = (tileX + point.x / extent) / n; const v = (tileY + point.y / extent) / n; const lon = u * 2 * Math.PI - Math.PI; const lat = Math.atan(Math.sinh(Math.PI * (1 - 2 * v))); Cartesian3.fromRadians(lon, lat, height, undefined, scratchWorld); Cartesian3.subtract(scratchWorld, origin, scratchLocal); out.push(scratchLocal.x, scratchLocal.y, scratchLocal.z); } /** * Packs a glTF JSON object and binary buffer into a GLB (Binary glTF) Uint8Array. * GLB spec: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#binary-gltf-layout * * @param {object} gltfJson * @param {Uint8Array} binaryChunk * @returns {Uint8Array} * @ignore */ function buildGlb(gltfJson, binaryChunk) { const GLB_MAGIC = 0x46546c67; // "glTF" const GLB_VERSION = 2; const CHUNK_TYPE_JSON = 0x4e4f534a; // "JSON" const CHUNK_TYPE_BIN = 0x004e4942; // "BIN\0" const jsonBytes = new TextEncoder().encode(JSON.stringify(gltfJson)); // Pad JSON to 4-byte boundary with spaces (0x20) const jsonPaddedLength = Math.ceil(jsonBytes.length / 4) * 4; const jsonChunk = new Uint8Array(jsonPaddedLength); jsonChunk.fill(0x20); jsonChunk.set(jsonBytes); // Pad binary to 4-byte boundary with zeros const binPaddedLength = Math.ceil(binaryChunk.byteLength / 4) * 4; const binChunk = new Uint8Array(binPaddedLength); binChunk.set(binaryChunk); const totalLength = 12 + // header 8 + jsonPaddedLength + // JSON chunk header + data 8 + binPaddedLength; // BIN chunk header + data const glb = new Uint8Array(totalLength); const view = new DataView(glb.buffer); let offset = 0; // Header view.setUint32(offset, GLB_MAGIC, true); offset += 4; view.setUint32(offset, GLB_VERSION, true); offset += 4; view.setUint32(offset, totalLength, true); offset += 4; // JSON chunk view.setUint32(offset, jsonPaddedLength, true); offset += 4; view.setUint32(offset, CHUNK_TYPE_JSON, true); offset += 4; glb.set(jsonChunk, offset); offset += jsonPaddedLength; // BIN chunk view.setUint32(offset, binPaddedLength, true); offset += 4; view.setUint32(offset, CHUNK_TYPE_BIN, true); offset += 4; glb.set(binChunk, offset); return glb; } /** * @param {Uint8Array[]} chunks * @param {number} totalByteLength * @returns {Uint8Array} * @ignore */ function concatChunks(chunks, totalByteLength) { const out = new Uint8Array(totalByteLength); let offset = 0; for (const chunk of chunks) { out.set(chunk, offset); offset += chunk.byteLength; } return out; } export default buildVectorGltfFromMVT;