Skip to content

Hololuminescent Displays

quiltwright.hld

Hololuminescent Display (HLD) Renderer

Renders PyVista scenes as videos for Looking Glass Hololuminescent Displays -- the HLD product line (16" / 27" / 86" Portrait).

HLDs are a different technology from the classic light-field Looking Glass devices (which consume multi-view quilts; see :mod:quiltwright.lfd). An HLD is an LCD with a fixed holographic "alcove" volume embedded in its optical stack; ordinary flat 2-D video is multiply-blended into that volume. The consequences for rendering:

  • Pure white pixels are invisible -- they show only the holographic alcove. Subjects must sit on a white background.
  • The subject should be centred inside safe-area margins (~9% top, 3% bottom/left/right) so it stays within the alcove.
  • A slow turntable orbit with an otherwise static camera reads best.
  • The master format is 3840x2160 (16:9 landscape) HEVC MP4, 30 or 60 fps, bt709. HLD Author requires landscape input and handles device orientation internally.

Spec: https://hlddocs.lookingglassfactory.com/resources/media-specs-and-encoding

Delivery: run the rendered *_hld.mp4 through Looking Glass's free HLD Author app, then copy the exported file to the player's USB drive. For signage players (BrightSign/Yodeck) or direct HDMI, use the master as-is.

Typical usage::

import pyvista as pv
from quiltwright.hld import render_hld_video, style_plotter_for_hld

p = pv.Plotter(off_screen=True)
p.add_mesh(pv.ParametricTorus(), color="teal")
style_plotter_for_hld(p)                  # white bg, safe-area framing
render_hld_video(p, "torus")              # -> torus_hld.mp4 (10s orbit)
p.close()

Part of Quiltwright -- https://github.com/Flux-Frontiers/quiltwright Author: Eric G. Suchanek, PhD

HLDDeviceSpec(resolution, fps=30, encode_args=None, max_seconds=None) dataclass

A named HLD-family device's concrete media requirements.

:func:render_hld_video / :func:~quiltwright.povray.render_pov_hld_video default to the official HLD master spec (:data:HLD_RESOLUTION, HEVC via :func:_hld_encode_args) -- what HLD Author and the big Portrait HLD panels want. A device that consumes video directly instead, without going through HLD Author, may want something else entirely; look one up in :data:HLD_DEVICES rather than hand-rolling it per render.

Parameters:

Name Type Description Default
resolution tuple[int, int]

Render (width, height).

required
fps int

Frame rate the device expects.

30
encode_args tuple[str, ...] | None

ffmpeg output arguments for this device, or None to use the official HEVC master spec. Does not include -crf -- append that separately so quality stays a caller choice.

None
max_seconds float | None

Longest clip the device's own import flow accepts, or None if unbounded/unknown.

None

add_floor_shadow(plotter, subject_bounds, *, opacity=0.25, scale=1.4, dark_bg=False)

Add a soft fake contact shadow under the subject.

The HLD guidelines call contact shadows on the alcove floor "crucial for the 3D effect". PyVista's ray-traced shadows are unreliable with translucent voxel clouds, so this paints a flattened grey disc just below the subject's bounding box.

On a white background (HLD): the disc fades centre->light-grey, rim->white, so the rim is invisible against the background.

On a dark background (interactive viewer): pass dark_bg=True to flip the ramp -- centre->dark-grey, rim->black -- so the shadow reads correctly instead of appearing as a glowing white disc.

Parameters:

Name Type Description Default
plotter

Active pv.Plotter.

required
subject_bounds tuple[float, float, float, float, float, float]

(xmin, xmax, ymin, ymax, zmin, zmax) of the subject (e.g. grid.bounds).

required
opacity float

Shadow darkness (0 = none, 1 = full grey at centre).

0.25
scale float

Shadow radius as a fraction of the subject's half-extent; keep it > 1 so the shadow spreads past the footprint.

1.4
dark_bg bool

If True, use a dark-background-compatible ramp (centre = dark grey, rim = black). Default False is optimised for HLD's white background (centre = light grey, rim = white).

False
Source code in src/quiltwright/hld.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
def add_floor_shadow(
    plotter,
    subject_bounds: tuple[float, float, float, float, float, float],
    *,
    opacity: float = 0.25,
    scale: float = 1.4,
    dark_bg: bool = False,
) -> None:
    """Add a soft fake contact shadow under the subject.

    The HLD guidelines call contact shadows on the alcove floor "crucial
    for the 3D effect".  PyVista's ray-traced shadows are unreliable with
    translucent voxel clouds, so this paints a flattened grey disc just
    below the subject's bounding box.

    On a **white** background (HLD): the disc fades centre->light-grey,
    rim->white, so the rim is invisible against the background.

    On a **dark** background (interactive viewer): pass ``dark_bg=True``
    to flip the ramp -- centre->dark-grey, rim->black -- so the shadow reads
    correctly instead of appearing as a glowing white disc.

    :param plotter: Active ``pv.Plotter``.
    :param subject_bounds: ``(xmin, xmax, ymin, ymax, zmin, zmax)`` of the
        subject (e.g. ``grid.bounds``).
    :param opacity: Shadow darkness (0 = none, 1 = full grey at centre).
    :param scale: Shadow radius as a fraction of the subject's half-extent;
        keep it > 1 so the shadow spreads past the footprint.
    :param dark_bg: If ``True``, use a dark-background-compatible ramp
        (centre = dark grey, rim = black).  Default ``False`` is optimised
        for HLD's white background (centre = light grey, rim = white).
    """
    require_pyvista("add_floor_shadow")
    xmin, xmax, ymin, ymax, zmin, zmax = subject_bounds
    cx, cy = (xmin + xmax) / 2.0, (ymin + ymax) / 2.0
    radius = scale * max(xmax - xmin, ymax - ymin) / 2.0
    drop = 0.02 * (zmax - zmin)
    disc = pv.Disc(
        center=(cx, cy, zmin - drop),
        inner=0.0,
        outer=radius,
        normal=(0, 0, 1),
        c_res=64,
    )
    pts = disc.points
    r = np.linalg.norm(pts[:, :2] - np.array([cx, cy]), axis=1) / max(radius, 1e-12)
    disc.point_data["shadow"] = (1.0 - np.clip(r, 0.0, 1.0)) ** 2
    if dark_bg:
        # "Greys_r": 0=black -> 1=white.
        # clim=(0, 3) maps the peak scalar (1.0) to position 0.33 -> ~#555555.
        # Rim scalar (0.0) stays black and blends into the dark background.
        cmap, clim = "Greys_r", (0.0, 3.0)
    else:
        # "Greys": 0=white -> 1=black.  Rim (scalar=0) fades to white and
        # disappears into the HLD white background.
        # clim max = 1.5 maps the peak scalar to 0.67 -> ~#555555 dark grey,
        # clearly visible against the white HLD background.
        cmap, clim = "Greys", (0.0, 1.5)
    plotter.add_mesh(
        disc,
        scalars="shadow",
        cmap=cmap,
        clim=clim,
        show_scalar_bar=False,
        lighting=False,
    )

apply_safe_area(camera, margins=HLD_SAFE_MARGINS)

Frame the current camera view inside the HLD safe area.

Assumes the camera currently frames the subject to the full viewport (e.g. after reset_camera()). Zooms out so the subject fits the safe-area box and shifts the projection window so the box's centre (slightly below frame centre, because the top margin is larger) holds the subject.

Parameters:

Name Type Description Default
camera

pv.Camera / vtkCamera to mutate.

required
margins tuple[float, float, float, float]

(top, bottom, left, right) fractions of frame size.

HLD_SAFE_MARGINS
Source code in src/quiltwright/hld.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def apply_safe_area(camera, margins: tuple[float, float, float, float] = HLD_SAFE_MARGINS) -> None:
    """Frame the current camera view inside the HLD safe area.

    Assumes the camera currently frames the subject to the full viewport
    (e.g. after ``reset_camera()``).  Zooms out so the subject fits the
    safe-area box and shifts the projection window so the box's centre
    (slightly below frame centre, because the top margin is larger) holds
    the subject.

    :param camera: ``pv.Camera`` / vtkCamera to mutate.
    :param margins: ``(top, bottom, left, right)`` fractions of frame size.
    """
    top, bottom, left, right = margins
    fit = min(1.0 - left - right, 1.0 - top - bottom)
    camera.Zoom(fit)
    # WindowCenter shifts the frustum in NDC (half-extent = 1): moving the
    # frustum up by (top - bottom) moves the rendered subject down to the
    # safe-area centre.
    wcx = camera.GetWindowCenter()[0] + (left - right)
    wcy = camera.GetWindowCenter()[1] + (top - bottom)
    camera.SetWindowCenter(wcx, wcy)

hld_orbit_speed(n_frames, fps)

Degrees of rotation per second for a given clip configuration.

Source code in src/quiltwright/hld.py
401
402
403
def hld_orbit_speed(n_frames: int, fps: int) -> float:
    """Degrees of rotation per second for a given clip configuration."""
    return 360.0 * fps / n_frames if n_frames else 0.0

render_hld_still(plotter, out_stem, *, resolution=HLD_RESOLUTION)

Render a single HLD-ready PNG at the current camera position.

Same white-background, safe-area framing as :func:render_hld_video but outputs one *_hld.png instead of a video. Useful for previews or signage systems that accept still images.

Parameters:

Name Type Description Default
plotter

An off-screen pv.Plotter with the scene composed and already styled via :func:style_plotter_for_hld.

required
out_stem str | Path

Output path; _hld.png is appended.

required
resolution tuple[int, int]

Render (width, height); default 3840×2160.

HLD_RESOLUTION

Returns:

Type Description
Path

Path of the PNG written.

Source code in src/quiltwright/hld.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def render_hld_still(
    plotter,
    out_stem: str | Path,
    *,
    resolution: tuple[int, int] = HLD_RESOLUTION,
) -> Path:
    """Render a single HLD-ready PNG at the current camera position.

    Same white-background, safe-area framing as :func:`render_hld_video` but
    outputs one ``*_hld.png`` instead of a video.  Useful for previews or
    signage systems that accept still images.

    :param plotter: An *off-screen* ``pv.Plotter`` with the scene composed and
        already styled via :func:`style_plotter_for_hld`.
    :param out_stem: Output path; ``_hld.png`` is appended.
    :param resolution: Render ``(width, height)``; default 3840×2160.
    :return: Path of the PNG written.
    """
    require_pyvista("render_hld_still")
    try:
        from PIL import Image
    except ImportError as exc:
        raise ImportError(
            "render_hld_still() requires pillow.\nInstall with:  poetry install --with viz"
        ) from exc

    out_stem = Path(out_stem)
    if out_stem.suffix.lower() in (".png", ".jpg", ".jpeg"):
        out_stem = out_stem.with_suffix("")
    out_path = out_stem.parent / f"{out_stem.name}_hld.png"
    out_path.parent.mkdir(parents=True, exist_ok=True)

    plotter.window_size = resolution
    plotter.renderer.reset_camera_clipping_range()
    plotter.render()
    img = plotter.screenshot(None, return_img=True)[..., :3]
    Image.fromarray(img).save(out_path)
    return out_path

render_hld_video(plotter, out_stem, *, n_frames=300, fps=30, orbit_degrees=360.0, resolution=HLD_RESOLUTION, crf=18, rotate_for_player=False, on_frame=None, progress=True)

Render a turntable HLD master video of the plotter's scene.

One ordinary 2-D render per frame (no multi-view sweep), camera orbiting the focal point, encoded to the official HLD master spec (3840×2160 landscape HEVC bt709). Style the scene first with :func:style_plotter_for_hld (white background is what makes the hologram read on the device).

Parameters:

Name Type Description Default
plotter

An off-screen pv.Plotter with the scene composed.

required
out_stem str | Path

Output path; _hld.mp4 is appended.

required
n_frames int

Frame count (default 300 @ 30 fps = 10 s loop).

300
fps int

30 or 60 per the HLD spec.

30
orbit_degrees float

Total orbit over the clip; 360 loops seamlessly. Pass 0 to disable the turntable (use on_frame).

360.0
resolution tuple[int, int]

Render (width, height); default 3840×2160.

HLD_RESOLUTION
crf int

x265 quality (lower = better; 15-20 sensible).

18
rotate_for_player bool

Rotate 90° CCW before output. Leave False (default) for HLD Author and signage/HDMI delivery.

False
on_frame

Optional callback(frame_index) before each frame.

None
progress bool

Print a progress line while rendering.

True

Returns:

Type Description
Path

Path of the MP4 written.

Source code in src/quiltwright/hld.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def render_hld_video(
    plotter,
    out_stem: str | Path,
    *,
    n_frames: int = 300,
    fps: int = 30,
    orbit_degrees: float = 360.0,
    resolution: tuple[int, int] = HLD_RESOLUTION,
    crf: int = 18,
    rotate_for_player: bool = False,
    on_frame=None,
    progress: bool = True,
) -> Path:
    """Render a turntable HLD master video of the plotter's scene.

    One ordinary 2-D render per frame (no multi-view sweep), camera
    orbiting the focal point, encoded to the official HLD master spec
    (3840×2160 landscape HEVC bt709).  Style the scene first with
    :func:`style_plotter_for_hld` (white background is what makes the
    hologram read on the device).

    :param plotter: An *off-screen* ``pv.Plotter`` with the scene composed.
    :param out_stem: Output path; ``_hld.mp4`` is appended.
    :param n_frames: Frame count (default 300 @ 30 fps = 10 s loop).
    :param fps: 30 or 60 per the HLD spec.
    :param orbit_degrees: Total orbit over the clip; 360 loops seamlessly.
        Pass 0 to disable the turntable (use *on_frame*).
    :param resolution: Render ``(width, height)``; default 3840×2160.
    :param crf: x265 quality (lower = better; 15-20 sensible).
    :param rotate_for_player: Rotate 90° CCW before output.  Leave ``False``
        (default) for HLD Author and signage/HDMI delivery.
    :param on_frame: Optional ``callback(frame_index)`` before each frame.
    :param progress: Print a progress line while rendering.
    :return: Path of the MP4 written.
    """
    require_pyvista("render_hld_video")
    ffmpeg = find_ffmpeg()

    try:
        from PIL import Image
    except ImportError as exc:
        raise ImportError(
            "render_hld_video() requires pillow.\nInstall with:  poetry install --with viz"
        ) from exc

    out_stem = Path(out_stem)
    if out_stem.suffix.lower() == ".mp4":
        out_stem = out_stem.with_suffix("")
    out_path = out_stem.parent / f"{out_stem.name}_hld.mp4"
    out_path.parent.mkdir(parents=True, exist_ok=True)

    plotter.window_size = resolution
    if not plotter.camera.is_set:
        plotter.camera_position = plotter.renderer.get_default_cam_pos()
        plotter.reset_camera()

    step = orbit_degrees / n_frames if n_frames else 0.0
    with tempfile.TemporaryDirectory(prefix="hld_frames_") as tmp:
        for i in range(n_frames):
            if on_frame is not None:
                on_frame(i)
            plotter.renderer.reset_camera_clipping_range()
            plotter.render()
            img = plotter.screenshot(None, return_img=True)[..., :3]
            Image.fromarray(img).save(f"{tmp}/frame{i:05d}.png")
            plotter.camera.Azimuth(step)
            if progress:
                print(f"\r  HLD frame {i + 1}/{n_frames}", end="", flush=True)
        if progress:
            print()

        args = _hld_encode_args(fps, crf)
        if rotate_for_player:
            args += ["-vf", "transpose=2"]  # 90° counter-clockwise
        cmd = [
            ffmpeg,
            "-y",
            "-framerate",
            str(fps),
            "-i",
            f"{tmp}/frame%05d.png",
            *args,
            str(out_path),
        ]
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise RuntimeError(f"ffmpeg failed ({result.returncode}):\n{result.stderr[-2000:]}")
    return out_path

style_plotter_for_hld(plotter, *, safe_area=True, zoom=1.0, resolution=HLD_RESOLUTION)

Apply HLD content rules to a composed plotter.

Sets the pure-white background (white = transparent on the device), switches the window to the 16:9 master resolution (3840×2160), refits the camera to the scene at that aspect, applies safe-area margins, then applies an optional zoom factor so subjects fill the frame.

Parameters:

Name Type Description Default
plotter

pv.Plotter with the scene composed.

required
safe_area bool

Apply :func:apply_safe_area framing.

True
zoom float

Extra zoom applied after safe-area framing. Values > 1 scale the subject up to fill more of the frame; 1.0 = no extra zoom. Useful for portrait-shaped subjects (brains, bodies) that appear small inside a 16:9 landscape frame after reset_camera().

1.0
resolution tuple[int, int]

Render (width, height); default 3840×2160.

HLD_RESOLUTION
Source code in src/quiltwright/hld.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def style_plotter_for_hld(
    plotter,
    *,
    safe_area: bool = True,
    zoom: float = 1.0,
    resolution: tuple[int, int] = HLD_RESOLUTION,
) -> None:
    """Apply HLD content rules to a composed plotter.

    Sets the pure-white background (white = transparent on the device),
    switches the window to the 16:9 master resolution (3840×2160), refits
    the camera to the scene at that aspect, applies safe-area margins, then
    applies an optional *zoom* factor so subjects fill the frame.

    :param plotter: ``pv.Plotter`` with the scene composed.
    :param safe_area: Apply :func:`apply_safe_area` framing.
    :param zoom: Extra zoom applied after safe-area framing.  Values > 1
        scale the subject up to fill more of the frame; 1.0 = no extra zoom.
        Useful for portrait-shaped subjects (brains, bodies) that appear
        small inside a 16:9 landscape frame after ``reset_camera()``.
    :param resolution: Render ``(width, height)``; default 3840×2160.
    """
    require_pyvista("style_plotter_for_hld")
    plotter.set_background("white")
    plotter.window_size = resolution
    plotter.render()  # apply window size so reset_camera sees 16:9
    if not plotter.camera.is_set:
        plotter.camera_position = plotter.renderer.get_default_cam_pos()
    plotter.reset_camera()
    if safe_area:
        apply_safe_area(plotter.camera)
    if zoom != 1.0:
        plotter.camera.Zoom(zoom)