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>
56 lines
2.3 KiB
C++
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
|