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 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 unitEdge
- connecting two different verticesWire
- a list of the edges, whose entity isVecDeque
Face
- whose boundaries are described by a closed wireShell
- a set of faces, the order of faces does not matterSolid
- 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.
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.
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.
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.
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.
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.
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
- cube
- torus
- 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.