Skip to content

Extract Vector glTF tile data into BufferPrimitiveCollection#13271

Merged
danielzhong merged 46 commits intomainfrom
DanielZhong/VectorTiles
Mar 20, 2026
Merged

Extract Vector glTF tile data into BufferPrimitiveCollection#13271
danielzhong merged 46 commits intomainfrom
DanielZhong/VectorTiles

Conversation

@danielzhong
Copy link
Copy Markdown
Contributor

@danielzhong danielzhong commented Mar 5, 2026

Description

This PR adds support for rendering glTF-based vector tile content using the current CESIUM_mesh_vector spec.

The runtime decodes the glTF, extracts vector data into BufferPrimitiveCollection types, and renders them through the existing buffer collection renderer path.

Supported vector primitive handling in this PR

  • POINTS → BufferPointCollection
  • LINE_STRIP → BufferPolylineCollection
    • Primitive restart values are used to split a single indexed line strip into multiple polylines.
  • TRIANGLES → BufferPolygonCollection
    • Polygon metadata is read from CESIUM_mesh_vector.
    • The current implementation uses polygonAttributeOffsets, polygonIndicesOffsets, and optional hole metadata.

High-level flow

  1. Cesium3DTileset loads a .gltf / .glb tile.
  2. In Cesium3DTileContentFactory, glTF content is routed to VectorGltf3DTileContent when tileset.isGltfExtensionUsed("CESIUM_mesh_vector") is true.
  3. VectorGltf3DTileContent.fromGltf() calls Model.fromGltfAsync() to decode the asset without rendering the model directly.
  4. On the first update(), once the decoded model is ready, initializeVectorPrimitives() calls createVectorTileBuffersFromModelComponents().
  5. createVectorTileBuffersFromModelComponents() converts glTF primitives into buffer collections:
    • BufferPointCollection[]
    • BufferPolylineCollection[]
    • BufferPolygonCollection[]
  6. One source mesh primitive maps to one buffer collection. Local node transforms are preserved per collection and applied through collection.modelMatrix during update.
  7. On each frame, VectorGltf3DTileContent.update() updates each collection and calls collection.update(frameState).
  8. Each collection uses the existing renderer path:
    • renderBufferPointCollection
    • renderBufferPolylineCollection
    • renderBufferPolygonCollection

Issue number and link

Testing plan

Author checklist

  • I have submitted a Contributor License Agreement
  • I have added my name to CONTRIBUTORS.md
  • I have updated CHANGES.md with a short summary of my change
  • I have added or updated unit tests to ensure consistent code coverage
  • I have updated the inline documentation, and included code examples where relevant
  • I have performed a self-review of my code

@danielzhong danielzhong requested a review from donmccurdy March 5, 2026 22:36
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 5, 2026

Thank you for the pull request, @danielzhong!

✅ We can confirm we have a CLA on file for you.

@danielzhong danielzhong changed the base branch from main to donmccurdy/feat/bufferprimitivecollection-render-gl March 5, 2026 22:37
@danielzhong danielzhong changed the base branch from donmccurdy/feat/bufferprimitivecollection-render-gl to donmccurdy/feat/bufferprimitivecollection-render-gl-v2 March 5, 2026 22:38
@danielzhong danielzhong changed the base branch from donmccurdy/feat/bufferprimitivecollection-render-gl-v2 to donmccurdy/feat/bufferprimitivecollection-render-gl March 5, 2026 22:39
Copy link
Copy Markdown
Member

@donmccurdy donmccurdy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielzhong Great progress here, I like how this is coming together! I left a few initial comments, mainly since we're writing the code and the spec in parallel just trying to make sure things align.

Comment on lines +256 to +266
const values = new Float64Array(indices.length * 3);
for (let i = 0; i < indices.length; i++) {
const vertexIndex = indices[i];
const srcOffset = vertexIndex * 3;
values[i * 3] = positions[srcOffset];
values[i * 3 + 1] = positions[srcOffset + 1];
values[i * 3 + 2] = positions[srcOffset + 2];
}

polylineCollection.add({ positions: values }, polylineView);
setPrimitiveFeatureId(polylineView, featureIdSource, indices[0]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping we'll be able to upload a range of the position array directly to the collection, but I guess the collection needs to allow more than float64array here? Opened:

Comment on lines +278 to +283
if (primitiveType === PrimitiveType.TRIANGLE_STRIP) {
triangleIndices =
ModelReader.convertTriangleStripToTriangleIndices(indices);
} else if (primitiveType === PrimitiveType.TRIANGLE_FAN) {
triangleIndices = ModelReader.convertTriangleFanToTriangleIndices(indices);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's OK just to error out if the primitive uses the vector extension with TRIANGLE_STRIP or TRIANGLE_FAN topology, since we won't be able to identify the original polygon connectivity in those cases anyway.

polygonCollection.add(
{
positions: positions,
triangles: toUint32Array(triangleIndices),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above, with #13277 it should be OK to use whatever the source array type is.

polygonView,
) {
const primitiveType = primitive.primitiveType;
const positions = readTransformedPositions(primitive, nodeTransform);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to transform the vertex positions? I'm hoping that we can apply the node transform to collection.modelMatrix or something like that, but not 100% sure how this all fits with the rest of the loading process!

Comment on lines +336 to +369
if (PrimitiveType.isLines(primitiveType) && defined(polylineCollection)) {
if (primitiveType === PrimitiveType.LINES) {
for (let i = 0; i + 1 < indices.length; i += 2) {
appendPolylinePrimitive(
polylineCollection,
polylineView,
featureIdSource,
positions,
[indices[i], indices[i + 1]],
);
}
} else if (primitiveType === PrimitiveType.LINE_STRIP) {
appendPolylinePrimitive(
polylineCollection,
polylineView,
featureIdSource,
positions,
indices,
);
} else if (primitiveType === PrimitiveType.LINE_LOOP) {
const loopIndices = new Uint32Array(indices.length + 1);
for (let i = 0; i < indices.length; i++) {
loopIndices[i] = indices[i];
}
loopIndices[indices.length] = indices[0];
appendPolylinePrimitive(
polylineCollection,
polylineView,
featureIdSource,
positions,
loopIndices,
);
}
return;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I think it's OK if we throw an error if we see any primitive types not allowed by the vector extension.

…ollection-typedarray' into DanielZhong/VectorTiles

# Conflicts:
#	packages/engine/Source/Scene/BufferPolygonCollection.js
@danielzhong danielzhong changed the base branch from donmccurdy/feat/bufferprimitivecollection-render-gl to donmccurdy/feat/bufferprimitivecollection-render-gl-v2 March 9, 2026 01:43
@danielzhong danielzhong changed the base branch from donmccurdy/feat/bufferprimitivecollection-render-gl-v2 to donmccurdy/feat/bufferprimitivecollection-typedarray March 9, 2026 01:44
@danielzhong danielzhong changed the base branch from donmccurdy/feat/bufferprimitivecollection-typedarray to donmccurdy/feat/bufferprimitivecollection-render-gl March 9, 2026 01:52
@danielzhong danielzhong changed the base branch from donmccurdy/feat/bufferprimitivecollection-render-gl to donmccurdy/feat/bufferprimitivecollection-typedarray March 9, 2026 04:33
@danielzhong danielzhong changed the base branch from donmccurdy/feat/bufferprimitivecollection-typedarray to donmccurdy/feat/bufferprimitivecollection-render-gl March 9, 2026 04:34
Base automatically changed from donmccurdy/feat/bufferprimitivecollection-render-gl to main March 11, 2026 15:43
Copy link
Copy Markdown
Member

@donmccurdy donmccurdy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work, thanks Daniel! Just a few comments, no major concerns on my side.

I'd slightly prefer fewer defensive checks still, but that's easier to do with the // @ts-check suggestion which might be out of scope here, so not a big deal for now. Tests are working well locally.

Heads up that in earlier version of the sample tilesets I generated, the "extensionsUsed" property wasn't declared properly and the tilesets could go down the existing glTF rendering path rather than using the new vector code. I've fixed the tilesets: polylines and polygons are working as-is, there's a small fix needed for points I think (see comments).

Approving now since the comments are mostly optional stuff, the point primitive fix is the only critical thing.

const url = multipleContents._innerContentResources[index].url;
const message = defined(error.message) ? error.message : error.toString();
if (tileset.tileFailed.numberOfListeners > 0) {
if (defined(tileset.tileFailed) && tileset.tileFailed.numberOfListeners > 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you recall why this change was needed? I'm not seeing errors if I remove it currently, maybe it can be removed?

If related to my sample tilesets ... those were just thrown together with a quick script, so mostly I'm wanting to check that we're not adding workarounds for what might have been invalid data on my part. :)

Copy link
Copy Markdown
Contributor Author

@danielzhong danielzhong Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entirely optional suggestion, feel very free to skip this — For new files you might find it helpful to include a // @ts-check comment at the top of the file. If you're using an editor with TypeScript Language Services enabled (like VSCode or Zed) you should start to see better autocomplete, and errors where type information is missing or conflicting. Any errors visible in the editor will also be reported by npm run tsc.

For example if #13313 happens to merge before this PR, npm run tsc would start complaining about calls to point.setColor(...) (which has been removed).

We could certainly do this in a future PR too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +217 to +221
this._decodeModel = this._decodeModel && this._decodeModel.destroy();
this._pointCollections = undefined;
this._polylineCollections = undefined;
this._polygonCollections = undefined;
this._vectorBuffers = undefined;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be best to destroy the collections before we dereference them:

for (const collection of this._pointCollections) {
  collection.destroy();
}
this._pointCollections = undefined;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +650 to +658
if (points.length === 0 && polylines.length === 0 && polygons.length === 0) {
return undefined;
}

return {
points: points.length > 0 ? points : undefined,
polylines: polylines.length > 0 ? polylines : undefined,
polygons: polygons.length > 0 ? polygons : undefined,
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional - I think if we allowed empty arrays here, and made the arrays non-nullable, we'd need fewer if/defined protections elsewhere in the PR. But that sort of change is easier to do safely with the // @ts-check mode on, which might be out of scope for this PR, so feel free to leave this as-is too. :)

Suggested change
if (points.length === 0 && polylines.length === 0 && polygons.length === 0) {
return undefined;
}
return {
points: points.length > 0 ? points : undefined,
polylines: polylines.length > 0 ? polylines : undefined,
polygons: polygons.length > 0 ? polygons : undefined,
};
return { points, polylines, polygons };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continue;
}

collection._vectorLocalModelMatrix = Matrix4.clone(nodeTransform);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I was hoping it might be possible to avoid putting extra data onto the collection, but I don't see an obvious way right now. Not sure if that should be the responsibility of the Cesium3DTiles classes or if the BufferPrimitiveCollection needs more metadata. Ok with me as-is though, no changes needed for now unless you have more thoughts on it.

Comment on lines +155 to +179
function getFeatureId(featureIdSource, vertexIndex) {
if (!defined(featureIdSource)) {
return undefined;
}

let featureId;
if (defined(featureIdSource.values)) {
if (vertexIndex < 0 || vertexIndex >= featureIdSource.values.length) {
return undefined;
}
featureId = featureIdSource.values[vertexIndex];
} else {
featureId =
Math.floor(vertexIndex / featureIdSource.repeat) + featureIdSource.offset;
}

featureId = Math.trunc(featureId);
if (
defined(featureIdSource.nullFeatureId) &&
featureId === featureIdSource.nullFeatureId
) {
return undefined;
}
return featureId;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we're re-implementing a bit from packages/engine/Source/Scene/Model/FeatureIdPipelineStage.js here and in getFeatureIdSource above. That's probably OK, I don't see an easier way to access its results, maybe it's just worth including a TODO for later.

Copy link
Copy Markdown
Contributor

@lilleyse lilleyse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick pass, not going super deep. The approach here looks good.

@danielzhong danielzhong added this pull request to the merge queue Mar 20, 2026
Merged via the queue into main with commit 75ca7f2 Mar 20, 2026
5 checks passed
@danielzhong danielzhong deleted the DanielZhong/VectorTiles branch March 20, 2026 17:58
@danielzhong danielzhong self-assigned this Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants