// Based on works and code by Shawn Silverman. #include "fl/stdint.h" #include "fl/clamp.h" #include "fl/namespace.h" #include "fl/wave_simulation.h" #include "fl/int.h" namespace { fl::u8 half_duplex_blend_sqrt_q15(fl::u16 x) { x = MIN(x, 32767); // Q15 const int Q = 15; fl::u32 X = (fl::u32)x << Q; // promote to Q30 fl::u32 y = (1u << Q); // start at “1.0” in Q15 // 3–4 iterations is plenty for 15‑bit precision: for (int i = 0; i < 4; i++) { y = (y + (X / y)) >> 1; } return static_cast(y) >> 8; } fl::u8 half_duplex_blend_linear(fl::u16 x) { x = MIN(x, 32767); // Q15 x *= 2; return x >> 8; } } // namespace namespace fl { void WaveSimulation2D::setSpeed(float speed) { mSim->setSpeed(speed); } WaveSimulation2D::WaveSimulation2D(u32 W, u32 H, SuperSample factor, float speed, float dampening) { init(W, H, factor, speed, dampening); } void WaveSimulation2D::init(u32 width, u32 height, SuperSample factor, float speed, int dampening) { mOuterWidth = width; mOuterHeight = height; mMultiplier = static_cast(factor); mSim.reset(); // clear out memory first. u32 w = width * mMultiplier; u32 h = height * mMultiplier; mSim.reset(new WaveSimulation2D_Real(w, h, speed, dampening)); // Only allocate change grid if it's enabled (saves memory when disabled) if (mUseChangeGrid) { mChangeGrid.reset(w, h); } // Extra frames are needed because the simulation slows down in // proportion to the supersampling factor. mExtraFrames = u8(factor) - 1; } void WaveSimulation2D::setDampening(int damp) { mSim->setDampening(damp); } int WaveSimulation2D::getDampenening() const { return mSim->getDampenening(); } float WaveSimulation2D::getSpeed() const { return mSim->getSpeed(); } float WaveSimulation2D::getf(fl::size x, fl::size y) const { if (!has(x, y)) return 0.0f; float sum = 0.0f; for (u32 j = 0; j < mMultiplier; ++j) { for (u32 i = 0; i < mMultiplier; ++i) { sum += mSim->getf(x * mMultiplier + i, y * mMultiplier + j); } } return sum / static_cast(mMultiplier * mMultiplier); } i16 WaveSimulation2D::geti16(fl::size x, fl::size y) const { if (!has(x, y)) return 0; i32 sum = 0; u8 mult = MAX(1, mMultiplier); for (u32 j = 0; j < mult; ++j) { for (u32 i = 0; i < mult; ++i) { u32 xx = x * mult + i; u32 yy = y * mult + j; i32 pt = mSim->geti16(xx, yy); if (mUseChangeGrid) { // i32 ch_pt = mChangeGrid[(yy * mMultiplier) + xx]; i32 ch_pt = mChangeGrid(xx, yy); if (ch_pt != 0) { // we got a hit. sum += ch_pt; } else { sum += pt; } } else { sum += pt; } } } i16 out = static_cast(sum / (mult * mult)); return out; } i16 WaveSimulation2D::geti16Previous(fl::size x, fl::size y) const { if (!has(x, y)) return 0; i32 sum = 0; u8 mult = MAX(1, mMultiplier); for (u32 j = 0; j < mult; ++j) { for (u32 i = 0; i < mult; ++i) { sum += mSim->geti16Previous(x * mult + i, y * mult + j); } } i16 out = static_cast(sum / (mult * mult)); return out; } bool WaveSimulation2D::geti16All(fl::size x, fl::size y, i16 *curr, i16 *prev, i16 *diff) const { if (!has(x, y)) return false; *curr = geti16(x, y); *prev = geti16Previous(x, y); *diff = *curr - *prev; return true; } i8 WaveSimulation2D::geti8(fl::size x, fl::size y) const { return static_cast(geti16(x, y) >> 8); } u8 WaveSimulation2D::getu8(fl::size x, fl::size y) const { i16 value = geti16(x, y); if (mSim->getHalfDuplex()) { u16 v2 = static_cast(value); switch (mU8Mode) { case WAVE_U8_MODE_LINEAR: return half_duplex_blend_linear(v2); case WAVE_U8_MODE_SQRT: return half_duplex_blend_sqrt_q15(v2); } } return static_cast(((static_cast(value) + 32768)) >> 8); } bool WaveSimulation2D::has(fl::size x, fl::size y) const { return (x < mOuterWidth) && (y < mOuterHeight); } void WaveSimulation2D::seti16(fl::size x, fl::size y, i16 v16) { if (!has(x, y)) return; u8 mult = MAX(1, mMultiplier); // radius in pixels of your diamond int rad = static_cast(mult) / 2; for (fl::size j = 0; j < mult; ++j) { for (fl::size i = 0; i < mult; ++i) { // compute offset from the center of this block int dx = static_cast(i) - rad; int dy = static_cast(j) - rad; // keep only those points whose Manhattan distance ≤ rad if (ABS(dx) + ABS(dy) > rad) { continue; } fl::size xx = x * mult + i; fl::size yy = y * mult + j; if (mSim->has(xx, yy)) { if (mUseChangeGrid) { i16 &pt = mChangeGrid.at(xx, yy); if (pt == 0) { // not set yet so set unconditionally. pt = v16; } else { const bool sign_matches = (pt >= 0) == (v16 >= 0); if (!sign_matches) { // opposite signs, so overwrite pt = v16; } else { // if the magnitude of the new pt is greater than what // was already there, then overwrite. u16 abs_pt = static_cast(ABS(pt)); u16 abs_v16 = static_cast(ABS(v16)); if (abs_v16 > abs_pt) { pt = v16; } } } } else { // Directly set the value in the simulation when change grid is disabled mSim->seti16(xx, yy, v16); } } } } } void WaveSimulation2D::setf(fl::size x, fl::size y, float value) { if (!has(x, y)) return; value = fl::clamp(value, 0.0f, 1.0f); i16 v16 = wave_detail::float_to_fixed(value); seti16(x, y, v16); } void WaveSimulation2D::update() { if (mUseChangeGrid) { const vec2 min_max = mChangeGrid.minMax(); const bool has_updates = min_max != vec2(0, 0); for (u8 i = 0; i < mExtraFrames + 1; ++i) { if (has_updates) { // apply them const u32 w = mChangeGrid.width(); const u32 h = mChangeGrid.height(); for (u32 x = 0; x < w; ++x) { for (u32 y = 0; y < h; ++y) { i16 v16 = mChangeGrid(x, y); if (v16 != 0) { mSim->seti16(x, y, v16); } } } } mSim->update(); } // zero out mChangeGrid mChangeGrid.clear(); } else { // When change grid is disabled, just run the simulation updates for (u8 i = 0; i < mExtraFrames + 1; ++i) { mSim->update(); } } } u32 WaveSimulation2D::getWidth() const { return mOuterWidth; } u32 WaveSimulation2D::getHeight() const { return mOuterHeight; } void WaveSimulation2D::setExtraFrames(u8 extra) { mExtraFrames = extra; } void WaveSimulation2D::setUseChangeGrid(bool enabled) { if (mUseChangeGrid == enabled) { return; // No change needed } mUseChangeGrid = enabled; if (mUseChangeGrid) { // Allocate change grid if enabling u32 w = mOuterWidth * mMultiplier; u32 h = mOuterHeight * mMultiplier; mChangeGrid.reset(w, h); } else { // Deallocate change grid if disabling (saves memory) mChangeGrid.reset(0, 0); } } WaveSimulation1D::WaveSimulation1D(u32 length, SuperSample factor, float speed, int dampening) { init(length, factor, speed, dampening); } void WaveSimulation1D::init(u32 length, SuperSample factor, float speed, int dampening) { mOuterLength = length; mMultiplier = static_cast(factor); mSim.reset(); // clear out memory first. mSim.reset( new WaveSimulation1D_Real(length * mMultiplier, speed, dampening)); // Extra updates (frames) are applied because the simulation slows down in // proportion to the supersampling factor. mExtraFrames = static_cast(factor) - 1; } void WaveSimulation1D::setSpeed(float speed) { mSim->setSpeed(speed); } void WaveSimulation1D::setDampening(int damp) { mSim->setDampening(damp); } int WaveSimulation1D::getDampenening() const { return mSim->getDampenening(); } void WaveSimulation1D::setExtraFrames(u8 extra) { mExtraFrames = extra; } float WaveSimulation1D::getSpeed() const { return mSim->getSpeed(); } float WaveSimulation1D::getf(fl::size x) const { if (!has(x)) return 0.0f; float sum = 0.0f; u8 mult = MAX(1, mMultiplier); for (u32 i = 0; i < mult; ++i) { sum += mSim->getf(x * mult + i); } return sum / static_cast(mult); } i16 WaveSimulation1D::geti16(fl::size x) const { if (!has(x)) return 0; u8 mult = MAX(1, mMultiplier); i32 sum = 0; for (u32 i = 0; i < mult; ++i) { sum += mSim->geti16(x * mult + i); } return static_cast(sum / mult); } i16 WaveSimulation1D::geti16Previous(fl::size x) const { if (!has(x)) return 0; u8 mult = MAX(1, mMultiplier); i32 sum = 0; for (u32 i = 0; i < mult; ++i) { sum += mSim->geti16Previous(x * mult + i); } return static_cast(sum / mult); } bool WaveSimulation1D::geti16All(fl::size x, i16 *curr, i16 *prev, i16 *diff) const { if (!has(x)) return false; *curr = geti16(x); *prev = geti16Previous(x); *diff = *curr - *prev; return true; } i8 WaveSimulation1D::geti8(fl::size x) const { return static_cast(geti16(x) >> 8); } // u8 WaveSimulation2D::getu8(fl::size x, fl::size y) const { // i16 value = geti16(x, y); // if (mSim->getHalfDuplex()) { // u16 v2 = static_cast(value); // switch (mU8Mode) { // case WAVE_U8_MODE_LINEAR: // return half_duplex_blend_linear(v2); // case WAVE_U8_MODE_SQRT: // return half_duplex_blend_sqrt_q15(v2); // } // } // return static_cast(((static_cast(value) + 32768)) >> // 8); // } u8 WaveSimulation1D::getu8(fl::size x) const { i16 value = geti16(x); if (mSim->getHalfDuplex()) { u16 v2 = static_cast(value); switch (mU8Mode) { case WAVE_U8_MODE_LINEAR: return half_duplex_blend_linear(v2); case WAVE_U8_MODE_SQRT: return half_duplex_blend_sqrt_q15(v2); } } return static_cast(((static_cast(value) + 32768)) >> 8); } bool WaveSimulation1D::has(fl::size x) const { return (x < mOuterLength); } void WaveSimulation1D::setf(fl::size x, float value) { if (!has(x)) return; value = fl::clamp(value, -1.0f, 1.0f); u8 mult = MAX(1, mMultiplier); for (u32 i = 0; i < mult; ++i) { mSim->set(x * mult + i, value); } } void WaveSimulation1D::update() { mSim->update(); for (u8 i = 0; i < mExtraFrames; ++i) { mSim->update(); } } u32 WaveSimulation1D::getLength() const { return mOuterLength; } } // namespace fl