Files
effector/src/effects/grain.cpp
T
SiandClaude Opus 5.5 ce0207c41f Add Film gate, a video effect of film running through a projector
Film grain's field moves to effects/grain_field.hpp so both share it; Film
grain renders exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:39:57 +02:00

56 lines
2.3 KiB
C++

#include "effects/grain.hpp"
#include <algorithm>
#include <cstdint>
#include <vector>
#include "effects/grain_field.hpp"
#include "effects/source_fill.hpp"
namespace effector {
const std::vector<ParamSpec>& GrainEffect::params() const {
static const std::vector<ParamSpec> specs = {
ParamSpec::makeDouble("grain_amount", "Amount", 0.35, 0.0, 1.0,
"How far a grain may push a pixel, as a fraction of full scale."),
ParamSpec::makeInt("grain_size", "Grain size", 1, 1, 32,
"Width in pixels of one grain; larger sizes look coarser."),
ParamSpec::makeBool("grain_colour", "Coloured grain", false,
"Draw the channels separately instead of one grey grain per pixel."),
ParamSpec::makeBool("grain_midtones", "Weight towards midtones", true,
"Fade the grain out in the shadows and highlights, as film does."),
};
return specs;
}
Image GrainEffect::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 double amount = std::clamp(paramDouble(values, "grain_amount", 0.35), 0.0, 1.0);
const int size = static_cast<int>(std::max(1L, paramInt(values, "grain_size", 1)));
const bool colour = paramBool(values, "grain_colour", false);
const bool midtones = paramBool(values, "grain_midtones", true);
Image canvas(width, height);
if (!sources.empty()) {
// Drawn before any threading, so that a seed always gives the same image.
const std::size_t index = 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);
}
if (amount <= 0.0) return canvas;
// The whole grain field is drawn serially from the rng, exactly like the
// scanlines plan, and only the pixel arithmetic afterwards is parallel.
const GrainField field = drawGrain(rng, width, height, size, colour);
applyGrain(canvas, field, amount, midtones);
return canvas;
}
} // namespace effector