-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add EXT_mesh_polygon #2570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add EXT_mesh_polygon #2570
Changes from 3 commits
ef0184c
d1ea437
6a2f50e
cf47f38
c1a0354
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"). | ||
|
|
||
| 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. | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: Should CW vs CCW order of LINE_LOOP indices be specified?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
MVT is unspecified (not sure what the convention is)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` (UNSIGNED_BYTE) | `255` (0xFF) | | ||
| | `5123` (UNSIGNED_SHORT) | `65535` (0xFFFF) | | ||
| | `5125` (UNSIGNED_INT) | `4294967295` (0xFFFFFFFF) | | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems smarter and safer to me to define This avoids us creating conflicts between the two specifications.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason we wouldn't just use a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
|
||
|  | ||
|
|
||
| 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 | ||


Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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_FANget produced a lot, but in some casesTRIANGLE_STRIPmay 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 producesTRIANGLE.There was a problem hiding this comment.
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.There was a problem hiding this comment.
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
TRIANGLESand not complicate the extension metadata with additional draw modes.