Skip to content

04 Lerping

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

Introduction

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.

Code

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 Vecs.

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]); }
}

lerping Vecs only makes sense for floating point Types, 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:

test.ppm

Clone this wiki locally