Skip to content

02 Vector (Part 1)

Karn Kaul edited this page Oct 1, 2022 · 1 revision

Introduction

Geometric vectors are very useful in graphics and gamedev, as they can make code much more robust, succint, and easier to reason about, than having operations and trigonometry splattered all over the codebase. Designing an efficient, multi-purpose, multi-dimensional vector is not easy, and we will develop it over multiple parts.

Starting with extent, we note that to accommodate it, a vector would need to store two u32s:

struct Vec {
  std::uint32_t x{};
  std::uint32_t y{};
};

Rgb is similar in semantics, but different in representation, much like other kinds of 2D / 3D (even 4D) vectors:

struct Vec {
  std::uint8_t x{};
  std::uint8_t y{};
  std::uint8_t z{};
};

While we can use a template parameter to hoist the types of each member, we can't conditionally make a data member disappear based on a template argument. But we can make a member function disappear! So, this is a viable approach, which is what we will be expanding upon:

template <typename Type, std::size_t Dim>
struct Vec {
  std::array<Type, Dim> values{};

  // zero-sized arrays don't exist in C++, so all Vecs will have at least one dimension
  constexpr Type& x() { return values[0]; }
  constexpr Type const& x() const { return values[0]; }

  // conditional member functions for y, z (, w) ...
};

Constrained Member Functions

The syntax for this is surprisingly simple:

constexpr Type& y() requires (Dim > 1) { return values[1]; }
constexpr Type const& y() const requires (Dim > 1) { return values[1]; }

Attempting to call Vec<T, 1>{}.y() will result in a compile-time error.

Why a struct?

Vec should be a class and keep its array member private to prevent invalid accesses. Right now having it public enables easier initialization of such objects. We will refactor it and add appropriate constructors instead soon.

Code

CMake

We want to put struct Vec in its own header, so other files can also reference it. Create a new file src/tray/vec.hpp, update the CMake target to add src to its include path, and add the new header to its list of sources.

target_include_directories(${PROJECT_NAME} PRIVATE src)
target_sources(${PROJECT_NAME} PRIVATE
  src/tray/vec.hpp

  src/main.cpp
)

Since this is an executable target with no sources to export, we don't need a dedicated include/ directory, and can instead stuff all headers into src/ itself.

C++

Create a new header tray/vec.hpp:

#include <array>
#include <cstdint>

namespace tray {
template <typename Type, std::size_t Dim>
struct Vec {
  std::array<Type, Dim> values{};

  constexpr Type& x() { return values[0]; }
  constexpr Type const& x() const { return values[0]; }
  constexpr Type& y() requires(Dim > 1) { return values[1]; }
  constexpr Type const& y() const requires(Dim > 1) { return values[1]; }
  constexpr Type& z() requires(Dim > 2) { return values[2]; }
  constexpr Type const& z() const requires(Dim > 2) { return values[2]; }
};

using fvec2 = Vec<float, 2>;
using uvec2 = Vec<std::uint32_t, 2>;
using ivec2 = Vec<std::int32_t, 2>;

using fvec3 = Vec<float, 3>;
using uvec3 = Vec<std::uint32_t, 3>;
using ivec3 = Vec<std::int32_t, 3>;
} // namespace tray

Use it in main.cpp:

#include <tray/vec.hpp>

namespace {
struct Rgb : Vec<std::uint8_t, 3> {};

struct ImageData {
  std::span<Rgb const> data{};
  uvec2 extent{};
};

std::ostream& operator<<(std::ostream& out, ImageData const& image) {
  assert(image.data.size() == image.extent.x() * image.extent.y());
  // write header
  out << "P3\n" << image.extent.x() << ' ' << image.extent.y() << "\n255\n";
  // write each row
  for (std::uint32_t row = 0; row < image.extent.y(); ++row) {
    // write each column
    for (std::uint32_t col = 0; col < image.extent.x(); ++col) {
      // compute index
      auto const index = row * image.extent.x() + col;
      // obtain corresponding Rgb
      auto const& rgb = image.data[index];
      // write out each channel
      for (auto const channel : rgb.values) { out << static_cast<int>(channel) << ' '; }
    }
    out << '\n';
  }
  return out;
}
} // namespace

int main() {
  static constexpr auto extent = uvec2{40U, 30U};
  static constexpr auto white_v = Rgb{0xff, 0xff, 0xff};
  auto buffer = std::array<Rgb, extent.x() * extent.y()>{};
  std::fill_n(buffer.data(), buffer.size(), white_v);
  auto const image_data = ImageData{.data = buffer, .extent = extent};
  std::cout << image_data;
}
Clone this wiki locally