A procedural mesh generation library for Wonderland Engine, with support for asynchronous Constructive Solid Geometry powered by Manifold.
Install Gypsum and the custom Manifold build with:
npm install --save-dev gypsum-mesh github:playkostudios/manifold#package
In your build script, make sure to copy the Manifold library and the Gypsum
worker to the deployment folder, by doing something such as this in your
package.json
:
{
"scripts": {
"copy-worker": "shx cp node_modules/gypsum-mesh/dist/gypsum-manifold.worker.* deploy/",
"copy-manifold": "shx cp node_modules/manifold-3d/manifold.js deploy/ && shx cp node_modules/manifold-3d/manifold.wasm deploy/",
"build-bundle": "esbuild ./js/bundle.js --minify --sourcemap --bundle --platform=browser --outfile=\"deploy/gypsum-example-bundle.js\"",
"build": "npm run build-bundle && npm run copy-worker && npm run copy-manifold"
}
}
npm install
npm run build
Builds will be placed in the dist
directory.
Note that, for now, this project uses a custom build of the Manifold WebAssembly bindings, which can be found here. Once version 2 of the Manifold bindings is released, the official build will be used.
An example Wonderland Engine project can be found on a different respository
API documentation can be found on Github Pages.
Procedural meshes don't use the WL.Mesh class. Instead, they use their own class
(MeshGroup
) which contains a list of WL.Mesh and WL.Material pairs
(submeshes), so that a single procedural mesh can have multiple materials.
Procedural meshes also have utilities for applying transformations.
To get the submeshes of a procedural mesh procMesh
, call
procMesh.getSubmeshes()
. This will return a list of submeshes, where each
submesh is a pair of WL.Mesh and WL.Material instances. Note that if no material
is provided for a submesh, then the material will be null, so make sure to
either always specify a material, or to have a fallback material ready.
Getting the submeshes is necessary for rendering the procedural mesh:
for (const [mesh, material] of procMesh.getSubmeshes()) {
this.object.addComponent('mesh', {
mesh,
material: material ?? fallbackMaterial
});
}
A procedural mesh procMesh
can be translated (translate
method), scaled
(scale
and uniformScale
methods) and rotated (rotate
method). A
gl-matrix
transformation matrix can also be applied to a procedural mesh by
calling the transform
method.
For example, this rotates a procedural mesh by 45 degrees around the Y axis:
procMesh.rotate(quat.fromEuler(quat.create(), 0, 45 ,0));
Transformation methods are chainable.
All procedural meshes are implemented as subclasses of the MeshGroup
class. To
make a new procedural mesh, simply instantiate a subclass. The constructor
arguments usually have 2 or more required argument, and all optional arguments
are passed in an options object which can be omitted. The first argument is
always a Wonderland Engine instance (the WL
object for 0.9.5, or this.engine
in future versions); this is to prepare for future versions where the WL object
will no longer be global.
For example, a 1x1x1 cube can be created by creating a new CubeMesh
instance:
const procMesh = new CubeMesh(WL, 1);
However, more options can be passed via an options object. For example, in this 6x6x6 cube, the cube is not centered unlike before, each face has a separate material, and the UVs for each face are set to each UV corner:
const procMesh = new CubeMesh(WL, 6, {
center: false,
// supply WL.Material instances for the following properties:
// instead of a separate material for each side, `material` can also be passed to set all sides
leftMaterial: this.leftMaterial,
rightMaterial: this.rightMaterial,
downMaterial: this.downMaterial,
upMaterial: this.upMaterial,
backMaterial: this.backMaterial,
frontMaterial: this.frontMaterial,
// supply UV coordinates for the top-left, top-right, bottom-left and bottom-right corners of a face by passing the following properties:
// a position-to-UV ratio can also be provided instead of an array of UVs
leftUVs: [[0, 1], [1, 1], [0, 0], [1, 0]],
rightUVs: [[0, 1], [1, 1], [0, 0], [1, 0]],
downUVs: [[0, 1], [1, 1], [0, 0], [1, 0]],
upUVs: [[0, 1], [1, 1], [0, 0], [1, 0]],
backUVs: [[0, 1], [1, 1], [0, 0], [1, 0]],
frontUVs: [[0, 1], [1, 1], [0, 0], [1, 0]],
});
A list of all procedural mesh classes can be found in the API documentation, in
the Procedural Mesh
category.
Currently, the following procedural meshes are available:
User-provided meshes (as WL.Mesh instances) can be converted to MeshGroup
instances if you need to trasform them:
// with material
const procMesh = MeshGroup.fromWLEMesh(mesh, material);
// ... or without material (material will be null)
// const procMesh = MeshGroup.fromWLEMesh(mesh);
// apply transformation to mesh group
procMesh.uniformScale(0.01);
The constructor can also be used if you want to make a MeshGroup
consisting of
multiple user-provided meshes. In this case, a list of submeshes (mesh and
material pairs) must be supplied:
const procMesh = new MeshGroup([
[firstMesh, firstMaterial],
[secondMesh, null], // second mesh has no material, null must be passed
[thirdMesh, thirdMaterial],
]);
// apply transformation to mesh group
procMesh.uniformScale(0.01);
Note that creating MeshGroup
instances with user-provided meshes can fail if
the meshes are used for CSG operations. This is because there is no connectivity
information in the meshes, so the connectivity between triangles must be
guessed. Currently, a naive algorithm based on vertex distance is used for
guessing connectivity, but this fails if singularities exist, or if there are
shared edges between more than 2 triangles.
Note that transforming MeshGroup
instances created from user-provided meshes
will modify the original meshes in-place. If you want to keep the original
mesh intact, then pass a clone of the original mesh to the MeshGroup
, instead
of using the original mesh. A mesh clone function is available in the library
(cloneMesh
), but note that it does not copy skinning data.
Procedural meshes create new WL.Mesh
instances used for the submeshes of a
MeshGroup
. WL.Mesh
instances are not automatically destroyed, as they are an
engine resource, and therefore aren't garbage-collected.
If you are creating a procedural mesh and only using it for rendering, then the meshes don't have to be destroyed. They only need to be destroyed after you know you will no longer render them.
To destroy all the meshes in a MeshGroup
, call MeshGroup.dispose()
. For
example, if you have a procedural mesh procMesh
, then call
procMesh.dispose()
. Note that if a MeshGroup
is created from a user-provided
mesh, then the mesh will still be destroyed, meaning that if you passed the
original mesh without cloning to the MeshGroup
, then that mesh will be
destroyed. If a clone was passed instead, then the clone will be destroyed.
If you are creating a mesh that is only used for a CSG operation, and then never
used again, then it is recommended that you set the auto-dispose flag by calling
MeshGroup.mark()
. For example, if you have a procedural cube diffCube
that
is only used for subtracting another mesh in a CSG operation, then you can mark
the cube to be auto-disposed by calling diffCube.mark()
, and the cube will be
disposed after it's used for a CSG operation. Attempting to use the cube after
it was disposed will not work. The mark
method is chainable.
CSG operations are done by making a tree of CSG operations, sending the tree to a worker, and waiting for the operations to finish asynchronously. However, there can be more than 1 worker, and the number of workers is configurable, so some extra setup work needs to be done; a pool of workers needs to be created, with the wanted number of workers, and the CSG operations are dispatched via this worker pool. The worker pool will then decide which worker to send the CSG operation to; the CSG pool load-balances.
Example (CSG pool has only 1 worker here):
// create a new pool of workers. note that the workers are not initialised until
// the first csg operation is dispatched, or until `initialize` is called. note
// that this should be shared between multiple scripts to avoid creating too
// many workers. always reuse a csg pool
const csg = new CSGPool(1);
// if you want to make sure that the pool is initialized before the first csg
// operation to prevent stuttering, then do the following:
// await csg.initialize();
// subtract 2 cubes, where the subtracting cube is offset by (0.5, 0.5, 0.5)
const resultMesh = await csg.dispatch(WL, {
operation: 'subtract',
left: new CubeMesh(WL, 1).mark(),
right: new CubeMesh(WL, 1).translate([0.5, 0.5, 0.5]).mark(),
});
// get submeshes of the result of the CSG operation
const resultSubmeshes = resultMesh.getSubmeshes();
// add each submesh to the scene
for (const [mesh, material] of resultSubmeshes) {
this.object.addComponent('mesh', {
mesh,
material: material ?? this.fallbackMaterial
});
}
CSGPool
are expensive to create, and there is a limit of workers that can be
created in a browser. Ideally, there should be only 1 CSGPool
instance with a
reasonable amount of workers (such as 3), and the pool should be reused accross
all scripts that do CSG operations.
If no more CSG operations will be done, then a CSGPool
can be destroyed by
calling the dispose
method. For example, in a pool csg
:
csg.dispose();
Disposing a pool will invalidate it; any operations done on a disposed pool will throw an error. Disposing a pool will also terminate all workers created by the pool.
The current API is ugly and should be considered unstable; expect changes to the API in the future. Contributions improving the usability of the API would be greatly appreciated, especially around the Triangle and MeshBuilder classes.
The Kanban for this library can be found here.
Summary:
This project depends on:
This projects uses following tooling:
This project reuses some procedural mesh generation code from the OctreeCSG-ea library, which is a fork of the OctreeCSG library.
Finally, the following resources were used to implement some algorithms:
Computational Geometry: Algorithms and Applications
(Mark de Berg, Otfried Cheong, Marc van Kerveld, Mark Overmars) - used for the triangulation algorithmComputation of Rotation Minimizing Frames
(Wenping Wang, Bert Juttler, Dayue Zheng, and Yang Liu) - used for curve frame generationGenerated using TypeDoc