0
\$\begingroup\$

I am working on a vector graphics rendering library for OpenGL in Rust. Currently, the library's API is as follows:

let mut canvas = Canvas::new();
canvas.set_background(Color(1.0, 1.0, 1.0));
canvas.add_rect(-0.5, 0.0, 0.8, 0.5, Color(1.0, 0.0, 0.0));
canvas.add_polygon(0.0, 0.0, 0.3, 6, Color(1.0, 0.0, 1.0));
canvas.add_rect(0.1, 0.3, 0.4, 0.3, Color(0.0, 1.0, 0.0));
canvas.add_circle(0.0, -0.2, 0.2, Color(0.0, 1.0, 1.0));

Internally, canvas is represented as an array of shapes, each of which is composed of multiple vertices:

#[derive(Debug)]
struct Canvas {
    points: Vec<Vec<[f32; 5]>>,
    background: Color
}

When a drawing command is issued for the canvas, internally, the library draws the shape as a set of connected triangles. Each vertex in each triangle is composed of an (x, y) position and a (r, g, b) color. For example, for drawing rectangles:

fn add_rect(&mut self, x: f32, y: f32, w: f32, h: f32, fill: Color) {
    // 6 vertices to make up 2 triangles that are part of rectangle
    let p1 = [x + w, y, fill.0, fill.1, fill.2]; // top right
    let p2 = [x + w, y + h, fill.0, fill.1, fill.2]; // bottom right
    let p3 = [x, y, fill.0, fill.1, fill.2]; // top left
    let p4 = [x + w, y + h, fill.0, fill.1, fill.2]; // bottom right
    let p5 = [x, y + h, fill.0, fill.1, fill.2]; // bottom left
    let p6 = [x, y, fill.0, fill.1, fill.2]; // top left
    let rect = vec![p1, p2, p3, p4, p5, p6];
    self.add_shape(rect);
}

When drawing is completed, rendering is conducted by flattening the shapes array into a single 1D array of vertices, which are then sent to OpenGL to be rendered. This approach works reasonably well for just rendering 2D shapes. However, I would like to introduce rendering textured quads for eventual text rendering.

An approach I have thought of so far is to add two texture coordinate (u, v) values to the vertices of the quad, so each vertex would be composed of the following:

let vertex = [x, y, r, g, b, u, v];

However, requiring texture coordinates for shapes that don't even need textures seems redundant, and if texture coordinates were required for every object, textures would be incredibly badly distorted as they would be warped to fit every shape. Clearly, this approach does not work.

What would the approach to doing this form of rendering that would best interoperate with the existing system I have devised?

\$\endgroup\$
3
  • \$\begingroup\$ "Best" is usually not a well-defined category on its own. Can you describe how you've thought of doing this so far, and in what specific way you'd like to improve over that baseline? Or, if you haven't found a way to do this yet, where specifically are you stuck? \$\endgroup\$
    – DMGregory
    Jun 23, 2023 at 0:00
  • \$\begingroup\$ @DMGregory Noted, I edited the question as per your comment \$\endgroup\$
    – JS4137
    Jun 23, 2023 at 0:44
  • \$\begingroup\$ Why would textures be distorted when texturing 2D shapes? That sounds like an error in how you're choosing the texture coordinate values, not a necessary consequence of having texture coordinate values at all. \$\endgroup\$
    – DMGregory
    Jun 23, 2023 at 1:56

1 Answer 1

1
\$\begingroup\$

Most general-purpose 3D/GPU rendering systems have a concept of “material”. A material is a definition of how surfaces should appear, separate from the shape of the object that has those surfaces. Or, we can say that it is information about the object that is conveyed separately from the vertices. (Exactly what information makes up a material depends on what the renderer supports.)

So, in your case we could say have multiple materials:

  • Vertex-colored only
  • Texture #1
  • Texture #2
  • ...

Each material has different information which must be conveyed to the GPU (in a basic OpenGL application, using uniforms), and therefore it is generally desirable to group vertices by the material they use. In your application, this means you might as well separate the colored vertices from the textured vertices and give them a different layout.

#[derive(Debug)]
struct Canvas {
    colored_verts: Vec<Vec<ColorVertex>>,
    textured_verts: Vec<Vec<TexVertex>>,
    background: Color
}

#[repr(C)]
#[derive(Debug)]
struct ColorVertex {
    position: [f32; 2],
    color: [f32; 3],
}

#[repr(C)]
#[derive(Debug)]
struct TexVertex {
    position: [f32; 2],
    texcoord: [f32; 2],
}

(Rust-specific note: the #[repr(C)] guarantees straightforward in-order layout for the two structs, so that they will each be laid out just like a [f32; N] would be and are suitable for use in vertex buffers.)

You'll specify different attribute layouts for your two materials, since one has 5 floats and the other has 4.


Another common option, instead of having multiple vertex layouts, is to have a single vertex layout with all properties, as you thought of — [x, y, r, g, b, u, v]. In your fragment shader, multiply the color from the vertex with the color from the texture. Then, for your colored vertices, select a texture which has just 1 white texel, and for your textured vertices, make the color white (or some other color to “tint” the texture). This means that your materials only differ by choice of texture, so your code is more straightforward, and you can tint the textures (e.g. for colored text). However, these vertices occupy more memory.

A weirder option is to select the type of information per-vertex: for example, by storing either [r, g, b] or [-1, u, v] in the same 3 floats. Since color components are never negative (unless you are doing special wide-gamut color processing), the shader can tell which is which and decide whether to use a texture or not.


However, even if you choose one of those two alternatives, you'll still need to break up vertices into groups if you want those groups to be rendered with different textures rather than just reasons of a single texture.

\$\endgroup\$
3
  • \$\begingroup\$ Would it be possible to perhaps pack all the images required for rendering to a singular texture and sample different parts of that same texture to render multiple images? \$\endgroup\$
    – JS4137
    Jun 23, 2023 at 16:16
  • \$\begingroup\$ Also, for the first solution you outlined, I'd assume the approach of flattening all the vertices for rendering wouldn't work, right? You'd need to most likely need to create 2 vbos and vaos, one for the textured quads, another for the regular shapes, right? \$\endgroup\$
    – JS4137
    Jun 23, 2023 at 17:22
  • 1
    \$\begingroup\$ "pack all the images required for rendering to a singular texture" — Yes, that's called a texture atlas. "create 2 vbos and vaos" — sorry, I haven't worked with OpenGL in a while, but I believe the answer is that you can use one VBO if you wish, since it's just memory and you can address parts of it with two VAOs. But using two VBOs may be easier to get right. \$\endgroup\$
    – Kevin Reid
    Jun 23, 2023 at 19:24

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .