Modeling

In this section, we model and display three simple shapes: the cube, torus, and cylinder in order. The completed code is shown section2_4.rs.

modeling

Modeling functions

Previously, we read model data from an external obj file. This time, we will create a model from scratch. Truck is an OSS that is designed to be a CAD Kernel, and it has already implemented a modeling function using boundary representation, although it is still simple. Leaving aside the details of the internal implementation, let's quickly model with the intuitive interface provided by truck-modeling.

Types

Before we get into modeling, let's talk about the types of topological elements defined in truck. There are the following types of topological elements in truck-modeling::topology.

  • Vertex - the minimum topological unit
  • Edge - connecting two different vertices
  • Wire - a list of the edges, whose entity is VecDeque
  • Face - whose boundaries are described by a closed wire
  • Shell - a set of faces, the order of faces does not matter
  • Solid - whose boundaries are described by a closed shell

Cube

Let's model a cube. The module builder in truck-modeling has a function "tsweep" that sweeps points, lines, and surfaces along a straight line, so we can draw a cube in just four steps!

#![allow(unused)]
fn main() {
// modeling a cube
fn cube() -> Solid {
    let vertex: Vertex = builder::vertex(Point3::new(-1.0, 0.0, -1.0));
    let edge: Edge = builder::tsweep(&vertex, 2.0 * Vector3::unit_z());
    let face: Face = builder::tsweep(&edge, 2.0 * Vector3::unit_x());
    builder::tsweep(&face, 2.0 * Vector3::unit_y())
}
}

If you are trained in shapes, you may have already understood everything from the code alone. From here on, I will explain these four lines of code, adding comments and pictures.

First, put a vertex at the coordinates (-1, 0, -1).

#![allow(unused)]
fn main() {
// put a vertex at the point (-1, 0, -1)
let vertex: Vertex = builder::vertex(Point3::new(-1.0, 0.0, -1.0));
}

This is what it looks like in a diagram. It is so obvious that it may be difficult to understand.

takeavertex-cube.svg

Next, sweep the vertex by 2 in the z-axis direction to create a line segment.

#![allow(unused)]
fn main() {
// sweep the vertex along the z-axis
let edge: Edge = builder::tsweep(
    // the reference to the vertex
    &vertex,
    // sweep along the z-axis for length 2
    2.0 * Vector3::unit_z(),
);
}

This is what it looks like in the diagram. Now you can see what we want to do a little bit better.

tsweep-vertex.svg

In the third line, the segment is swept by 2 in the x-axis direction to create a surface.

#![allow(unused)]
fn main() {
// sweep the edge along the x-axis
let face: Face = builder::tsweep(
    // the reference to the edge
    &edge,
    // sweep along the x-axis for length 2
    2.0 * Vector3::unit_x(),
);
}

Euclid once said. A plane is a flat surface with straight lines on it.

tsweep-edge.svg

Finally, sweep the plane in the y-axis direction to make a cube.

#![allow(unused)]
fn main() {
// sweep the face along the y-axis
builder::tsweep(
    // the reference to the face
    &face,
    // sweep along the y-axis for length 2
    2.0 * Vector3::unit_y(),
)
}

This is the last of the diagrams.

tsweep-face.svg

Well, the vector image I tried so hard to draw with the mouse turned out to be a bit rectangular. Even this clumsy author can draw a cube in just four steps with truck!

Torus

Next, we model a torus. This one is modeled with the function builder::rsweep, which allows sweeping along a circle arc. One can create a torus in just three lines with truck.

#![allow(unused)]
fn main() {
// modeling torus
fn torus() -> Shell {
    let vertex: Vertex = builder::vertex(Point3::new(0.0, 0.0, 1.0));
    let circle: Wire = builder::rsweep(&vertex, Point3::new(0.0, 0.5, 1.0), Vector3::unit_x(), Rad(7.0));
    builder::rsweep(&circle, Point3::origin(), Vector3::unit_y(), Rad(7.0))
}
}

These three lines are illustrated in the figure below.

model-torus

The previous function cube returned Solid, now torus returns Shell. Since the torus is a closed surface, we can create Solid from the output Shell. However, since we need to use a more primitive API, we leave it as Shell for now.

Anyway, let's look at it line by line. First, put the vertices.

#![allow(unused)]
fn main() {
// put a vertex at the point (0, 0, 1).
let vertex: Vertex = builder::vertex(Point3::new(0.0, 0.0, 1.0));
}

Next, sweep the vertices along a circle to create a circle.

#![allow(unused)]
fn main() {
// sweep the vertex along a circle
let circle: Wire = builder::rsweep(
    // the reference to the vertex
    &vertex,
    // a point on the axis
    Point3::new(0.0, 0.5, 1.0),
    // the direction of the axis
    Vector3::unit_x(),
    // If the specified value is greater than 2π radian, a closed shape will be generated.
    Rad(7.0),
);
}

Now we are considering 3D rotation. While 2D rotation requires a center and an angle, 3D rotation requires an axis and an angle. The axis of rotation is a line with an orientation, so we need to specify a point on the axis and the direction of the axis.

3-dim-rotation

If you specify an angle whose absolute value is no less than 2π, the curve have a closed topology. In this example, the value is 7.0, which is definitely more than 2π, It may be better to use 2.0 * PI.

Now, rotate this circle around the y-axis, and the torus is complete.

#![allow(unused)]
fn main() {
// sweep the circle along a circle
builder::rsweep(
    // the reference to the wire
    &circle,
    // a point on the axis
    Point3::origin(),
    // the direction of the axis
    Vector3::unit_y(),
    // If a value no less than 2π radian is specified, a closed shape will be generated.
    Rad(7.0),
)
}

Cylinder

Up to here, we have been working with sweeps alone, but this is not the case with cylinders. We need to attach a plane to the closed wire. As usual, let's look at the code first.

#![allow(unused)]
fn main() {
// modeling a cylinder
fn cylinder() -> Solid {
    let vertex: Vertex = builder::vertex(Point3::new(0.0, 0.0, -1.0));
    let circle: Wire = builder::rsweep(&vertex, Point3::new(0.0, 1.0, -1.0), Vector3::unit_z(), Rad(7.0));
    let disk: Face = builder::try_attach_plane(&vec![circle]).expect("cannot attach plane");
    builder::tsweep(&disk, 2.0 * Vector3::unit_z())
}
}

As usual, I will explain it line by line. Taking a vertex and turning it in a circle is the same as the torus.

#![allow(unused)]
fn main() {
// put a vertex at the point (0, 0, -1).
let vertex: Vertex = builder::vertex(Point3::new(0.0, 0.0, -1.0));
// sweep the vertex along circle
let wire: Wire = builder::rsweep(
    // the reference to the vertex
    &vertex,
    // a point on the axis
    Point3::new(0.0, 1.0, -1.0),
    // the direction of the axis
    Vector3::unit_z(),
    // If a value greater than 2π radian is specified, a closed shape will be generated.
    Rad(7.0),
);
}

The problem is next. Create a disk by attaching a plane to the circle.

#![allow(unused)]
fn main() {
// make a disk by attaching a plane to the circle
let face: Face = builder::try_attach_plane(&vec![wire]).expect("cannot attach plane");
}

The function try_attach_plane attaches a plane to a closed Wire in the same plane. As a whole truck, functions with the prefix try or search return Option or Result. In many cases, we provide unprefixed functions that cause panic, but this function is an exception.

Finally, sweep along the z-axis to complete the cylinder.

#![allow(unused)]
fn main() {
// sweep the face along the z-axis
builder::tsweep(
    // the reference to the disk
    &face,
    // sweep along the z-axis
    2.0 * Vector3::unit_z(),
)
}

Application handler

The shapes we created in the previous section were Shell and Solid. As with meshes, we need to convert these data into instances so that they can be rendered. Since this process is more time-consuming than mesh's one, we will not run them in the frame, but during application initialization and store the generated instances in member variables in the application handler.

#![allow(unused)]
fn main() {
// Declare the application handler
struct MyApp {
    // scene
    scene: Scene,
    // current drawn shape
    current_shape: i32,
    // the instance of cube
    cube: ShapeInstance,
    // the instance of torus
    torus: ShapeInstance,
    // the instance of cylinder
    cylinder: ShapeInstance,
}
}

App::init

We now override App::init to initialize the application handler. We don't do anything new until the scene initialization, so we just post the code here.

#![allow(unused)]
fn main() {
// radius of circumscribed circle
let radius = 5.0 * f64::sqrt(2.0);
// Useful constants for lights placement.
let omega = [0.5, f64::sqrt(3.0) * 0.5];

// the vector of lights
let lights = vec![
    Light {
        position: Point3::new(radius * omega[0], 6.0, radius * omega[1]),
        // The color vector should be divided by 3.0. If not, the white will be satiated.
        color: Vector3::new(1.0, 1.0, 1.0) / 3.0,
        ..Default::default()
    },
    Light {
        position: Point3::new(-radius, 5.0, 0.0),
        // The color vector should be divided by 3.0. If not, the white will be satiated.
        color: Vector3::new(1.0, 1.0, 1.0) / 3.0,
        ..Default::default()
    },
    Light {
        position: Point3::new(radius * omega[0], 4.0, -radius * omega[1]),
        // The color vector should be divided by 3.0. If not, the white will be satiated.
        color: Vector3::new(1.0, 1.0, 1.0) / 3.0,
        ..Default::default()
    },
];

// Create the scene
let scene = Scene::new(
    device_handler.clone(),
    &SceneDescriptor {
        // use the default camera
        camera: Default::default(),
        lights,
        ..Default::default()
    },
);
}

Model the shape using the created functions so far, and instantiate it as it is.

#![allow(unused)]
fn main() {
// create cube instance
let cube = scene.create_instance(&cube(), &Default::default());
// create torus instance
let torus = scene.create_instance(&torus(), &Default::default());
// create cylinder instance
let cylinder = scene.create_instance(&cylinder(), &Default::default());
}

Let's use these to initialize and return the application handler.

#![allow(unused)]
fn main() {
// Return the application handler
MyApp {
    scene,
    current_shape: -1,
    cube,
    torus,
    cylinder,
}
}

App::update

This is the updating process for each frame. The camera rotation is almost the same as the previous section, so we just post the code.

#![allow(unused)]
fn main() {
// the seconds since the application started.
let time = self.scene.elapsed().as_secs_f64();

// the mutable references to the camera
let camera = &mut self.scene.descriptor_mut().camera;

// update camera matrix
camera.matrix = Matrix4::from_axis_angle(Vector3::unit_y(), Rad(time))
    * Matrix4::look_at(
        Point3::new(4.0, 5.0, 4.0),
        Point3::new(0.0, 1.0, 0.0),
        Vector3::unit_y(),
    )
    .invert()
    .unwrap();
}

Now let's write the process of replacing the model. In this video, each time the camera goes around the

  1. cube
  2. torus
  3. cylinder

is displayed in order. In other words, we divide the number of times the camera went around the model by three, and the remainder number represents the shape that should be displayed. Since the camera rotates by one radian per second, it takes 2.0 * PI seconds per revolution. Therefore, the number of revolutions of the camera can be calculated by dividing the application start time by 2.0 * PI.

#![allow(unused)]
fn main() {
// the number of the shape which should be displayed
let laps = (time / (2.0 * PI)) as i32 % 3;
}

If you replace the shape only when the currently instance number self.current_shape is different from the shape number laps, which is derived from the number of camera lapses, you can complete the behavior that the shape is replaced every lap.

#![allow(unused)]
fn main() {
// the timing for changing the drawn shape
if laps != self.current_shape { /* changing the shape */ }
}

Let's write the contents of "changing the shape" process. First of all, synchronize self.current_shape and laps before forgetting them.

#![allow(unused)]
fn main() {
// synchronize variables
self.current_shape = laps;
}

Removes the currently rendered object from the scene.

#![allow(unused)]
fn main() {
// clear all objects in the scene
self.scene.clear_objects();
}

Register an instance in the scene according to the laps number.

#![allow(unused)]
fn main() {
// laps == 0 => cube, laps == 1 => torus, laps == 2 => cylinder
match laps {
    0 => self.scene.add_objects(&self.cube.render_faces()),
    1 => self.scene.add_objects(&self.torus.render_faces()),
    _ => self.scene.add_objects(&self.cylinder.render_faces()),
};
}

For polygon meshes, we used Scene::add_object, but this time Scene::add_objects is used. Unlike with polygons, each face of a modeled shape is rendered, so we use ShapeInstance::render_faces to create the FaceInstance vector and register them with Scene::add_objects.

This completes all the new implementation. If you define the usual main function and the spell App::render, you can play the video at the beginning of this section.