diff --git a/README.md b/README.md index 582ab46..1808fe4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,11 @@ Several components are still work in progress or just benchmarks. Here are the c - CuttingPerformer: will define a fine cut or incision in the mesh (using [MeshRefinement plugin](https://github.com/InfinyTech3D/MeshRefinement)) - + +### Sub-plugins +- **InfinyToolkit.MeshRefinement:** provides the `TetrahedronSubdivisionController` component, to cut and refine a tetrahedral mesh during the simulation: cutting along a plane or between two triangles, refining a chosen set of tetrahedra or the whole mesh. It is the public interface of the [MeshRefinement plugin](https://github.com/InfinyTech3D/MeshRefinement), which does the work, and is configured automatically when that plugin is found. +**See [extensions/MeshRefinement/README.md](extensions/MeshRefinement/README.md) for the full documentation, the Data reference and the example scenes.** + ### Architecture - **examples:** with several examples of the components and some benchmarks. - **scripts:** a bunch of python scripts to apply changes to the SOFA code base or scenes. Such as: @@ -45,6 +49,7 @@ Several components are still work in progress or just benchmarks. Here are the c - Update headers inclusion - ... - **src/InfinyToolkit:** All the source code of the SOFA components. +- **extensions:** sub-plugins, built only when their own dependencies are found. See [Sub-plugins](#sub-plugins) above. ## Installation diff --git a/examples/MeshRefinement/SubdivisionController_CutFromPlane.scn b/examples/MeshRefinement/SubdivisionController_CutFromPlane.scn new file mode 100644 index 0000000..d842030 --- /dev/null +++ b/examples/MeshRefinement/SubdivisionController_CutFromPlane.scn @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/MeshRefinement/SubdivisionController_CutWithSurface.scn b/examples/MeshRefinement/SubdivisionController_CutWithSurface.scn new file mode 100644 index 0000000..d3b292a --- /dev/null +++ b/examples/MeshRefinement/SubdivisionController_CutWithSurface.scn @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/MeshRefinement/SubdivisionController_RefineFullMesh.scn b/examples/MeshRefinement/SubdivisionController_RefineFullMesh.scn new file mode 100644 index 0000000..858b743 --- /dev/null +++ b/examples/MeshRefinement/SubdivisionController_RefineFullMesh.scn @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/MeshRefinement/SubdivisionController_RefineTetrahedra.scn b/examples/MeshRefinement/SubdivisionController_RefineTetrahedra.scn new file mode 100644 index 0000000..445bd8e --- /dev/null +++ b/examples/MeshRefinement/SubdivisionController_RefineTetrahedra.scn @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/MeshRefinement/CMakeLists.txt b/extensions/MeshRefinement/CMakeLists.txt index 74adcaa..8d7c105 100644 --- a/extensions/MeshRefinement/CMakeLists.txt +++ b/extensions/MeshRefinement/CMakeLists.txt @@ -43,6 +43,7 @@ sofa_create_package_with_targets( TARGETS ${PROJECT_NAME} AUTO_SET_TARGET_PROPERTIES INCLUDE_SOURCE_DIR "src" INCLUDE_INSTALL_DIR "${PROJECT_NAME}" + EXAMPLE_INSTALL_DIR "examples" RELOCATABLE "plugins" ) diff --git a/extensions/MeshRefinement/README.md b/extensions/MeshRefinement/README.md new file mode 100644 index 0000000..a313b48 --- /dev/null +++ b/extensions/MeshRefinement/README.md @@ -0,0 +1,373 @@ +# InfinyToolkit.MeshRefinement + +Cut and refine a tetrahedral mesh during a SOFA simulation, from a scene file, from Python or from +C++. + +This sub-plugin provides a single component, **`TetrahedronSubdivisionController`**. It is the +public face of the [MeshRefinement](https://github.com/InfinyTech3D/MeshRefinement) plugin, which +does the actual work: identifying the tetrahedra concerned, splitting them with the right +subdivision pattern, and rebuilding their neighbours so the mesh stays conforming. + +| | | +|--|--| +| **Requires** | The `MeshRefinement` plugin, and `Sofa.Component.Topology.Container.Dynamic` | +| **Provides** | `TetrahedronSubdivisionController` | +| **Templates** | `Vec3d` (default). The Cuda types are prepared but not enabled yet | +| **Examples** | [`/examples/MeshRefinement`](../../examples/MeshRefinement) | +| **Tests** | [`tests/`](tests) | + +## Contents + +- [Installation](#installation) +- [Quick start](#quick-start) +- [What the controller can do](#what-the-controller-can-do) + - [Cut along a plane](#1-cut-along-a-plane) + - [Cut between two triangles](#2-cut-between-two-triangles) + - [Refine a set of tetrahedra](#3-refine-a-set-of-tetrahedra) + - [Refine the whole mesh](#4-refine-the-whole-mesh) +- [Two-phase cutting](#two-phase-cutting) +- [Creating the cut surface](#creating-the-cut-surface) +- [Data reference](#data-reference) +- [Keyboard shortcuts](#keyboard-shortcuts) +- [Driving it from Python](#driving-it-from-python) +- [Driving it from C++](#driving-it-from-c) +- [Example scenes](#example-scenes) +- [Troubleshooting](#troubleshooting) + +## Installation + +Both repositories are added as external SOFA plugins, through +`SOFA_EXTERNAL_DIRECTORIES`. `InfinyToolkit.MeshRefinement` is configured automatically as a +sub-plugin of `InfinyToolkit` once `MeshRefinement` has been found - there is no separate option to +turn on, but `MeshRefinement` has to be configured first. + +``` +cmake -S -B \ + -DSOFA_EXTERNAL_DIRECTORIES= \ + -DPLUGIN_MESHREFINEMENT=ON -DPLUGIN_INFINYTOOLKIT=ON +cmake --build --config Release --target InfinyToolkit.MeshRefinement +``` + +In a scene, one `RequiredPlugin` is enough - `MeshRefinement` is pulled in as a dependency: + +```xml + +``` + +## Quick start + +The controller sits in the node holding the tetrahedral topology, beside the container, the +modifier and the geometry algorithms. It finds them by itself; there is nothing to link. + +```xml + + + + + + + + + + + + + + +``` + +Run it, press `1` to build the cut and `2` to apply it. + +The node **must** contain, next to the controller: + +| Component | Why | +|--|--| +| `TetrahedronSetTopologyContainer` | The mesh being modified | +| `TetrahedronSetTopologyModifier` | Applies the topological changes | +| `TetrahedronSetGeometryAlgorithms` | Geometric queries | +| A `MechanicalObject` of the matching template | The positions | + +Each missing one is reported by name at init, and the controller then does nothing rather than +misbehaving. + +## What the controller can do + +Four operations. Each has a key, a C++/Python method, or both. + +### 1. Cut along a plane + +The cut region is the quad spanned by `cutPointA` and `cutPointB`, extruded along `cutDir` over +`cutDepth`. Every tetrahedron the quad crosses is split along it, and the points on the cut are +duplicated so the two sides come apart. + +```xml + +``` + +Keys `1` then `2`, or `performCut="1"` to run it at the end of the next time step, or: + +```cpp +bool cutFromPlane(const Vec3& pointA, const Vec3& pointB, const Vec3& direction, + SReal depth, bool createSurface = false); +``` + +Make the quad overrun the mesh on the sides where the cut should go all the way through - a quad +that stops inside the mesh produces an incision that stops there, which is often what you want for +a partial cut. + +> `cutDepth` does double duty: it is the extrusion length **and** it sets the snapping tolerance +> (`cutDepth * 0.25`). Points of the mesh within that distance of the plane are pulled onto the cut +> instead of a new point being created for them. A very large depth on a small mesh therefore snaps +> more aggressively than you might expect. + +### 2. Cut between two triangles + +Convenience over the plane version for a caller that has picked triangles rather than computed a +plane - a collision or a mouse pick, typically. The quad runs from the barycentre of `triangleA` to +the barycentre of `triangleB`, extruded *into* the mesh over `depth`, along the normal of +`triangleA`. + +```cpp +bool cutFromTriangles(TriangleID triangleA, TriangleID triangleB, + SReal depth, bool createSurface = false); +``` + +There is no key and no Data for this one; it is an API-only entry point. The ids are indices into +the **tetrahedral** topology's triangle array (the same container the controller operates on), not +into a separately mapped surface topology. Out-of-range ids are reported and nothing is cut. + +### 3. Refine a set of tetrahedra + +Each listed tetrahedron has all six of its edges split, so it becomes eight. Its neighbours are +then rebuilt with the pattern matching however many of *their* edges got split, which is what keeps +the mesh conforming. + +```xml + +``` + +Key `3`, or: + +```cpp +bool refineTetrahedra(const std::set& ids, SReal criteria = 0.0); +``` + +`refineCriteria` stops the refinement running away when it is applied repeatedly: it is a fraction +of the mesh's **mean tetrahedron volume**, measured once at init. A tetrahedron whose volume is not +above `refineCriteria x` that mean is left alone. `0`, the default, refines regardless of size. + +Ids past the end of the topology are reported and nothing is refined. + +### 4. Refine the whole mesh + +Every tetrahedron becomes eight. One midpoint is created per edge and shared by all the tetrahedra +using it. + +Key `4`, or: + +```cpp +bool refineFullMesh(); +``` + +Each call multiplies the tetrahedron count by eight - 5 000 tetrahedra become 40 000, then 320 000. +Start small. + +## Two-phase cutting + +Building the cut path and applying it are separate steps, so a scene can draw the path and inspect +it before the topology changes: + +```cpp +bool prepareCutFromPlane(const Vec3& pointA, const Vec3& pointB, const Vec3& direction, SReal depth); +bool prepareCutFromTriangles(TriangleID triangleA, TriangleID triangleB, SReal depth); +bool applyCut(bool createSurface); +``` + +`prepareCutFrom*()` computes the intersections and leaves the topology untouched - with +`drawDebugCut="1"` they are drawn immediately. `applyCut()` commits them. `cutFromPlane()` and +`cutFromTriangles()` are exactly the two calls chained, for when the inspection step is not needed. + +That is what the `1` / `2` keys are: `1` prepares from the Data, `2` applies. + +`delayMode="1"` gives refinement the same treatment on key `3`: the first press computes the +neighbourhood table, the second subdivides from it. + +## Creating the cut surface + +With `surfaceCut="1"`, applying a cut also builds a renderable surface on each side of the cut and +maps it to the deforming mesh, so the inside of the cut is visible rather than showing through. +`textureName` is applied to both. + +This needs a specific scene structure. The tetrahedral node must contain a node holding a +`TriangleSetTopologyContainer` (mapped from the tetrahedra), and **that** node must itself have a +child node - the two cut surfaces are created underneath it: + +```xml + + ... + + + + + + + + + + + + + + + +``` + +If either node is missing the surface creation reports an error and is skipped. **The cut itself +still happens** - only the rendered surface is lost. + +> `textureName` is read at `init()`, and only when `surfaceCut` is already true. Turning +> `surfaceCut` on later from the GUI cuts and builds the surfaces, but leaves them untextured. + +See [`/InfinyToolkit/examples/MeshRefinement/SubdivisionController_CutWithSurface.scn`](../../examples/MeshRefinement/SubdivisionController_CutWithSurface.scn). + +## Data reference + +| Data | Type | Default | Description | +|--|--|--|--| +| `performCut` | `bool` | `false` | Performs the cut described by the Data below at the end of the next time step, then resets itself to `false` | +| `cutPointA` | `Vec3` | `0 0 0` | First point of the cut quad | +| `cutPointB` | `Vec3` | `0 0 0` | Second point of the cut quad | +| `cutDir` | `Vec3` | `0 -1 0` | Direction the quad is extruded along | +| `cutDepth` | `SReal` | `0` | Extrusion length. Also sets the snapping tolerance, at a quarter of this value | +| `surfaceCut` | `bool` | `false` | Build and render a surface on each side of the cut. [Scene requirements above](#creating-the-cut-surface) | +| `textureName` | `string` | `""` | Texture applied to those surfaces. Only read when `surfaceCut` is true at init | +| `testID` | `set` | empty | Tetrahedra refined by the `3` key | +| `refineCriteria` | `SReal` | `0` | Fraction of the mesh's mean tetrahedron volume under which a tetrahedron is left alone. `0` refines regardless | +| `delayMode` | `bool` | `false` | Split the `3` key in two presses: compute the neighbourhood table, then subdivide from it | +| `drawTetra` | `bool` | `false` | Draw the tetrahedra currently held by the subdividers | +| `drawScaleTetrahedra` | `float` | `1.0` | Scale of those drawn tetrahedra. Below `1.0` it leaves gaps between them, which makes the subdivision readable | +| `drawDebugCut` | `bool` | `false` | Draw the cut quad and the intersections it computed | + +Plus the standard `name`, `printLog` and `listening`. `printLog="1"` also turns on the engine's own +logging, which reports the point and tetrahedron counts before and after each operation - the +quickest way to tell whether an operation did anything. + +`listening` is already enabled by the component itself, so the keys work without setting it. + +The two `draw*` Data need a GUI: nothing is drawn under `runSofa -g batch`. + +## Keyboard shortcuts + +| Key | Action | +|--|--| +| `1` | Build the cut path from `cutPointA` / `cutPointB` / `cutDir` / `cutDepth`. The topology is not changed | +| `2` | Apply the cut built by `1`, honouring `surfaceCut` | +| `3` | Refine the tetrahedra listed in `testID`. With `delayMode`, two presses | +| `4` | Refine the whole mesh | + +Pressing `2` with no cut prepared does nothing. + +## Driving it from Python + +```python +controller = node.addObject('TetrahedronSubdivisionController', name='Cutter', + cutPointA=[-12, 0, -12], cutPointB=[12, 6, -12], + cutDir=[0, 0, 1], cutDepth=20) + +# set the Data and let the controller run the cut at the end of the next step +controller.performCut.value = True + +# or pick the tetrahedra to refine, then send the key +controller.testID.value = [40, 41, 42] +``` + +Only `performCut` triggers an operation from a Data. The other three are reached through a key +event or from C++. + +## Driving it from C++ + +Useful when a carving, collision or tool component decides where to cut: + +```cpp +#include + +using Controller = sofa::infinytoolkit::TetrahedronSubdivisionController; + +auto* controller = node->get(); + +// one-shot cut, with the surface +controller->cutFromPlane({-12, 0, -12}, {12, 6, -12}, {0, 0, 1}, 20.0, true); + +// or build it first, draw it, then commit +if (controller->prepareCutFromTriangles(triA, triB, 5.0)) +{ + // ... inspect / draw ... + controller->applyCut(false); +} + +// refinement +controller->refineTetrahedra({40, 41, 42}); +controller->refineFullMesh(); +``` + +Every method returns `false` rather than throwing: the controller failed to initialise, or an id +was out of range. Each failure also logs an error naming the reason. + +Link against `InfinyToolkit.MeshRefinement` and the component is instantiated for `Vec3Types`. Other +vector types need an explicit instantiation in both this plugin and `MeshRefinement`. + +## Example scenes + +All four are self-contained - the mesh is generated by the scene, so nothing external is needed. + +| Scene | Shows | Try | +|--|--|--| +| [`/InfinyToolkit/examples/MeshRefinement/SubdivisionController_CutFromPlane.scn`](../../examples/MeshRefinement/SubdivisionController_CutFromPlane.scn) | Cutting a beam along a plane, with the cut drawn before it is applied | `1` then `2` | +| [`/InfinyToolkit/examples/MeshRefinement/SubdivisionController_CutWithSurface.scn`](../../examples/MeshRefinement/SubdivisionController_CutWithSurface.scn) | The same cut with `surfaceCut="1"`, and the scene structure that needs | `1` then `2` | +| [`/InfinyToolkit/examples/MeshRefinement/SubdivisionController_RefineTetrahedra.scn`](../../examples/MeshRefinement/SubdivisionController_RefineTetrahedra.scn) | Refining three chosen tetrahedra and the neighbours that follow | `3` | +| [`/InfinyToolkit/examples/MeshRefinement/SubdivisionController_RefineFullMesh.scn`](../../examples/MeshRefinement/SubdivisionController_RefineFullMesh.scn) | Refining the whole mesh | `4` | + +``` +runSofa examples/SubdivisionController_CutFromPlane.scn +``` + +Further scenes, including Geomagic and CUDA variants and larger anatomical meshes, are in the +`examples/` folder of the MeshRefinement repository. + +## Troubleshooting + +| Symptom | Cause | +|--|--| +| `No topology found` at init | No `TetrahedronSetTopologyContainer` in the controller's node. Nothing else will run | +| `No TetrahedronSetTopologyModifier found.` / `...GeometryAlgorithms...` / `No BaseMechanicalState found.` | One of the [required components](#quick-start) is missing from the node. The engine reports each by name and stays unusable | +| `Controller is not initialised, cannot cut.` | Follows one of the above. Fix the init error first | +| Nothing happens on `1` and `2` | The cut quad misses the mesh. Turn on `drawDebugCut="1"` and check where it lands - `cutPointA`/`cutPointB` are absolute positions, not offsets | +| Nothing happens on `3` | `testID` is empty, or `refineCriteria` is high enough that every listed tetrahedron is below the volume threshold | +| `Tetrahedron id out of range` | `testID` names a tetrahedron the mesh does not have. Nothing is refined | +| `Triangle id out of range` | Same for `cutFromTriangles` | +| `not possible to compute Cut surface...` | `surfaceCut="1"` without the [node structure it needs](#creating-the-cut-surface). The cut still happened | +| Nothing is drawn | `drawTetra` / `drawDebugCut` are off, or the scene is running under `-g batch` | + +## Tests + +`InfinyToolkit.MeshRefinement_test` covers the four operations, the two-phase form, the key +handling and the out-of-range guards. Its scenes live in `tests/scenes/` so the test needs nothing +from the MeshRefinement repository at run time. + +``` +ctest --test-dir -C Release -R InfinyToolkit.MeshRefinement_test +``` + +## License + +Dual-licensed, GPL or commercial, like the rest of InfinyToolkit - see +[LICENSE.md](../../LICENSE.md). The `MeshRefinement` plugin it drives is licensed separately; +contact contact@infinytech3d.com. diff --git a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.h b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.h index 90c99f2..7454ec7 100644 --- a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.h +++ b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.h @@ -107,8 +107,9 @@ class TetrahedronSubdivisionController : public core::behavior::BaseController /// @return false if the controller is unusable. bool refineFullMesh(); - /// Subdivides the tetrahedra given by @p ids. @p criteria is the edge length - /// under which an edge is left whole; 0 subdivides regardless of length. + /// Subdivides the tetrahedra given by @p ids. @p criteria is a fraction of the + /// mean tetrahedron volume of the mesh: a tetrahedron whose volume is not above + /// criteria * that mean is left alone. 0 subdivides regardless of volume. /// @return false if the controller is unusable. bool refineTetrahedra(const std::set& ids, SReal criteria = 0.0); /// @} @@ -148,7 +149,8 @@ class TetrahedronSubdivisionController : public core::behavior::BaseController /// Tetrahedra subdivided by the '3' key. @sa refineTetrahedra Data > d_testID; - /// Edge length under which an edge is left whole; 0 subdivides regardless. + /// Fraction of the mesh's mean tetrahedron volume under which a tetrahedron is + /// left alone; 0 subdivides regardless of volume. Data d_refineCriteria; /// Splits the '3' key in two presses: the first computes the neighbourhood table, /// the second subdivides from it. diff --git a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.inl b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.inl index d9f5900..004381a 100644 --- a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.inl +++ b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.inl @@ -43,7 +43,7 @@ TetrahedronSubdivisionController::TetrahedronSubdivisionController() , d_surfaceCut(initData(&d_surfaceCut, false, "surfaceCut", "to activate new surface cut")) , d_textureName(initData(&d_textureName, std::string(""), "textureName", "texture to apply on the surface created by the cut")) , d_testID(initData(&d_testID, std::set(), "testID", "Ids of the tetrahedra subdivided by the '3' key")) - , d_refineCriteria(initData(&d_refineCriteria, SReal(0.0), "refineCriteria", "Edge length under which an edge is not subdivided. 0 subdivides regardless of length")) + , d_refineCriteria(initData(&d_refineCriteria, SReal(0.0), "refineCriteria", "Fraction of the mesh mean tetrahedron volume under which a tetrahedron is not subdivided. 0 subdivides regardless of volume")) , d_delayMode(initData(&d_delayMode, false, "delayMode", "Split the '3' key in two presses: compute the neighbourhood table, then subdivide from it")) , d_drawTetra(initData(&d_drawTetra, false, "drawTetra", "Draw the tetrahedra held by the subdividers")) , d_drawScaleTetrahedra(initData(&d_drawScaleTetrahedra, (float) 1.0, "drawScaleTetrahedra", "Scale of the terahedra (between 0 and 1; if <1.0, it produces gaps between the tetrahedra)"))