#include "effects/grain.hpp" #include #include #include #include "effects/grain_field.hpp" #include "effects/source_fill.hpp" namespace effector { const std::vector& GrainEffect::params() const { static const std::vector 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& 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(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(rng.inRange(0, std::max(0, src.width() - width))); const int sy = static_cast(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