diff --git a/Math/private/Color.cpp b/Math/private/Color.cpp index de167997..f3488e80 100644 --- a/Math/private/Color.cpp +++ b/Math/private/Color.cpp @@ -1,6 +1,8 @@ #include "Color.hpp" +#include + 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 { diff --git a/Math/private/Noise.cpp b/Math/private/Noise.cpp new file mode 100644 index 00000000..13059b79 --- /dev/null +++ b/Math/private/Noise.cpp @@ -0,0 +1,81 @@ +#include "Noise.hpp" + +#include +#include + +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 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; + } +} diff --git a/Math/public/Color.hpp b/Math/public/Color.hpp index 7cebe3db..55574b65 100644 --- a/Math/public/Color.hpp +++ b/Math/public/Color.hpp @@ -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); } diff --git a/Math/public/Noise.hpp b/Math/public/Noise.hpp new file mode 100644 index 00000000..22336a9d --- /dev/null +++ b/Math/public/Noise.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Environment.hpp" + +#include +#include + +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 mPermutation{}; + }; +} diff --git a/Math/public/SpringAnimations.hpp b/Math/public/SpringAnimations.hpp index 0c8b4f21..20a58e46 100644 --- a/Math/public/SpringAnimations.hpp +++ b/Math/public/SpringAnimations.hpp @@ -2,154 +2,151 @@ #include "Color.hpp" #include "Rect.hpp" -#include "Timing.hpp" + +#include 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 + 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) }; } }; -} \ No newline at end of file + + 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) }; + } + }; +} diff --git a/Math/tests/Tests.cpp b/Math/tests/Tests.cpp index a5514495..5625baec 100644 --- a/Math/tests/Tests.cpp +++ b/Math/tests/Tests.cpp @@ -41,6 +41,63 @@ SUITE(Math) { } } -int main() { - return UnitTest::RunAllTests(); +int main() { return UnitTest::RunAllTests(); } + +#include "Color.hpp" +#include "Noise.hpp" + +#include + +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)); + } }