Skip to content
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

EXT_mesh_manifold #2286

Merged
merged 8 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
171 changes: 171 additions & 0 deletions extensions/2.0/Vendor/EXT_mesh_manifold/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# EXT\_mesh\_manifold

## Contributors

* Emmett Lalish, [@elalish](https://github.com/elalish/)

## Status

Draft

## Dependencies

Written against the glTF 2.0 spec.

## Overview

A mesh is typically considered manifold when it topologically represents a solid object with well-defined volume, sometimes known as watertight. For the purpose of this extension, a mesh is considered manifold when it is an oriented 2-manifold triangle mesh, meaning for each halfedge (each of the three ordered vertex index pairs around a triangle) there is exactly one other halfedge with swapped vertices, and no other halfedge with the same vertices.

>NOTE: This is the same definition as used in the 3D Manufacturing Format (3MF) for maximum interoperability.

Manifoldness is critical for reliability of geometric algorithms that operate on solid objects, and so is important in CAD, FEA (Finite Element Analysis), CFD (Computational Fluid Dynamics), and more. However, GPU-friendly formats like glTF with indexed triangles lose manifoldness data because anywhere vertex properties change at an edge (e.g. change of materials, UV swatch, or normals at a crease) the entire vertex must be duplicated and referenced separately. This means the only way to determine linked edges is by merging vertices with identical positions, but this geometric check cannot losslessly recreate the topology, as some separate vertices may happen to coincide in position.

This extension allows authors to denote that the contained mesh data is in fact manifold, and adds a small amount of extra data to losslessly recover this manifold mesh by leveraging sparse accessors.

## Extending Meshes With Manifold Data

A glTF mesh is denoted as a manifold by adding an `EXT_mesh_manifold` extension object to it and obeying the following restrictions:

- all primitives **MUST** use the `TRIANGLES` topology type;
- same attributes of different mesh primitives **MUST** reference the same accessor;
- the `indices` property **MUST** be defined for all primitives and the referenced accessors with index data **MUST** reference the same buffer view.

The required `manifoldPrimitive` property added in the extension is an alternative `primitive` with additional restrictions:
- it **MUST** use the `TRIANGLES` topology type;
- it **MUST** have only the `POSITION` `attribute`, which **MUST** reference the same position `accessor` as the other primitives use;
- its `indices` property **MUST** reference an `accessor`, which **MUST** in turn reference the same `bufferView` as the indices accessors of other primitives of the same mesh;
- when its `indices` `accessor` includes a `sparse` property, the sparse data also defines which vertices are to be merged, but every index merged **MUST** have identical position values for the before and after vertices referenced, so that the geometry is unchanged;
- it **MUST NOT** contain a material;
- it **MUST NOT** contain morph targets, though the other primitives can.

If the `indices` `accessor` is sparse, then `mergeIndices` and `mergeValues` **MUST** be included and reference `accessors` which are equivalent to the sparse indices and values, by referencing the same buffer views, offsets, component type, and count. This small amount of JSON is duplicated here because sparsity is generally considered an internal detail of compression, but here it also represents the relationship between the separate primitives and the unified manifold. If the mesh contains only primitives that are already disjoint and independently manifold, then no sparsity or merge `accessors` are needed.

An example is given below, representing a portion of the included sample's JSON. This object has two materials/primitives with a single shared vertex attribute array. The triangle indices are also a single shared array, partitioned into separate primitive accessors.

```json
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 3,
"TEXCOORD_0": 4
},
"mode": 4,
"material": 0,
"indices": 5
},
{
"attributes": {
"POSITION": 3,
"TEXCOORD_0": 4
},
"mode": 4,
"material": 1,
"indices": 6
}
],
"extensions": {
"EXT_mesh_manifold": {
"manifoldPrimitive": {
"indices": 2,
"mode": 4,
"attributes": {
"POSITION": 3
}
},
"mergeIndices": 0,
"mergeValues": 1
}
}
}
],
"accessors": [
{
"name": "sparse indices",
"type": "SCALAR",
"componentType": 5125,
"count": 1728,
"bufferView": 2,
"byteOffset": 0
},
{
"name": "sparse values",
"type": "SCALAR",
"componentType": 5125,
"count": 1728,
"bufferView": 2,
"byteOffset": 6912
},
{
"name": "manifold indices",
"type": "SCALAR",
"componentType": 5125,
"count": 10320,
"bufferView": 3,
"byteOffset": 0,
"sparse": {
"count": 1728,
"indices": {
"bufferView": 2,
"byteOffset": 0,
"componentType": 5125
},
"values": {
"bufferView": 2,
"byteOffset": 6912
}
}
},
{
"name": "position",
"type": "VEC3",
"componentType": 5126,
"count": 2292,
"max": [
50,
50,
50
],
"min": [
-50,
-50,
-50
],
"bufferView": 4,
"byteOffset": 0
},
{
"name": "tex-coord",
"type": "VEC2",
"componentType": 5126,
"count": 2292,
"bufferView": 4,
"byteOffset": 12
},
{
"name": "first primitive indices",
"type": "SCALAR",
"componentType": 5125,
"count": 1728,
"bufferView": 3,
"byteOffset": 0
},
{
"name": "second primitive indices",
"type": "SCALAR",
"componentType": 5125,
"count": 8592,
"bufferView": 3,
"byteOffset": 6912
}
],
```

## Usage

While this extension places additional restrictions on how a mesh is stored, it is still a valid glTF 2.0 and thus will render properly even on software that does not implement support for this extension. For software that needs a manifold mesh rather than a rendering mesh, the simplest path is to simply replace the set of `primitives` with the `manifoldPrimitive`, which is still a valid glTF, but is simply missing all non-position `attributes`.

As an oriented 2-manifold, it **MUST** be true that after applying the sparse accessor, every halfedge of every triangle has exactly one paired halfedge that has indices in the opposite order. In the case of a `mesh` consisting of just a single manifold `primitive`, the `manifoldPrimitive` **SHOULD** match aside from missing the non-position `attributes`.

In practice, the amount of extra data to represent manifoldness is very small since the sparse accessors only need to be defined along the boundaries between discontinuous vertex properties. The properties themselves are untouched.
Binary file not shown.
199 changes: 199 additions & 0 deletions extensions/2.0/Vendor/EXT_mesh_manifold/samples/manifold/manifold.gltf
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
{
"asset": {
"generator": "glTF-Transform v3.2.1",
"version": "2.0"
},
"accessors": [
{
"type": "VEC3",
"componentType": 5126,
"count": 2292,
"max": [
50,
50,
50
],
"min": [
-50,
-50,
-50
],
"bufferView": 1,
"byteOffset": 0
},
{
"type": "VEC2",
"componentType": 5126,
"count": 2292,
"bufferView": 1,
"byteOffset": 12
},
{
"type": "SCALAR",
"componentType": 5125,
"count": 1728,
"bufferView": 0,
"byteOffset": 0
},
{
"type": "SCALAR",
"componentType": 5125,
"count": 8592,
"bufferView": 0,
"byteOffset": 6912
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 41280,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 41280,
"byteLength": 45840,
"byteStride": 20,
"target": 34962
}
],
"samplers": [
{
"wrapS": 10497,
"wrapT": 10497
}
],
"textures": [
{
"source": 0,
"sampler": 0
},
{
"source": 1,
"sampler": 0
}
],
"images": [
{
"name": "space",
"mimeType": "image/jpeg",
"uri": "space.jpg"
},
{
"name": "moon",
"mimeType": "image/jpeg",
"uri": "moon.jpg"
}
],
"buffers": [
{
"name": "manifold",
"byteLength": 87120,
"uri": "manifold.bin"
}
],
"materials": [
{
"name": "space",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0,
"texCoord": 0
},
"baseColorFactor": [
1,
1,
1,
1
],
"metallicFactor": 1,
"roughnessFactor": 1
},
"emissiveFactor": [
0,
0,
0
],
"alphaMode": "OPAQUE",
"doubleSided": false
},
{
"name": "moon",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 1,
"texCoord": 0
},
"baseColorFactor": [
1,
1,
1,
1
],
"metallicFactor": 1,
"roughnessFactor": 1
},
"emissiveFactor": [
0,
0,
0
],
"alphaMode": "OPAQUE",
"doubleSided": false
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 0,
"TEXCOORD_0": 1
},
"mode": 4,
"material": 0,
"indices": 2
},
{
"attributes": {
"POSITION": 0,
"TEXCOORD_0": 1
},
"mode": 4,
"material": 1,
"indices": 3
}
],
"extensions": {
"EXT_mesh_manifold": {
"manifoldPrimitive": {
"indices": 2,
"mode": 4,
"attributes": {
"POSITION": 3
}
},
"mergeIndices": 0,
"mergeValues": 1
}
}
}
],
"nodes": [
{
"mesh": 0
}
],
"scenes": [
{
"nodes": [
0
]
}
],
"extensionsUsed": [
"EXT_mesh_manifold"
],
"scene": 0
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading