Add Datamosh, a video effect whose Corrupt blocks break and heal over the clip

Corrupt's block damage moves to effects/corrupt_blocks.hpp so both share it;
Corrupt renders exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-26 20:41:52 +02:00
co-authored by Claude Opus 5.5
parent ce0207c41f
commit a7cb2505d7
8 changed files with 273 additions and 75 deletions
+2
View File
@@ -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<EffectPtr>& effects() {
std::make_shared<FlowEffect>(),
std::make_shared<SweepEffect>(),
std::make_shared<FilmGateEffect>(),
std::make_shared<DatamoshEffect>(),
};
return registry;
}
+3 -75
View File
@@ -1,10 +1,9 @@
#include "effects/corrupt.hpp"
#include <algorithm>
#include <cstring>
#include <vector>
#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<ParamSpec>& 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<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.");
@@ -55,23 +41,6 @@ Image CorruptEffect::render(const std::vector<Image>& 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<Damage>(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<int>(rng.inRange(0, std::max(0, width - size)));
block.sy = static_cast<int>(rng.inRange(0, std::max(0, height - size)));
} else if (block.damage == Damage::Colour) {
for (int& shift : block.shift) shift = static_cast<int>(rng.inRange(-128, 128));
}
return block;
};
// The whole damage map is drawn up front, block by block in reading order.
std::vector<Block> plan(static_cast<std::size_t>(cols) * rows);
for (int by = 0; by < rows; ++by) {
@@ -88,54 +57,13 @@ Image CorruptEffect::render(const std::vector<Image>& 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 = &block;
}
}
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<std::size_t>(rows), [&](std::size_t begin, std::size_t end) {
for (std::size_t by = begin; by < end; ++by) {
const int y0 = static_cast<int>(by) * size;
const int h = std::min(size, height - y0);
for (int bx = 0; bx < cols; ++bx) {
const Block& block = plan[by * static_cast<std::size_t>(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<std::size_t>(w) * 3;
for (int line = 0; line < h; ++line) {
std::uint8_t* out = canvas.row(y0 + line) + static_cast<std::size_t>(x0) * 3;
switch (block.damage) {
case Damage::Repeat:
std::memmove(out, out - static_cast<std::size_t>(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<std::size_t>(x) * 3,
in + static_cast<std::size_t>(sx) * 3, 3);
}
break;
}
case Damage::Colour:
for (std::size_t i = 0; i < bytes; ++i) {
out[i] = static_cast<std::uint8_t>(
std::clamp(out[i] + block.shift[i % 3], 0, 255));
}
break;
case Damage::None:
break;
}
}
}
}
});
paintBlocks(canvas, input, plan, cols, rows, size);
return canvas;
}
+97
View File
@@ -0,0 +1,97 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#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<Damage>(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<int>(rng.inRange(0, std::max(0, width - size)));
block.sy = static_cast<int>(rng.inRange(0, std::max(0, height - size)));
} else if (block.damage == Damage::Colour) {
for (int& shift : block.shift) shift = static_cast<int>(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<Block>& 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<std::size_t>(rows), [&](std::size_t begin, std::size_t end) {
for (std::size_t by = begin; by < end; ++by) {
const int y0 = static_cast<int>(by) * size;
const int h = std::min(size, height - y0);
for (int bx = 0; bx < cols; ++bx) {
const Block& block = plan[by * static_cast<std::size_t>(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<std::size_t>(w) * 3;
for (int line = 0; line < h; ++line) {
std::uint8_t* out = canvas.row(y0 + line) + static_cast<std::size_t>(x0) * 3;
switch (block.damage) {
case Damage::Repeat:
std::memmove(out, out - static_cast<std::size_t>(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<std::size_t>(x) * 3,
in + static_cast<std::size_t>(sx) * 3, 3);
}
break;
}
case Damage::Colour:
for (std::size_t i = 0; i < bytes; ++i) {
out[i] = static_cast<std::uint8_t>(
std::clamp(out[i] + block.shift[i % 3], 0, 255));
}
break;
case Damage::None:
break;
}
}
}
}
});
}
} // namespace effector
+135
View File
@@ -0,0 +1,135 @@
#include "effects/datamosh.hpp"
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
#include "effects/corrupt_blocks.hpp"
#include "effects/sampling.hpp"
namespace effector {
const std::vector<ParamSpec>& DatamoshEffect::params() const {
static const std::vector<ParamSpec> 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<int>(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<int>(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<Break> breaks(static_cast<std::size_t>(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<std::size_t>(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<Block> 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<int>(std::lround(slide * piece.driftX));
block.sx = (sx % width + width) % width;
block.sy = std::clamp(block.sy + static_cast<int>(std::lround(slide * piece.driftY)), 0,
height - 1);
}
}
Image canvas = input;
paintBlocks(canvas, input, plan, cols, rows, size);
return canvas;
}
} // namespace effector
+28
View File
@@ -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<ParamSpec>& params() const override;
protected:
Image animate(Image input, const ParamValues& values, const Moment& at) const override;
};
} // namespace effector