-
Notifications
You must be signed in to change notification settings - Fork 0
02 Vector (Part 1)
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 u32
s:
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) ...
};
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.
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.
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.
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;
}