UNPKG

@cesium/engine

Version:

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

1,653 lines (1,508 loc) 48.5 kB
import createTaskProcessorWorker from "./createTaskProcessorWorker.js"; import defined from "../Core/defined.js"; import WebMercatorProjection from "../Core/WebMercatorProjection.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import Cartographic from "../Core/Cartographic.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Color from "../Core/Color.js"; import Matrix3 from "../Core/Matrix3.js"; import CesiumMath from "../Core/Math.js"; import dracoModule from "draco3d/draco_decoder_nodejs.js"; import srgbToLinear from "../Core/srgbToLinear.js"; let draco; function bilinearInterpolate(tx, ty, h00, h10, h01, h11) { const a = h00 * (1 - tx) + h10 * tx; const b = h01 * (1 - tx) + h11 * tx; return a * (1 - ty) + b * ty; } function sampleMap(u, v, width, data) { const address = u + v * width; return data[address]; } function sampleGeoid(sampleX, sampleY, geoidData) { const extent = geoidData.nativeExtent; let x = ((sampleX - extent.west) / (extent.east - extent.west)) * (geoidData.width - 1); let y = ((sampleY - extent.south) / (extent.north - extent.south)) * (geoidData.height - 1); const xi = Math.floor(x); let yi = Math.floor(y); x -= xi; y -= yi; const xNext = xi < geoidData.width ? xi + 1 : xi; let yNext = yi < geoidData.height ? yi + 1 : yi; yi = geoidData.height - 1 - yi; yNext = geoidData.height - 1 - yNext; const h00 = sampleMap(xi, yi, geoidData.width, geoidData.buffer); const h10 = sampleMap(xNext, yi, geoidData.width, geoidData.buffer); const h01 = sampleMap(xi, yNext, geoidData.width, geoidData.buffer); const h11 = sampleMap(xNext, yNext, geoidData.width, geoidData.buffer); let finalHeight = bilinearInterpolate(x, y, h00, h10, h01, h11); finalHeight = finalHeight * geoidData.scale + geoidData.offset; return finalHeight; } function sampleGeoidFromList(lon, lat, geoidDataList) { for (let i = 0; i < geoidDataList.length; i++) { const localExtent = geoidDataList[i].nativeExtent; let localPt = new Cartesian3(); if (geoidDataList[i].projectionType === "WebMercator") { const radii = geoidDataList[i].projection._ellipsoid._radii; const webMercatorProj = new WebMercatorProjection( new Ellipsoid(radii.x, radii.y, radii.z), ); localPt = webMercatorProj.project(new Cartographic(lon, lat, 0)); } else { localPt.x = lon; localPt.y = lat; } if ( localPt.x > localExtent.west && localPt.x < localExtent.east && localPt.y > localExtent.south && localPt.y < localExtent.north ) { return sampleGeoid(localPt.x, localPt.y, geoidDataList[i]); } } return 0; } function orthometricToEllipsoidal( vertexCount, position, scale_x, scale_y, center, geoidDataList, fast, ) { if (fast) { // Geometry is already relative to the tile origin which has already been shifted to account for geoid height // Nothing to do here return; } // For more precision, sample the geoid height at each vertex and shift by the difference between that value and the height at the center of the tile const centerHeight = sampleGeoidFromList( center.longitude, center.latitude, geoidDataList, ); for (let i = 0; i < vertexCount; ++i) { const height = sampleGeoidFromList( center.longitude + CesiumMath.toRadians(scale_x * position[i * 3]), center.latitude + CesiumMath.toRadians(scale_y * position[i * 3 + 1]), geoidDataList, ); position[i * 3 + 2] += height - centerHeight; } } function transformToLocal( vertexCount, positions, normals, cartographicCenter, cartesianCenter, parentRotation, ellipsoidRadiiSquare, scale_x, scale_y, ) { if (vertexCount === 0 || !defined(positions) || positions.length === 0) { return; } const ellipsoid = new Ellipsoid( Math.sqrt(ellipsoidRadiiSquare.x), Math.sqrt(ellipsoidRadiiSquare.y), Math.sqrt(ellipsoidRadiiSquare.z), ); for (let i = 0; i < vertexCount; ++i) { const indexOffset = i * 3; const indexOffset1 = indexOffset + 1; const indexOffset2 = indexOffset + 2; const cartographic = new Cartographic(); cartographic.longitude = cartographicCenter.longitude + CesiumMath.toRadians(scale_x * positions[indexOffset]); cartographic.latitude = cartographicCenter.latitude + CesiumMath.toRadians(scale_y * positions[indexOffset1]); cartographic.height = cartographicCenter.height + positions[indexOffset2]; const position = {}; ellipsoid.cartographicToCartesian(cartographic, position); position.x -= cartesianCenter.x; position.y -= cartesianCenter.y; position.z -= cartesianCenter.z; const rotatedPosition = {}; Matrix3.multiplyByVector(parentRotation, position, rotatedPosition); positions[indexOffset] = rotatedPosition.x; positions[indexOffset1] = rotatedPosition.y; positions[indexOffset2] = rotatedPosition.z; if (defined(normals)) { const normal = new Cartesian3( normals[indexOffset], normals[indexOffset1], normals[indexOffset2], ); const rotatedNormal = {}; Matrix3.multiplyByVector(parentRotation, normal, rotatedNormal); normals[indexOffset] = rotatedNormal.x; normals[indexOffset1] = rotatedNormal.y; normals[indexOffset2] = rotatedNormal.z; } } } function cropUVs(vertexCount, uv0s, uvRegions) { for (let vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { const minU = uvRegions[vertexIndex * 4] / 65535.0; const minV = uvRegions[vertexIndex * 4 + 1] / 65535.0; const scaleU = (uvRegions[vertexIndex * 4 + 2] - uvRegions[vertexIndex * 4]) / 65535.0; const scaleV = (uvRegions[vertexIndex * 4 + 3] - uvRegions[vertexIndex * 4 + 1]) / 65535.0; uv0s[vertexIndex * 2] *= scaleU; uv0s[vertexIndex * 2] += minU; uv0s[vertexIndex * 2 + 1] *= scaleV; uv0s[vertexIndex * 2 + 1] += minV; } } function generateIndexArray( vertexCount, indices, colors, splitGeometryByColorTransparency, ) { // Allocate array const indexArray = new Uint32Array(vertexCount); const vertexIndexFn = defined(indices) ? (vertexIndex) => indices[vertexIndex] : (vertexIndex) => vertexIndex; let transparentVertexOffset = 0; if (splitGeometryByColorTransparency && defined(colors)) { // The blending alpha mode for opaque colors is not rendered properly. // If geometry contains both opaque and transparent colors we need to split vertices into two mesh primitives. // Each mesh primitive could use a separate material with the specific alpha mode depending on the vertex trancparency. const isVertexTransparentFn = (vertexIndex) => colors[vertexIndexFn(vertexIndex) * 4 + 3] < 255; // Copy opaque vertices first for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex += 3) { if ( !isVertexTransparentFn(vertexIndex) && !isVertexTransparentFn(vertexIndex + 1) && !isVertexTransparentFn(vertexIndex + 2) ) { indexArray[transparentVertexOffset++] = vertexIndexFn(vertexIndex); indexArray[transparentVertexOffset++] = vertexIndexFn(vertexIndex + 1); indexArray[transparentVertexOffset++] = vertexIndexFn(vertexIndex + 2); } } if (transparentVertexOffset > 0) { // Copy transparent vertices let offset = transparentVertexOffset; for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex += 3) { if ( isVertexTransparentFn(vertexIndex) || isVertexTransparentFn(vertexIndex + 1) || isVertexTransparentFn(vertexIndex + 2) ) { indexArray[offset++] = vertexIndexFn(vertexIndex); indexArray[offset++] = vertexIndexFn(vertexIndex + 1); indexArray[offset++] = vertexIndexFn(vertexIndex + 2); } } } else { // All vertices are tranparent for (let vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { indexArray[vertexIndex] = vertexIndexFn(vertexIndex); } } } else { // All vertices are considered opaque transparentVertexOffset = vertexCount; for (let vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { indexArray[vertexIndex] = vertexIndexFn(vertexIndex); } } return { indexArray: indexArray, transparentVertexOffset: transparentVertexOffset, }; } function getFeatureHash(symbologyData, outlinesHash, featureIndex) { const featureHash = outlinesHash[featureIndex]; if (defined(featureHash)) { return featureHash; } const newFeatureHash = (outlinesHash[featureIndex] = { positions: {}, indices: {}, edges: {}, }); const featureSymbology = symbologyData[featureIndex] ?? symbologyData.default; newFeatureHash.hasOutline = defined(featureSymbology?.edges); return newFeatureHash; } function addVertexToHash(indexHash, positionHash, vertexIndex, positions) { if (!defined(indexHash[vertexIndex])) { const startPositionIndex = vertexIndex * 3; let coordinateHash = positionHash; for (let index = 0; index < 3; index++) { const coordinate = positions[startPositionIndex + index]; if (!defined(coordinateHash[coordinate])) { coordinateHash[coordinate] = {}; } coordinateHash = coordinateHash[coordinate]; } if (!defined(coordinateHash.index)) { coordinateHash.index = vertexIndex; } indexHash[vertexIndex] = coordinateHash.index; } } function addEdgeToHash( edgeHash, vertexAIndex, vertexBIndex, vertexAIndexUnique, vertexBIndexUnique, normalIndex, ) { let startVertexIndex; let endVertexIndex; if (vertexAIndexUnique < vertexBIndexUnique) { startVertexIndex = vertexAIndexUnique; endVertexIndex = vertexBIndexUnique; } else { startVertexIndex = vertexBIndexUnique; endVertexIndex = vertexAIndexUnique; } let edgeStart = edgeHash[startVertexIndex]; if (!defined(edgeStart)) { edgeStart = edgeHash[startVertexIndex] = {}; } let edgeEnd = edgeStart[endVertexIndex]; if (!defined(edgeEnd)) { edgeEnd = edgeStart[endVertexIndex] = { normalsIndex: [], outlines: [], }; } edgeEnd.normalsIndex.push(normalIndex); if ( edgeEnd.outlines.length === 0 || vertexAIndex !== vertexAIndexUnique || vertexBIndex !== vertexBIndexUnique ) { edgeEnd.outlines.push(vertexAIndex, vertexBIndex); } } function generateOutlinesHash( symbologyData, featureIndexArray, indexArray, positions, ) { const outlinesHash = []; for (let i = 0; i < indexArray.length; i += 3) { const featureIndex = defined(featureIndexArray) ? featureIndexArray[indexArray[i]] : "default"; const featureHash = getFeatureHash( symbologyData, outlinesHash, featureIndex, ); if (!featureHash.hasOutline) { continue; } const indexHash = featureHash.indices; const positionHash = featureHash.positions; for (let vertex = 0; vertex < 3; vertex++) { const vertexIndex = indexArray[i + vertex]; addVertexToHash(indexHash, positionHash, vertexIndex, positions); } const edgeHash = featureHash.edges; for (let vertex = 0; vertex < 3; vertex++) { const vertexIndex = indexArray[i + vertex]; const nextVertexIndex = indexArray[i + ((vertex + 1) % 3)]; const uniqueVertexIndex = indexHash[vertexIndex]; const uniqueNextVertexIndex = indexHash[nextVertexIndex]; addEdgeToHash( edgeHash, vertexIndex, nextVertexIndex, uniqueVertexIndex, uniqueNextVertexIndex, i, ); } } return outlinesHash; } const calculateFaceNormalA = new Cartesian3(); const calculateFaceNormalB = new Cartesian3(); const calculateFaceNormalC = new Cartesian3(); function calculateFaceNormal(normals, vertexAIndex, indexArray, positions) { const positionAIndex = indexArray[vertexAIndex] * 3; const positionBIndex = indexArray[vertexAIndex + 1] * 3; const positionCIndex = indexArray[vertexAIndex + 2] * 3; Cartesian3.fromArray(positions, positionAIndex, calculateFaceNormalA); Cartesian3.fromArray(positions, positionBIndex, calculateFaceNormalB); Cartesian3.fromArray(positions, positionCIndex, calculateFaceNormalC); Cartesian3.subtract( calculateFaceNormalB, calculateFaceNormalA, calculateFaceNormalB, ); Cartesian3.subtract( calculateFaceNormalC, calculateFaceNormalA, calculateFaceNormalC, ); Cartesian3.cross( calculateFaceNormalB, calculateFaceNormalC, calculateFaceNormalA, ); const magnitude = Cartesian3.magnitude(calculateFaceNormalA); if (magnitude !== 0) { Cartesian3.divideByScalar( calculateFaceNormalA, magnitude, calculateFaceNormalA, ); } const normalAIndex = vertexAIndex * 3; const normalBIndex = (vertexAIndex + 1) * 3; const normalCIndex = (vertexAIndex + 2) * 3; Cartesian3.pack(calculateFaceNormalA, normals, normalAIndex); Cartesian3.pack(calculateFaceNormalA, normals, normalBIndex); Cartesian3.pack(calculateFaceNormalA, normals, normalCIndex); } const isEdgeSmoothA = new Cartesian3(); const isEdgeSmoothB = new Cartesian3(); function isEdgeSmooth(normals, normalAIndex, normalBIndex) { Cartesian3.fromArray(normals, normalAIndex, isEdgeSmoothA); Cartesian3.fromArray(normals, normalBIndex, isEdgeSmoothB); const cosine = Cartesian3.dot(isEdgeSmoothA, isEdgeSmoothB); const sine = Cartesian3.magnitude( Cartesian3.cross(isEdgeSmoothA, isEdgeSmoothB, isEdgeSmoothA), ); return Math.atan2(sine, cosine) < 0.25; } function addOutlinesForEdge( outlines, edgeData, indexArray, positions, normals, ) { if (edgeData.normalsIndex.length > 1) { const normalsByIndex = positions.length === normals.length; for (let indexA = 0; indexA < edgeData.normalsIndex.length; indexA++) { const vertexAIndex = edgeData.normalsIndex[indexA]; if (!defined(normals[vertexAIndex * 3])) { calculateFaceNormal(normals, vertexAIndex, indexArray, positions); } if (indexA === 0) { continue; } for (let indexB = 0; indexB < indexA; indexB++) { const vertexBIndex = edgeData.normalsIndex[indexB]; const normalAIndex = normalsByIndex ? indexArray[vertexAIndex] * 3 : vertexAIndex * 3; const normalBIndex = normalsByIndex ? indexArray[vertexBIndex] * 3 : vertexBIndex * 3; if (isEdgeSmooth(normals, normalAIndex, normalBIndex)) { return; } } } } outlines.push(...edgeData.outlines); } function addOutlinesForFeature( outlines, edgeHash, indexArray, positions, normals, ) { const edgeStartKeys = Object.keys(edgeHash); for (let startIndex = 0; startIndex < edgeStartKeys.length; startIndex++) { const edgeEnds = edgeHash[edgeStartKeys[startIndex]]; const edgeEndKeys = Object.keys(edgeEnds); for (let endIndex = 0; endIndex < edgeEndKeys.length; endIndex++) { const edgeData = edgeEnds[edgeEndKeys[endIndex]]; addOutlinesForEdge(outlines, edgeData, indexArray, positions, normals); } } } function generateOutlinesFromHash( outlinesHash, indexArray, positions, normals, ) { const outlines = []; const features = Object.keys(outlinesHash); for (let featureIndex = 0; featureIndex < features.length; featureIndex++) { const edgeHash = outlinesHash[features[featureIndex]].edges; addOutlinesForFeature(outlines, edgeHash, indexArray, positions, normals); } return outlines; } function generateOutlinesIndexArray( symbologyData, featureIndexArray, indexArray, positions, normals, ) { if (!defined(symbologyData) || Object.keys(symbologyData).length === 0) { return undefined; } const outlinesHash = generateOutlinesHash( symbologyData, featureIndexArray, indexArray, positions, ); if (!defined(normals) || indexArray.length * 3 !== normals.length) { // Need to calculate flat normals per faces normals = []; } const outlines = generateOutlinesFromHash( outlinesHash, indexArray, positions, normals, ); const outlinesIndexArray = outlines.length > 0 ? new Uint32Array(outlines) : undefined; return outlinesIndexArray; } function convertColorsArray(colors) { // Colors are assumed to be normalized sRGB [0,255] while in glTF they are interpreted as linear. // All values RGBA need to be stored as float to keep the precision after sRGB to linear conversion. const colorsArray = new Float32Array(colors.length); for (let index = 0; index < colors.length; index += 4) { colorsArray[index] = srgbToLinear(Color.byteToFloat(colors[index])); colorsArray[index + 1] = srgbToLinear(Color.byteToFloat(colors[index + 1])); colorsArray[index + 2] = srgbToLinear(Color.byteToFloat(colors[index + 2])); colorsArray[index + 3] = Color.byteToFloat(colors[index + 3]); } return colorsArray; } function generateNormals( vertexCount, indices, positions, normals, uv0s, colors, featureIndex, ) { const result = { normals: undefined, positions: undefined, uv0s: undefined, colors: undefined, featureIndex: undefined, vertexCount: undefined, }; if ( vertexCount === 0 || !defined(positions) || positions.length === 0 || defined(normals) ) { return result; } if (defined(indices)) { result.vertexCount = indices.length; result.positions = new Float32Array(indices.length * 3); result.uv0s = defined(uv0s) ? new Float32Array(indices.length * 2) : undefined; result.colors = defined(colors) ? new Uint8Array(indices.length * 4) : undefined; result.featureIndex = defined(featureIndex) ? new Array(indices.length) : undefined; for (let i = 0; i < indices.length; i++) { const index = indices[i]; result.positions[i * 3] = positions[index * 3]; result.positions[i * 3 + 1] = positions[index * 3 + 1]; result.positions[i * 3 + 2] = positions[index * 3 + 2]; if (defined(result.uv0s)) { result.uv0s[i * 2] = uv0s[index * 2]; result.uv0s[i * 2 + 1] = uv0s[index * 2 + 1]; } if (defined(result.colors)) { result.colors[i * 4] = colors[index * 4]; result.colors[i * 4 + 1] = colors[index * 4 + 1]; result.colors[i * 4 + 2] = colors[index * 4 + 2]; result.colors[i * 4 + 3] = colors[index * 4 + 3]; } if (defined(result.featureIndex)) { result.featureIndex[i] = featureIndex[index]; } } vertexCount = indices.length; positions = result.positions; } indices = new Array(vertexCount); for (let i = 0; i < vertexCount; i++) { indices[i] = i; } result.normals = new Float32Array(indices.length * 3); for (let i = 0; i < indices.length; i += 3) { calculateFaceNormal(result.normals, i, indices, positions); } return result; } function generateGltfBuffer( vertexCount, indices, positions, normals, uv0s, colors, featureIndex, parameters, ) { if (vertexCount === 0 || !defined(positions) || positions.length === 0) { return { buffers: [], bufferViews: [], accessors: [], meshes: [], nodes: [], nodesInScene: [], }; } const buffers = []; const bufferViews = []; const accessors = []; const meshes = []; const nodes = []; const nodesInScene = []; const rootExtensions = {}; const extensionsUsed = []; // If we provide indices, then the vertex count is the length // of that array, otherwise we assume non-indexed triangle if (defined(indices)) { vertexCount = indices.length; } // Generate index array const { indexArray, transparentVertexOffset } = generateIndexArray( vertexCount, indices, colors, parameters.splitGeometryByColorTransparency, ); // Push to the buffers, bufferViews and accessors const indicesBlob = new Blob([indexArray], { type: "application/binary" }); const indicesURL = URL.createObjectURL(indicesBlob); const endIndex = vertexCount; // Feature index array gives a higher level of detail, each feature object can be accessed separately const featureIndexArray = parameters.enableFeatures && defined(featureIndex) ? new Float32Array(featureIndex.length) : undefined; let featureCount = 0; if (defined(featureIndexArray)) { for (let index = 0; index < featureIndex.length; ++index) { featureIndexArray[index] = featureIndex[index]; const countByIndex = featureIndex[index] + 1; if (featureCount < countByIndex) { // Feature count is defined by the maximum feature index featureCount = countByIndex; } } } // Outlines indices let outlinesIndicesURL; const outlinesIndexArray = generateOutlinesIndexArray( parameters.symbologyData, featureIndex, indexArray, positions, normals, ); if (defined(outlinesIndexArray)) { const outlinesIndicesBlob = new Blob([outlinesIndexArray], { type: "application/binary", }); outlinesIndicesURL = URL.createObjectURL(outlinesIndicesBlob); } // POSITIONS const meshPositions = positions.subarray(0, endIndex * 3); const positionsBlob = new Blob([meshPositions], { type: "application/binary", }); const positionsURL = URL.createObjectURL(positionsBlob); let minX = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; let minZ = Number.POSITIVE_INFINITY; let maxZ = Number.NEGATIVE_INFINITY; for (let i = 0; i < meshPositions.length / 3; i++) { minX = Math.min(minX, meshPositions[i * 3 + 0]); maxX = Math.max(maxX, meshPositions[i * 3 + 0]); minY = Math.min(minY, meshPositions[i * 3 + 1]); maxY = Math.max(maxY, meshPositions[i * 3 + 1]); minZ = Math.min(minZ, meshPositions[i * 3 + 2]); maxZ = Math.max(maxZ, meshPositions[i * 3 + 2]); } // NORMALS const meshNormals = normals ? normals.subarray(0, endIndex * 3) : undefined; let normalsURL; if (defined(meshNormals)) { const normalsBlob = new Blob([meshNormals], { type: "application/binary", }); normalsURL = URL.createObjectURL(normalsBlob); } // UV0s const meshUv0s = uv0s ? uv0s.subarray(0, endIndex * 2) : undefined; let uv0URL; if (defined(meshUv0s)) { const uv0Blob = new Blob([meshUv0s], { type: "application/binary" }); uv0URL = URL.createObjectURL(uv0Blob); } // COLORS const meshColorsInBytes = defined(colors) ? convertColorsArray(colors.subarray(0, endIndex * 4)) : undefined; let colorsURL; if (defined(meshColorsInBytes)) { const colorsBlob = new Blob([meshColorsInBytes], { type: "application/binary", }); colorsURL = URL.createObjectURL(colorsBlob); } // _FEATURE_ID_0 // The actual feature identifiers don't make much sense for reading attribute values, it is enough to use feature index const meshFeatureId0 = defined(featureIndexArray) ? featureIndexArray.subarray(0, endIndex) : undefined; let featureId0URL; if (defined(meshFeatureId0)) { const featureId0Blob = new Blob([meshFeatureId0], { type: "application/binary", }); featureId0URL = URL.createObjectURL(featureId0Blob); } // Feature index property table // This table has no practical use, but at least one property table is required to build a feature table const meshPropertyTable0 = defined(featureIndexArray) ? new Float32Array(featureCount) : undefined; let propertyTable0URL; if (defined(meshPropertyTable0)) { // This table just maps the feature index to itself for (let index = 0; index < meshPropertyTable0.length; ++index) { meshPropertyTable0[index] = index; } const propertyTable0Blob = new Blob([meshPropertyTable0], { type: "application/binary", }); propertyTable0URL = URL.createObjectURL(propertyTable0Blob); } const attributes = {}; const extensions = {}; // POSITIONS attributes.POSITION = accessors.length; buffers.push({ uri: positionsURL, byteLength: meshPositions.byteLength, }); bufferViews.push({ buffer: buffers.length - 1, byteOffset: 0, byteLength: meshPositions.byteLength, target: 34962, }); accessors.push({ bufferView: bufferViews.length - 1, byteOffset: 0, componentType: 5126, count: meshPositions.length / 3, type: "VEC3", max: [minX, minY, minZ], min: [maxX, maxY, maxZ], }); // NORMALS if (defined(normalsURL)) { attributes.NORMAL = accessors.length; buffers.push({ uri: normalsURL, byteLength: meshNormals.byteLength, }); bufferViews.push({ buffer: buffers.length - 1, byteOffset: 0, byteLength: meshNormals.byteLength, target: 34962, }); accessors.push({ bufferView: bufferViews.length - 1, byteOffset: 0, componentType: 5126, count: meshNormals.length / 3, type: "VEC3", }); } // UV0 if (defined(uv0URL)) { attributes.TEXCOORD_0 = accessors.length; buffers.push({ uri: uv0URL, byteLength: meshUv0s.byteLength, }); bufferViews.push({ buffer: buffers.length - 1, byteOffset: 0, byteLength: meshUv0s.byteLength, target: 34962, }); accessors.push({ bufferView: bufferViews.length - 1, byteOffset: 0, componentType: 5126, count: meshUv0s.length / 2, type: "VEC2", }); } // COLORS if (defined(colorsURL)) { attributes.COLOR_0 = accessors.length; buffers.push({ uri: colorsURL, byteLength: meshColorsInBytes.byteLength, }); bufferViews.push({ buffer: buffers.length - 1, byteOffset: 0, byteLength: meshColorsInBytes.byteLength, target: 34962, }); accessors.push({ bufferView: bufferViews.length - 1, byteOffset: 0, componentType: 5126, count: meshColorsInBytes.length / 4, type: "VEC4", }); } // _FEATURE_ID_0 if (defined(featureId0URL)) { attributes._FEATURE_ID_0 = accessors.length; buffers.push({ uri: featureId0URL, byteLength: meshFeatureId0.byteLength, }); bufferViews.push({ buffer: buffers.length - 1, byteOffset: 0, byteLength: meshFeatureId0.byteLength, target: 34963, }); accessors.push({ bufferView: bufferViews.length - 1, byteOffset: 0, componentType: 5126, count: meshFeatureId0.length, type: "SCALAR", }); // Mesh features extension associates feature ids by vertex extensions.EXT_mesh_features = { featureIds: [ { attribute: 0, propertyTable: 0, featureCount: featureCount, }, ], }; extensionsUsed.push("EXT_mesh_features"); } // Feature index property table if (defined(propertyTable0URL)) { buffers.push({ uri: propertyTable0URL, byteLength: meshPropertyTable0.byteLength, }); bufferViews.push({ buffer: buffers.length - 1, byteOffset: 0, byteLength: meshPropertyTable0.byteLength, target: 34963, }); rootExtensions.EXT_structural_metadata = { schema: { id: "i3s-metadata-schema-001", name: "I3S metadata schema 001", description: "The schema for I3S metadata", version: "1.0", classes: { feature: { name: "feature", description: "Feature metadata", properties: { index: { description: "The feature index", type: "SCALAR", componentType: "FLOAT32", required: true, }, }, }, }, }, propertyTables: [ { name: "feature-indices-mapping", class: "feature", count: featureCount, properties: { index: { values: bufferViews.length - 1, }, }, }, ], }; extensionsUsed.push("EXT_structural_metadata"); } // Outlines indices if (defined(outlinesIndicesURL)) { buffers.push({ uri: outlinesIndicesURL, byteLength: outlinesIndexArray.byteLength, }); bufferViews.push({ buffer: buffers.length - 1, byteOffset: 0, byteLength: outlinesIndexArray.byteLength, target: 34963, }); accessors.push({ bufferView: bufferViews.length - 1, byteOffset: 0, componentType: 5125, count: outlinesIndexArray.length, type: "SCALAR", }); extensions.CESIUM_primitive_outline = { indices: accessors.length - 1, }; extensionsUsed.push("CESIUM_primitive_outline"); } // INDICES buffers.push({ uri: indicesURL, byteLength: indexArray.byteLength, }); bufferViews.push({ buffer: buffers.length - 1, byteOffset: 0, byteLength: indexArray.byteLength, target: 34963, }); const meshPrimitives = []; if (transparentVertexOffset > 0) { // Add opaque mesh primitive accessors.push({ bufferView: bufferViews.length - 1, byteOffset: 0, componentType: 5125, count: transparentVertexOffset, type: "SCALAR", }); meshPrimitives.push({ attributes: attributes, indices: accessors.length - 1, material: meshPrimitives.length, extensions: extensions, }); } if (transparentVertexOffset < vertexCount) { // Add transparent mesh primitive accessors.push({ bufferView: bufferViews.length - 1, byteOffset: 4 * transparentVertexOffset, // skip 4 bytes for each opaque vertex componentType: 5125, count: vertexCount - transparentVertexOffset, type: "SCALAR", }); // Indicate the vertices transparancy for the mesh primitive meshPrimitives.push({ attributes: attributes, indices: accessors.length - 1, material: meshPrimitives.length, extensions: extensions, extra: { isTransparent: true, }, }); } meshes.push({ primitives: meshPrimitives, }); nodesInScene.push(0); nodes.push({ mesh: 0 }); return { buffers: buffers, bufferViews: bufferViews, accessors: accessors, meshes: meshes, nodes: nodes, nodesInScene: nodesInScene, rootExtensions: rootExtensions, extensionsUsed: extensionsUsed, }; } function decode(data, schema, bufferInfo, featureData) { const magicNumber = new Uint8Array(data, 0, 5); if ( magicNumber[0] === "D".charCodeAt() && magicNumber[1] === "R".charCodeAt() && magicNumber[2] === "A".charCodeAt() && magicNumber[3] === "C".charCodeAt() && magicNumber[4] === "O".charCodeAt() ) { return decodeDracoEncodedGeometry(data, bufferInfo); } return decodeBinaryGeometry(data, schema, bufferInfo, featureData); } function decodeDracoEncodedGeometry(data) { // Create the Draco decoder. const dracoDecoderModule = draco; const buffer = new dracoDecoderModule.DecoderBuffer(); const byteArray = new Uint8Array(data); buffer.Init(byteArray, byteArray.length); // Create a buffer to hold the encoded data. const dracoDecoder = new dracoDecoderModule.Decoder(); const geometryType = dracoDecoder.GetEncodedGeometryType(buffer); const metadataQuerier = new dracoDecoderModule.MetadataQuerier(); // Decode the encoded geometry. // See: https://github.com/google/draco/blob/master/src/draco/javascript/emscripten/draco_web_decoder.idl let dracoGeometry; let status; if (geometryType === dracoDecoderModule.TRIANGULAR_MESH) { dracoGeometry = new dracoDecoderModule.Mesh(); status = dracoDecoder.DecodeBufferToMesh(buffer, dracoGeometry); } const decodedGeometry = { vertexCount: [0], featureCount: 0, }; // if all is OK if (defined(status) && status.ok() && dracoGeometry.ptr !== 0) { const faceCount = dracoGeometry.num_faces(); const attributesCount = dracoGeometry.num_attributes(); const vertexCount = dracoGeometry.num_points(); decodedGeometry.indices = new Uint32Array(faceCount * 3); const faces = decodedGeometry.indices; decodedGeometry.vertexCount[0] = vertexCount; decodedGeometry.scale_x = 1; decodedGeometry.scale_y = 1; // Decode faces // @TODO: Replace that code with GetTrianglesUInt32Array for better efficiency const face = new dracoDecoderModule.DracoInt32Array(3); for (let faceIndex = 0; faceIndex < faceCount; ++faceIndex) { dracoDecoder.GetFaceFromMesh(dracoGeometry, faceIndex, face); faces[faceIndex * 3] = face.GetValue(0); faces[faceIndex * 3 + 1] = face.GetValue(1); faces[faceIndex * 3 + 2] = face.GetValue(2); } dracoDecoderModule.destroy(face); for (let attrIndex = 0; attrIndex < attributesCount; ++attrIndex) { const dracoAttribute = dracoDecoder.GetAttribute( dracoGeometry, attrIndex, ); const attributeData = decodeDracoAttribute( dracoDecoderModule, dracoDecoder, dracoGeometry, dracoAttribute, vertexCount, ); // initial mapping const dracoAttributeType = dracoAttribute.attribute_type(); let attributei3sName = "unknown"; if (dracoAttributeType === dracoDecoderModule.POSITION) { attributei3sName = "positions"; } else if (dracoAttributeType === dracoDecoderModule.NORMAL) { attributei3sName = "normals"; } else if (dracoAttributeType === dracoDecoderModule.COLOR) { attributei3sName = "colors"; } else if (dracoAttributeType === dracoDecoderModule.TEX_COORD) { attributei3sName = "uv0s"; } // get the metadata const metadata = dracoDecoder.GetAttributeMetadata( dracoGeometry, attrIndex, ); if (metadata.ptr !== 0) { const numEntries = metadataQuerier.NumEntries(metadata); for (let entry = 0; entry < numEntries; ++entry) { const entryName = metadataQuerier.GetEntryName(metadata, entry); if (entryName === "i3s-scale_x") { decodedGeometry.scale_x = metadataQuerier.GetDoubleEntry( metadata, "i3s-scale_x", ); } else if (entryName === "i3s-scale_y") { decodedGeometry.scale_y = metadataQuerier.GetDoubleEntry( metadata, "i3s-scale_y", ); } else if (entryName === "i3s-attribute-type") { attributei3sName = metadataQuerier.GetStringEntry( metadata, "i3s-attribute-type", ); } } } if (defined(decodedGeometry[attributei3sName])) { console.log("Attribute already exists", attributei3sName); } decodedGeometry[attributei3sName] = attributeData; if (attributei3sName === "feature-index") { decodedGeometry.featureCount++; } } dracoDecoderModule.destroy(dracoGeometry); } dracoDecoderModule.destroy(metadataQuerier); dracoDecoderModule.destroy(dracoDecoder); return decodedGeometry; } function decodeDracoAttribute( dracoDecoderModule, dracoDecoder, dracoGeometry, dracoAttribute, vertexCount, ) { const bufferSize = dracoAttribute.num_components() * vertexCount; let dracoAttributeData; const handlers = [ function () {}, // DT_INVALID - 0 function () { // DT_INT8 - 1 dracoAttributeData = new dracoDecoderModule.DracoInt8Array(bufferSize); const success = dracoDecoder.GetAttributeInt8ForAllPoints( dracoGeometry, dracoAttribute, dracoAttributeData, ); if (!success) { console.error("Bad stream"); } const attributeData = new Int8Array(bufferSize); for (let i = 0; i < bufferSize; ++i) { attributeData[i] = dracoAttributeData.GetValue(i); } return attributeData; }, function () { // DT_UINT8 - 2 dracoAttributeData = new dracoDecoderModule.DracoInt8Array(bufferSize); const success = dracoDecoder.GetAttributeUInt8ForAllPoints( dracoGeometry, dracoAttribute, dracoAttributeData, ); if (!success) { console.error("Bad stream"); } const attributeData = new Uint8Array(bufferSize); for (let i = 0; i < bufferSize; ++i) { attributeData[i] = dracoAttributeData.GetValue(i); } return attributeData; }, function () { // DT_INT16 - 3 dracoAttributeData = new dracoDecoderModule.DracoInt16Array(bufferSize); const success = dracoDecoder.GetAttributeInt16ForAllPoints( dracoGeometry, dracoAttribute, dracoAttributeData, ); if (!success) { console.error("Bad stream"); } const attributeData = new Int16Array(bufferSize); for (let i = 0; i < bufferSize; ++i) { attributeData[i] = dracoAttributeData.GetValue(i); } return attributeData; }, function () { // DT_UINT16 - 4 dracoAttributeData = new dracoDecoderModule.DracoInt16Array(bufferSize); const success = dracoDecoder.GetAttributeUInt16ForAllPoints( dracoGeometry, dracoAttribute, dracoAttributeData, ); if (!success) { console.error("Bad stream"); } const attributeData = new Uint16Array(bufferSize); for (let i = 0; i < bufferSize; ++i) { attributeData[i] = dracoAttributeData.GetValue(i); } return attributeData; }, function () { // DT_INT32 - 5 dracoAttributeData = new dracoDecoderModule.DracoInt32Array(bufferSize); const success = dracoDecoder.GetAttributeInt32ForAllPoints( dracoGeometry, dracoAttribute, dracoAttributeData, ); if (!success) { console.error("Bad stream"); } const attributeData = new Int32Array(bufferSize); for (let i = 0; i < bufferSize; ++i) { attributeData[i] = dracoAttributeData.GetValue(i); } return attributeData; }, function () { // DT_UINT32 - 6 dracoAttributeData = new dracoDecoderModule.DracoInt32Array(bufferSize); const success = dracoDecoder.GetAttributeUInt32ForAllPoints( dracoGeometry, dracoAttribute, dracoAttributeData, ); if (!success) { console.error("Bad stream"); } const attributeData = new Uint32Array(bufferSize); for (let i = 0; i < bufferSize; ++i) { attributeData[i] = dracoAttributeData.GetValue(i); } return attributeData; }, function () { // DT_INT64 - 7 }, function () { // DT_UINT64 - 8 }, function () { // DT_FLOAT32 - 9 dracoAttributeData = new dracoDecoderModule.DracoFloat32Array(bufferSize); const success = dracoDecoder.GetAttributeFloatForAllPoints( dracoGeometry, dracoAttribute, dracoAttributeData, ); if (!success) { console.error("Bad stream"); } const attributeData = new Float32Array(bufferSize); for (let i = 0; i < bufferSize; ++i) { attributeData[i] = dracoAttributeData.GetValue(i); } return attributeData; }, function () { // DT_FLOAT64 - 10 }, function () { // DT_FLOAT32 - 11 dracoAttributeData = new dracoDecoderModule.DracoUInt8Array(bufferSize); const success = dracoDecoder.GetAttributeUInt8ForAllPoints( dracoGeometry, dracoAttribute, dracoAttributeData, ); if (!success) { console.error("Bad stream"); } const attributeData = new Uint8Array(bufferSize); for (let i = 0; i < bufferSize; ++i) { attributeData[i] = dracoAttributeData.GetValue(i); } return attributeData; }, ]; const attributeData = handlers[dracoAttribute.data_type()](); if (defined(dracoAttributeData)) { dracoDecoderModule.destroy(dracoAttributeData); } return attributeData; } const binaryAttributeDecoders = { position: function (decodedGeometry, data, offset) { const count = decodedGeometry.vertexCount * 3; decodedGeometry.positions = new Float32Array(data, offset, count); offset += count * 4; return offset; }, normal: function (decodedGeometry, data, offset) { const count = decodedGeometry.vertexCount * 3; decodedGeometry.normals = new Float32Array(data, offset, count); offset += count * 4; return offset; }, uv0: function (decodedGeometry, data, offset) { const count = decodedGeometry.vertexCount * 2; decodedGeometry.uv0s = new Float32Array(data, offset, count); offset += count * 4; return offset; }, color: function (decodedGeometry, data, offset) { const count = decodedGeometry.vertexCount * 4; decodedGeometry.colors = new Uint8Array(data, offset, count); offset += count; return offset; }, featureId: function (decodedGeometry, data, offset) { // We don't need to use this for anything so just increment the offset const count = decodedGeometry.featureCount; offset += count * 8; return offset; }, id: function (decodedGeometry, data, offset) { // We don't need to use this for anything so just increment the offset const count = decodedGeometry.featureCount; offset += count * 8; return offset; }, faceRange: function (decodedGeometry, data, offset) { const count = decodedGeometry.featureCount * 2; decodedGeometry.faceRange = new Uint32Array(data, offset, count); offset += count * 4; return offset; }, uvRegion: function (decodedGeometry, data, offset) { const count = decodedGeometry.vertexCount * 4; decodedGeometry["uv-region"] = new Uint16Array(data, offset, count); offset += count * 2; return offset; }, region: function (decodedGeometry, data, offset) { const count = decodedGeometry.vertexCount * 4; decodedGeometry["uv-region"] = new Uint16Array(data, offset, count); offset += count * 2; return offset; }, }; function decodeBinaryGeometry(data, schema, bufferInfo, featureData) { // From this spec: // https://github.com/Esri/i3s-spec/blob/master/docs/1.7/defaultGeometrySchema.cmn.md const decodedGeometry = { vertexCount: 0, }; const dataView = new DataView(data); try { let offset = 0; decodedGeometry.vertexCount = dataView.getUint32(offset, 1); offset += 4; decodedGeometry.featureCount = dataView.getUint32(offset, 1); offset += 4; if (defined(bufferInfo)) { for ( let attrIndex = 0; attrIndex < bufferInfo.attributes.length; attrIndex++ ) { if ( defined(binaryAttributeDecoders[bufferInfo.attributes[attrIndex]]) ) { offset = binaryAttributeDecoders[bufferInfo.attributes[attrIndex]]( decodedGeometry, data, offset, ); } else { console.error( "Unknown decoder for", bufferInfo.attributes[attrIndex], ); } } } else { let ordering = schema.ordering; let featureAttributeOrder = schema.featureAttributeOrder; if ( defined(featureData) && defined(featureData.geometryData) && defined(featureData.geometryData[0]) && defined(featureData.geometryData[0].params) ) { ordering = Object.keys( featureData.geometryData[0].params.vertexAttributes, ); featureAttributeOrder = Object.keys( featureData.geometryData[0].params.featureAttributes, ); } // Use default geometry schema for (let i = 0; i < ordering.length; i++) { const decoder = binaryAttributeDecoders[ordering[i]]; offset = decoder(decodedGeometry, data, offset); } for (let j = 0; j < featureAttributeOrder.length; j++) { const curDecoder = binaryAttributeDecoders[featureAttributeOrder[j]]; offset = curDecoder(decodedGeometry, data, offset); } } } catch (e) { console.error(e); } decodedGeometry.scale_x = 1; decodedGeometry.scale_y = 1; return decodedGeometry; } function decodeAndCreateGltf(parameters) { // Decode the data into geometry const geometryData = decode( parameters.binaryData, parameters.schema, parameters.bufferInfo, parameters.featureData, ); // Adjust height from orthometric to ellipsoidal if ( defined(parameters.geoidDataList) && parameters.geoidDataList.length > 0 ) { orthometricToEllipsoidal( geometryData.vertexCount, geometryData.positions, geometryData.scale_x, geometryData.scale_y, parameters.cartographicCenter, parameters.geoidDataList, false, ); } // Transform vertices to local transformToLocal( geometryData.vertexCount, geometryData.positions, geometryData.normals, parameters.cartographicCenter, parameters.cartesianCenter, parameters.parentRotation, parameters.ellipsoidRadiiSquare, geometryData.scale_x, geometryData.scale_y, ); // Adjust UVs if there is a UV region if (defined(geometryData.uv0s) && defined(geometryData["uv-region"])) { cropUVs( geometryData.vertexCount, geometryData.uv0s, geometryData["uv-region"], ); } let featureIndex; if (defined(geometryData["feature-index"])) { featureIndex = geometryData["feature-index"]; } else if (defined(geometryData["faceRange"])) { // Build the feature index array from the faceRange. featureIndex = new Array(geometryData.vertexCount); for ( let range = 0; range < geometryData["faceRange"].length - 1; range += 2 ) { const curIndex = range / 2; const rangeStart = geometryData["faceRange"][range]; const rangeEnd = geometryData["faceRange"][range + 1]; for (let i = rangeStart; i <= rangeEnd; i++) { featureIndex[i * 3] = curIndex; featureIndex[i * 3 + 1] = curIndex; featureIndex[i * 3 + 2] = curIndex; } } } if (parameters.calculateNormals) { const data = generateNormals( geometryData.vertexCount, geometryData.indices, geometryData.positions, geometryData.normals, geometryData.uv0s, geometryData.colors, featureIndex, ); if (defined(data.normals)) { geometryData.normals = data.normals; if (defined(data.vertexCount)) { geometryData.vertexCount = data.vertexCount; geometryData.indices = data.indices; geometryData.positions = data.positions; geometryData.uv0s = data.uv0s; geometryData.colors = data.colors; featureIndex = data.featureIndex; } } } // Create the final buffer const meshData = generateGltfBuffer( geometryData.vertexCount, geometryData.indices, geometryData.positions, geometryData.normals, geometryData.uv0s, geometryData.colors, featureIndex, parameters, ); const customAttributes = { positions: geometryData.positions, indices: geometryData.indices, featureIndex: featureIndex, sourceURL: parameters.url, cartesianCenter: parameters.cartesianCenter, parentRotation: parameters.parentRotation, }; meshData._customAttributes = customAttributes; const results = { meshData: meshData, }; return results; } async function initWorker(parameters, transferableObjects) { // Require and compile WebAssembly module, or use fallback if not supported const wasmConfig = parameters.webAssemblyConfig; if (defined(wasmConfig) && defined(wasmConfig.wasmBinaryFile)) { draco = await dracoModule(wasmConfig); } else { draco = await dracoModule(); } return true; } function decodeI3S(parameters, transferableObjects) { // Expect the first message to be to load a web assembly module const wasmConfig = parameters.webAssemblyConfig; if (defined(wasmConfig)) { return initWorker(parameters, transferableObjects); } return decodeAndCreateGltf(parameters, transferableObjects); } export default createTaskProcessorWorker(decodeI3S);