Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions extensions/2.0/Vendor/EXT_mesh_polygon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<!--
Copyright 2025 Bentley Systems, Incorporated
SPDX-License-Identifier: CC-BY-4.0
-->

# EXT_mesh_polygon

## Contributors

- Don McCurdy, Bentley Systems, [@donmccurdy](https://github.com/donmccurdy)
- TODO

## Status

Draft

## Dependencies

Written against the glTF 2.0 spec.

## Overview

Extends glTF mesh primitives, adding an encoding of polygon primitive topology, including triangle indices for backwards-compatible rendering. While the core glTF 2.0 specification already allows polygons to be triangulated and encoded as TRIANGLES, TRIANGLE_STRIP, or TRIANGLE_FAN primitive modes, the original topology would be lost without the additional specification and metadata provided by this extension.

## Extending Mesh Primitives

The `EXT_mesh_polygon` extension may be added to a mesh primitive, indicating that the primitive represents a series of polygons.

An extended mesh primitive **MUST** include `primitive.mode = 4` ("TRIANGLES"), `primitive.mode = 5` ("TRIANGLE_STRIP"), or `primitive.mode = 6` ("TRIANGLE_FAN").

@donmccurdy donmccurdy Apr 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Should the extension permit triangulation to use TRIANGLE_STRIP and TRIANGLE_FAN modes, or require TRIANGLES? I'm not convinced the other two are likely to be generally helpful, but it would be relatively painless to implement on the client if desired.

@xuanhuang1 xuanhuang1 Apr 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah agreed here. I don't see TRIANGLE_FAN get produced a lot, but in some cases TRIANGLE_STRIP may make sense depending on mesh type + triangulation implementation? If it doesn't create too much trouble then I'm down including all three in the spec, just that our tiler always produces TRIANGLE.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think we should include TRIANGLE_FAN. It hasn't been supported in Direct3D since at least 2006, and Metal doesn't support it either.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Related to https://github.com/KhronosGroup/glTF/pull/2570/changes#r3118715864 ... if we decide to make LINE_LOOP the 'base' topology, and supply triangle indices as extension metadata (reverse of current draft), then it feels cleaner to stick to just TRIANGLES and not complicate the extension metadata with additional draw modes.


An extended mesh primitive **MUST** include `indices`. Indices for each polygon must be contiguous: for a polygon composed of 4 triangles, indices defining these triangles must occupy a single range within the primitive's indices accessor. Only indices, not vertex attributes, are required to be contiguous.

The `EXT_mesh_polygon` extension includes the following additional properties, all required.

### count

Integer number of polygons encoded in the mesh primitive.

- **Type:** `number`
- **Required:** ✓ Yes
- **Minimum:** ≥ 1

### loopIndices

Index of an accessor containing indices of the polygons' exterior and interior loops. The accessor **MUST** have `SCALAR` type and an unsigned integer component type.

A polygon is composed of 1 or more loops, encoded as indices equivalent to `primitive.mode = 2` ("LINE_LOOP") topology. The first loop in each polygon represents the polygon's exterior ring, or boundary. Additional loops, if any, represent interior rings ("holes") within the polygon. Polygons must be fully-connected: holes cannot intersect the exterior ring, and additional exterior rings ("islands") are not allowed, whether outside the exterior ring or within holes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

TODO: Should CW vs CCW order of LINE_LOOP indices be specified?

@lilleyse lilleyse Apr 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It could probably be left unspecified since there's no requirements for interior loops to have an opposite winding order like in GeoJSON or MVT. And being 3D I'm not sure if winding order can defined in the same way as GeoJSON or MVT.

Here's what GeoJSON and MVT say:

GeoJSON is CCW (though used to be unspecifed)

image

MVT is unspecified (not sure what the convention is)

image

@donmccurdy donmccurdy Apr 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hm... I think ideally we'd say something about winding order, since it's helpful information we wouldn't want to lose when converting from GeoJSON, but I see your point that it's not always well-defined in 3D.

Maybe let's find some wording to encourage the same approach as GeoJSON, but (since correct behavior is not verifiable) maybe that cannot be a normative requirement.

One thing we could require, maybe, is that the winding order of the triangle indices match the winding order of the (exterior) loop indices. Ideally material.doubleSided would still be usable (even if materials aren't a core goal for vector data).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Isn't there still the concept of a front face and a back face here since we're making a polygon? Wouldn't we want to define the winding order for that?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, I think the concept is important. We should (at minimum) give guidance on the winding order, if not also a normative requirement.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Terms like “holes cannot intersect the exterior ring” are clear in 2D planar polygon terms, but glTF positions are 3D and polygons might be non-planar. It’s unclear:

  • whether polygons are required to be planar (and in what space),
  • what “intersect” means (touching at a vertex? shared edges? epsilon tolerance?),
  • whether rings may be self-intersecting.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Important questions, thanks. I think some of these answers might follow from requiring that the authoring implementation provide triangle indices. See latest comment on the main issue thread.

I suspect we cannot require polygons to be planar; that would present an obstacle for converting polygon data from EPSG 4326 to 3D ECEF coordinates. For example, Antarctica. Any non-planar curvature would need to be encoded in the triangle indices.


Each loop must be separated by a primitive restart value, including loops associated with different polygons. Primitive restart values applicable to each accessor type are:

| `accessor.componentType` | restart value |
| ---------------------------- | ------------------------- |
| `5121`&nbsp;(UNSIGNED_BYTE) | `255` (0xFF) |
| `5123`&nbsp;(UNSIGNED_SHORT) | `65535` (0xFFFF) |
| `5125`&nbsp;(UNSIGNED_INT) | `4294967295` (0xFFFFFFFF) |

@donmccurdy donmccurdy Apr 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I suppose this table could be left undefined, requiring some unspecified extension to provide primitive restart values? In a Cesium.js implementation that would be the proposed KHR_mesh_primitive_restart, which is needed to represent polylines anyway.

But plausibly a future extension could allow specifying different restart values, which some graphics APIs support. I have no plans to propose such an extension, however.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems smarter and safer to me to define KHR_primitive_restart as a dependency of this extension and allow that to specify the primitive restart requirements. Then we just specify that implementations SHALL separate each loop by a primitive restart value.

This avoids us creating conflicts between the two specifications.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sounds good to me! A dependency if polyline or polygon vectors are represented, at least, maybe not for points.


- **Type:** `number`
- **Required:** ✓ Yes
- **Minimum:** ≥ 0

### loopIndicesOffsets

Index of an accessor containing one integer offset per polygon in the primitive, indicating the first index of the first linear ring associated with that polygon. The accessor **MUST** have `SCALAR` type and an unsigned integer component type, and the accessor's `count` **MUST** be the same as `EXT_mesh_polygon.count` for the primitive.

All loops associated with a polygon MUST be contiguous: one exterior ring followed immediately by zero or more interior rings.

> [!NOTE]
> The range of loop indices for the `nth` polygon is `loopIndicesOffsets[n]` to `loopIndicesOffsets[n+1]` if `n < count - 1`, otherwise `loopIndicesOffsets[n]` to the end of the `loopIndices` accessor.

- **Type:** `number`
- **Required:** ✓ Yes
- **Minimum:** ≥ 0

### indicesOffsets

Index of an accessor containing one integer offset per polygon in the primitive, indicating the first index of the first triangle associated with that polygon. The accessor **MUST** have `SCALAR` type and an unsigned integer component type, and the accessor's `count` **MUST** be the same as `EXT_mesh_polygon.count` for the primitive.

Indices for each polygon **MUST** be contiguous.

- **Type:** `number`
- **Required:** ✓ Yes
- **Minimum:** ≥ 0

## Additional Restrictions

Triangles in the extended mesh primitive **MUST** be associated with exactly one polygon. Loose triangles cannot be included, and a single triangle cannot be associated with multiple polygons.

Polygon `loopIndices` values **MUST** be associated with at least one triangle. Exterior and interior loops may not contain additional vertex indices missing from triangulation.

Polygon `loopIndices` values **MUST** be unique within each exterior or interior loop. The same vertex index cannot be used twice within a loop.

> [!NOTE]
> As in LINE_LOOP topology, the first and last indices are defined to be connected by a line segment. Unlike in some geospatial formats, it is NOT necessary that the first index be repeated at the end of the loop to indicate a closed ring, and doing so would violate the requirement above.

## Example

_This section is non-normative._

The JSON example below shows a mesh having one mesh primitive, which contains 100 polygon primitives. The `indices` accessor defines the triangle indices required to draw the polygons, and the `loopIndices` accessor defines the `LINE_LOOP` indices delimiting the exterior (and interior, if holes are present) boundaries of each polygon. The `loopIndicesOffsets` and `indicesOffsets` accessors enable random access, allowing implementations to immediately find the particular triangle and loop indices associated with the Nth polygon.

```jsonc
{
"extensionsUsed": ["EXT_mesh_polygon"],
...
"meshes": [{
"name": "MyMesh",
"primitives": [{
"mode": 4,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note that this extension makes the TRIANGLES mode 'primary', and defines LINE_LOOP indices as additional metadata. We could easily reverse that, making the LINE_LOOP indices primary and providing triangle indices as additional metadata.

The only obvious difference I can see is that 'plain' glTF implementations would render a filled polygon in one case, and only the exterior and interior linear rings of the polygons in the other. In some sense a polygon "is" a closed linear ring, so there's some poetic argument for that, but I haven't come up with a strong preference either way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I slightly lean towards LINE_LOOP being 'primary' though it has less desirable fallback behavior. Either approach works.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@weegeekps @xuanhuang1 does making LINE_LOOP the base primitive mode, and putting triangle indices in the extension properties, sound OK? That might resolve our discussion above #2570 (comment) about TRIANGLE_STRIP and TRIANGLE_FAN. I don't think it's worthwhile to include additional extension metadata just to add triangulation metadata in multiple modes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason we wouldn't just use a LINE_STRIP and just add the first vertex as the final vertex to close our loop? LINE_LOOP support is better than TRIANGLE_FAN but still poor. None of the modern graphics APIs support LINE_LOOP. The guidance for those APIs is to manually add the first vertex to treat as a LINE_STRIP and manually add the first vertex to the end. Why not just specify that and avoid the implementation footgun?

@donmccurdy donmccurdy May 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hm... it's a good point that LINE_LOOP isn't available in (for example) WebGPU. 🫤

OK, stepping back a bit — the ultimate goal of having the loop data at all is to unambiguously identify the polygon, in terms of its exterior and interior rings.

Rendering the rings directly as graphics primitives is not really the purpose. For my present goals, geospatial vector data rendering, no serious vector renderer will be using LINE_LOOP, LINE_STRIP, or even LINES: thick and dashed lines are table stakes, which typically means rendering lines as triangles. Other implementors might have other goals, of course, and EXT_mesh_polygon is meant to be broad enough to handle at least some of the use cases described in #1620.

I think perhaps lack of graphics API support might be an argument against using LINE_LOOP as the 'primary' topology. But making the primary topology LINE_STRIP instead, I'm not so sure about, since we'd need to override the specification to define it as a loop. I might be leaning toward keeping this as-is (in the PR) then — with TRIANGLES as the primary topology, and loop-like indices as extension data, provided for identifying topology. Then any extension-unaware fallback remains on the "easy path" where rendering TRIANGLES just works.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Another bit of feedback we've received (off GitHub) is that for services converting from existing vector formats to glTF on-the-fly, the requirement of triangulating polygons and multi-polygons is very costly, and they'd prefer to have the option of omitting triangle indices — in which case the client must tesselate, e.g. in a worker thread.

By comparison, MVT doesn't support triangle indices at all. MapLibre Tiles (MLT) supports triangle indices but (as far as I can tell) the indices are optional.

I realize it's usually the preference in glTF to offload work from the client, but it's worth considering options here. If triangle indices are optional, the default/primary topology should be likely LINE_LOOP or LINE_STRIP.

"attributes": {
"POSITION": 0,
},
"indices": 1,
"extensions": {
"EXT_mesh_polygon": {
"count": 100,
"loopIndices": 2,
"loopIndicesOffsets": 3,
"indicesOffsets": 4
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Don't forget the JSON schema files.

}
}]
}]
}
```

Consider a simple mesh primitive containing three polygons, one with a single hole, and two without:

![Polygon encoding](./figures/polygon-encoding.png)

One valid encoding of `EXT_mesh_polygon` for this polygon set, based on `UNSIGNED_SHORT` indices and restart values, would be as follows:


## JSON Schema

The `"EXT_mesh_polygon"` string must be added to the root-level `extensionsUsed` array. Where preservation of polygon topology — not just display of equivalent triangles — is required, the string should also be added to the root-level `extensionsRequired` array. As the extension is intentionally backwards-compatible for rendering purposes, the extension is expected to be optional in most cases.

## Known Implementations

- TODO
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading