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
+5
View File
@@ -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 the gate, flicker, dust and scratches. On the command line it is
`-e filmgate` with `--gate-grain`, `--gate-weave`, `--gate-flicker`, `-e filmgate` with `--gate-grain`, `--gate-weave`, `--gate-flicker`,
`--gate-dust` and `--gate-scratches`. `--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 - 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 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 command line it is `--video-quality small|standard|high`. Standard writes
+1
View File
@@ -46,6 +46,7 @@ add_library(effector_core STATIC
src/effects/blur.cpp src/effects/blur.cpp
src/effects/channelsplit.cpp src/effects/channelsplit.cpp
src/effects/corrupt.cpp src/effects/corrupt.cpp
src/effects/datamosh.cpp
src/effects/feedback.cpp src/effects/feedback.cpp
src/effects/filmgate.cpp src/effects/filmgate.cpp
src/effects/flow.cpp src/effects/flow.cpp
+2
View File
@@ -4,6 +4,7 @@
#include "effects/blur.hpp" #include "effects/blur.hpp"
#include "effects/channelsplit.hpp" #include "effects/channelsplit.hpp"
#include "effects/corrupt.hpp" #include "effects/corrupt.hpp"
#include "effects/datamosh.hpp"
#include "effects/feedback.hpp" #include "effects/feedback.hpp"
#include "effects/filmgate.hpp" #include "effects/filmgate.hpp"
#include "effects/flow.hpp" #include "effects/flow.hpp"
@@ -49,6 +50,7 @@ const std::vector<EffectPtr>& effects() {
std::make_shared<FlowEffect>(), std::make_shared<FlowEffect>(),
std::make_shared<SweepEffect>(), std::make_shared<SweepEffect>(),
std::make_shared<FilmGateEffect>(), std::make_shared<FilmGateEffect>(),
std::make_shared<DatamoshEffect>(),
}; };
return registry; return registry;
} }
+3 -75
View File
@@ -1,10 +1,9 @@
#include "effects/corrupt.hpp" #include "effects/corrupt.hpp"
#include <algorithm> #include <algorithm>
#include <cstring>
#include <vector> #include <vector>
#include "core/parallel.hpp" #include "effects/corrupt_blocks.hpp"
#include "effects/sampling.hpp" #include "effects/sampling.hpp"
#include "effects/source_fill.hpp" #include "effects/source_fill.hpp"
@@ -28,19 +27,6 @@ const std::vector<ParamSpec>& CorruptEffect::params() const {
return specs; 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, Image CorruptEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
const ParamValues& values) const { const ParamValues& values) const {
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels."); 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 cols = (width + size - 1) / size;
const int rows = (height + 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. // 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); std::vector<Block> plan(static_cast<std::size_t>(cols) * rows);
for (int by = 0; by < rows; ++by) { for (int by = 0; by < rows; ++by) {
@@ -88,54 +57,13 @@ Image CorruptEffect::render(const std::vector<Image>& sources, int width, int he
continue; continue;
} }
if (!chance(rng.next(), rate)) continue; if (!chance(rng.next(), rate)) continue;
block = drawBlock(bx); block = drawDamage(rng, kinds, bx, width, height, size);
if (bleed) carried = &block; if (bleed) carried = &block;
} }
} }
Image canvas = input; Image canvas = input;
// Block rows are independent (a repeat only ever reads to its left), so paintBlocks(canvas, input, plan, cols, rows, size);
// 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;
}
}
}
}
});
return canvas; 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
+2
View File
@@ -2115,6 +2115,8 @@ int main() {
{"gate_scratches", 0L}}, {"gate_scratches", 0L}},
{{"gate_weave", 0L}, {"gate_grain", 0.0}, {"gate_flicker", 0.0}, {"gate_dust", 0L}, {{"gate_weave", 0L}, {"gate_grain", 0.0}, {"gate_flicker", 0.0}, {"gate_dust", 0L},
{"gate_scratches", 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) { for (const auto& [name, lively, smooth, off] : moving) {
const auto effect = effectByName(name); const auto effect = effectByName(name);