Math: tileable noise, display gamma and explicit channel springs #7

Merged
ilusha merged 2 commits from Math into master 2026-10-27 23:46:16 +03:00
6 changed files with 293 additions and 117 deletions

View file

@ -1,6 +1,8 @@
#include "Color.hpp"
#include <cmath>
using namespace tp;
RGB::RGB() { r = g = b = 1.f; }
@ -89,7 +91,7 @@ HSV::operator RGB() const {
return out;
}
hh = h / (PI2) *360;
hh = h / (PI2) * 360;
if (hh >= 360.0) {
hh = 0.0;
@ -139,6 +141,18 @@ HSV::operator RGB() const {
return out;
}
halnf tp::gammaToLinear(halnf encoded) {
if (encoded <= 0.0f) return 0.0f;
if (encoded >= 1.0f) return 1.0f;
return std::pow(encoded, kDisplayGamma);
}
halnf tp::linearToGamma(halnf linear) {
if (linear <= 0.0f) return 0.0f;
if (linear >= 1.0f) return 1.0f;
return std::pow(linear, 1.0f / kDisplayGamma);
}
/*
struct rgbab {

81
Math/private/Noise.cpp Normal file
View file

@ -0,0 +1,81 @@
#include "Noise.hpp"
#include <cmath>
#include <numeric>
namespace tp {
namespace {
constexpr halnf kGradients[8][2] = {
{ 1, 0 },
{ -1, 0 },
{ 0, 1 },
{ 0, -1 },
{ 0.7071f, 0.7071f },
{ -0.7071f, 0.7071f },
{ 0.7071f, -0.7071f },
{ -0.7071f, -0.7071f },
};
halnf fade(halnf t) { return t * t * t * (t * (t * 6 - 15) + 10); }
int4 wrap(int4 value, int4 period) {
const int4 remainder = value % period;
return remainder < 0 ? remainder + period : remainder;
}
halnf dotGradient(int4 hash, halnf x, halnf y) {
const halnf* gradient = kGradients[hash & 7];
return gradient[0] * x + gradient[1] * y;
}
}
TileableNoise::TileableNoise(uint32_t seed) {
std::array<uint8_t, 256> order{};
std::iota(order.begin(), order.end(), 0);
uint32_t state = seed * 747796405u + 2891336453u;
for (int4 index = 255; index > 0; index--) {
state = state * 1664525u + 1013904223u;
const int4 other = (int4) ((state >> 8) % (uint32_t) (index + 1));
std::swap(order[index], order[other]);
}
for (int4 index = 0; index < 512; index++)
mPermutation[index] = order[index & 255];
}
int4 TileableNoise::hash(int4 x, int4 y) const { return mPermutation[mPermutation[x & 255] + (y & 255)]; }
halnf TileableNoise::gradient(halnf x, halnf y, int4 periodX, int4 periodY) const {
const halnf floorX = std::floor(x);
const halnf floorY = std::floor(y);
const int4 x0 = wrap((int4) floorX, periodX);
const int4 y0 = wrap((int4) floorY, periodY);
const int4 x1 = wrap(x0 + 1, periodX);
const int4 y1 = wrap(y0 + 1, periodY);
const halnf fx = x - floorX;
const halnf fy = y - floorY;
const halnf n00 = dotGradient(hash(x0, y0), fx, fy);
const halnf n10 = dotGradient(hash(x1, y0), fx - 1, fy);
const halnf n01 = dotGradient(hash(x0, y1), fx, fy - 1);
const halnf n11 = dotGradient(hash(x1, y1), fx - 1, fy - 1);
const halnf u = fade(fx);
const halnf v = fade(fy);
const halnf top = n00 + (n10 - n00) * u;
const halnf bottom = n01 + (n11 - n01) * u;
return (top + (bottom - top) * v) * 1.4142f;
}
halnf TileableNoise::fractal(halnf x, halnf y, int4 period, const halnf* weights, int4 octaves) const {
halnf sum = 0;
halnf total = 0;
int4 scale = 1;
for (int4 octave = 0; octave < octaves; octave++) {
sum += gradient(x * (halnf) scale, y * (halnf) scale, period * scale, period * scale) * weights[octave];
total += weights[octave];
scale *= 2;
}
return total > 0 ? sum / total : 0;
}
}

View file

@ -106,4 +106,9 @@ namespace tp {
HSV rgbs;
flt4 a = 1.f;
};
constexpr halnf kDisplayGamma = 2.2f;
[[nodiscard]] halnf gammaToLinear(halnf encoded);
[[nodiscard]] halnf linearToGamma(halnf linear);
}

22
Math/public/Noise.hpp Normal file
View file

@ -0,0 +1,22 @@
#pragma once
#include "Environment.hpp"
#include <array>
#include <cstdint>
namespace tp {
class TileableNoise {
public:
explicit TileableNoise(uint32_t seed = 0);
[[nodiscard]] halnf gradient(halnf x, halnf y, int4 periodX, int4 periodY) const;
[[nodiscard]] halnf fractal(halnf x, halnf y, int4 period, const halnf* weights, int4 octaves) const;
private:
[[nodiscard]] int4 hash(int4 x, int4 y) const;
std::array<uint8_t, 512> mPermutation{};
};
}

View file

@ -2,154 +2,151 @@
#include "Color.hpp"
#include "Rect.hpp"
#include "Timing.hpp"
#include <cmath>
namespace tp {
struct SpringParameters {
halnf stiffness = 0.0008f;
halnf damping = 0.0566f;
static SpringParameters criticallyDamped(halnf responseMs) {
const halnf angularFrequency = 2.0f * 3.14159265f / responseMs;
return { angularFrequency * angularFrequency, 2.0f * angularFrequency };
}
};
class SpringValue {
public:
SpringValue() = default;
void updateCurrentPosition() {
const auto deltaTime = halnf(gCurrentTime - mPrevTime);
const auto deltaPos = mTargetPosition - mCurrentPosition;
explicit SpringValue(halnf value) :
mValue(value),
mTarget(value) {}
auto deltaVelocity = deltaPos * mSpringStiffness;
deltaVelocity -= mVelocity * (mVelocityDamping);
void setParameters(const SpringParameters& parameters) { mParameters = parameters; }
mVelocity += deltaVelocity;
mCurrentPosition += mVelocity * deltaTime;
mPrevTime = gCurrentTime;
}
void setTarget(halnf target) { mTarget = target; }
void setTargetPosition(halnf pos) {
mTargetPosition = pos;
if (mVelocity == 0) mPrevTime = gCurrentTime;
}
[[nodiscard]] halnf getCurrentPos() const { return mCurrentPosition; }
[[nodiscard]] halnf getTargetPos() const { return mTargetPosition; }
[[nodiscard]] halnf getVelocity() const { return mVelocity; }
[[nodiscard]] halnf getDeltaPos() const { return mTargetPosition - mCurrentPosition; }
void endAnimation() {
mCurrentPosition = mTargetPosition;
void snapTo(halnf value) {
mValue = value;
mTarget = value;
mVelocity = 0;
}
void snapToTarget() { snapTo(mTarget); }
void launch(halnf value, halnf velocity, halnf target) {
mValue = value;
mVelocity = velocity;
mTarget = target;
}
void step(halnf deltaMs) {
while (deltaMs > 0) {
const halnf substep = deltaMs < kMaxSubstepMs ? deltaMs : kMaxSubstepMs;
const halnf acceleration = mParameters.stiffness * (mTarget - mValue) - mParameters.damping * mVelocity;
mVelocity += acceleration * substep;
mValue += mVelocity * substep;
deltaMs -= substep;
}
}
[[nodiscard]] bool isSettled(halnf tolerance) const {
const bool closeToTarget = std::fabs(mTarget - mValue) <= tolerance;
const bool nearlyStill = std::fabs(mVelocity) * kVelocityHorizonMs <= tolerance;
return closeToTarget && nearlyStill;
}
[[nodiscard]] halnf value() const { return mValue; }
[[nodiscard]] halnf target() const { return mTarget; }
[[nodiscard]] halnf velocity() const { return mVelocity; }
private:
time_ms mPrevTime = 0;
halnf mTargetPosition = 0;
halnf mCurrentPosition = 0;
static constexpr halnf kMaxSubstepMs = 1.0f;
static constexpr halnf kVelocityHorizonMs = 16.0f;
SpringParameters mParameters;
halnf mValue = 0;
halnf mTarget = 0;
halnf mVelocity = 0;
halnf mSpringStiffness = 0.0041;
halnf mVelocityDamping = 0.25f;
};
class SpringVec {
template <int ChannelCount>
class SpringChannels {
public:
SpringVec() = default;
void updateCurrentPosition() {
mPosX.updateCurrentPosition();
mPosY.updateCurrentPosition();
void setParameters(const SpringParameters& parameters) {
for (auto& channel : mChannels)
channel.setParameters(parameters);
}
void setTargetPosition(Vec2F pos) {
mPosX.setTargetPosition(pos.x);
mPosY.setTargetPosition(pos.y);
void step(halnf deltaMs) {
for (auto& channel : mChannels)
channel.step(deltaMs);
}
[[nodiscard]] Vec2F getCurrentPos() const {
return { mPosX.getCurrentPos(), mPosY.getCurrentPos() };
void snapToTarget() {
for (auto& channel : mChannels)
channel.snapToTarget();
}
[[nodiscard]] Vec2F getTargetPos() const {
return { mPosX.getTargetPos(), mPosY.getTargetPos() };
[[nodiscard]] bool isSettled(halnf tolerance) const {
for (const auto& channel : mChannels) {
if (!channel.isSettled(tolerance)) return false;
}
return true;
}
[[nodiscard]] Vec2F getVelocity() const {
return { mPosX.getVelocity(), mPosY.getVelocity() };
protected:
void setChannelTargets(const halnf (&targets)[ChannelCount]) {
for (int index = 0; index < ChannelCount; index++)
mChannels[index].setTarget(targets[index]);
}
[[nodiscard]] bool checkAnimationShouldEnd() const {
halnf velocityEpsilon = 0.0001f;
halnf positionEpsilon = 1;
const auto vel = abs(mPosY.getVelocity()) > velocityEpsilon || abs(mPosX.getVelocity()) > velocityEpsilon;
const auto pos = abs(mPosY.getDeltaPos()) > positionEpsilon || abs(mPosX.getDeltaPos()) > positionEpsilon;
return !(pos || vel);
void snapChannels(const halnf (&values)[ChannelCount]) {
for (int index = 0; index < ChannelCount; index++)
mChannels[index].snapTo(values[index]);
}
void endAnimation() {
mPosX.endAnimation();
mPosY.endAnimation();
}
[[nodiscard]] halnf channelValue(int index) const { return mChannels[index].value(); }
[[nodiscard]] halnf channelTarget(int index) const { return mChannels[index].target(); }
private:
SpringValue mPosY;
SpringValue mPosX;
SpringValue mChannels[ChannelCount];
};
class SpringRect {
class SpringVec2 : public SpringChannels<2> {
public:
SpringRect() = default;
void setTarget(const Vec2F& target) { setChannelTargets({ target.x, target.y }); }
void snapTo(const Vec2F& value) { snapChannels({ value.x, value.y }); }
[[nodiscard]] RectF getTargetRect() const {
const auto start = mStartPos.getTargetPos();
const auto end = mEndPos.getTargetPos();
return { start, end - start };
}
[[nodiscard]] RectF getCurrentRect() const {
const auto start = mStartPos.getCurrentPos();
const auto end = mEndPos.getCurrentPos();
return { start, end - start };
}
[[nodiscard]] RGBA getCurrentColor() const {
const auto start = mStartPos.getCurrentPos();
const auto end = mEndPos.getCurrentPos();
return { start.x, start.y, end.x, end.y };
}
void setTargetRect(const RectF& rect) {
mStartPos.setTargetPosition(rect.p1());
mEndPos.setTargetPosition(rect.p3());
}
void setTargetColor(const RGBA& color) {
mStartPos.setTargetPosition({ color.r, color.g });
mEndPos.setTargetPosition({ color.b, color.a });
}
SpringVec& getStart() {
return mStartPos;
}
SpringVec& getEnd() {
return mEndPos;
}
void updateCurrentRect() {
mStartPos.updateCurrentPosition();
mEndPos.updateCurrentPosition();
}
[[nodiscard]] bool shouldEndTransition() const {
return mStartPos.checkAnimationShouldEnd() && mEndPos.checkAnimationShouldEnd();
}
void endAnimation() {
mStartPos.endAnimation();
mEndPos.endAnimation();
}
private:
SpringVec mStartPos;
SpringVec mEndPos;
[[nodiscard]] Vec2F value() const { return { channelValue(0), channelValue(1) }; }
[[nodiscard]] Vec2F target() const { return { channelTarget(0), channelTarget(1) }; }
};
}
class SpringRect : public SpringChannels<4> {
public:
void setTarget(const RectF& target) { setChannelTargets({ target.x, target.y, target.z, target.w }); }
void snapTo(const RectF& value) { snapChannels({ value.x, value.y, value.z, value.w }); }
[[nodiscard]] RectF value() const { return { channelValue(0), channelValue(1), channelValue(2), channelValue(3) }; }
[[nodiscard]] RectF target() const {
return { channelTarget(0), channelTarget(1), channelTarget(2), channelTarget(3) };
}
};
class SpringColor : public SpringChannels<4> {
public:
void setTarget(const RGBA& target) { setChannelTargets({ target.r, target.g, target.b, target.a }); }
void snapTo(const RGBA& value) { snapChannels({ value.r, value.g, value.b, value.a }); }
[[nodiscard]] RGBA value() const { return { channelValue(0), channelValue(1), channelValue(2), channelValue(3) }; }
[[nodiscard]] RGBA target() const {
return { channelTarget(0), channelTarget(1), channelTarget(2), channelTarget(3) };
}
};
}

View file

@ -41,6 +41,63 @@ SUITE(Math) {
}
}
int main() {
return UnitTest::RunAllTests();
int main() { return UnitTest::RunAllTests(); }
#include "Color.hpp"
#include "Noise.hpp"
#include <cmath>
SUITE(TileableNoise) {
TEST(NoiseTilesAcrossItsPeriod) {
const TileableNoise noise(7);
for (int index = 0; index < 20; index++) {
const halnf x = 0.37f * (halnf) index;
const halnf y = 1.13f + 0.21f * (halnf) index;
CHECK_CLOSE(noise.gradient(x, y, 8, 4), noise.gradient(x + 8, y, 8, 4), 1e-5f);
CHECK_CLOSE(noise.gradient(x, y, 8, 4), noise.gradient(x, y + 4, 8, 4), 1e-5f);
}
}
TEST(NoiseIsZeroOnLatticeAndBounded) {
const TileableNoise noise(3);
CHECK_CLOSE(0.0f, noise.gradient(2, 5, 16, 16), 1e-6f);
halnf low = 0;
halnf high = 0;
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) {
const halnf value = noise.gradient((halnf) x * 0.13f, (halnf) y * 0.17f, 16, 16);
low = std::min(low, value);
high = std::max(high, value);
}
}
CHECK(low >= -1.0f && high <= 1.0f);
CHECK(high - low > 0.8f);
}
TEST(SeedsChangeThePattern) {
const TileableNoise a(1);
const TileableNoise b(2);
halnf difference = 0;
for (int index = 0; index < 32; index++)
difference += std::fabs(a.gradient(0.3f * index, 0.7f, 32, 32) - b.gradient(0.3f * index, 0.7f, 32, 32));
CHECK(difference > 1);
}
TEST(FractalStaysTileableAtEveryOctave) {
const TileableNoise noise(5);
const halnf weights[] = { 1, 0.5f, 0.25f, 0.125f };
CHECK_CLOSE(noise.fractal(0.4f, 0.6f, 4, weights, 4), noise.fractal(4.4f, 4.6f, 4, weights, 4), 1e-5f);
}
}
SUITE(DisplayGamma) {
TEST(GammaRoundTripsAndMatchesReferenceTable) {
CHECK_CLOSE(169.0f / 16384.0f, gammaToLinear(0.125f), 2e-4f);
CHECK_CLOSE(6367.0f / 16384.0f, linearToGamma(0.125f), 2e-4f);
for (halnf value = 0; value <= 1.0f; value += 0.05f)
CHECK_CLOSE(value, linearToGamma(gammaToLinear(value)), 1e-5f);
CHECK_EQUAL(0.0f, gammaToLinear(-1));
CHECK_EQUAL(1.0f, linearToGamma(2));
}
}