Grain
Film grain gives digital images a less sterile surface by reintroducing the kind of irregularity that real sensors and film stocks always have. The effect works because the eye reads small, uneven density changes as texture and physical presence rather than as a perfectly smooth synthetic field.
How it works
grain.rs applies grain in the gamma Rec.2020 working space after the main tonal and detail work. The core pipeline is:
- Build a deterministic white-noise field from the render seed.
- Optionally Gaussian-blur that noise with
sigmaderived fromsize. - Compute per-pixel luminance and use it to weight the grain strength.
- Blend the noise back into each RGB channel with a type-specific amount curve.
The current implementation has three grain presets: GrainType::{Fine,Silver,Harsh}. The original design considered six (adding Soft, Cubic, and Tabular for finer-grained stock emulation) but only the three above are implemented today; the others are deferred until preset authors actually need that resolution. Each preset maps to a fixed internal tuple in GrainTypeConfig:
contrast: scales the final noise amplitude.luma_falloff: controls how quickly grain fades from shadows into highlights.chromatic: controls how much the RGB channels diverge on saturated pixels.amount_curve: shapes the user-facingamountslider before it reaches the noise.
The presets are intentionally simple:
Finefavors subtle texture: lower contrast, steeper luminance falloff, and the lightest chromatic split.Silveris the default middle ground: moderate contrast, moderate falloff, and balanced chromatic behavior.Harshpushes the effect hardest: highest contrast, the weakest falloff, and the strongest chromatic separation.
Internally, the implementation generates one shared noise field plus three channel-specific noise fields. For neutral pixels, the shared field dominates and the grain stays monochrome. For colorful pixels, the channel-specific fields are mixed in more strongly, so the grain picks up the slight color disagreement that real emulsions tend to show.
Why we chose it
AgX uses blur-based sizing instead of a frequency-based size control. That removes the failure mode where extreme size values collapse into blotchy low-frequency artifacts. The blur approach keeps the visual result effectively the same for normal settings, but it is much easier to reason about and tune.
Chromatic variation also lives in the grain type itself rather than in a purely digital RGB-noise model. Real film layers are correlated, not independent, so a small amount of per-channel decorrelation is enough. That is why the implementation uses a mostly shared noise field with only a modest type-specific channel split.
Parameters and constants
The user-facing GrainParams fields are grain_type, amount, size, and seed. Everything below is internal and fixed in code.
| Constant | Value | Role | Sensitivity |
|---|---|---|---|
GRAIN_PARAM_MIN | 0.0 | Lower bound for amount/size validation | Pure schema value; changing it would change the accepted preset range. |
GRAIN_PARAM_MAX | 100.0 | Upper bound for amount/size validation | Same — bumping it widens the slider but doesn't recalibrate downstream constants, so the rest of the math would have to be retuned. |
GRAIN_DEFAULT_SIZE | 50.0 | Default grain size when omitted | Sets the "no size specified" feel to a balanced middle. Lower defaults push the omitted case toward fine grain; higher toward coarse. |
GRAIN_SIZE_CURVE_EXPONENT | 1.5 | Shapes the size-to-sigma curve | Higher exponent makes low size values feel even finer and high values jump to coarse faster; 1.0 would be linear and feel too aggressive at low slider settings. |
GRAIN_LUMINANCE_WEIGHT_SCALE | 0.5 | Scales luminance falloff sensitivity | Doubling it makes shadow-vs-highlight grain emphasis snap harder; halving it flattens grain across the tonal range. |
GRAIN_BLUR_SIGMA_THRESHOLD | 0.3 | Skips blur below this sigma | Threshold below which the Gaussian blur is skipped entirely (it would barely change the noise anyway); raising it makes more "fine" sizes leave the noise un-blurred. |
GRAIN_MAX_SIGMA | 1.0 | Maximum sigma at size 100 | The single biggest knob for "how coarse can grain get?". Raising to 2.0 doubles the visible grain footprint at size = 100. |
GRAIN_REF_RESOLUTION | 2000.0 | Reference long-edge resolution for sigma scaling | Defines the "1× zoom" image. Lower references make grain scale up more aggressively on large images; higher makes it shrink more. |
GRAIN_STRENGTH_MULT | 0.04 | Maps amount to the final modulation strength | A 25% change here is visibly different at the same amount slider; doubling makes mid-grain look harsh and Silver start to feel like Harsh. |
GRAIN_ADDITIVE_END | 0.1 | End of the additive-grain shadow region | Defines where additive shadow grain stops fading in. Raising it pulls the shadow grain band higher up the tonal range. |
GRAIN_MULTIPLICATIVE_START | 0.2 | Start of the multiplicative-grain midtone region | Pairs with the previous knob; together they set the smooth handoff from additive (shadows) to multiplicative (mid+) grain. |
GRAIN_ADDITIVE_SCALE | 0.35 | Scales the additive delta in deep shadows | Direct multiplier on shadow grain visibility; halving it makes shadows almost grain-free. |
GRAIN_FALLOFF_REDUCTION | 0.4 | Reduces luminance falloff as amount rises | At high amount values, this dampens the shadow-emphasis falloff so heavy grain spreads into highlights instead of just blowing out the dark areas. |
| GrainType | contrast | luma_falloff | chromatic | amount_curve | Reasoning |
|---|---|---|---|---|---|
Fine | 0.95 | 2.5 | 0.05 | 0.7 | The softest preset. It keeps contrast low, pushes grain out of highlights, and keeps channel decorrelation barely visible. |
Silver | 1.2 | 1.5 | 0.10 | 0.6 | The default stock-like preset. It balances visible grain with enough chromatic separation to feel filmic without looking digital. |
Harsh | 1.5 | 0.8 | 0.15 | 0.5 | The strongest preset. It preserves grain across more of the tonal range and allows the most visible channel disagreement on saturated pixels. |
Beyond the expected range: preset validation rejects amount and
size outside 0.0..=100.0, so out-of-range values never reach the
algorithm. The seed field is Option<u64> so any non-negative integer
is fine. grain_type accepts only the three string variants (fine,
silver, harsh); anything else fails preset parsing with an explicit
error.
Preset-slider mapping
In a preset TOML file, the [grain] block uses the serialized keys type, amount, size, and seed; that type key corresponds to the Rust GrainParams.grain_type field. The current implementation does not expose chromatic as a user slider; chromatic intensity is baked into the selected grain type.
amount is not linear. The code raises the normalized slider value to the preset's amount_curve before scaling the noise, so low settings stay subtle and the effect ramps in more gently than a straight linear blend would. In practice:
- Low
amountvalues keep the effect mostly invisible and are useful for a light texture pass. - Mid-range values produce the classic visible grain look.
- High values become obvious quickly, especially for
Harsh, because the amount curve is shallower and the contrast multiplier is higher.
size controls the Gaussian blur sigma applied to the noise field. Small values leave the noise nearly unblurred, which reads as fine grain. Larger values increase sigma nonlinearly, so the grain grows coarser without collapsing into the low-frequency blobs that the earlier frequency-based algorithm could produce.
Source
- CPU (Rust):
crates/agx/src/adjust/grain.rs - GPU (WGSL):
The CPU and GPU implementations line up on the user-facing controls, luminance weighting, and the non-chromatic preset behavior, but the current GPU path does not yet implement the CPU chromatic-grain split: the CPU mixes a shared noise field with per-channel chromatic noise, while the GPU applies a single noise field.
References
No canonical external paper applies — AgX's grain is an original implementation. The decisions above are recorded in the AgX design docs: grain, grain size fix, and chromatic grain.
See also
- Concept references: Effects (grain entry), Color models (luminance section — grain is luma-weighted)
- API references: grain
- Related explanations: Noise reduction (the inverse direction), Vignette
- How-tos: Write your own preset, Compose layered looks