Alex on software development and music


Techniques for Showing and Hiding Triangles in 3D Meshes

In this post, I want to discuss how to hide and show triangles in a 3D meshes. This serves as a precursor to my next post, which will delve into my favorite sophisticated data structure: the half-edge data structure. I want to mention that I will be using some “abstract rendering API” for this post, as I prefer not to get into the specifics of each rendering system. Although they kind of look similar, but offer different levels of detail and flexibility. I will explain how 3D meshes are being prepared currently to be displayed on the screen and how to hide and show triangles within the mesh.

All 3D meshes, as known, consist of triangles. To display, for example, a quad on the screen, it is enough to list the vertices of two triangles that form the quad. For example, we can define simple quad(-0.5, -0.5, 0.5, 0.5) as:

glm::dvec3 quad[] = {
// First Triangle
glm::dvec3(0.5, 0.5, 0.0),
glm::dvec3(-0.5, 0.5, 0.0),
glm::dvec3(-0.5, -0.5, 0.0),
// Second Triangle
glm::dvec3(0.5, 0.5, 0.0),
glm::dvec3(-0.5, -0.5, 0.0),
glm::dvec3(0.5, -0.5, 0.0)
};

I specifically use glm::dvec3 from glm library, as it simplifies the arithmetic in calculating the number of vertices. And it’s more convenient.

Vertex buffer

Next, a vertex buffer (vb) is created, into which these vertices are passed (here, it is also assumed that normals and colors are not needed, only vertices):

vb = allocateVB(std::size(quad)); 
std::span<glm::dvec3> vertices = vb.lockVertices(0, std::size(quad));
for (size_t index = 0; auto vertex : quad)
vertices[index++] = vertex;
vb.unlockVertices();

The lockVertices method is designed to provide a direct and efficient way to access a subset of the vertex buffer memory that was previously allocated. The interface of this method mirrors the convenience and flexibility found in the std::span::subspan method, accepting two parameters to specify the range of interest within the vertex buffer:

Offset – parameter specifies the starting index from which the vertices should be accessed within the buffer.

Count – indicates the total number of vertices to be accessed starting from the offset. This defines the extent of the span over the vertex buffer.

By utilizing this approach, lockVertices effectively acts as a window into the vertex buffer, allowing for operations on a specific range of vertices. This method enhances code readability and safety by abstracting away raw pointer arithmetic and providing a clear, bounds-checked view of the vertex data.

Using the classic lock mechanism in Direct3D often results in a less intuitive and convenient workflow, as it provides a raw pointer. This approach can obscure the intent and management of the underlying array. In contrast, adopting std::span as a method for referencing arrays significantly enhances both code clarity and efficiency. std::span explicitly communicates to developers that the function returns a span over an array, not merely a pointer. This modern C++ feature is increasingly prevalent in contemporary codebases due to its ability to combine both clarity and efficient array handling.

Now in order to render this quad you may want pass this vertex buffer to render system and set some render primitive type to Triangles to interpret consecutive triples of vertices as a triangle. I intentionally skipped steps involving shader setup and vertex declarations, as they are not the focus of this post.

If you need to change a vertices later, you can lock the vertex buffer and modify the necessary vertices, for example, scaling the first triangle by 2.0 like so:

std::span<glm::dvec3> vertices = vb.lockVertices(0, 3);
vertices[0] *= 2.0;
vertices[1] *= 2.0;
vertices[2] *= 2.0;
vb.unlockVertices();

I also intentionally omitted the part about needing to invalidate the vertex buffer in some way, depending on the rendering API, to display the updated data, but that’s not important for this post.

An interesting question is how to make a triangle disappear? For instance, if you need to remove the first triangle. You could change the size of the vertex buffer and overwrite values, but what if you have a huge mesh and need to remove triangles in the middle? This can be easily done by assigning infinity to the needed vertices (hiding the first triangle in the example) like this:

std::span<glm::dvec3> vertices = vb.lockVertices(0, 3);

vertices[0] = std::numeric_limits<double>::infinity();

vertices[1] = std::numeric_limits<double>::infinity();

vertices[2] = std::numeric_limits<double>::infinity();

vb.unlockVertices();

Index buffer

Another method of displaying 3D meshes involves using a vertex buffer and an index buffer. The idea of the index buffer is simple: to avoid specifying extra vertices, as in the example above, it’s enough to list only the unique vertices that form a quad in our example and assign them indices. Here’s how:

glm::dvec3 quad[] = {
glm::dvec3(-0.5, -0.5, 0.0), // Vertex 0
glm::dvec3(0.5, -0.5, 0.0), // Vertex 1
glm::dvec3(0.5, 0.5, 0.0), // Vertex 2
glm::dvec3(-0.5, 0.5, 0.0) // Vertex 3
};

// Indices for 2 triangles, sharing 0 and 2 verts
uint64_t quadIndices[] = {
0, 1, 2, // First Triangle
2, 3, 0 // Second Triangle
};

vb = allocateVB(std::size(quad));
std::span<glm::dvec3> vertices = vb.lockVertices(0, std::size(quad));
for (size_t index = 0; auto vertex : quad)
vertices[index++] = vertex;
vb.unlockVertices();

ib = allocateIB(std::size(quadIndices));
std::span<glm::dvec3> indices = ib.lockIndices(0, std::size(quadIndices));
for (size_t index = 0; auto idx : quadIndices)
indices[index++] = idx;
ib.unlockIndices();

Again, I intentionally skipped many steps related to rendering settings. Now, if you want to change the contents of the buffers, you can do so as before. But now you also have a tool for changing the vertex indices. To remove the first triangle in a quad, it’s not necessary to assign infinity to the needed vertices. Simply duplicating one index several times will suffice. This results in a degenerate triangle (one where all vertices lie in the same vertex, and hence, its area equals zero), which will not be visible on the screen.

std::span<uint64_t> indices = ib.lockIndices(0, 3);
indices[1] = indices[2] = indices[0];
ib.unlockIndices();

Summary

This post is a bit chaotic and very technical for something as simple as showing or hiding a triangle, but it’s just the first post before diving into more complex things. And later it will be shown how vertex and index buffers will be used for rendering the half-edge data structure alongside with designing Render System interface.



Leave a comment