Higher Grade Modeling

In the earlier section, we used the functions of the builder module to create simple shapes such as cubes and cylinders. In this section, we will model a bottle using a more primitive API. The completed code is shown section2_6.rs.

bottle

This bottle is based on the tutorial by Open CASCADE Technology (OCCT), a great senior of truck. OCCT's tutorial properly modeled the fillets and the threading. This shows that truck is still in its infancy in terms of functionality. Our immediate goal for truck is to be able to model this bottle perfectly with as much coding as fits to this section.

GUI part

First, let's create the GUI part for checking the modeling process. In the bottle function, which is a function for modeling a bottle, we now put a temporary code for modeling a unit cube.

#![allow(unused)]
fn main() {
// modeling the bottle
fn bottle(_height: f64, _width: f64, _thickness: f64) -> Solid {
    // temporary code for modeling a unit cube
    let vertex: Vertex = builder::vertex(Point3::new(-0.5, -0.5, -0.5));
    let edge: Edge = builder::tsweep(&vertex, Vector3::unit_z());
    let face: Face = builder::tsweep(&edge, Vector3::unit_x());
    builder::tsweep(&face, Vector3::unit_y())
}
}

Import

Since we will be doing event processing, we will import the structure to be used for event processing.

#![allow(unused)]
fn main() {
mod app;
use app::App;
use std::f64::consts::PI;
use truck_platform::*;
use truck_rendimpl::*;
use wgpu::{AdapterInfo, SwapChainFrame};
use winit::{dpi::*, event::*, event_loop::ControlFlow}; // <- New!!
}

Application handler

This time, we will move the model by dragging. In the application handler, we will add a flag for mouse dragging and a member variable to remember the mouse position in the previous frame.

#![allow(unused)]
fn main() {
// the application handler
struct MyApp {
    // scene
    scene: Scene,
    // dragging flag
    rotate_flag: bool,
    // position of the cursor at the previous frame.
    prev_cursor: Vector2,
}
}

App::init

There is nothing new to explain about App::init anymore. I'll just post the source code.

#![allow(unused)]
fn main() {
// constructor
fn init(device_handler: &DeviceHandler, _: AdapterInfo) -> Self {
    let mut scene = Scene::new(
        device_handler.clone(),
        &SceneDescriptor {
            camera: Camera::perspective_camera(
                Matrix4::look_at(
                    Point3::new(1.5, 1.5, 1.5),
                    Point3::origin(),
                    Vector3::unit_y(),
                )
                .invert()
                .unwrap(),
                Rad(PI / 4.0),
                0.1,
                40.0,
            ),
            lights: vec![Light {
                position: Point3::new(1.5, 1.5, 1.5),
                color: Vector3::new(1.0, 1.0, 1.0),
                light_type: LightType::Point,
            }],
            ..Default::default()
        },
    );
    
    // modeling the bottle and signup to the scene
    let bottle = bottle(1.4, 1.0, 0.6);
    let instance = scene.create_instance(
        &bottle,
        &InstanceDescriptor {
            // smooth plastic texture
            material: Material {
                albedo: Vector4::new(0.75, 0.75, 0.75, 1.0),
                reflectance: 0.2,
                roughness: 0.2,
                ambient_ratio: 0.02,
            },
            ..Default::default()
        },
    );
    scene.add_objects(&instance.render_faces());

    // Return the application handler
    MyApp {
        scene,
        // The mouse is not dragged when the application starts.
        rotate_flag: false,
        prev_cursor: Vector2::zero(),
    }
}
}

Now, add App::render and main as usual, and run it. You will see the window depicting the cube, as shown below.

cube

Update scene

App::update does not have to be overridden this time because there are no changes that need to be made in every frame. Instead, we will set up various event handlers. However, as explained at the very beginning when we got app.rs, app.rs provides an MFC-like API, so there is no need to set up complicated event listeners. We just need to override the corresponding functions.

Mouse wheel

It works with the mouse wheel to zoom in and out on the model. There are two ways to enlarge a model in a perspective projection camera:

  • Move the camera itself closer
  • Reducing the FOV

This time, we go with the one that brings the physical distance of the camera closer. The behavior of the mouse wheel event can be defined by overriding App::mouse_wheel.

#![allow(unused)]
fn main() {
/// Processing when the mouse wheel is moved.
fn mouse_wheel(&mut self, delta: MouseScrollDelta, _: TouchPhase) -> ControlFlow {
    match delta {
        // use only y-delta
        MouseScrollDelta::LineDelta(_, y) => {
            // get the mutable references to camera and light
            let sc_desc = self.scene.descriptor_mut();
            let (camera, light) = (&mut sc_desc.camera, &mut sc_desc.lights[0]);
            // Translation to the eye direction by 0.2 times the value obtained from the wheel.
            let trans = Matrix4::from_translation(camera.eye_direction() * 0.2 * y as f64);
            // move the camera and light
            camera.matrix = trans * camera.matrix;
            light.position = camera.position();
        }
        _ => {}
    };
    // Return a command to wait 1/60 second.
    Self::default_control_flow()
}
}

Now you can use the mouse wheel to zoom in and out.

wheel

Mouse dragging

The rotation of the model by dragging the mouse is achieved by a combination of the following two events:

  • Set rotate_flag to true and false when the left mouse button is pressed and released, respectively.
  • If rotate_flag == true, the camera will be rotated when the cursor is moved.

The behavior for mouse button events can be defined by overriding App::mouse_input.

#![allow(unused)]
fn main() {
// Called when the mouse button is pressed and released.
fn mouse_input(&mut self, state: ElementState, button: MouseButton) -> ControlFlow {
    match button {
        // Behavior when the left button is pressed or unpressed
        MouseButton::Left => {
            // pressed => start dragging, released => end dragging.
            self.rotate_flag = state == ElementState::Pressed;
        }
        _ => {}
    }
    // Return a command to wait 1/60 second.
    Self::default_control_flow()
}
}

The behavior when the cursor is moved can be defined by overriding App::cursor_moved.

#![allow(unused)]
fn main() {
// Called when the cursor is moved
fn cursor_moved(&mut self, position: PhysicalPosition<f64>) -> ControlFlow {
    let position = Vector2::new(position.x, position.y);
    if self.rotate_flag { /* rotate the camera */  }
    // assign the current cursor position to "previous cursor position"
    self.prev_cursor = position;
    // Return a command to wait 1/60 second.
    Self::default_control_flow()
}
}

All that is left is to implement the rotation of the camera according to the mouse movement. This is not related to the main topic of modeling, but it is complex. So, we will not explain it in detail. One can copy and paste the following code as a sample code. If you are good at geometry on 3D space and are interested in implementation, please search for "trackball camera implementation".

#![allow(unused)]
fn main() {
// get the mutable references of camera and light
let desc = self.scene.descriptor_mut();
let (camera, light) = (&mut desc.camera, &mut desc.lights[0]);
// get the delta of cursor move
let dir2d = position - self.prev_cursor;
// Do nothing if the delta is so small.
if dir2d.so_small() {
    return Self::default_control_flow();
}
// axis of rotation
let axis = (dir2d[1] * camera.matrix[0].truncate()
    + dir2d[0] * camera.matrix[1].truncate())
.normalize();
// angle of rotation. 0.01 times the pixel distance.
let angle = dir2d.magnitude() * 0.01;
// rotation matrix. The rotation angle is minus, as the camera is moved.
let mat = Matrix4::from_axis_angle(axis, Rad(-angle));
// move the camera and light.
camera.matrix = mat * camera.matrix;
light.position = camera.position();
}

With the above implementation of event handling, we are finally able to rotate the model.

rotate-cube

Modeling

Creating the shapes of the parts

First, we will create the basic shape of the bottle. We will create the bottle by cutting and pasting the boundaries of the shape created here.

Cylinder

First, we will create a function to model the cylinder of the tap. This is almost the same as the one we made in the earlier section, so we post the whole source code.

#![allow(unused)]
fn main() {
// modeling a cylinder
// # Arguments
// - bottom: y-coordinate of the bottom disk
// - height: height of the cylinder
// - radius: radius of the bottom disk
fn cylinder(bottom: f64, height: f64, radius: f64) -> Shell {
    // make a solid cylinder
    let vertex = builder::vertex(Point3::new(0.0, bottom, radius));
    let circle = builder::rsweep(&vertex, Point3::origin(), Vector3::unit_y(), Rad(7.0));
    let disk = builder::try_attach_plane(&vec![circle]).unwrap();
    let solid = builder::tsweep(&disk, Vector3::new(0.0, height, 0.0));
    // Return the solid as a boundary shell for easier processing later.
    solid.into_boundaries().pop().unwrap()
}
}

The new part is the use of Solid::into_boundaries. Since Solid is a complete solid surrounded by closed boundaries, it is not possible to extract a single boundary face and process it. Therefore, the function should return the Shell of the boundary. Let's rewrite the contents of bottle to check the operation.

#![allow(unused)]
fn main() {
fn bottle(_height: f64, _width: f64, _thickness: f64) -> Solid {
    // remake solid
    Solid::new(vec![cylinder(-0.5, 1.0, 0.5)])
}
}

When you run it, you will see a screen with the cylinder displayed.

rotate-cylinder

Body

Let's create the body part. The module builder provides the functions circle_arc and homotopy. circle_arc creates a circular arc connecting two vertices and passing through a point. The homotopy function may be unfamiliar. As shown in the figure below, for given two ridges edge0 and edge1, it creates a surface by connecting them with a straight line along the parameter.

homotopy

Connect the two arcs made by circle_arc with homotopy to make the bottom of the body part.

body-bottom

Then, sweep it upward to form the body shape.

#![allow(unused)]
fn main() {
// modeling the body shape
// # Arguments
// - bottom: y-coordinate of the bottom face
// - height: height of the body
// - width: width of the body
// - thickness: thickness of the body
fn body_shell(bottom: f64, height: f64, width: f64, thickness: f64) -> Shell {
    // draw a circle arc
    let vertex0 = builder::vertex(Point3::new(-width / 2.0, bottom, thickness / 4.0));
    let vertex1 = builder::vertex(Point3::new(width / 2.0, bottom, thickness / 4.0));
    let transit = Point3::new(0.0, bottom, thickness / 2.0);
    let arc0 = builder::circle_arc(&vertex0, &vertex1, transit);
    // copy and rotate the circle arc
    let arc1 = builder::rotated(&arc0, Point3::origin(), Vector3::unit_y(), Rad(PI));
    // create the homotopy from arc0 to arc1.inverse()
    let face = builder::homotopy(&arc0, &arc1.inverse());
    // create the body
    let solid = builder::tsweep(&face, Vector3::new(0.0, height, 0.0));
    // Return the solid as a boundary shell for easier processing later.
    solid.into_boundaries().pop().unwrap()
}
}

Let's rewrite the contents of bottle again and run it.

#![allow(unused)]
fn main() {
fn bottle(height: f64, width: f64, thickness: f64) -> Solid {
    // remake solid
    Solid::new(vec![body_shell(-height / 2.0, height, width, thickness)])
}
}

The dimensions of the bottle are height = 1.4, width = 1.0, thickness = 0.6, although we passed it by without saying anything earlier. If you run it and see the following solid, you have succeeded.

The boundaries of a face and gluing two solids

Using the boundaries of a face, one can make a hole in the face. First, rewrite bottle as follows.

#![allow(unused)]
fn main() {
fn bottle(height: f64, width: f64, thickness: f64) -> Shell {
    // create body
    let mut body = body_shell(-height / 2.0, height, width, thickness);
    // the mutable reference to the ceiling of the body
    let ceiling = body.last_mut().unwrap();
    // the boundary to create a hole in the seiling
    let circle = builder::rsweep(
        &builder::vertex(Point3::new(thickness / 4.0, height / 2.0, 0.0)),
        Point3::new(0.0, height / 2.0, 0.0),
        -Vector3::unit_y(),
        Rad(7.0),
    );
    // add a boundary to the seiling
    ceiling.add_boundary(circle);
    body
}
}

If you run this, you can see a pitch black hole in the ceiling.

body-hole

In the function bottle above, the second line from the bottom, ceiling.add_boundary(circle), creates a hole in the face ceiling by a boundary circle. If one adds a cylinder with the bottom removed to the hole, xe can create a bottle without a hole.

#![allow(unused)]
fn main() {
// modeling a bottle
fn bottle(height: f64, width: f64, thickness: f64) -> Solid {
    // create the body of the bottle
    let mut body = body_shell(-height / 2.0, height, width, thickness);
    // create the neck of the bottle
    let neck = cylinder(height / 2.0, height / 10.0, thickness / 4.0);
    // sew the body and the neck
    grue_body_neck(&mut body, neck);
    // returns a solid
    Solid::new(vec![body])
}

// sew the body and the neck
fn glue_body_neck(body: &mut Shell, neck: Shell) {
    // get the body's ceiling
    let body_ceiling = body.last_mut().unwrap();
    // the boundary of the neck's bottom
    let wire = neck[0].boundaries()[0].clone();
    // drill a hole in the body using the boundary of the neck's bottom
    body_ceiling.add_boundary(wire);
    // add the faces of the neck to the body other than the bottom
    body.extend(neck.into_iter().skip(1));
}
}

If executed, it does indeed create a bottle shape that does not hold water.

non-hole-bottle

Orientations of faces and inner boundaries

Finally, the inside of the bottle is created. Create the inner part of the bottle in the same way as the outer part, then turn all the surfaces over and glue the inner and outer ceilings together on the boundary.

#![allow(unused)]
fn main() {
// modeling a bottle
fn bottle(height: f64, width: f64, thickness: f64) -> Solid {
    // create the body of the bottle
    let mut body = body_shell(-height / 2.0, height, width, thickness);
    // create the neck of the bottle
    let neck = cylinder(height / 2.0, height / 10.0, thickness / 4.0);
    // sew the body and the neck
    glue_body_neck(&mut body, neck);

    // distance between outer and inner surface, i.e. the thickness of the faces.
    let eps = height / 50.0;
    // inner body. Make it small enough to account for thickness.
    let mut inner_body = body_shell(
        -height / 2.0 + eps,
        height - 2.0 * eps,
        width - 2.0 * eps,
        thickness - 2.0 * eps,
    );
    // inner neck. Make it long and narrow to account for thickness.
    let inner_neck = cylinder(
        height / 2.0 - eps,
        height / 10.0 + eps,
        thickness / 4.0 - eps,
    );
    // sew the inner body and the inner neck
    glue_body_neck(&mut inner_body, inner_neck);

    // invert all faces of the inner body
    inner_body.face_iter_mut().for_each(|face| {
        face.invert();
    });
    // pop the ceiling of the inner body
    let inner_ceiling = inner_body.pop().unwrap();
    // make the inner ceiling the boundary wire
    let wire = inner_ceiling.into_boundaries().pop().unwrap();
    // the mutable reference to the outer ceiling
    let ceiling = body.last_mut().unwrap();
    // drill a hole in the outer ceiling using the boundary of inner ceiling
    ceiling.add_boundary(wire);
    // add the faces of the neck to the body
    body.extend(inner_body.into_iter());
    // returns the solid
    Solid::new(vec![body])
}
}

Thank you very much for reading! Now you're in the clear for creating the bottle at the beginning!