Grain is the third effect and the second that declares needsSources() false, so it works as a filter stage in a cascade and can also lead one, filling the canvas from a source the way blur does. Its knobs are the amount a grain may push a pixel, the grain size in pixels, whether the channels are drawn apart into coloured grain, and whether the response is weighted towards the midtones — which it is by default, since film grains least where it is least exposed. That last one means grain over an empty canvas stays black unless the weighting is turned off. Determinism comes from the same shape the scanlines effect uses: the whole grain field is drawn serially from the Rng into one cell per grain before any threading, and only the integer pixel arithmetic afterwards runs under parallelFor. The canvas fill blur used to keep to itself moves to effects/source_fill.hpp unchanged, now that two effects want it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
113 lines
4.3 KiB
C++
113 lines
4.3 KiB
C++
#include "effects/blur.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#include "core/parallel.hpp"
|
|
#include "effects/source_fill.hpp"
|
|
|
|
namespace effector {
|
|
|
|
const std::vector<ParamSpec>& BlurEffect::params() const {
|
|
static const std::vector<ParamSpec> specs = {
|
|
ParamSpec::makeInt("radius", "Radius", 8, 1, 256,
|
|
"Half width in pixels of the blur window."),
|
|
ParamSpec::makeInt("passes", "Passes", 2, 1, 8,
|
|
"How often the box blur is repeated; more passes look more gaussian."),
|
|
ParamSpec::makeChoice("direction", "Direction", "both", {"both", "horizontal", "vertical"},
|
|
"Axis the blur is applied along."),
|
|
ParamSpec::makeBool("first_source", "Use the first image", false,
|
|
"Always blur the first source instead of picking one at random."),
|
|
};
|
|
return specs;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// One separable box blur pass. `stride` is the distance in bytes between two
|
|
// neighbouring samples along the blurred axis, `count` how many of them there
|
|
// are; the window is clamped at both ends.
|
|
void boxPass(const std::uint8_t* in, std::uint8_t* out, int count, std::size_t stride, int radius) {
|
|
const int window = 2 * radius + 1;
|
|
int sum[3] = {0, 0, 0};
|
|
|
|
for (int c = 0; c < 3; ++c) {
|
|
sum[c] = in[c] * (radius + 1);
|
|
for (int i = 1; i <= radius; ++i) {
|
|
sum[c] += in[static_cast<std::size_t>(std::min(i, count - 1)) * stride + c];
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
for (int c = 0; c < 3; ++c) {
|
|
out[static_cast<std::size_t>(i) * stride + c] =
|
|
static_cast<std::uint8_t>(sum[c] / window);
|
|
}
|
|
const int add = std::min(i + radius + 1, count - 1);
|
|
const int drop = std::max(i - radius, 0);
|
|
for (int c = 0; c < 3; ++c) {
|
|
sum[c] += in[static_cast<std::size_t>(add) * stride + c] -
|
|
in[static_cast<std::size_t>(drop) * stride + c];
|
|
}
|
|
}
|
|
}
|
|
|
|
void blurHorizontal(const Image& src, Image& dst, int radius) {
|
|
parallelFor(static_cast<std::size_t>(src.height()), [&](std::size_t begin, std::size_t end) {
|
|
for (std::size_t y = begin; y < end; ++y) {
|
|
boxPass(src.row(static_cast<int>(y)), dst.row(static_cast<int>(y)), src.width(), 3,
|
|
radius);
|
|
}
|
|
});
|
|
}
|
|
|
|
void blurVertical(const Image& src, Image& dst, int radius) {
|
|
const std::size_t stride = src.stride();
|
|
parallelFor(static_cast<std::size_t>(src.width()), [&](std::size_t begin, std::size_t end) {
|
|
for (std::size_t x = begin; x < end; ++x) {
|
|
const std::size_t offset = x * 3;
|
|
boxPass(src.data() + offset, dst.data() + offset, src.height(), stride, radius);
|
|
}
|
|
});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Image BlurEffect::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 int radius = static_cast<int>(std::max(1L, paramInt(values, "radius", 8)));
|
|
const int passes = static_cast<int>(std::max(1L, paramInt(values, "passes", 2)));
|
|
const std::string direction = paramChoice(values, "direction", "both");
|
|
const bool firstSource = paramBool(values, "first_source", false);
|
|
|
|
Image canvas(width, height);
|
|
if (!sources.empty()) {
|
|
// Every random value is drawn here, before any threading, so that a
|
|
// seed always gives the same image.
|
|
const std::size_t index = firstSource ? 0 : rng.below(sources.size() - 1);
|
|
const Image& src = sources[index];
|
|
const int sx = static_cast<int>(rng.inRange(0, std::max(0, src.width() - width)));
|
|
const int sy = static_cast<int>(rng.inRange(0, std::max(0, src.height() - height)));
|
|
fillFromSource(canvas, src, sx, sy);
|
|
}
|
|
|
|
Image scratch(width, height);
|
|
for (int pass = 0; pass < passes; ++pass) {
|
|
if (direction != "vertical") {
|
|
blurHorizontal(canvas, scratch, std::min(radius, std::max(1, width - 1)));
|
|
std::swap(canvas, scratch);
|
|
}
|
|
if (direction != "horizontal") {
|
|
blurVertical(canvas, scratch, std::min(radius, std::max(1, height - 1)));
|
|
std::swap(canvas, scratch);
|
|
}
|
|
}
|
|
|
|
return canvas;
|
|
}
|
|
|
|
} // namespace effector
|