Skip to content

Add EXT_mesh_polygon#2570

Open
donmccurdy wants to merge 5 commits into
KhronosGroup:mainfrom
CesiumGS:donmccurdy/EXT_mesh_polygon
Open

Add EXT_mesh_polygon#2570
donmccurdy wants to merge 5 commits into
KhronosGroup:mainfrom
CesiumGS:donmccurdy/EXT_mesh_polygon

Conversation

@donmccurdy

@donmccurdy donmccurdy commented Apr 21, 2026

Copy link
Copy Markdown
Member

Defines EXT_mesh_polygon, an encoding for n-sided polygons optionally containing holes.

Briefly, the proposed approach would be to represent each polygon with one (1) LINE_LOOP topology defining the exterior ring of each polygon, and optional (N) interior rings representing holes. Encoding LINE_LOOP primitives efficiently (many polygons per mesh primitive) depends on the proposed KHR_mesh_primitive_restart extension. EXT_mesh_polygon would include triangle indices for realtime rendering, in addition to the source line loop topology, so that implementations that don't recognize the extension can still display a reasonable approximation of the intent.

Perhaps important to note that subdivision is not among our current use cases... so I can't comment on whether the proposal can be (or should be) extended to handle subdivision.

Related work:

  • CESIUM_primitive_outline
    • Defines edges, but does not preserve polygon topology.
  • EXT_mesh_primitive_edge_visibility (draft)
    • Defines edges, but does not preserve polygon topology.
  • FB_ngon_encoding (draft)
    • Defines polygon topologies, but is limited to topologies that can be encoded as triangle fans, and does not preserve topology of polygons failing this requirement. In our case, the original polygon is likely to be necessary for visual styling like outlines.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

| `5123` (UNSIGNED_SHORT) | `65535` (0xFFFF) |
| `5125` (UNSIGNED_INT) | `4294967295` (0xFFFFFFFF) |

The first loop in each polygon represents the polygon's exterior ring, or boundary. Additional loops, if any, represent interior rings ("holes") within the exterior ring. 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.

Does winding order matter in LINE_LOOP topology, and should it matter to this extension?

I don't think it matters for LINE_LOOP primitives, but we could impose a convention in this extension, because the distinction between "inside" and "outside" is important to a polygon.


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 `primitive.mode = 4` ("TRIANGLES"), `primitive.mode = 5` ("TRIANGLE_STRIP"), or `primitive.mode = 6` ("TRIANGLE_FAN").

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 contiguous range within the primitive's indices accessor. Primitive vertices associated with the polygon are not required to be contiguous.

@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.

Note that some other standards — notably Mapbox Vector Tiles (MVT) — do not encode triangle indices, and leave this step to the client. We could do the same here, sending just a LINE_LOOP topology and defining which loops are exterior vs. interior rings, and leaving the client to perform triangulation.

Given the compression already available in glTF, and the non-trivial cost of triangulation at runtime, I'm leaning towards including the triangle indices.

"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.

Comment on lines +49 to +55
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` (UNSIGNED_BYTE) | `255` (0xFF) |
| `5123` (UNSIGNED_SHORT) | `65535` (0xFFFF) |
| `5125` (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.

Comment thread extensions/2.0/Vendor/EXT_mesh_polygon/README.md Outdated
Comment on lines +49 to +55
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` (UNSIGNED_BYTE) | `255` (0xFF) |
| `5123` (UNSIGNED_SHORT) | `65535` (0xFFFF) |
| `5125` (UNSIGNED_INT) | `4294967295` (0xFFFFFFFF) |

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.

Comment thread extensions/2.0/Vendor/EXT_mesh_polygon/README.md Outdated
Comment thread extensions/2.0/Vendor/EXT_mesh_polygon/README.md Outdated

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").

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.

Comment on lines +112 to +117
"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.


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.

Co-authored-by: Sean Lilley <lilleyse@gmail.com>
Co-authored-by: Adam Morris <adam@kernelpanicstudios.com>
@donmccurdy

Copy link
Copy Markdown
Member Author

I've pushed some updates here, most notably making LINE_LOOP the base primitive mode, and supplying TRIANGLES indices in extension.triangleIndices rather than the other way around. Two motivations for that include:

  1. loops are the more representative topology, and
  2. triangle indices might be made optional.

(2) is complex, and still under discussion. Consider the outline of Antarctica, which encircles the South Pole. How should a client triangulate this polygon, if triangulation is not provided? Based on CCW winding order tells us which side is "up". But as these are 3D coordinates, not 2D coordinates on the surface of the ellipsoid, ... we'd expect the triangulated polygon to cut directly through the ellipsoid. That's intuitive in 3D space, perhaps, but doesn't serve the use case of streaming polygon geometry from, e.g., a Postgres database, as would be common with GeoJSON or MVT.

If we complicate the extension with ellipsoids and 2D coordinates on their surface, it ceases to be a general-purpose glTF extension. If we don't, it may be necessary to require triangle indices, despite the cost to certain authoring applications to produce them.

Following from that ... when tessellation IS provided by the authoring tool, tessellation that follows the curvature of the ellipsoid will require additional vertices not included in the polygon's exterior/interior rings. The specification does not yet mention this, but presumably it must be supported.

@aaronfranke aaronfranke mentioned this pull request Jun 12, 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.

6 participants