1
\$\begingroup\$

Introduction

After finishing most of the tutorials on https://learnopengl.com/ I started to encapsulate code in their respective classes and started to write a mesh class and a polygon class as my first primitive geometry object to figure out the structure of my code. There are a few questions about the code I'll add at the end and apart from them I am looking for possible improvement. Eventually I want to end up with a 2D sprite class and a tilemap class to efficiently render bigger maps. Currently all the code is inlined in header files and I will eventually split it into translation units once I am convinced about my design.

Vertex.hpp

/// Describes a custom vertex format structure that contains position, color,
/// and one set of texture coordinates.
struct FVertex final {
  /// XYZ position.
  glm::vec3 Position;
  /// The vertex color.
  glm::vec3 Color;
  /// UV texture coordinates.
  glm::vec2 TextureCoordinate;
};

Mesh.hpp

/// A mesh is a collection of vertices that define the geometry of an object.
class FMesh final {
public:
  /// Creates a new FMesh.
  explicit FMesh() { initialize(); };

  /// Default destructor.
  ~FMesh() {
    if (!IsInitialized) {
      return;
    }
    glDeleteVertexArrays(1, &VertexArrayHandle);
    glDeleteBuffers(1, &VertexBufferHandle);
    IsInitialized = false;
  }

  /// Deleted copy-constructor.
  FMesh(const FMesh &) = delete;
  /// Deleted move-constructor.
  FMesh(FMesh &&) = delete;
  /// Deleted copy-assignment operator.
  auto operator=(const FMesh &) -> FMesh & = delete;
  /// Deleted move-assignment operator.
  auto operator=(FMesh &&) -> FMesh & = delete;

  /// Draws all vertices of this FMesh.
  /// @param Mode Specifies what kind of primitives to render. Can be one of @p
  /// GL_POINTS, @p GL_LINES, @p GL_LINE_STRIP, @p GL_LINE_LOOP, @p
  /// GL_TRIANGLES, @p GL_TRIANGLE_STRIP or @p GL_TRIANGLE_FAN.
  auto draw(const GLenum Mode) const -> void {
    glBindVertexArray(VertexArrayHandle);
    glDrawArrays(Mode, 0, static_cast<GLint>(Vertices.size()));
  }

  /// Sets the vertex with the specified index of this FMesh.
  /// @param Index Index of the vertex to set.
  /// @param Vertex Vertex data to store at the specified index.
  /// @remark Before using this method you must call setVertexCount() first to
  /// initialize this FMesh.
  auto setVertex(const std::size_t Index, const FVertex &Vertex) -> void {
    assert(Vertices.size() >= 2 &&
           "The FMesh hasn't been initialized with setVertexCount.");
    Vertices.at(Index) = Vertex;
    update();
  }

  /// Sets the number of vertices of this FMesh.
  /// @param Count Number of vertices of this FMesh.
  auto setVertexCount(const std::size_t Count) -> void {
    assert(Count >= 2 && "The FMesh must have at least two vertices.");
    Vertices.resize(Count);
    update();
  }

private:
  /// Initializes the OpenGL objects required for this FMesh.
  auto initialize() -> void {
    if (IsInitialized) {
      return;
    }

    glGenVertexArrays(1, &VertexArrayHandle);
    glGenBuffers(1, &VertexBufferHandle);

    glBindVertexArray(VertexArrayHandle);

    glBindBuffer(GL_ARRAY_BUFFER, VertexBufferHandle);
    glBufferData(GL_ARRAY_BUFFER, Vertices.size() * sizeof(FVertex),
                 Vertices.data(), GL_STATIC_DRAW);

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, decltype(FVertex::Position)::length(), GL_FLOAT,
                          GL_FALSE, sizeof(FVertex),
                          getOffsetPtrOf(&FVertex::Position));

    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, decltype(FVertex::Color)::length(), GL_FLOAT,
                          GL_FALSE, sizeof(FVertex),
                          getOffsetPtrOf(&FVertex::Color));

    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, decltype(FVertex::TextureCoordinate)::length(),
                          GL_FLOAT, GL_FALSE, sizeof(FVertex),
                          getOffsetPtrOf(&FVertex::TextureCoordinate));

    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    IsInitialized = true;
  }

  /// Recompute the internal geometry of the FMesh. This function must be called
  /// every time the vertices change.
  auto update() const -> void {
    assert(IsInitialized == true &&
           "Cannot update FMesh that is not initialized.");
    glBindBuffer(GL_ARRAY_BUFFER, VertexBufferHandle);
    glBufferData(GL_ARRAY_BUFFER, Vertices.size() * sizeof(FVertex),
                 Vertices.data(), GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
  }

  /// Boolean value indicating whether this FMesh has been intialized.
  bool IsInitialized;
  /// OpenGL handle to the vertex buffer object of this FMesh.
  FHandle VertexBufferHandle;
  /// OpenGL handle to the vertex array object of this FMesh.
  FHandle VertexArrayHandle;
  /// List of FVertex objects describing the geometry of this FMesh.
  std::vector<FVertex> Vertices;
};

Polygon.hpp

/// A primitive shape with a finite number of straight line segments that form a
/// closed chain.
class FPolygon final {
public:
  /// Creates a new FPolygon.
  explicit FPolygon() = default;
  /// Default destructor.
  ~FPolygon() = default;

  /// Deleted copy-constructor.
  FPolygon(const FPolygon &) = delete;
  /// Deleted move-constructor.
  FPolygon(FPolygon &&) = delete;
  /// Deleted copy-assignment operator.
  auto operator=(const FPolygon &) -> FPolygon & = delete;
  /// Deleted move-assignment operator.
  auto operator=(FPolygon &&) -> FPolygon & = delete;

  /// Draws this FPolygon.
  auto draw() const -> void { Mesh.draw(GL_TRIANGLE_FAN); }
  /// Gets a reference to the underlying FMesh of this FPolygon.
  /// @return Reference to the underlying FMesh of this FPolygon.
  [[nodiscard]] auto getMesh() -> FMesh & { return Mesh; }

private:
  /// FMesh describing the geometry of this FPolygon.
  FMesh Mesh;
};

Usage

Using and initialization of the FPolygon class looks as follows right now:

Polygon.getMesh().setVertexCount(3);
Polygon.getMesh().setVertex(
    0, FVertex{glm::vec3{0, 0.5, 0}, glm::vec3{1, 0, 0}, glm::vec2{1}});
Polygon.getMesh().setVertex(
    1, FVertex{glm::vec3{0.5, -0.5, 0}, glm::vec3{0, 1, 0}, glm::vec2{1}});
Polygon.getMesh().setVertex(
    2, FVertex{glm::vec3{-0.5, -0.5, 0}, glm::vec3{0, 0, 1}, glm::vec2{1}});

Remarks

  • The FVertex struct already has texture coordinates that are currently unused and I will add the handling later on.
  • No projections or transformation are currently set up, this is just about the very basics to get up and running.

Questions

  1. Right now all meshes would have their own vertex array object even though the data layout, based on FVertex, is identical between all of them. Is it common to use a shared vertex array object or is the overhead of duplicating them negligible?
  2. Considering I want to create classes for sprites and tilemaps I was wondering whether it's better to inherit from FMesh or add it as a member variable. Is there anything I have to consider for this decision?
\$\endgroup\$
1
  • \$\begingroup\$ Is there a reason why FVertex contain no normal? \$\endgroup\$
    – convert
    Sep 17, 2022 at 18:15

1 Answer 1

2
\$\begingroup\$

Right now all meshes would have their own vertex array object even though the data layout, based on FVertex, is identical between all of them. Is it common to use a shared vertex array object or is the overhead of duplicating them negligible?

If the mesh represents just a simple tile that have the always have the same geometry then, yes, it is a bad idea to use multiple VBOS. In general with openGL the less glX calls you make the better. You should use a single VBO and use instanced rendering where you basically tell openGL to draw the same stuff many times but with a different input parameter, in your case the offset or the 2D transform and a texture offset to point to the correct tile texture which is part of a larger one.

But it is not clear to me what is the purpose of the Mesh class, is it a generic container of data for any mesh or is it specific to the tile rendering?

At the moment you have a Mesh that can only have one kind of vertex and can only be drawn with that 3 attributes so it is very specific to your problem. No way to extend it to, for example, to render something with 2 set of texture coordinates, normals or whatever. The Vertex class also have both color and texture coordinates which does not make much sense unless you want to perform some strange effects with open GL, you also seem to have 3D coordinates but you also want to render 2D sprites with it. If you want to be able to extend and re-use the Mesh class, it must be more generic. You probably want the Vertex class or directly the Mesh class if you want, to describe the format of the Vertex instead of containing the data itself. For example it could contain an Attribute list that describe different kind of attributes and their properties (Stride, Size, Type, etc..) and have the data stored somewhere else. You could then iterate over the vertex descriptor and load the data into OGL accordingly.

You should separate the OpenGL specific code and the mesh data. Ideally the Mesh class should have no dependencies on OpenGL.

Have a "Renderable Object" class contain a mesh and describe additional information (e.g. position in the world (transform), texture, shaders and other "per-object" attributes so that you can recycle the same mesh with different positions and e.g. textures. Usually Objects like this contains a list of childrens which positions are relative to the transform of the parent.

Have another module/class handle the openGL specifics. It should take the list of objects and draw them in the best possible way. For example all objects that share the same mesh and texture and differs only by position should be rendered together as instances. The renderer itself should only load into OpenGL one copy of each vbo and one copy of each texture. Build whatever data structures you need to accomplish this efficiently.

The Polygon class seems unnecessary, what is its purpose? It only specifies that you want to draw that mesh with GL_TRIANGLE_FAN but that information should already be in the mesh itself.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.