@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
692 lines (614 loc) • 20.6 kB
JavaScript
// @ts-check
import Cartesian3 from "../../Core/Cartesian3.js";
import defined from "../../Core/defined.js";
import Matrix4 from "../../Core/Matrix4.js";
import PrimitiveType from "../../Core/PrimitiveType.js";
import RuntimeError from "../../Core/RuntimeError.js";
import BufferPoint from "../BufferPoint.js";
import BufferPointCollection from "../BufferPointCollection.js";
import BufferPolygon from "../BufferPolygon.js";
import BufferPolygonCollection from "../BufferPolygonCollection.js";
import BufferPolyline from "../BufferPolyline.js";
import BufferPolylineCollection from "../BufferPolylineCollection.js";
import VertexAttributeSemantic from "../VertexAttributeSemantic.js";
import ModelReader from "./ModelReader.js";
import ModelUtility from "./ModelUtility.js";
import ModelComponents from "../ModelComponents.js";
import Cesium3DTileVectorFeature from "../Cesium3DTileVectorFeature.js";
/** @import { TypedArray, TypedArrayConstructor } from "../../Core/globalTypes.js"; */
/** @import BufferPrimitive from "../BufferPrimitive.js"; */
/** @import BufferPrimitiveCollection from "../BufferPrimitiveCollection.js"; */
/** @import VectorGltf3DTileContent from "../VectorGltf3DTileContent.js"; */
/** @import {Attribute, Components, Node, Polygon, Primitive, Vector} from "../ModelComponents.js"; */
/**
* @typedef {object} VectorTileResult
* @property {Array<BufferPrimitiveCollection<BufferPrimitive>>} collections List of all vector primitive collections.
* @property {Array<Matrix4>} collectionLocalMatrices List of transform matrices for each collection; order matches 'collections'.
* @property {Map<BufferPrimitiveCollection<BufferPrimitive>, number>} collectionFeatureTableIds Maps vector primitive collection -> propertyTableId.
* @property {Map<number, Map<number, Cesium3DTileVectorFeature>>} featuresByTableId Maps propertyTableId -> featureId -> feature.
* @ignore
*/
const scratchPosition = new Cartesian3();
const scratchPoint = new BufferPoint();
const scratchPolyline = new BufferPolyline();
const scratchPolygon = new BufferPolygon();
/**
* Creates vector buffers from model components.
*
* @param {VectorGltf3DTileContent} content
* @param {Components} components
* @returns {VectorTileResult}
*
* @ignore
*/
function createVectorTileBuffersFromModelComponents(content, components) {
const rootNodes = components.scene.nodes;
/** @type {VectorTileResult} */
const result = {
collections: [],
collectionLocalMatrices: [],
collectionFeatureTableIds: new Map(),
featuresByTableId: new Map(),
};
for (let i = 0; i < rootNodes.length; i++) {
appendNodeToBuffers(content, rootNodes[i], Matrix4.IDENTITY, result);
}
return result;
}
/**
* @param {TypedArray} indices
* @ignore
*/
function getPrimitiveRestartIndex(indices) {
const bits = indices.BYTES_PER_ELEMENT * 8;
return Math.pow(2, bits) - 1;
}
/**
* @param {Primitive} primitive
* @ignore
*/
function gatherPrimitiveStats(primitive) {
const stats = {
pointPrimitiveCount: 0,
pointVertexCount: 0,
polylinePrimitiveCount: 0,
polylineVertexCount: 0,
polygonPrimitiveCount: 0,
polygonVertexCount: 0,
polygonHoleCount: 0,
polygonTriangleCount: 0,
};
const vector = primitive.vector;
const polygon = primitive.polygon;
const primitiveType = primitive.primitiveType;
const positionAttribute = ModelUtility.getAttributeBySemantic(
primitive,
VertexAttributeSemantic.POSITION,
);
const positionCount = positionAttribute.count;
const indices = primitive.indices
? ModelReader.readIndicesAsTypedArray(primitive.indices)
: undefined;
if (primitiveType === PrimitiveType.POINTS) {
stats.pointPrimitiveCount += positionCount;
stats.pointVertexCount += positionCount;
} else if (primitiveType === PrimitiveType.LINE_STRIP) {
const stripCount = vector ? vector.count : getPrimitiveCount(indices);
stats.polylinePrimitiveCount += stripCount;
stats.polylineVertexCount += indices.length - stripCount + 1;
} else if (
primitiveType === PrimitiveType.TRIANGLES ||
primitiveType === PrimitiveType.LINE_LOOP
) {
if (vector) {
stats.polygonPrimitiveCount += vector.count;
stats.polygonVertexCount += positionCount;
stats.polygonTriangleCount += indices.length / 3;
const polygonHoleCounts = vector.polygonHoleCounts;
if (polygonHoleCounts) {
for (let i = 0; i < polygonHoleCounts.length; i++) {
stats.polygonHoleCount += polygonHoleCounts[i];
}
}
} else {
const loopCount = getPrimitiveCount(polygon.loopIndices);
stats.polygonPrimitiveCount += polygon.count;
stats.polygonVertexCount += polygon.loopIndices.length - loopCount + 1;
stats.polygonTriangleCount += polygon.triangleIndices.length;
stats.polygonHoleCount += loopCount - polygon.count;
}
} else {
throw new RuntimeError(`Unexpected primitive type: ${primitiveType}`);
}
return stats;
}
/**
* Returns the number of GL primitives, separated by primitive restart index
* values, defined in the given index list.
*
* @param {TypedArray} indices
* @ignore
*/
function getPrimitiveCount(indices) {
const restart = getPrimitiveRestartIndex(indices);
let count = 0;
for (let i = 0; i < indices.length; i++) {
if (indices[i] === restart && i + 1 < indices.length) {
count++;
}
}
return count + 1;
}
/**
* @param {VectorGltf3DTileContent} content
* @param {Primitive} primitive
* @param {BufferPrimitiveCollection<BufferPrimitive>} collection
* @param {number} collectionIndex
* @param {Map<number, Cesium3DTileVectorFeature>} features
* @ignore
*/
function appendPrimitiveToBuffers(
content,
primitive,
collection,
collectionIndex,
features,
) {
const positionAttribute = ModelUtility.getAttributeBySemantic(
primitive,
VertexAttributeSemantic.POSITION,
);
/** @type {TypedArray} */
const collectionPositions =
positionAttribute.typedArray ??
(collection.positionNormalized
? ModelReader.readAttributeAsRawCompactTypedArray(positionAttribute)
: ModelReader.readAttributeAsTypedArray(positionAttribute));
/** @type {TypedArray} */
const indices = primitive.indices
? ModelReader.readIndicesAsTypedArray(primitive.indices)
: undefined;
const getFeature = createFeatureFactoryFn(content, primitive, features);
if (collection instanceof BufferPointCollection) {
appendBufferPoints(
collection,
collectionIndex,
collectionPositions,
indices,
getFeature,
);
} else if (collection instanceof BufferPolylineCollection) {
appendBufferPolylines(
collection,
collectionIndex,
collectionPositions,
indices,
getFeature,
);
} else if (collection instanceof BufferPolygonCollection) {
if (defined(primitive.vector)) {
appendBufferPolygonsDeprecated(
collection,
collectionIndex,
collectionPositions,
indices,
getFeature,
primitive.vector,
);
} else {
appendBufferPolygons(
collection,
collectionIndex,
collectionPositions,
getFeature,
primitive.polygon,
);
}
}
}
/**
* @callback FeatureFactoryFn
* @param {number} vertexOffset
* @returns {Cesium3DTileVectorFeature|undefined}
* @ignore
*/
/**
* Returns a factory function that, given a vertex offset, returns the
* Cesium3DTileVectorFeature instance associated with the vertex.
*
* @param {VectorGltf3DTileContent} content
* @param {Primitive} primitive
* @param {Map<number, Cesium3DTileVectorFeature>} features
* @returns {FeatureFactoryFn}
* @ignore
*/
function createFeatureFactoryFn(content, primitive, features) {
// @ts-expect-error Missing types.
const label = content.tileset.featureIdLabel;
const featureIdComponent = ModelUtility.getFeatureIdsByLabel(
primitive.featureIds,
label,
);
if (!(featureIdComponent instanceof ModelComponents.FeatureIdAttribute)) {
return () => undefined;
}
const featureIdAttribute = ModelUtility.getAttributeBySemantic(
primitive,
VertexAttributeSemantic.FEATURE_ID,
featureIdComponent.setIndex,
);
/** @type {TypedArray} */
const featureIdArray =
featureIdAttribute.typedArray ??
ModelReader.readAttributeAsTypedArray(featureIdAttribute);
for (let i = 0; i < featureIdArray.length; i++) {
const featureId = featureIdArray[i];
if (!features.has(featureId)) {
const feature = new Cesium3DTileVectorFeature(
content,
featureId,
featureIdComponent.propertyTableId,
);
features.set(featureId, feature);
}
}
/** @type {FeatureFactoryFn} */
return function getFeature(vertexOffset) {
const featureId = featureIdArray[vertexOffset];
if (featureId !== featureIdComponent.nullFeatureId) {
return features.get(featureId);
}
};
}
/**
* @param {BufferPointCollection} collection
* @param {number} collectionIndex
* @param {TypedArray} collectionPositions
* @param {TypedArray} indices
* @param {FeatureFactoryFn} getFeature
* @ignore
*/
function appendBufferPoints(
collection,
collectionIndex,
collectionPositions,
indices,
getFeature,
) {
const vertexCount = collectionPositions.length / 3;
for (let i = 0, il = indices ? indices.length : vertexCount; i < il; i++) {
const vertexOffset = indices ? indices[i] : i;
Cartesian3.fromArray(
// @ts-expect-error https://github.com/CesiumGS/cesium/pull/13302
collectionPositions,
vertexOffset * 3,
scratchPosition,
);
const feature = getFeature(vertexOffset);
if (feature) {
feature.addPrimitiveByCollection(collectionIndex, i);
}
collection.add(
{
position: scratchPosition,
pickObject: feature,
featureId: feature?.featureId,
},
scratchPoint,
);
}
}
/**
* @param {BufferPolylineCollection} collection
* @param {number} collectionIndex
* @param {TypedArray} collectionPositions
* @param {TypedArray} indices
* @param {FeatureFactoryFn} getFeature
* @ignore
*/
function appendBufferPolylines(
collection,
collectionIndex,
collectionPositions,
indices,
getFeature,
) {
const restartIndex = getPrimitiveRestartIndex(indices);
let lineIndexStart = 0;
let lineIndexCount = 0;
let primitiveIndex = 0;
for (let i = 0; i < indices.length; i++) {
// Iteration has reached the end of a line strip primitive if the current
// index is a "restart index", or the next index is out of bounds.
const index = indices[i];
if (index === restartIndex) {
lineIndexCount = i - lineIndexStart;
} else if (i + 1 === indices.length) {
lineIndexCount = i + 1 - lineIndexStart;
}
if (lineIndexCount === 0) {
continue;
}
const lineIndexEnd = lineIndexStart + lineIndexCount;
const lineIndices = indices.subarray(lineIndexStart, lineIndexEnd);
const positions = copyArrayByIndices(collectionPositions, lineIndices, 3);
const feature = getFeature(indices[lineIndexStart]);
if (feature) {
feature.addPrimitiveByCollection(collectionIndex, primitiveIndex);
}
collection.add(
{ positions, pickObject: feature, featureId: feature?.featureId },
scratchPolyline,
);
lineIndexStart = i + 1;
lineIndexCount = 0;
primitiveIndex++;
}
}
/**
* @param {BufferPolygonCollection} collection
* @param {number} collectionIndex
* @param {TypedArray} collectionPositions
* @param {TypedArray} indices
* @param {FeatureFactoryFn} getFeature
* @param {Vector} vector
* @deprecated To be removed after v1.142 release.
* @ignore
*/
function appendBufferPolygonsDeprecated(
collection,
collectionIndex,
collectionPositions,
indices,
getFeature,
vector,
) {
const polygonAttributeOffsets = vector.polygonAttributeOffsets;
const polygonIndicesOffsets = vector.polygonIndicesOffsets;
const polygonHoleCounts = vector.polygonHoleCounts;
const polygonHoleOffsets = vector.polygonHoleOffsets;
const vertexCount = collectionPositions.length / 3;
const polygonCount = polygonAttributeOffsets.length;
for (let i = 0; i < polygonCount; i++) {
const polygonVertexStart = polygonAttributeOffsets[i];
const polygonVertexEnd =
i + 1 < polygonCount ? polygonAttributeOffsets[i + 1] : vertexCount;
const positions = collectionPositions.subarray(
polygonVertexStart * 3,
polygonVertexEnd * 3,
);
let holes;
if (defined(polygonHoleCounts) && polygonHoleCounts[i] > 0) {
const holeCount = polygonHoleCounts[i];
holes = polygonHoleOffsets.slice(i, i + holeCount);
for (let h = 0; h < holeCount; h++) {
holes[h] -= polygonVertexStart;
}
}
const triangleIndexStart = polygonIndicesOffsets[i];
const triangleIndexEnd =
i + 1 < polygonCount ? polygonIndicesOffsets[i + 1] : indices.length;
const triangles = indices.slice(triangleIndexStart, triangleIndexEnd);
for (let t = 0; t < triangles.length; t++) {
triangles[t] -= polygonVertexStart;
}
const feature = getFeature(polygonVertexStart);
if (feature) {
feature.addPrimitiveByCollection(collectionIndex, i);
}
collection.add(
{
positions,
triangles,
holes,
pickObject: feature,
featureId: feature?.featureId,
},
scratchPolygon,
);
}
}
/**
* @param {BufferPolygonCollection} collection
* @param {number} collectionIndex
* @param {TypedArray} collectionPositions
* @param {FeatureFactoryFn} getFeature
* @param {Polygon} polygon
* @ignore
*/
function appendBufferPolygons(
collection,
collectionIndex,
collectionPositions,
getFeature,
polygon,
) {
// Create mapping from vertex index in the source glTF primitive, to result
// vertex index in the extracted vector primitive.
const TypedArray = /** @type {TypedArrayConstructor} */ (
polygon.loopIndices.constructor
);
const resultLoopIndices = new TypedArray(polygon.loopIndices.length);
const loopRestartIndex = getPrimitiveRestartIndex(polygon.loopIndices);
for (let i = 0; i < polygon.count; i++) {
const isLastPolygon = i + 1 === polygon.count;
// Extract vertex loops, exterior loops followed by interior ("holes").
const loopIndicesStart = polygon.loopIndicesOffsets[i];
const loopIndicesEnd = isLastPolygon
? polygon.loopIndices.length
: polygon.loopIndicesOffsets[i + 1];
const loopIndices = polygon.loopIndices.subarray(
loopIndicesStart,
loopIndicesEnd,
);
const positions = copyArrayByIndices(
collectionPositions,
loopIndices,
3,
resultLoopIndices,
);
// List start indices of interior loops ("holes").
const holesArray = [];
for (let j = 0; j < loopIndices.length; j++) {
const isLastIndex = j + 1 === loopIndices.length;
if (loopIndices[j] === loopRestartIndex && !isLastIndex) {
holesArray.push(j);
}
}
const holes = new TypedArray(holesArray);
// Extract or tessellate triangle indices.
let triangles;
if (defined(polygon.triangleIndices)) {
const triangleIndicesStart = polygon.triangleIndicesOffsets[i];
const triangleIndicesEnd = isLastPolygon
? polygon.triangleIndices.length
: polygon.triangleIndicesOffsets[i + 1];
triangles = polygon.triangleIndices.subarray(
triangleIndicesStart,
triangleIndicesEnd,
);
// Rewrite collection-local indices to polygon-local indices.
for (let j = 0; j < triangles.length; j++) {
triangles[j] = resultLoopIndices[triangles[j]];
}
} else {
throw new Error("Runtime triangulation not yet supported.");
}
const feature = getFeature(loopIndices[0]);
if (feature) {
feature.addPrimitiveByCollection(collectionIndex, i);
}
collection.add(
{
positions,
triangles,
holes,
pickObject: feature,
featureId: feature?.featureId,
},
scratchPolygon,
);
}
}
/**
* @param {VectorGltf3DTileContent} content
* @param {Node} node
* @param {Matrix4} parentTransform
* @param {VectorTileResult} result
* @ignore
*/
function appendNodeToBuffers(content, node, parentTransform, result) {
const localTransform = ModelUtility.getNodeTransform(node);
const nodeTransform = Matrix4.multiplyTransformation(
parentTransform,
localTransform,
new Matrix4(),
);
const primitives = node.primitives;
for (let i = 0; i < primitives.length; i++) {
const primitive = primitives[i];
const primitiveType = primitive.primitiveType;
/** @type {BufferPrimitiveCollection<BufferPrimitive>} */
let collection;
const stats = gatherPrimitiveStats(primitive);
const positionAttribute = ModelUtility.getAttributeBySemantic(
primitive,
VertexAttributeSemantic.POSITION,
);
const positionNormalized = positionAttribute.normalized ?? false;
const positionDatatype = positionAttribute.componentDatatype;
if (primitiveType === PrimitiveType.POINTS) {
collection = new BufferPointCollection({
primitiveCountMax: stats.pointPrimitiveCount,
allowPicking: true,
positionNormalized,
positionDatatype,
});
} else if (primitiveType === PrimitiveType.LINE_STRIP) {
collection = new BufferPolylineCollection({
primitiveCountMax: stats.polylinePrimitiveCount,
vertexCountMax: stats.polylineVertexCount,
allowPicking: true,
positionNormalized,
positionDatatype,
});
} else if (
primitiveType === PrimitiveType.TRIANGLES ||
primitiveType === PrimitiveType.LINE_LOOP
) {
collection = new BufferPolygonCollection({
primitiveCountMax: stats.polygonPrimitiveCount,
vertexCountMax: stats.polygonVertexCount,
holeCountMax: stats.polygonHoleCount,
triangleCountMax: stats.polygonTriangleCount,
allowPicking: true,
positionNormalized,
positionDatatype,
});
}
const featureIdComponent = primitive.featureIds?.[0];
const propertyTableId = featureIdComponent?.propertyTableId;
if (!result.featuresByTableId.has(propertyTableId)) {
result.featuresByTableId.set(propertyTableId, new Map());
}
result.collections.push(collection);
result.collectionLocalMatrices.push(Matrix4.clone(nodeTransform));
result.collectionFeatureTableIds.set(collection, propertyTableId);
appendPrimitiveToBuffers(
content,
primitive,
collection,
result.collections.length - 1,
result.featuresByTableId.get(propertyTableId),
);
}
const children = node.children;
for (let i = 0; i < children.length; i++) {
appendNodeToBuffers(content, children[i], nodeTransform, result);
}
}
/**
* Given a source array and a list of indices, creates and returns a new array
* containing all values of the source array specified by the indices. Indices
* may contain "primitive restart index" values. Optionally, a 'resultIndices'
* parameter can be populated with a mapping from source index to result index.
*
* @param {TypedArray} array
* @param {TypedArray} indices
* @param {number} stride
* @param {TypedArray} [resultIndices]
* @returns {TypedArray}
* @ignore
*/
function copyArrayByIndices(array, indices, stride, resultIndices) {
const restartIndex = getPrimitiveRestartIndex(indices);
// Scan and count the number of non-restart indices.
let count = 0;
for (let i = 0; i < indices.length; i++) {
const index = indices[i];
if (index !== restartIndex) {
count++;
}
}
const TypedArray = /** @type {TypedArrayConstructor} */ (array.constructor);
const result = new TypedArray(count * stride);
// Write each result value, skipping primitive restart indices. Write source
// index -> result index to 'resultIndices' if provided.
let dstIndex = 0;
for (let i = 0; i < indices.length; i++) {
const srcIndex = indices[i];
if (resultIndices) {
resultIndices[srcIndex] =
srcIndex === restartIndex ? restartIndex : dstIndex;
}
if (srcIndex === restartIndex) {
continue;
}
for (let j = 0; j < stride; j++) {
result[dstIndex * stride + j] = array[srcIndex * stride + j];
}
dstIndex++;
}
return result;
}
export default createVectorTileBuffersFromModelComponents;