-
Notifications
You must be signed in to change notification settings - Fork 0
04 Lerping
Linear interpolation is a standard technique in graphics, animation, gamedev, etc, where the current value of some quantity is determined by scaling the value to between a supplied min and max, by a supplied factor (a floating point number normalized to [0-1]). Eg, lerping between 1
and 5
by 0.4
yields 2
. C++20 offers std::lerp
in <cmath>
.
The goal is to lerp between different colours, but in normalized float space (fvec3
), so we begin by adding the feature to the base Vec
class (template), and then some convenience functions in Rgb
to convert between the different representations.
Lerping between two vectors is quite straightforward: just lerp each component / dimension individually. Since there will be several such per-component operations, let's add a little abstraction to consolidate the logic: a member function that takes a callable and invokes it with the index and value for each component. Making it static
and taking the Vec
as a T&&
allows us to reuse the same code for const
and non-const
Vec
s.
template <typename T, typename F>
static constexpr void for_each(T&& vec, F&& func) {
for (std::size_t i = 0; i < Dim; ++i) { func(i, vec.values[i]); }
}
lerp
ing Vec
s only makes sense for floating point Type
s, so we define a free function only for float
instantiations:
template <std::size_t Dim>
constexpr Vec<float, Dim> lerp(Vec<float, Dim> const& a, Vec<float, Dim> const& b, float const t) {
auto ret = Vec<float, Dim>{};
Vec<float, Dim>::for_each(ret,
[&a, &b, t](std::size_t i, float& value) { value = std::lerp(a.values[i], b.values[i], t); }
);
return ret;
}
Adding the conversion functions to Rgb
:
struct Rgb : Vec<std::uint8_t, 3> {
static constexpr float to_f32(std::uint8_t channel) { return static_cast<float>(channel) / 0xff; }
static constexpr std::uint8_t to_u8(float channel) { return static_cast<std::uint8_t>(channel * 0xff); }
static constexpr Rgb from_f32(fvec3 const& normalized) { return {to_u8(normalized.x()), to_u8(normalized.y()), to_u8(normalized.z())}; }
static constexpr Rgb from_hex(std::uint32_t hex) {
auto ret = Rgb{};
ret.z() = hex & 0xff;
hex >>= 8;
ret.y() = hex & 0xff;
hex >>= 8;
ret.x() = hex & 0xff;
return ret;
}
constexpr fvec3 to_f32() const { return {to_f32(x()), to_f32(y()), to_f32(z())}; }
};
And modifying main
to use a 2D gradient:
static constexpr auto extent = uvec2{400U, 300U};
static constexpr auto top = std::array{Rgb::from_hex(0xff0000).to_f32(), Rgb::from_hex(0x00ff00).to_f32()};
static constexpr auto bottom = std::array{Rgb::from_hex(0x0000ff).to_f32(), Rgb::from_hex(0xff00ff).to_f32()};
auto image = Image{extent};
for (std::uint32_t row = 0; row < image.extent().y(); ++row) {
auto const yt = static_cast<float>(row) / static_cast<float>(image.extent().y() - 1);
auto const range = std::array{lerp(top[0], bottom[0], yt), lerp(top[1], bottom[1], yt)};
for (std::uint32_t col = 0; col < image.extent().x(); ++col) {
auto const xt = static_cast<float>(col) / static_cast<float>(image.extent().x() - 1);
image[{row, col}] = Rgb::from_f32(lerp(range[0], range[1], xt));
}
}
io::write(image, "test.ppm");
Results in a quad with red, green, blue, and magenta corners: