IAMCCS-CinePostEfx-v2
Film grain, tape damage and broadcast rot in one node
- images
- grain_plate
- images
- grain_map
- report
Grain is the cheapest de-AI pass there is: a gamma node, a colour match and a grain pass will do in milliseconds what people keep burning diffusion passes on. It is also the pass everyone gets wrong by overlaying the same noise texture on every frame, which reads as a filter rather than as film.
IAMCCS-CinePostEfx-v2 is the ambitious version of that idea. It renders stochastic grain per frame in linear light, scales it to your actual resolution, makes it respond to tone, and then - if you want - drags the whole frame through a tape deck or a broadcast chain on the way out.
Five engines, one node
The engine dropdown is the fork in the road:
film_emulsion(default) - density-aware grain from a stock profiledigital_sensor- sensor noise rather than emulsionscanned_grain_plate- uses a real grain plate you supplyanalog_tape- the video/tape branchbroadcast_signal- the signal-path branch
preset (34 options) picks the stock: 65mm_clean_scan, 35mm_fine_negative, 35mm_high_speed, 16mm_documentary, 8mm_expression, soft_cassette_memory, worn_video_copy, family_camcorder_1988, pristine_tape_master, and so on. Presets set the whole look; the individual fields override on top.
The grain core is a short list of knobs you actually touch. strength defaults to a very restrained 0.1. grain_size_4k_px is resolution-aware - it is expressed at 4K and scaled, so the same number looks right on a 720p preview and a 4K master. grain_profile picks the emulsion character (negative_stock, high_speed_negative, reversal_stock, print_stock, modern_fine). complexity (1–4) buys finer structure at more compute. softness and roughness shape it. temporal_correlation is the one that matters for video: at 0 grain is completely re-rolled every frame, which is physically right for fine grain and a flicker disaster on a slow shot. Nudge it up to stabilise.
Tone weights do the thing that separates real film from a noise overlay: shadow_response (default 0.62), midtone_response (1.0) and highlight_response (0.38) decide where grain lives. Highlights at 0.38 is why bright skies don't turn into sandpaper. Per-channel red_response / green_response / blue_response are the chroma asymmetry, with chroma_amount (0.055) on top.
input_transfer (srgb, rec709, linear) tells it what your frames actually are, and blend_method (log_density default, density_exposure, linear_additive, soft_light_luma) decides how the field is combined. If your grain looks chalky or washes the mids, that pairing is what you change, not strength.
The analogue half
Everything from horizontal_instability downward is the tape/broadcast toolkit, and all of it sits at zero by default: tracking_error, tape_warp, ghost_echo, line_dropout, scanline_strength, field_interlace, vertical_roll, head_switch_distortion, luma_trail, chroma_delay, chroma_spill, color_drift, edge_echo, highlight_glow, plus signal_saturation, black_lift, chroma_loss, chroma_phase_noise and an analog_mix master. analog_color_look gives you 14 graded looks (consumer_color, warm_camcorder, late_night_blue, archival_amber, crt_broadcast, night_vision_green…).
Only eight fields have their own *_randomness partner - the cyclic signal-motion families: horizontal_instability_randomness, tracking_randomness, tape_warp_randomness, color_drift_randomness, scanline_randomness, interlace_randomness, vertical_roll_randomness, head_switch_randomness. That is the difference between a loop that looks like a loop and one that looks like a dying VHS tape. disturbance_amount (default 1.0) scales the intrusion of all of it.
The lens_* group is optical, not analogue: lens_preset (11 options, from fisheye_8mm to anamorphic_2x_cinema) plus manual lens_distortion, lens_tilt_blur, lens_chromatic_aberration, lens_vignette and friends. lens_master at 0 means the preset's own values are used unchanged.
Two fields look scary and aren't. seed plus frame_start make the noise field reproducible: same seed, same frame_start, same grain on the same frames - which is how you get consistent grain across shots cut from separate renders. settings_json is the UI's own snapshot of all of the above; leave it alone and let the frontend write it.
Outputs
images is your batch. grain_map is a MASK of the grain field itself - wire it to a PreviewImage when you want to see what is being added instead of guessing. report is a text summary naming the engine, preset, blend, active randomness count and lens preset; when a render looks wrong, that string tells you which settings actually took effect.
Install
Manager → search IAMCCS, or:
cd ComfyUI/custom_nodes
git clone https://github.com/IAMCCS/IAMCCS-nodes.git
Restart. No model downloads, no extra pip packages - it is pure tensor math on your frames. The pack's README quotes ComfyUI ≥ 0.3.0, Python ≥ 3.12, Torch ≥ 2.8 (CUDA 12.6/12.8).
Where people get burned
- Cranking
strength. 0.1 is the default for a reason. This node is doing physical grain, not a "film look" filter; 0.3+ on a 4K master is a different aesthetic. - Flicker at
temporal_correlation = 0. Fine for stills and high-speed stock, visible on slow camera moves. Raise it before you blame the encoder. - Wrong
input_transfer. If your frames are linear (a few video pipelines hand you linear EXR-ish data) and you leave it onsrgb, the grain sits wrong in the tones. - A plate that doesn't match your resolution.
scanned_grain_platewants the optionalgrain_plateIMAGE input; feed a plate at a very different resolution and the grain will look like a texture overlay again.
The obvious comparison point is the pack's own older grain nodes, and the two legacy IDs IAMCCS_CineFilmGrainPro and IAMCCS_CinePostEfxV2 are aliases of this one - same code, older names, kept so old workflows still load.
Inputs (69)
| Name | Type | Default | Description |
|---|---|---|---|
| images | IMAGE | — | |
| engine | COMBO | film_emulsion | 5 options: film_emulsion, digital_sensor, scanned_grain_plate, analog_tape, broadcast_signal |
| preset | COMBO | 35mm_fine_negative | 34 options: 65mm_clean_scan, 35mm_fine_negative, 35mm_high_speed, 16mm_documentary, 8mm_expression, digital_cinema_sensor, +28 |
| input_transfer | COMBO | srgb | 3 options: srgb, rec709, linear |
| blend_method | COMBO | log_density | 4 options: log_density, density_exposure, linear_additive, soft_light_luma |
| strength | FLOAT | 0.1000–1 | — |
| grain_size_4k_px | FLOAT | 1.000.35–6 | — |
| softness | FLOAT | 0.180–1 | — |
| roughness | FLOAT | 0.240–1 | — |
| complexity | INT | 31–4 | — |
| temporal_correlation | FLOAT | 0.000–0.95 | — |
| chroma_amount | FLOAT | 0.0550–0.5 | — |
| shadow_response | FLOAT | 0.620–2 | — |
| midtone_response | FLOAT | 1.000–2 | — |
| highlight_response | FLOAT | 0.380–2 | — |
| red_response | FLOAT | 1.000.5–1.5 | — |
| green_response | FLOAT | 0.960.5–1.5 | — |
| blue_response | FLOAT | 1.060.5–1.5 | — |
| seed | INT | 10–9223372036854776000 | — |
| frame_start | INT | 00–2147483647 | — |
| horizontal_instability | FLOAT | 0.000–1 | — |
| chroma_spill | FLOAT | 0.000–1 | — |
| luma_noise | FLOAT | 0.000–1 | — |
| chroma_noise | FLOAT | 0.000–1 | — |
| color_drift | FLOAT | 0.000–1 | — |
| edge_echo | FLOAT | 0.000–1 | — |
| line_dropout | FLOAT | 0.000–1 | — |
| scanline_strength | FLOAT | 0.000–1 | — |
| field_interlace | FLOAT | 0.000–1 | — |
| vertical_roll | FLOAT | 0.000–1 | — |
| highlight_glow | FLOAT | 0.000–1 | — |
| head_switch_distortion | FLOAT | 0.000–1 | — |
| luma_trail | FLOAT | 0.000–1 | — |
| chroma_delay | FLOAT | 0.000–1 | — |
| analog_mix | FLOAT | 1.000–1 | — |
| tracking_error | FLOAT | 0.000–1 | — |
| tape_warp | FLOAT | 0.000–1 | — |
| ghost_echo | FLOAT | 0.000–1 | — |
| signal_saturation | FLOAT | 1.000–2 | — |
| black_lift | FLOAT | 0.000–1 | — |
| chroma_phase_noise | FLOAT | 0.000–1 | — |
| chroma_loss | FLOAT | 0.000–1 | — |
| grain_profile | COMBO | negative_stock | 5 options: modern_fine, negative_stock, high_speed_negative, reversal_stock, print_stock |
| texture_microcontrast | FLOAT | 0.180–1 | — |
| analog_color_look | COMBO | neutral | 14 options: neutral, consumer_color, warm_camcorder, cool_camcorder, tungsten_home_video, fluorescent_cctv, +8 |
| disturbance_amount | FLOAT | 1.000–2 | — |
| lens_preset | COMBO | lens_none | 11 options: lens_none, wide_angle_14mm, ultra_wide_10mm, fisheye_8mm, anamorphic_1_33x, anamorphic_2x_cinema, +5 |
| lens_master | FLOAT | 0.000–2 | — |
| lens_distortion | FLOAT | 0.00-1–1 | — |
| lens_edge_stretch | FLOAT | 0.00-1–1 | — |
| lens_anamorphic_width | FLOAT | 0.00-0.5–0.5 | — |
| lens_zoom | FLOAT | 1.000.7–1.6 | — |
| lens_keystone_x | FLOAT | 0.00-0.75–0.75 | — |
| lens_keystone_y | FLOAT | 0.00-0.75–0.75 | — |
| lens_tilt_angle | FLOAT | 0-90–90 | — |
| lens_focus_position | FLOAT | 0.00-1–1 | — |
| lens_tilt_blur | FLOAT | 0.000–1 | — |
| lens_chromatic_aberration | FLOAT | 0.000–1 | — |
| lens_vignette | FLOAT | 0.000–1 | — |
| settings_json | STRING | — | |
| horizontal_instability_randomness | FLOAT | 0.000–1 | — |
| tracking_randomness | FLOAT | 0.000–1 | — |
| tape_warp_randomness | FLOAT | 0.000–1 | — |
| color_drift_randomness | FLOAT | 0.000–1 | — |
| scanline_randomness | FLOAT | 0.000–1 | — |
| interlace_randomness | FLOAT | 0.000–1 | — |
| vertical_roll_randomness | FLOAT | 0.000–1 | — |
| head_switch_randomness | FLOAT | 0.000–1 | — |
| grain_plateopt | IMAGE | — |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | — |
| grain_map | MASK | — |
| report | STRING | — |