diff --git a/CHANGELOG.md b/CHANGELOG.md index 03f1bfc..e526175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ commit that made it. The interface shows this file under Changelog in its menu. the gate, flicker, dust and scratches. On the command line it is `-e filmgate` with `--gate-grain`, `--gate-weave`, `--gate-flicker`, `--gate-dust` and `--gate-scratches`. +- Datamosh, a video effect: blocks of the picture break as Corrupt breaks + them, each for a stretch of the clip before it heals, and a displaced + block's picture smears along while it shows. On the command line it is + `-e datamosh` with `--mosh-rate`, `--mosh-hold`, `--mosh-drift`, + `--mosh-speed` and the rest. - A Quality choice under Video on Setup: Small file, Standard or High, for how the saved video is encoded. It is kept in the project, and on the command line it is `--video-quality small|standard|high`. Standard writes diff --git a/CMakeLists.txt b/CMakeLists.txt index f59b935..2db2d02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ add_library(effector_core STATIC src/effects/blur.cpp src/effects/channelsplit.cpp src/effects/corrupt.cpp + src/effects/datamosh.cpp src/effects/feedback.cpp src/effects/filmgate.cpp src/effects/flow.cpp diff --git a/src/core/registry.cpp b/src/core/registry.cpp index b5ab07c..ead829b 100644 --- a/src/core/registry.cpp +++ b/src/core/registry.cpp @@ -4,6 +4,7 @@ #include "effects/blur.hpp" #include "effects/channelsplit.hpp" #include "effects/corrupt.hpp" +#include "effects/datamosh.hpp" #include "effects/feedback.hpp" #include "effects/filmgate.hpp" #include "effects/flow.hpp" @@ -49,6 +50,7 @@ const std::vector& effects() { std::make_shared(), std::make_shared(), std::make_shared(), + std::make_shared(), }; return registry; } diff --git a/src/effects/corrupt.cpp b/src/effects/corrupt.cpp index c240815..32bbb73 100644 --- a/src/effects/corrupt.cpp +++ b/src/effects/corrupt.cpp @@ -1,10 +1,9 @@ #include "effects/corrupt.hpp" #include -#include #include -#include "core/parallel.hpp" +#include "effects/corrupt_blocks.hpp" #include "effects/sampling.hpp" #include "effects/source_fill.hpp" @@ -28,19 +27,6 @@ const std::vector& CorruptEffect::params() const { return specs; } -namespace { - -enum class Damage : std::uint8_t { None, Repeat, Displace, Colour }; - -struct Block { - Damage damage = Damage::None; - int sx = 0; // displace: where the block is read from - int sy = 0; - int shift[3] = {0, 0, 0}; // colour: added to each channel -}; - -} // namespace - Image CorruptEffect::render(const std::vector& 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."); @@ -55,23 +41,6 @@ Image CorruptEffect::render(const std::vector& sources, int width, int he const int cols = (width + size - 1) / size; const int rows = (height + size - 1) / size; - auto drawBlock = [&](int bx) { - Block block; - if (kinds == "repeat") block.damage = Damage::Repeat; - else if (kinds == "displace") block.damage = Damage::Displace; - else if (kinds == "colour") block.damage = Damage::Colour; - else block.damage = static_cast(1 + rng.below(2)); - // The first block of a row has nothing to its left to repeat. - if (block.damage == Damage::Repeat && bx == 0) block.damage = Damage::Displace; - if (block.damage == Damage::Displace) { - block.sx = static_cast(rng.inRange(0, std::max(0, width - size))); - block.sy = static_cast(rng.inRange(0, std::max(0, height - size))); - } else if (block.damage == Damage::Colour) { - for (int& shift : block.shift) shift = static_cast(rng.inRange(-128, 128)); - } - return block; - }; - // The whole damage map is drawn up front, block by block in reading order. std::vector plan(static_cast(cols) * rows); for (int by = 0; by < rows; ++by) { @@ -88,54 +57,13 @@ Image CorruptEffect::render(const std::vector& sources, int width, int he continue; } if (!chance(rng.next(), rate)) continue; - block = drawBlock(bx); + block = drawDamage(rng, kinds, bx, width, height, size); if (bleed) carried = █ } } Image canvas = input; - // Block rows are independent (a repeat only ever reads to its left), so - // they run in parallel and each walks its row in order. - parallelFor(static_cast(rows), [&](std::size_t begin, std::size_t end) { - for (std::size_t by = begin; by < end; ++by) { - const int y0 = static_cast(by) * size; - const int h = std::min(size, height - y0); - for (int bx = 0; bx < cols; ++bx) { - const Block& block = plan[by * static_cast(cols) + bx]; - if (block.damage == Damage::None) continue; - const int x0 = bx * size; - const int w = std::min(size, width - x0); - const std::size_t bytes = static_cast(w) * 3; - - for (int line = 0; line < h; ++line) { - std::uint8_t* out = canvas.row(y0 + line) + static_cast(x0) * 3; - switch (block.damage) { - case Damage::Repeat: - std::memmove(out, out - static_cast(size) * 3, bytes); - break; - case Damage::Displace: { - const int sy = std::min(block.sy + line, height - 1); - const std::uint8_t* in = input.row(sy); - for (int x = 0; x < w; ++x) { - const int sx = (block.sx + x) % width; - std::memcpy(out + static_cast(x) * 3, - in + static_cast(sx) * 3, 3); - } - break; - } - case Damage::Colour: - for (std::size_t i = 0; i < bytes; ++i) { - out[i] = static_cast( - std::clamp(out[i] + block.shift[i % 3], 0, 255)); - } - break; - case Damage::None: - break; - } - } - } - } - }); + paintBlocks(canvas, input, plan, cols, rows, size); return canvas; } diff --git a/src/effects/corrupt_blocks.hpp b/src/effects/corrupt_blocks.hpp new file mode 100644 index 0000000..aba4af2 --- /dev/null +++ b/src/effects/corrupt_blocks.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "core/image.hpp" +#include "core/parallel.hpp" +#include "core/rng.hpp" + +namespace effector { + +// The damage a codec does to one block of the picture. Shared by the corrupt +// and the datamosh effects. +enum class Damage : std::uint8_t { None, Repeat, Displace, Colour }; + +struct Block { + Damage damage = Damage::None; + int sx = 0; // displace: where the block is read from + int sy = 0; + int shift[3] = {0, 0, 0}; // colour: added to each channel +}; + +// Draws the damage of one block, of the kind `kinds` names ("mixed" picks +// one), for a block `bx` blocks from the left of a canvas `width` by `height`. +inline Block drawDamage(Rng& rng, const std::string& kinds, int bx, int width, int height, + int size) { + Block block; + if (kinds == "repeat") block.damage = Damage::Repeat; + else if (kinds == "displace") block.damage = Damage::Displace; + else if (kinds == "colour") block.damage = Damage::Colour; + else block.damage = static_cast(1 + rng.below(2)); + // The first block of a row has nothing to its left to repeat. + if (block.damage == Damage::Repeat && bx == 0) block.damage = Damage::Displace; + if (block.damage == Damage::Displace) { + block.sx = static_cast(rng.inRange(0, std::max(0, width - size))); + block.sy = static_cast(rng.inRange(0, std::max(0, height - size))); + } else if (block.damage == Damage::Colour) { + for (int& shift : block.shift) shift = static_cast(rng.inRange(-128, 128)); + } + return block; +} + +// Lays the damage map `plan` (`cols` by `rows` blocks of `size` pixels, in +// reading order) over `canvas`, which starts as a copy of `input`. A displaced +// block reads `input` from (sx, sy) on, wrapping across its width. +inline void paintBlocks(Image& canvas, const Image& input, const std::vector& plan, int cols, + int rows, int size) { + const int width = canvas.width(); + const int height = canvas.height(); + // Block rows are independent (a repeat only ever reads to its left), so + // they run in parallel and each walks its row in order. + parallelFor(static_cast(rows), [&](std::size_t begin, std::size_t end) { + for (std::size_t by = begin; by < end; ++by) { + const int y0 = static_cast(by) * size; + const int h = std::min(size, height - y0); + for (int bx = 0; bx < cols; ++bx) { + const Block& block = plan[by * static_cast(cols) + bx]; + if (block.damage == Damage::None) continue; + const int x0 = bx * size; + const int w = std::min(size, width - x0); + const std::size_t bytes = static_cast(w) * 3; + + for (int line = 0; line < h; ++line) { + std::uint8_t* out = canvas.row(y0 + line) + static_cast(x0) * 3; + switch (block.damage) { + case Damage::Repeat: + std::memmove(out, out - static_cast(size) * 3, bytes); + break; + case Damage::Displace: { + const int sy = std::min(block.sy + line, height - 1); + const std::uint8_t* in = input.row(sy); + for (int x = 0; x < w; ++x) { + const int sx = (block.sx + x) % width; + std::memcpy(out + static_cast(x) * 3, + in + static_cast(sx) * 3, 3); + } + break; + } + case Damage::Colour: + for (std::size_t i = 0; i < bytes; ++i) { + out[i] = static_cast( + std::clamp(out[i] + block.shift[i % 3], 0, 255)); + } + break; + case Damage::None: + break; + } + } + } + } + }); +} + +} // namespace effector diff --git a/src/effects/datamosh.cpp b/src/effects/datamosh.cpp new file mode 100644 index 0000000..be77a8f --- /dev/null +++ b/src/effects/datamosh.cpp @@ -0,0 +1,135 @@ +#include "effects/datamosh.hpp" + +#include +#include +#include +#include + +#include "effects/corrupt_blocks.hpp" +#include "effects/sampling.hpp" + +namespace effector { + +const std::vector& DatamoshEffect::params() const { + static const std::vector specs = { + ParamSpec::makeInt("mosh_block", "Block size", 16, 2, 256, + "Width and height in pixels of one block, as a codec would cut them."), + ParamSpec::makeDouble("mosh_rate", "Rate", 0.1, 0.0, 1.0, + "Chance of each block breaking at some moment of the cycle."), + ParamSpec::makeChoice("mosh_kinds", "Damage", "mixed", + {"mixed", "repeat", "displace", "colour"}, + "repeat freezes a block into a copy of the one to its left, " + "displace pulls one in from elsewhere in the image, colour knocks " + "it off colour; mixed picks one of them for every block."), + ParamSpec::makeDouble("mosh_hold", "Hold", 0.3, 0.01, 1.0, + "How much of each cycle a broken block stays broken, give or take " + "half; 1 keeps it broken throughout."), + ParamSpec::makeInt("mosh_drift", "Drift", 24, 0, 4096, + "How far in pixels a displaced block's picture slides while it " + "shows."), + ParamSpec::makeDouble("mosh_speed", "Speed", 0.5, 0.0, 10.0, + "Cycles per second, rounded so that the clip loops without a jump; " + "0 holds still.", + 0.05, 2), + ParamSpec::makeBool("mosh_bleed", "Bleed", false, + "Carry each broken block's fault on to the end of its row, the way " + "one broken bit spoils the rest of a stream."), + }; + return specs; +} + +namespace { + +struct Settings { + int size; + double rate; + std::string kinds; + double hold; + int drift; + double speed; + bool bleed; + + explicit Settings(const ParamValues& values) + : size(static_cast(std::max(2L, paramInt(values, "mosh_block", 16)))), + rate(std::clamp(paramDouble(values, "mosh_rate", 0.1), 0.0, 1.0)), + kinds(paramChoice(values, "mosh_kinds", "mixed")), + hold(std::clamp(paramDouble(values, "mosh_hold", 0.3), 0.01, 1.0)), + drift(static_cast(std::max(0L, paramInt(values, "mosh_drift", 24)))), + speed(std::max(0.0, paramDouble(values, "mosh_speed", 0.5))), + bleed(paramBool(values, "mosh_bleed", false)) {} +}; + +// A block that breaks at some moment of the cycle: how, from when, for how +// long (both as parts of a cycle), and which way a displaced one smears. +struct Break { + Block block; + double start = 0.0; + double length = 0.0; + double driftX = 0.0; + double driftY = 0.0; +}; + +} // namespace + +Image DatamoshEffect::animate(Image input, const ParamValues& values, const Moment& at) const { + const Settings settings(values); + const int width = input.width(); + const int height = input.height(); + const int size = settings.size; + const int cols = (width + size - 1) / size; + const int rows = (height + size - 1) / size; + + // What stays put for the whole clip: the whole map of breaks, block by + // block in reading order, as Corrupt draws its damage. + Rng steady = at.steady(); + std::vector breaks(static_cast(cols) * rows); + bool any = false; + for (int by = 0; by < rows; ++by) { + const Break* carried = nullptr; + for (int bx = 0; bx < cols; ++bx) { + Break& piece = breaks[static_cast(by) * cols + bx]; + if (carried != nullptr) { + piece = *carried; + // A displaced run keeps moving along with the row, so the + // stretch reads as one shifted strip. + if (piece.block.damage == Damage::Displace) piece.block.sx += size; + carried = &piece; + continue; + } + if (!chance(steady.next(), settings.rate)) continue; + piece.block = drawDamage(steady, settings.kinds, bx, width, height, size); + piece.start = unitDouble(steady.next()); + piece.length = std::min(1.0, settings.hold * (0.5 + unitDouble(steady.next()))); + sinCosDegrees(unitDouble(steady.next()) * 360.0, piece.driftY, piece.driftX); + if (settings.bleed) carried = &piece; + any = true; + } + } + if (!any) return input; + + // Where this frame is in time: which blocks are broken, and how far + // through its stretch each displaced one is. + const double t = settings.speed > 0.0 ? at.cycle(settings.speed) : 0.0; + std::vector plan(breaks.size()); + for (std::size_t i = 0; i < breaks.size(); ++i) { + const Break& piece = breaks[i]; + if (piece.block.damage == Damage::None) continue; + const double into = t - piece.start - std::floor(t - piece.start); + if (piece.length < 1.0 && into >= piece.length) continue; + Block& block = plan[i]; + block = piece.block; + if (block.damage == Damage::Displace && settings.drift > 0) { + const double slide = settings.drift * into / piece.length; + const int sx = block.sx + static_cast(std::lround(slide * piece.driftX)); + block.sx = (sx % width + width) % width; + block.sy = std::clamp(block.sy + static_cast(std::lround(slide * piece.driftY)), 0, + height - 1); + } + } + + Image canvas = input; + paintBlocks(canvas, input, plan, cols, rows, size); + return canvas; +} + +} // namespace effector diff --git a/src/effects/datamosh.hpp b/src/effects/datamosh.hpp new file mode 100644 index 0000000..3220b5d --- /dev/null +++ b/src/effects/datamosh.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "effects/video_effect.hpp" + +namespace effector { + +// The corrupt effect in motion: blocks of the picture break as a damaged +// stream breaks them, and each broken block holds for a stretch of the clip +// before it heals, a displaced one smearing its picture along while it shows. +// +// Which blocks break, how, and when in the cycle each is broken is drawn from +// at.steady(); a frame only asks which of them are broken at its moment, so +// the clip loops without a jump. +class DatamoshEffect : public VideoEffect { +public: + std::string name() const override { return "datamosh"; } + std::string title() const override { return "Datamosh"; } + std::string description() const override { + return "Breaks blocks of the picture as Corrupt does, each for a stretch of the clip, " + "displaced blocks smearing along while they show."; + } + const std::vector& params() const override; + +protected: + Image animate(Image input, const ParamValues& values, const Moment& at) const override; +}; + +} // namespace effector diff --git a/tests/core_tests.cpp b/tests/core_tests.cpp index ac58fe5..17242c3 100644 --- a/tests/core_tests.cpp +++ b/tests/core_tests.cpp @@ -2115,6 +2115,8 @@ int main() { {"gate_scratches", 0L}}, {{"gate_weave", 0L}, {"gate_grain", 0.0}, {"gate_flicker", 0.0}, {"gate_dust", 0L}, {"gate_scratches", 0L}}}, + {"datamosh", {{"mosh_rate", 0.4}, {"mosh_block", 8L}, {"mosh_bleed", true}}, {}, + {{"mosh_rate", 0.0}}}, }; for (const auto& [name, lively, smooth, off] : moving) { const auto effect = effectByName(name);