Math: tileable noise, display gamma and explicit channel springs #7
6 changed files with 293 additions and 117 deletions
|
|
@ -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
81
Math/private/Noise.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
22
Math/public/Noise.hpp
Normal 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{};
|
||||
};
|
||||
}
|
||||
|
|
@ -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) };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue