Cinematic rendering#
Warning
Experimental. cinematic is new in 3.0.0 and not yet on the same
footing as the other two renderers: the trait names and their defaults may
change, the image a given scene produces may change between versions, and
the coverage gaps listed below are real rather than temporary oversights
(volume_slice is not drawn, volumes stay outside the light simulation).
simple and advanced remain the stable choices; please report what
breaks.
plot = k3d.plot(renderer='cinematic')
Where advanced approximates indirect light with an occlusion
pass, cinematic traces it: rays scatter off surfaces up to
cinematic_bounces times, gathering colour from the environment and from each
other. Soft shadows, mirror and glossy reflections, and colour bleeding between
nearby objects all appear without a single extra knob - they are consequences of
the simulation rather than effects layered on top of it.
The image is progressive: one sample per animation frame, with a counter in the
corner, until it reaches cinematic_samples - a hard ceiling, after which the
loop stops and an idle plot costs nothing. Any change to the camera, the scene or
the lighting abandons the accumulation and starts it again from sample zero, so
what you see always describes the current state. While you drag the camera the
frame is rasterised instead (the same picture advanced would draw, minus the
occlusion pass), so the view follows the mouse; path tracing resumes the moment
the camera settles. Screenshots always render the full budget, so an exported
image is as clean as the budget allows regardless of what the interactive view
had reached.
The parameters#
plot.cinematic_samples = 64 # accumulation budget, [1, 100000]
plot.cinematic_bounces = 6 # light bounces, [1, 32]
plot.cinematic_glossy_filter = 0.25 # widen glossy lobes after a rough bounce, [0, 1]
plot.cinematic_seed = None # None: fresh noise each time; an int: repeatable
cinematic_samplesHow many samples the accumulation gathers before it parks. Noise falls off as the square root of this number, so 4x the samples means half the noise: the step from 32 to 128 is plainly visible, the one from 512 to 2048 rarely is. Cost is linear in it. The ceiling is deliberately far above anything interactive, because a final render is worth waiting for - and because the loop stops there rather than burning a GPU forever.
cinematic_bouncesHow far light is followed. 1 is direct lighting only: no colour bleeding, no reflection of one object in another, and interiors go black. 6 is enough for ordinary scenes; a closed white room or a stack of glossy surfaces keeps getting brighter up to 12 or so. Cost grows with it, though sub-linearly - paths that leave the scene stop early.
cinematic_glossy_filterFirefly control, described below. 0 leaves light transport unbiased.
cinematic_seedNone, the default, draws fresh noise for every accumulation, so two renders of the same scene differ in their residual grain the way two photographs do. An integer pins the noise: the same plot then renders the same image, sample for sample, across page loads and machines - what a reference-image test suite or a frame-by-frame animation needs, and what a notebook does not. Different seeds give different, equally valid patterns.
Note
Path tracing produces high dynamic range: bounced light between bright
surfaces genuinely exceeds 1.0, and without a tone curve those values clip.
A yellow menger sponge - all cavities, all bounce - blows out about 7% of its
pixels at tone_mapping='none' and none at all with 'aces'. If a
cinematic render looks hot where advanced looked fine, reach for
plot.tone_mapping before plot.lighting.
Fireflies#
A polished surface lit by a small very bright source - metal under a sunny HDRI, typically - throws fireflies: isolated bright pixels left by the rare path that happens to reach the sun through a mirror. They fade as the square root of the sample count, which is to say hardly at all.
cinematic_glossy_filter widens a glossy lobe in proportion to the roughness
already gathered along the path. A specular seen directly is unaffected - nothing
has accumulated yet - while the path that hits a rough surface first and a mirror
second gets spread out, and the speckle with it. That is why it defaults to 0.25:
the bias is invisible where you look straight at a reflection, and it removes the
artefact where the artefact lives.
Its limit follows from the same rule. A chain of smooth surfaces accumulates almost no roughness, so a mirror floor reflecting a polished model keeps its fireflies at any setting - raising the filter does nothing there. What helps is giving one of the two surfaces some roughness, or choosing an environment whose brightest spot is less concentrated than a sun.
Environments are the light#
There are no light objects in cinematic. The environment map is the only
source of illumination, and it is what every reflective surface reflects, so
choosing it is the single biggest decision about how a plot looks - more than any
material parameter.
It is not the backdrop, though: behind the scene you get plot.background_color,
exactly as in the other two renderers. The environment lights the model and shows
up in its reflections; the space behind the data stays yours. A photograph of a
warehouse behind a plot would look striking and say nothing - the light it casts is
what changes how a surface reads, and that is the part worth having.
plot.environment = 'studio' # procedural preset
plot.environment = 'venice_sunset' # photographic catalog (Poly Haven, CC0)
plot.environment = my_hdr_array # any (H, W, 3) float32 equirect
plot.environment_rotation = np.pi / 3 # spin it around the scene's up axis
plot.lighting = 1.5 # exposure, not a light count
plot.tone_mapping = 'aces' # filmic curve for the highlights
Every map is energy-normalised, so the environment carries the shape of the
light while plot.lighting stays the exposure knob. Rotating it moves the
highlights without changing their intensity, which is often the quickest way to
make a specific surface read well.
The same gold dragon on a polished floor, under six environments, at 256 samples
each. Nothing changes between these images except plot.environment - so every
difference you see is the light itself and what the metal reflects of it:
import k3d.environments
k3d.environments.available()
# ['autoshop_01', 'brown_photostudio_02', 'burnt_warehouse',
# 'moonless_golf', 'venice_sunset']
Note
The photographic maps live in the Python package, so a kernel-less page
cannot resolve their names. An exported HTML snapshot therefore offers only
what it can regenerate: the procedural presets plus the map that was baked
into it at export time. A page may widen that list by including the sideload
script generated by k3d.environments.save_js(path) next to
standalone.js.
Ambient-occlusion knobs are absent from the panel here: occlusion is not approximated, it is traced.
What changes shape-for-shape#
A path tracer needs surfaces with area, so objects drawn as screen-space impostors are rebuilt as real geometry. The result keeps the shape you asked for; the differences worth knowing:
Object |
In |
|---|---|
|
Traced as they are. |
|
Merged spheres of real geometry. Sphere detail adapts to the point count
and is capped by a triangle budget, so very large clouds render coarser
spheres. |
|
Tubes of world-space width. |
|
Shafts become tubes of radius |
|
Camera-facing quads, frozen in the orientation they had when the accumulation started; they do not turn with the camera mid-frame. |
|
Unchanged: HTML overlays drawn on top of the finished frame. |
|
Not rendered (a warning says so). A slice paints its cut plane with its
own shader and carries no depth-segment mechanism, so it can neither be
traced nor composited correctly; use |
Unlit primitives |
Lit. A path tracer has no unlit surface, so |
The grid |
Not drawn. |
Volumes and MIPs#
The path tracer knows only homogeneous fog, so volume and mip keep the
ray march they use in advanced - lit by the same environment harmonics -
and composite over the traced image. The march stops at the first traced
surface, so geometry inside or behind a volume occludes correctly and gas in
front of geometry dims it:
The limits of this hybrid, in exchange for keeping volumes at all:
a volume does not appear in reflections or refractions, and casts no light or shadow onto geometry - global illumination does not see the gas;
geometry seen through a reflection is not dimmed by gas in front of it, although geometry seen directly is;
mipis a maximum-intensity projection, a diagnostic view rather than a physical one; incinematicit stays exactly that, composited outside the light simulation.
Requirements and failure#
cinematic needs WebGL2 with renderable float textures. When the browser
cannot provide them, switching to it fails loudly: an error overlay names
the reason and the renderer trait reverts to its previous value. There is no
silent fallback to another renderer - a plot that says cinematic is always
path traced.
Cost scales with resolution, sample budget and bounce count. On a software renderer (CI, remote sessions without a GPU) a converged frame takes seconds to minutes; the library’s own reference images use 32 samples at a quarter resolution for exactly that reason.
Before the first sample the scene needs a ray-tracing acceleration structure, rebuilt whenever the geometry changes. Past a hundred thousand triangles that build moves to a worker - the counter reports its progress and the camera keeps responding on rasterised frames meanwhile. In a notebook the worker script comes from the kernel and nowhere else, which keeps an air-gapped deployment air-gapped; a standalone page looks for it next to the bundle it loaded, then on unpkg for that same version. When none of those answers - an unpublished build, a network without a route out - the structure is built on the main thread and the page stops responding for as long as that takes.