M8 · 相机控制
A 3D sphere that writes your camera prompt for you
- prompt
Camera language is one of the few prompt ingredients that still measurably works. Naming a physical piece of equipment - a point-and-shoot, an 85mm, a film stock - drops a model out of beauty-default mode faster than any pile of "realistic, candid, amateur" ever did. It's also a pain to write by hand: change the shot distance and you rewrite half the sentence, and you have to keep track of whether it's "from behind" or "from the back". M8 · Camera Control turns four sliders into that sentence.
What you're actually getting
It's a prompt generator, not a model and not a ControlNet. You tell it where the camera is; it emits text that goes straight into a positive prompt. The point is granularity - you change one dimension and only that dimension moves. Sweep pos_x across ten values with a fixed seed and you get a proper comparison grid instead of ten hand-written sentences you can't trust to differ in only one way.
How the numbers become words
The node proper is four floats plus one big config blob. pos_x (left/right), pos_y (up/down), pos_z (near/far) and roll all run −1 to 1. Everything else - tag vocabulary, weights, deadzones - lives in config, the JSON in the multiline field.
- Azimuth comes off
pos_x: multiplied by π to get an orbit angle, split with cos/sin into front/back/left/right, then normalised so the four directions share a 100% weight budget.deadzone_ratio(0.2 by default) drops directions whose share is negligible instead of emitting a swarm of 0.001-weight noise. A polar gate narrows the horizontal directions as the camera approaches straight up or straight down, because at that point "from the left" means nothing. - Elevation comes off
pos_yand picks exactly one band - bird, high, eye, low, worm. Never two: "from above" and "from below" in the same prompt is a contradiction, and the model receives it as one. Eye-level is centre-weighted (peaks at 3.00 when you're level, tapering toward 1.00 as you leave); the extremes scale with the absolute value. - Distance off
pos_zis five bands -ecu,cu,medium,full,wide- covering z from 1.0 down to −1.0. Inside a band the weight climbs linearly; crossing into the next band it restarts at zero. Close-ups get stronger the closer you are, and medium through wide do the opposite. - Roll past a 0.15 deadzone emits
dutch angle, weighted. - Extras - lens, depth of field, movement, composition, style - are five switches each with a free-text value, all off by default. That's where you put
85mm lensorhandheld camera.
Default output looks like this:
(from front:10.00), (eye-level:3.00), (medium shot:1.00),
The Python side is the authority. The JS panel recomputes the same maths only so the drag preview keeps up while you're dragging - a bad preview can't corrupt the run.
The inputs that matter
Five inputs, period: the four floats and config. One output, prompt (STRING), trailing comma included so you can splice it in front of a normal prompt or wire it into your text encoder.
The config switch you'll actually want is no_weight. Turn it on and the node emits plain tags with no (tag:2.00) syntax. That matters more than it sounds: on LLM-encoded models - Z-Image, Flux 2 Klein, Anima, Qwen - attention weights aren't weakened, they're passed through as literal punctuation. If your model is in that family and you leave weights on, you're feeding it parentheses and numbers as prompt tokens. weight_min / weight_max (0.1 to 10 by default) clamp everything else.
Camera setups save as one JSON per configuration under m8/data/camera-configs/, and there's a load dropdown beside the sphere.
Installing it
ComfyUI Manager, search the pack name, or:
cd ComfyUI/custom_nodes
git clone https://github.com/chenr5934-tech/ComfyUI-M8nodes.git
Restart ComfyUI. There is nothing to pip install - the pack's pyproject.toml declares zero dependencies on purpose, and the only node with a requirement (the local LLM one) keeps llama-cpp-python to itself. You'll find this one under right-click → M8 → Camera.
Where it bites
Config JSON that doesn't parse fails loudly with M8-CAM-001 instead of quietly falling back to defaults. That's deliberate - silent fallback turns "my settings did nothing" into an unbisectable mystery - but it does mean a stray comma stops the run until you fix it or clear the field.
The bigger caveat is the model. Camera-angle prompting is not equally effective everywhere; people running Z-Image Turbo report it largely ignoring viewpoint prompts, and the accepted route when you need an exact framing is still an image plus ControlNet rather than prose. Text camera language is best at tilting a composition, not dictating it.
And remember this is positive-prompt text. There's no negative output, no ControlNet, nothing to wire into a sampler - you feed the string to your prompt builder like any other text.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| pos_x | FLOAT | 0.00-1–1 | — |
| pos_y | FLOAT | 0.00-1–1 | — |
| pos_z | FLOAT | 0.00-1–1 | — |
| roll | FLOAT | 0.00-1–1 | — |
| config | STRING | {"weight_min": 0.1, "weight_max": 10.0, "no_weight": false, "no_weight_threshold": 0.5, "azimuth": {"enabled": true, "weight": 10.0, "deadzone_ratio": 0.2, "directions": {"front": {"tag": "from front", "enabled": true}, "back": {"tag": "from behind", "enabled": true}, "left": {"tag": "from right", "enabled": true}, "right": {"tag": "from left", "enabled": true}}}, "elevation": {"enabled": true, "extra": 10.0, "eye_peak": 3.0, "categories": {"bird": {"tag": "directly above, from above, aerial view,", "enabled": true}, "high": {"tag": "high angle, from above", "enabled": true}, "eye": {"tag": "eye-level", "enabled": true}, "low": {"tag": "low angle, from below,", "enabled": true}, "worm": {"tag": "directly below", "enabled": true}}}, "distance": {"enabled": true, "extra": 0.0, "categories": {"ecu": {"tag": "extreme close-up", "enabled": true}, "cu": {"tag": "close-up", "enabled": true}, "medium": {"tag": "medium shot", "enabled": true}, "full": {"tag": "full body", "enabled": true}, "wide": {"tag": "wide shot", "enabled": true}}}, "tilt": {"enabled": true, "deadzone": 0.15, "extra": 0.0, "dutch_tag": "dutch angle"}, "extra_master": 1.0, "drag_step": 0.004, "extras": {"lens": {"enabled": false, "value": "85mm lens"}, "dof": {"enabled": false, "value": "shallow depth of field", "weight": 1.3}, "movement": {"enabled": false, "value": "handheld camera"}, "composition": {"enabled": false, "value": "rule of thirds"}, "style": {"enabled": false, "value": "cinematic"}}} | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| prompt | STRING | — |