Viewing the katana cuts

Continuing from the previous post. Last time I had a slicer that spat out one SVG per sliced layer. That’s probably the worst UX ever built, so I was eager to get started on the viewer

The SVG UX was just enough for making sure the slicer worked, but staring at a directory of 400 SVG files is a terrible debugging experience haha. I wanted to see the mesh and the slices together, rotate them around, scrub through layers, and confirm quickly the implemented changes.

CPU first

To keep it simple at first: egui for a simple UI, and project 3D points to 2D manually.

The projection is a standard orthographic camera with two angles: azimuth (rotation around the vertical axis) and elevation (tilt up/down). Given a point (x, y, z) and a camera, project it from “world space” to “screen space” like this:

fn project(&self, x: f32, y: f32, z: f32, canvas: &egui::Rect) -> egui::Pos2 {
    let dx = x - self.center[0];
    let dy = y - self.center[1];
    let dz = z - self.center[2];

    let (cos_a, sin_a) = (self.azimuth.cos(), self.azimuth.sin());
    let rx = dx * cos_a - dy * sin_a;
    let ry = dx * sin_a + dy * cos_a;

    let (cos_e, sin_e) = (self.elevation.cos(), self.elevation.sin());
    let screen_x = rx;
    let screen_y = -(dz * cos_e - ry * sin_e);
    // ...scale, center on canvas, return Pos2
}

That’s literally it. Rotate around Z by azimuth, then tilt by elevation, drop the depth component, and you have a 2D projection. Now just use this function to project all triangle points and draw segments between them with egui::Painter.line_segment to get a wireframe of the STL. Same for the the sliced contours to draw the slice (though we can use closed_line in this case).

Most of the time I spent here was actually trying to make the camera dragging work, honestly it’s still kinda shit at this point, and it’s in the back of my mind to rewrite it properly. It’s surprisingly finnicky, because there’s a lot of math involved in the dragging sensibility and the dependency it has on the zoom level… it almost drove me insane.

The CPU path worked fine with simple models with up to a few thousand triangles. The liver STL picked I up to test has like 90k triangles, and things started breaking down fast. egui’s painter wasn’t made for hammering out hundreds of thousands of line segments per frame.

Going to the GPU

Time to bite the bullet. egui-glow exposes the raw glow context, which is a thin wrapper over OpenGL. I don’t have too much experience with it, but I know a bit of shaders, and I know OpenGL is everywhere, and it’s supposed to be more simple to use than lower level GPU interaction APIs like Metal, Vulkan, etc. Even so, I had to do quite some research.

So, this is the setup: two vertex shaders, one for lines (edges, contours, layer outlines) and one for meshes, with a simple light diffusing fragment shader. What these small beasts do is fantastic, I played a bit with them on this small project a while ago. All that projection computation is now done in parallel for each line and mesh.

Shaders are like little programs that run independently in each of the small cores of the GPU (with some shared data called uniforms):

layout (location = 0) in vec3 a_pos;
layout (location = 1) in vec4 a_color;

uniform mat4 u_mvp;
out vec4 v_color;

void main() {
    gl_Position = u_mvp * vec4(a_pos, 1.0);
    v_color = a_color;
}

The vector operation u_mvp * vec4(a_pos, 1.0) encodes the projection from the model space into the screen space (well, technically still the clip space, but that’s besides the point atm). mvp means “Model-View-Projection”, and it’s a 4x4 matrix (calculated based on the camera position using a lot of linear algebra), which gets multiplied by a position vec4 to yield gl_Position, which is an OpenGL built-in output variable for the vertex position, it’s how the GPU knows where to print the vertex. It’s magical, really really cool.

The way these shaders (glsl) are connected with the main code (rust) is though the glow API. We need to create a VAO, vertex array object, with gl.create_vertex_array, a VBO, vertex buffer object, with gl.create_buffer, push all the vertexes to the VBO (gl.buffer_data_u8_slice), and use a couple other gl functions to tell the GPU how the data is laid out in the buffer (eg.: our LINE_STRIDE here is seven -> [x, y, z, r, g, b, a], and each prop is a f32 occupying 4 bytes, so the GPU knows that, in our buffer, vertex number 250 is exactly on 250 * 7 * 4 bytes offset).

gl.enable_vertex_attrib_array(0);
gl.vertex_attrib_pointer_f32(0, 3, glow::FLOAT, false, stride, 0);
gl.enable_vertex_attrib_array(1);
gl.vertex_attrib_pointer_f32(1, 4, glow::FLOAT, false, stride, 3 * 4);

This line is basically telling the gpu that the VAO attribute 0 (position) is a vec3 and starts at position 0 on a “stride-sized” vertex, and attribute 1 (color) is a vec4 starting at 12 bytes (3*4) on the same “stride-sized” vertex. With this, the glsl code can set gl_Position and v_color, and we can manipulate the buffer as we please on Rust-land.

Ah, I also added an egui::Slider to scrub through the layers and visually inspect them! (Basically hiding layers with z above the slider value).

Next steps

This was quite an interesting rendering adventure, but rendering isn’t the main focus of the project, for the next steps, my idea was to focus on adding some basic slicing features - infill, parameterization of infill density, number of perimeter layers, and basic toolpathing (connect the segments in a sane fashion).

But for now, that’s it :D