Slit shift, channel split, pixel sort, corruption, shatter, warp, bit crush and feedback: tearing bands sideways, knocking the colour planes out of register, sorting runs of pixels, damaging blocks the way a broken stream does, breaking the picture into shards, pushing it along a noise field, mangling its bytes and laying it over itself again and again. All eight are pure transforms. None needs sources, so any of them can lead a chain, and like blur they start from a crop of a random source when they do and photographs are loaded. canvasFromSources holds that start once for them. Each draws its whole plan from the rng before touching a pixel, and pixel sort hands every line a seed of its own, so the parallel work never decides anything. The rotations in shatter and feedback take their sine and cosine from a short series of plain multiplications rather than libm, so a seed lands on the same pixels whatever the machine's maths library makes of std::sin. The tests hold every one of them to the same things -- a seed reproduces it, it runs without sources and as a later stage, a sliver of a canvas does not trip it, turned right down it leaves its input alone -- plus a check of what each actually does, and the registry now checks that no two effects declare the same param key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
4.4 KiB
C++
104 lines
4.4 KiB
C++
#include "effects/feedback.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <vector>
|
|
|
|
#include "core/parallel.hpp"
|
|
#include "effects/sampling.hpp"
|
|
#include "effects/source_fill.hpp"
|
|
|
|
namespace effector {
|
|
|
|
const std::vector<ParamSpec>& FeedbackEffect::params() const {
|
|
static const std::vector<ParamSpec> specs = {
|
|
ParamSpec::makeInt("echo_count", "Echoes", 6, 1, 64,
|
|
"How many times the image is laid back over itself."),
|
|
ParamSpec::makeDouble("echo_scale", "Scale", 0.85, 0.1, 2.0,
|
|
"Size of each echo against the one before; below 1 the echoes "
|
|
"tunnel inwards, above 1 they burst outwards.",
|
|
0.01),
|
|
ParamSpec::makeDouble("echo_rotate", "Rotation", 4.0, -180.0, 180.0,
|
|
"Turn in degrees of each echo against the one before.", 0.5, 1),
|
|
ParamSpec::makeInt("echo_drift", "Drift", 20, 0, 4096,
|
|
"Furthest in pixels each echo may wander off centre, either way."),
|
|
ParamSpec::makeDouble("echo_fade", "Opacity", 0.8, 0.0, 1.0,
|
|
"How strongly each echo covers what lies beneath it."),
|
|
};
|
|
return specs;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct Echo {
|
|
double dx = 0.0;
|
|
double dy = 0.0;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
Image FeedbackEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
|
const ParamValues& values) const {
|
|
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
|
|
|
const long count = std::clamp(paramInt(values, "echo_count", 6), 1L, 64L);
|
|
const double scale = std::clamp(paramDouble(values, "echo_scale", 0.85), 0.1, 2.0);
|
|
const double rotate = std::clamp(paramDouble(values, "echo_rotate", 4.0), -180.0, 180.0);
|
|
const int drift = static_cast<int>(std::max(0L, paramInt(values, "echo_drift", 20)));
|
|
const double fade = std::clamp(paramDouble(values, "echo_fade", 0.8), 0.0, 1.0);
|
|
|
|
const Image input = canvasFromSources(sources, width, height, rng);
|
|
|
|
std::vector<Echo> echoes(static_cast<std::size_t>(count));
|
|
for (auto& echo : echoes) {
|
|
echo.dx = static_cast<double>(rng.inRange(-drift, drift));
|
|
echo.dy = static_cast<double>(rng.inRange(-drift, drift));
|
|
}
|
|
|
|
double sine = 0.0;
|
|
double cosine = 1.0;
|
|
sinCosDegrees(rotate, sine, cosine);
|
|
const double cx = (width - 1) * 0.5;
|
|
const double cy = (height - 1) * 0.5;
|
|
const int mix = static_cast<int>(fade * 256.0 + 0.5);
|
|
|
|
// A camera filming its own monitor: every pass lays the whole picture so
|
|
// far, shrunk (or grown), turned and drifted, over the untouched input, so
|
|
// the older echoes nest inside the newer ones and fade as they go.
|
|
Image frame = input;
|
|
Image next(width, height);
|
|
for (const Echo& echo : echoes) {
|
|
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
|
for (std::size_t row = begin; row < end; ++row) {
|
|
const std::uint8_t* base = input.row(static_cast<int>(row));
|
|
std::uint8_t* out = next.row(static_cast<int>(row));
|
|
const double ry = static_cast<double>(row) - cy - echo.dy;
|
|
for (int col = 0; col < width; ++col) {
|
|
const std::size_t at = static_cast<std::size_t>(col) * 3;
|
|
const double rx = col - cx - echo.dx;
|
|
const double sx = cx + (rx * cosine + ry * sine) / scale;
|
|
const double sy = cy + (-rx * sine + ry * cosine) / scale;
|
|
if (sx < 0.0 || sy < 0.0 || sx > width - 1 || sy > height - 1) {
|
|
out[at] = base[at];
|
|
out[at + 1] = base[at + 1];
|
|
out[at + 2] = base[at + 2];
|
|
continue;
|
|
}
|
|
std::uint8_t echoed[3];
|
|
sampleBilinear(frame, sx, sy, echoed);
|
|
for (int c = 0; c < 3; ++c) {
|
|
const int under = base[at + static_cast<std::size_t>(c)];
|
|
out[at + static_cast<std::size_t>(c)] =
|
|
static_cast<std::uint8_t>(under + (echoed[c] - under) * mix / 256);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
std::swap(frame, next);
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
} // namespace effector
|