Exactly Grain Film Look
The Film-Grain Node That Doesn't Even Have a Prompt Box
- referenceImages
- advancedFeatures.watermark.image
- image
The most interesting thing about this node is what's missing. No prompt box. No negative prompt, no steps, no CFG. The Exactly Grain Film Look node takes your image in, adds a grainy photographic treatment, and hands it back. That's the whole job, and honestly, for a one-trick style filter, fewer knobs is a feature.
It's a Runware pack node, which means all the heavy lifting happens in the cloud. The model behind it is the "Exactly" photography house's grain-film-look style model - the same aesthetic applied by exactly.ai's photo tools - and the node is a thin shell that sends your image and settings to Runware's API, then returns the result as a native IMAGE tensor. No local GPU, no model files, no checkpoint to hunt down.
What you actually touch
The required inputs are just three:
- referenceImages - the image(s) you want treated. This is the star of the show: everything else is window dressing around it. Feed it a real photo and the model applies the film-grain look; you're not describing a scene, you're restyling one that already exists.
- width / height - output resolution in pixels. Defaults to 1024×1024, so bump them if you want to keep a landscape or portrait source's aspect ratio.
The optional stuff is mostly output plumbing: numberResults (up to 20 variations, each on a different seed), outputFormat (JPG/PNG/WEBP), outputQuality, and a watermark socket if you're generating client deliverables and want a badge baked in. The ttl toggle with its ttl_value only matters if you're pulling from a URL, so you can ignore it on the ComfyUI side.
Output is one image - wire it into Preview Image or Save Image like you would any other generator.
Why film grain is worth reaching for at all
This lands squarely in the "make AI output look captured, not generated" playbook. Subtle noise in post is one of the cheapest realism wins there is: it kills that smooth, over-rendered, everything-is-in-focus look that screams diffusion. Photographers know the reverse too - if you're scanning a clean digital shot and want it to feel like Ilford HP5, a grain pass does more than any prompt ever could. The trick with this node is it does the whole treatment in one cloud call instead of stacking a noise texture and a grade locally.
Installing and running it
This is a Runware pack node, so the setup is shared with every other node in the pack:
cd ComfyUI/custom_nodes
git clone https://github.com/Runware/ComfyUI-Runware
pip install -r ComfyUI-Runware/requirements.txt
Restart ComfyUI and you'll find it under Runware/Image/exactly. Easier still: install ComfyUI-Runware from ComfyUI Manager's search. Dependencies are light - runware-sdk, pillow, soundfile - and there are no model weights to download, which is the entire point of a cloud pack.
You will, however, need an API key from the Runware dashboard, dropped into ComfyUI Settings → Runware API key, or set as RUNWARE_API_KEY. Queue a run and you'll see the cost flash on the node title bar, so there's no mystery about what each shot billed.
Where people get burned
Three things, all shared across the pack. First, no key = "No Runware API key" error; there's no offline fallback, this node genuinely calls an API. Second, remember the cost is per generation - the film look is cheap, but if you crank numberResults to 20 you're paying for 20 cloud runs. Third, treat the grain amount as whatever exactly.ai tuned it to; there's no strength slider here, so if you want subtlety you add the grain pass to a copy and blend in post.
Inputs (17)
| Name | Type | Default | Description |
|---|---|---|---|
| referenceImages | RUNWARE_REFERENCEIMAGES | — | |
| advancedFeatures.watermark.imageopt | IMAGE | — | |
| sizeopt | COMBO | 1K (1:1) | Output size. Only one of resolution or dimensions is sent. |
| numberResultsopt | INT | 11–20 | Number of results to generate. Each result uses a different seed, producing variations of the same parameters. |
| advancedFeaturesopt | BOOLEAN | false | Enable to set advancedFeatures. Off uses the model's default. |
| advancedFeatures.watermark.bgColoropt | STRING | Background color in hex format. | |
| advancedFeatures.watermark.displayPositionopt | COMBO | (default) | Watermark position. |
| advancedFeatures.watermark.fontColoropt | STRING | Text color in hex format. | |
| advancedFeatures.watermark.opacityopt | BOOLEAN | false | Enable to set advancedFeatures.watermark.opacity. Off uses the model's default. |
| advancedFeatures.watermark.opacity_valueopt | FLOAT | 0.100.1–1 | Watermark opacity. |
| advancedFeatures.watermark.textopt | STRING | Watermark text. | |
| safetyopt | BOOLEAN | false | Enable to set safety. Off uses the model's default. |
| safety.checkContentopt | BOOLEAN | false | Enable or disable content safety checking. |
| ttlopt | BOOLEAN | false | Enable to set ttl. Off uses the model's default. |
| ttl_valueopt | INT | 60 | Time-to-live (TTL) in seconds for generated content. Only applies when `outputType` is `URL`. |
| outputFormatopt | COMBO | JPG | File format for the generated image. |
| outputQualityopt | INT | 9520–99 | Compression quality of the output. Higher values preserve quality but increase file size. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |