3
\$\begingroup\$

This is a simple 2D rendering abstraction that I've been using for my game. It's basically a vector of quads. It has functions for defining quads (position, rotation, texture coordinates) and a function to render the quads using my OpenGL rendering abstraction. I had to comment out the render function because it depends on the OpenGL renderer which depends on some other stuff (this is the top layer of many layers of abstraction). The vector is broken up into sections. Each section corresponds to its own set of uniforms and a separate call to glDrawElements.

I'd like to know how this can be made faster/cleaner or anything you'd like to say about the code.

quad writer.hpp

namespace Math {
  // this is a full class in it's own header
  template <typename T>
  struct RectPP {
    glm::tvec2<T> min;
    glm::tvec2<T> max;
  };
}

namespace G2D {
  struct Vertex {
    glm::vec3 pos;
    glm::vec2 texCoord;
  };
  using Quad = std::array<Vertex, 4>;

  // the values of the uniforms
  struct RenderParams {
    // camera matrix
    glm::mat3 viewProj = {};
    // ID of a texture stored in G2D::Renderer
    TextureID tex = 0;
    // color to be multiplied by the texture sample in the fragment shader
    glm::vec4 color = glm::vec4(1.0f);
  };

  enum class PlusXY {
    RIGHT_UP,
    LEFT_UP,
    RIGHT_DOWN,
    LEFT_DOWN
  };

  enum class Origin {
    TOP_LEFT,
    TOP_MID,
    TOP_RIGHT,
    MID_RIGHT,
    BOTTOM_RIGHT,
    BOTTOM_MID,
    BOTTOM_LEFT,
    MID_LEFT,
    CENTER
  };

  /// Get the depth of a depth enum. The last enumerator must be COUNT
  template <typename Enum>
  constexpr float depth(const Enum e) {
    return static_cast<float>(e) / static_cast<float>(Enum::COUNT);
  }

  class QuadWriter {
  public:
    QuadWriter();

    /// Remove all of the sections
    void clear();

    /// Start a new section with the given rendering parameters
    void section(const RenderParams &);
    /// Make space for the given number of quads to avoid further reallocations
    void sectionSize(size_t);

    /// Sort the quads in the current section by the given sorting predicate
    template <typename Function>
    void sort(Function &&);

    /// Start a new quad and return it
    Quad &quad();
    /// Start a new quad the is a duplicate of the previous quad and return it
    Quad &dup();

    /// Set the depth of the current quad
    void depth(float);
    /// Set the depth of the current quad using an enum. The last enumerator
    /// must be COUNT
    template <typename Enum>
    void depth(Enum);

    /// Copy the positions of the verticies from the previous quad into the
    /// current quad
    void dupPos();
    /// Copy the positions of the verticies and the depth from the previous
    /// quad into the current quad
    void dupPosDepth();
    /// Write positions of verticies on the current quad as an axis-aligned
    /// rectangle.
    void tilePos(glm::vec2, glm::vec2 = {1.0f, 1.0f});
    /// Write positions of verticies on the current quad as a rectangle rotated
    /// around it's center. The quad is position relative to it's bottom left
    /// corner like tilePos.
    void rotTilePos(float, glm::vec2, glm::vec2 = {1.0f, 1.0f});
    /// Write positions of verticies on the current quad as rectangle rotated
    /// around a given origin. The quad is positioned relative to the origin.
    /// This function isn't quite as fast as rotTilePos without an origin.
    template <Origin ORIGIN>
    void rotTilePos(float, glm::vec2, glm::vec2 = {1.0f, 1.0f});

    /// Copy the texture coordinates of the verticies on the previous quad onto
    /// the current quad
    void dupTex();
    /// Write texture coordinates of vertices on the current quad assuming that
    /// the texture is sampled as an axis-aligned rectangle
    template <PlusXY PLUS_XY = PlusXY::RIGHT_UP>
    void tileTex(glm::vec2, glm::vec2);
    /// Write texture coordinates of vertices on the current quad assuming that
    /// the texture is sampled as an axis-aligned rectangle
    template <PlusXY PLUS_XY = PlusXY::RIGHT_UP>
    void tileTex(Math::RectPP<float>);

    /// Copy the quads into GPU memory and issue an number of draw calls
    //void render(Renderer &) const;

  private:
    std::vector<Quad> quads;
    // each section is an index to it's first quad
    std::vector<size_t> sections;
    std::vector<RenderParams> params;
  };
}

#include "quad writer.inl"

quad writer.inl

#include <algorithm>
#include <glm/gtc/constants.hpp>

inline G2D::QuadWriter::QuadWriter() {
  quads.reserve(2048);
  sections.reserve(64);
  params.reserve(64);
}

inline void G2D::QuadWriter::clear() {
  quads.clear();
  sections.clear();
  params.clear();
}

inline void G2D::QuadWriter::section(const RenderParams &param) {
  sections.push_back(quads.size());
  params.push_back(param);
}

inline void G2D::QuadWriter::sectionSize(const size_t size) {
  quads.reserve(quads.size() + size);
}

template <typename Function>
void G2D::QuadWriter::sort(Function &&function) {
  assert(sections.size());

  std::sort(
    quads.data() + sections.back(),
    quads.data() + quads.size(),
    function
  );
}

inline G2D::Quad &G2D::QuadWriter::quad() {
  assert(sections.size());

  quads.push_back({});
  return quads.back();
}

inline G2D::Quad &G2D::QuadWriter::dup() {
  assert(sections.size() && quads.size());

  quads.push_back(quads.back());
  return quads.back();
}

inline void G2D::QuadWriter::depth(const float depth) {
  assert(quads.size() && sections.size());

  Quad &quad = quads.back();
  quad[0].pos.z =
  quad[1].pos.z =
  quad[2].pos.z =
  quad[3].pos.z = depth;
}

template <typename Enum>
void G2D::QuadWriter::depth(const Enum e) {
  depth(G2D::depth(e));
}

namespace G2D::detail {
  inline void setPos(glm::vec3 &dst, const glm::vec2 src) {
    dst.x = src.x;
    dst.y = src.y;
  }
}

inline void G2D::QuadWriter::dupPos() {
  assert(quads.size() > 1 && sections.size());

  Quad &quad = quads.back();
  const Quad &prev = *(quads.cend() - 2);
  for (size_t i = 0; i != 4; ++i) {
    detail::setPos(quad[i].pos, prev[i].pos);
  }
}

inline void G2D::QuadWriter::dupPosDepth() {
  assert(quads.size() > 1 && sections.size());

  Quad &quad = quads.back();
  const Quad &prev = *(quads.cend() - 2);
  for (size_t i = 0; i != 4; ++i) {
    quad[i].pos = prev[i].pos;
  }
}

inline void G2D::QuadWriter::tilePos(
  const glm::vec2 pos,
  const glm::vec2 size
) {
  assert(quads.size() && sections.size());

  Quad &quad = quads.back();
  detail::setPos(quad[0].pos, pos);
  detail::setPos(quad[1].pos, {pos.x + size.x, pos.y});
  detail::setPos(quad[2].pos, pos + size);
  detail::setPos(quad[3].pos, {pos.x, pos.y + size.y});
}

inline void G2D::QuadWriter::rotTilePos(
  const float angle,
  const glm::vec2 pos,
  const glm::vec2 size
) {
  assert(quads.size() && sections.size());

  const glm::vec2 halfSize = size * 0.5f;
  const glm::vec2 topRight = glm::vec2(
    std::cos(angle + glm::quarter_pi<float>()),
    std::sin(angle + glm::quarter_pi<float>())
  ) * halfSize * glm::root_two<float>();
  const glm::vec2 topLeft = {-topRight.y, topRight.x};
  const glm::vec2 botLeft = -topRight;
  const glm::vec2 botRight = -topLeft;
  const glm::vec2 shift = pos + halfSize;

  Quad &quad = quads.back();
  detail::setPos(quad[0].pos, botLeft + shift);
  detail::setPos(quad[1].pos, botRight + shift);
  detail::setPos(quad[2].pos, topRight + shift);
  detail::setPos(quad[3].pos, topLeft + shift);
}

template <G2D::Origin ORIGIN>
void G2D::QuadWriter::rotTilePos(
  const float angle,
  const glm::vec2 pos,
  const glm::vec2 size
) {
  assert(quads.size() && sections.size());

  constexpr glm::vec2 ORIGIN_POS[9] = {
    {0.5f, -0.5f}, {0.0f, -0.5f}, {-0.5f, -0.5f}, {-0.5f, 0.0f},
    {-0.5f, 0.5f}, {0.0f, 0.5f}, {0.5f, 0.5f}, {0.5f, 0.0f},
    {0.0f, 0.0f}
  };
  constexpr glm::vec2 originPos = ORIGIN_POS[static_cast<size_t>(ORIGIN)];

  const glm::vec2 origin = originPos * size;

  const float c = std::cos(angle);
  const float s = std::sin(angle);
  const glm::mat2 rot = {
    {c, s},
    {-s, c}
  };

  const glm::vec2 halfSize = size * 0.5f;
  const glm::vec2 tr = halfSize;
  const glm::vec2 bl = -halfSize;
  const glm::vec2 tl = {bl.x, tr.y};
  const glm::vec2 br = {tr.x, bl.y};

  Quad &quad = quads.back();
  detail::setPos(quad[0].pos, pos + rot * (bl + origin));
  detail::setPos(quad[1].pos, pos + rot * (br + origin));
  detail::setPos(quad[2].pos, pos + rot * (tr + origin));
  detail::setPos(quad[3].pos, pos + rot * (tl + origin));
}

inline void G2D::QuadWriter::dupTex() {
  assert(quads.size() > 1 && sections.size());

  Quad &quad = quads.back();
  const Quad &prev = *(quads.cend() - 2);
  for (size_t i = 0; i != 4; ++i) {
    quad[i].texCoord = prev[i].texCoord;
  }
}

template <G2D::PlusXY PLUS_XY>
void G2D::QuadWriter::tileTex(const glm::vec2 min, const glm::vec2 max) {
  assert(quads.size() && sections.size());

  constexpr size_t Is[4][4] = {
                  // +x      +y
    {0, 1, 2, 3}, // right   up
    {1, 0, 3, 2}, // left    up
    {3, 2, 1, 0}, // right   down
    {2, 3, 0, 1}  // left    down
  };
  constexpr size_t i = static_cast<size_t>(PLUS_XY);

  Quad &quad = quads.back();
  quad[Is[i][0]].texCoord = min;
  quad[Is[i][1]].texCoord = {max.x, min.y};
  quad[Is[i][2]].texCoord = max;
  quad[Is[i][3]].texCoord = {min.x, max.y};
}

template <G2D::PlusXY PLUS_XY>
void G2D::QuadWriter::tileTex(const Math::RectPP<float> coords) {
  tileTex<PLUS_XY>(coords.min, coords.max);
}

/*
Copy all of the quads directly into the GPU buffer with a single call to
glBufferSubData

For each section,
  set uniforms to the values in RenderParams
  make a call glDrawElements

inline void G2D::QuadWriter::render(Renderer &renderer) const {
  renderer.writeQuads({0, quads.size()}, quads.data());
  if (sections.empty()) {
    return;
  }
  QuadRange range;
  range.begin = sections[0];
  for (size_t s = 1; s != sections.size(); ++s) {
    range.end = sections[s];
    renderer.render(range, params[s - 1]);
    range.begin = range.end;
  }
  range.end = quads.size();
  renderer.render(range, params.back());
}*/

Example usage

enum class Depth {
  // probably some other stuff above
  RENDERABLE,
  // probably some other stuff below

  COUNT
};    

G2D::QuadWriter writer;
entt::DefaultRegistry reg; // ECS container. Stores entities and components

// for each entity with both a Position component and a Renderable component
const auto renderables = reg.view<Position, Renderable>();
for (const uint32_t entity : renderables) {
  writer.quad();
  writer.depth(Depth::RENDERABLE);
  writer.tilePos(renderables.get<Position>(entity).pos);
  writer.tileTex(renderables.get<Renderable>(entity).spriteRect);
}
\$\endgroup\$
  • \$\begingroup\$ How is this used? Is your game mostly a 2D tiled game? Are your quads mostly convex quads that are usually squares/rectangles, or do you have many concave quads, trapezoids, and parallelograms? \$\endgroup\$ – user1118321 May 24 '18 at 6:00
  • \$\begingroup\$ The functions are intended to be used for rectangular and square quads but there's nothing stopping the user from defining an arbitrary quad. The user could just set the Vertexs (vertices) of the Quad returned by quad() \$\endgroup\$ – Indiana Kernick May 24 '18 at 6:05
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$ – 301_Moved_Permanently May 24 '18 at 9:17
1
\$\begingroup\$
// this is a full class in it's own header

The word is “its”. “it's” is a contraction for “it is”.


Include all the proper library headers. I don’t see <array> or anything else.


Your techniques are modern, except:

glm::vec4 color = glm::vec4(1.0f);

You don’t need to name the type twice. Just use

glm::vec4 color {1.0f};

(I think it is fine even with the = added, in C++17)


⧺ES.9

Avoid ALL_CAPS names.
Do not use ALL_CAPS for constants just because constants used to be macros.


The style in C++ is to put the * or & with the type, not the identifier. This is called out specifically near the beginning of Stroustrup’s first book, and is an intentional difference from C style.


assert(sections.size());

Use std::vector::empty() not size here.


quad[0].pos.z =
quad[1].pos.z =
quad[2].pos.z =
quad[3].pos.z = depth;

Look at std::fill or fill_n.


I think you are doing well, just looking at the code itself — I’m not familiar with the library you are using, so I don’t know if you are approaching it well.

Keep it up!

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I always get "its" and "it's" mixed up! \$\endgroup\$ – Indiana Kernick May 24 '18 at 2:59
  • \$\begingroup\$ Also, #include <array> isn't there because I copy-and-pasted some stuff from other headers to try to keep my post short so I just forgot to #include the dependencies. \$\endgroup\$ – Indiana Kernick May 24 '18 at 3:01
  • \$\begingroup\$ @Kerndog73, it would be great to see the code as you see it in your IDE, i.e. with includes, formatting and everything. \$\endgroup\$ – Incomputable May 24 '18 at 9:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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