FeiFei Film Grain & Tone
The cheapest way to stop an image looking AI
- image
- image
People spend whole afternoons prompting "Kodak Portra 400, 35mm". That trick is real - but the cheapest move for making output read as captured rather than generated is a deterministic noise pass that costs ten milliseconds and no model. That's what this node is.
It's not a bloom filter with a random seed. The author clearly owns a camera: the stages run in the order real film does - lens (vignette, lateral chromatic aberration) → film base (halation) → print (S-curve, split tone, micro-contrast) → emulsion (grain) - and grain goes last so the print curve can't crush it. Pure torch, no external models, roughly 0.2s at 1024² on CUDA.
Wire it between VAEDecode and your watermark/save node.
The one decision: mode
mode is a hard switch, not a blend, and this is the single thing to understand about the node.
Preset(default): the stock inpresetdrives every effect. All the sliders are ignored - including the five main ones, which stay visible because ComfyUI can't hide widgets.preset_gainis the only strength control you get.Custom: the sliders drive everything, andfilm_type,presetandpreset_gainare ignored.
The author deliberately refused a per-parameter mix factor, and that's the right call - with a blend knob you can never tell whether the slider you just moved did anything.
preset_gain scales grain, halation, vignette, print curve, micro-contrast, split tone and colour fringing together, while the stock's own character (grain size, shadow bias, glow threshold) stays put. So 0.5 is a half-strength Tri-X, not half of Tri-X's parameters. 0.0 leaves the image untouched, which makes it a free A/B toggle.
film_type only groups the preset dropdown - it applies no effect of its own. There are 29 stocks across six families (Color Negative, Slide, B&W Negative, Cinema, Special, Other), from Portra 160 and Ektar 100 through Tri-X 400, CineStill 800T, and a "Digital Clean" stock that adds almost nothing and is genuinely useful as a baseline. No preset applies a colour grade; your colours survive, they just look like they were shot on film. Switch film_type without changing preset and it falls back to that family's first stock rather than appearing to do nothing.
The sliders that matter in Custom
Five main ones, and the defaults are well chosen - a real film scan, not a filter:
grain_amount0.25 - about ±3.5/255. Past ~0.6 it reads as TV snow. The tooltip says so.grain_size1.0 - clump size, scaled with frame size, so 4K gets physically bigger grain rather than finer.halation0.15 - highlight glow. Push to 0.3–0.6 for the CineStill light-leak look.vignette0.12 - gentle corner falloff.tone0.25 - print S-curve with a small black lift so shadows never reach pure black.
An optional group holds the tuning knobs; three are worth naming. micro_contrast (0.15) is, per its tooltip, the single most effective slider against the waxy AI-skin look. halation_radius (1.0) wants 1.5–2.0 for CineStill-style glow. grain_shadows (0.55) tilts grain into the shadows for a pushed/high-ISO feel.
seed is reproducible - same seed, same grain - and each image in a batch gets seed + index, so a frame sequence doesn't flicker. That detail is the difference between this being usable for animation and not.
Why the grain doesn't look like digital noise
Real silver halide grain is clumped, not independent per pixel. So the noise is generated on a low-resolution grid and bicubic-upscaled, with only about 6% per-pixel detail mixed in, then weighted by luminance - strongest in the midtones, falling off fast in highlights. That 6% is the critical number: crank the per-pixel component and you're back to salt-and-pepper. More informed than the usual torch.randn_like grain nodes.
Output is a single image. One input: image.
Install
ComfyUI Manager → ComfyUI-FeiFei, or:
cd ComfyUI/custom_nodes
git clone https://github.com/aifeifei798/ComfyUI-FeiFei.git
# restart; look under the FeiFei category
No checkpoints, no extra pip installs - the pack's requirements.txt adds only openai>=1.40, and only the LLM nodes use it. This is the best-value node in the pack because it costs you nothing at install time.
Where people get burned
- Grain disappears between the node and the file. 8-bit saving and
VAEEncodequantization destroy grain below 1/255. Don't dropgrain_amountunder ~0.02 and expect to see it - you're writing information the container can't hold. - Sliders not working. You're in
Presetmode. Switch toCustom, or leave the sliders alone and usepreset_gain. - Preset looks nothing like the slider setup you had. Correct - preset and custom never mix. That's the whole contract.
- A workflow saved against an older version needs the node re-picked.
preset_mixwas replaced bymodeandpreset_gain, and the widgets were reordered, so old graphs won't line up. Add a fresh node and reconnect. - RGBA, grayscale, video frames. RGBA keeps alpha untouched, grayscale and 2-channel work, and a frame sequence won't flicker because grain is derived deterministically per index.
If you're chasing photorealism, do this before you go re-rolling seeds.
Inputs (19)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| mode | COMBO | Preset | Preset or custom, never a blend. Preset = the film stock below drives every effect, all sliders are ignored and preset_gain sets how strong it is. Custom = the sliders below drive everything and film_type / preset / preset_gain are ignored. Sliders always stay visible in Preset mode, they just have no effect there. |
| film_type | COMBO | Color Negative | Film family. Only groups the preset list below - it applies no effect of its own, and is ignored entirely in Custom mode. |
| preset | COMBO | Portra 400 | Film stock to imitate. Only grain size, grain response, halation and lens falloff are changed - never a colour grade, so the image keeps its own colours. Ignored in Custom mode, where the sliders take over instead. |
| preset_gain | FLOAT | 1.000–1 | How strong the preset is, Preset mode only. Scales grain, halation, vignette, print curve, micro-contrast, split tone and colour fringing together, while the film's own character - grain size, shadow bias, glow threshold - stays put. 0.0 leaves the image untouched, 0.5 is a half-strength version of the same stock, not half of its parameters. Ignored in Custom mode. |
| grain_amount | FLOAT | 0.250–1 | Grain strength. Default is about +/-3.5/255, the level of a real film scan. Above roughly 0.6 it starts to look like TV snow instead of film. |
| grain_size | FLOAT | 1.000.2–4 | Size of the grain clumps, scaled automatically with the frame size. 1.0 is about 2.5px clumps on a 1024px image; 4.0 is coarse push-processed grain. |
| halation | FLOAT | 0.150–1 | Warm glow bleeding out of the highlights. Keep it subtle (0.1-0.2) for a normal film look; push to 0.3-0.6 for a light-leak or CineStill feel. |
| vignette | FLOAT | 0.120–1 | Corner falloff. The corners darken by at most this amount, so 0.12 is a gentle edge. |
| tone | FLOAT | 0.250–1 | Print S-curve: expands the midtones and holds the highlights. Includes a small black lift so shadows never reach pure black. |
| seed | INT | 00–18446744073709550000 | Same seed gives the same grain. Each image in a batch gets its own grain, so animating a frame sequence will not flicker. |
| grain_shadowsopt | FLOAT | 0.550–1 | Where the grain lives. Low keeps it in the midtones, high pushes it into the shadows for a pushed-film or high-ISO look. |
| grain_chromaopt | FLOAT | 0.250–1 | 0 = fully monochrome grain, 1 = each channel gets its own grain. Real film sits low here; black and white film is near 0.08. |
| halation_thresholdopt | FLOAT | 0.780.4–1 | How bright a pixel must be before it starts to bleed. Lower it to make the glow spread from mid-bright areas too. |
| halation_radiusopt | FLOAT | 1.000.2–4 | Spread of the glow, scaled with the frame size. CineStill-style halation wants 1.5-2.0. |
| vignette_sizeopt | FLOAT | 0.720.3–1.4 | How far in the darkening starts. Small values darken more of the frame. |
| chroma_shiftopt | FLOAT | 1.00–8 | Lateral chromatic aberration, in pixels of fringing at the corner of a 1024px image. Invisible in the centre. |
| micro_contrastopt | FLOAT | 0.150–1 | Fine detail sharpening on luminance only, no colour fringes. The single most effective slider against the waxy AI-skin look. |
| split_toneopt | FLOAT | 0.060–0.3 | Warm shadows and cool highlights. A very small amount, just enough to stop the colours looking purely digital. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |