@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
JavaScript
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);