Tutorial 2 - Using Mesh and Surface Set
- Build Instruction
- Tutorial 1 - Hello, Jet!
- Tutorial 2 - Using Mesh and Surface Set
- Tutorial 3 - Using Python API
- Manual (Feature) Tests
- Unit Tests
- Performance Tests
- Supported Platforms
- C++ API Reference
- Python API Reference
Introduction
In this tutorial, we will extend our previous example and learn how to use arbitrary input mesh in our scene. Also, we will see how to use multiple surfaces for either collider or emitter.
Initial Setup
Let’s start building our simulation by setting up the solver. This time, we will use ApicSolver3
which is based on 3-D Affine Particle-in-Cell (APIC) solver from the Jiang et al.’s 2015 SIGGRAPH paper. You can use any other solvers to have mesh in the scene; APIC is used just for the demonstration purpose. Anyway, our starter code looks like below:
#include <jet/jet.h>
using namespace jet;
int main() {
auto solver = ApicSolver3::builder()
.withResolution({50, 150, 50})
.withDomainSizeX(4)
.withOrigin({-2, -2, -2})
.makeShared();
// More codes to come
for (Frame frame; frame.index < 120; ++frame) {
solver->update(frame);
}
return 0;
}
The code above builds APIC solver with 50 x 100 x 50 resolution which occupies 4m x 8m x 4m box, and the corner of the box is positioned at (-2m, -2m, -2m).
Loading Mesh
Now consider we want to have a cup in our new scene and drop some water into it. In this example, we are going to use a mesh file for the cup and use that as our collider. Jet framework provides generic triangle mesh structure with TriangleMesh3
class which also comes with OBJ file loader. Take a look at the following code:
auto mesh = TriangleMesh3::builder().makeShared();
std::ifstream file("cup.obj");
if (file) {
mesh->readObj(&file);
file.close();
}
This code will load the following cup model which can be found under <jet>/resources
directory after building the code (thanks to Alexander M for providing the model):
Using Mesh
From our previous example, we simple plugged the sphere geometry to the collider. For TriangleMesh3
to be used as an input to a collider or emitter, we need one more step to convert the mesh into volumetric geometry. Take a look at the following code first:
auto cup = ImplicitTriangleMesh3::builder()
.withTriangleMesh(mesh)
.withResolutionX(100)
.makeShared();
The code above takes mesh
and wraps it ImplicitTriangleMesh3
. This new class, ImplicitTriangleMesh3
, converts mesh into a signed-distance field (SDF) using grid. In order to act as a collider surface or emitter volume, the surface must support SDF, and this new helper class provide such an interface. Other than the original mesh, this class also requires one more parameter that specifies the resolution of the SDF. We use 100 in this example.
Once the SDF is created, we can assign it to collider as usual:
auto collider = RigidBodyCollider3::builder()
.withSurface(cup)
.makeShared();
solver->setCollider(collider);
Using Multiple Surfaces
If we want to combine existing surfaces to a single surface, we can use merged OBJ file, but we can also define set of surfaces for better flexibility. Now, assume that we want to add two spheres into a single emitter input. We do that by introducing SurfaceSet3
object as shown below:
auto sphere1 = Sphere3::builder()
.withCenter({-0.01, 1.0, -0.01})
.withRadius(0.4)
.makeShared();
auto sphere2 = Sphere3::builder()
.withCenter({0.01, 2.0, 0.01})
.withRadius(0.4)
.makeShared();
auto spheres = SurfaceSet3::builder()
.withSurfaces({sphere1, sphere2})
.makeShared();
auto emitter = VolumeParticleEmitter3::builder()
.withSurface(spheres)
.withSpacing(0.5 * gridSpacing)
.withJitter(0.1)
.makeShared();
solver->setParticleEmitter(emitter);
As you can see, our new surface type, SurfaceSet3
takes list of other surfaces (using .withSurfaces({sphere1, sphere2})
) and merge them into a single surface object.
Putting All Together
To complete the scene, we also need to add emitter. Including the emitter setup, below is our final code:
#include <jet/jet.h>
using namespace jet;
int main() {
auto solver = ApicSolver3::builder()
.withResolution({50, 150, 50})
.withDomainSizeX(4)
.withOrigin({-2, -2, -2})
.makeShared();
// Read mesh
auto mesh = TriangleMesh3::builder().makeShared();
std::ifstream file("cup.obj");
if (file) {
mesh->readObj(&file);
file.close();
}
// Convert to SDF
auto cup = ImplicitTriangleMesh3::builder()
.withTriangleMesh(mesh)
.withResolutionX(100)
.makeShared();
// Setup collider
auto collider = RigidBodyCollider3::builder()
.withSurface(cup)
.makeShared();
solver->setCollider(collider);
// Setup emitter
const double gridSpacing = solver->gridSpacing().x;
auto sphere1 = Sphere3::builder()
.withCenter({-0.01, 1.0, -0.01})
.withRadius(0.4)
.makeShared();
auto sphere2 = Sphere3::builder()
.withCenter({0.01, 2.0, 0.01})
.withRadius(0.4)
.makeShared();
auto spheres = SurfaceSet3::builder()
.withSurfaces({sphere1, sphere2})
.makeShared();
auto emitter = VolumeParticleEmitter3::builder()
.withSurface(spheres)
.withSpacing(0.5 * gridSpacing)
.withJitter(0.1)
.makeShared();
solver->setParticleEmitter(emitter);
// Run simulation
for (Frame frame; frame.index < 120; ++frame) {
solver->update(frame);
}
return 0;
}
And the final result is shown below: