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):

Cup

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:

Result