Skip to content

forge3d.Viewer & Recorder

Viewer — realtime rendering

Viewer

Realtime viewer for a forge3d World.

Parameters

world : World to observe. mode : "realtime" is the only supported mode. width, height : Frame dimensions in pixels. title : If given, open a real OS window (windowed mode). If None (default), render offscreen (headless mode). fps : Target frames per second in windowed mode (default 60). max_frames : Stop after this many frames in both windowed and headless modes. None (default) = run until window close (windowed) or 300 frames (headless).

is_open property

is_open: bool

True until the viewer is closed.

Returns False when:

  • the user closes the window or presses ESC (windowed), or
  • max_frames frames have been rendered (both modes), or
  • the headless frame limit (300) is reached without an explicit max_frames value.

frame_count property

frame_count: int

Number of frames rendered so far.

input property

input: Input

Per-frame keyboard/mouse state snapshot.

Windowed mode: updated at the start of each :meth:draw call from real OS events. Headless mode: always returns an empty snapshot.

dt property

dt: float

Frame time in seconds.

Windowed mode: actual wall-clock time of the previous frame. Headless mode: fixed 1/60 s.

__init__

__init__(world: World, mode: str = 'realtime', *, width: int = 800, height: int = 600, title: str | None = None, fps: int = 60, max_frames: int | None = None, controls: Any = None, shadow_resolution: int = 0, sky_color: tuple | None = None, show_grid: bool = True) -> None

draw

draw() -> np.ndarray | None

Render one frame.

Windowed mode Processes OS events, flips the previous frame, renders the new frame. Returns None. Call :meth:draw_text after this to overlay HUD elements — they appear on the same visual frame.

Headless mode Returns an (H, W, 3) uint8 ndarray. Updates :attr:input from the internal event builder.

draw_text

draw_text(text: str, x: int | None = None, y: int | None = None, size: int = 20, color: tuple = (1.0, 1.0, 1.0), bg_alpha: float = 0.6, anchor: str = 'topleft', margin: int = 10) -> None

Render a HUD text overlay on top of the current frame.

Must be called after :meth:draw.

Windowed mode: GPU resources are cached — identical calls in successive frames reuse the same texture (zero re-upload).

Parameters

text : Text to display. x, y : Pixel position of the anchor point. When omitted, auto-placed from anchor + margin. size : Font size in pixels. color : RGB in [0, 1]. bg_alpha : Dark background rectangle opacity [0, 1]. anchor : "topleft" / "topright" / "topcenter" / "center" / "bottomleft" / "bottomcenter" / "bottomright". Hyphens and underscores are accepted (e.g. "top-left"). margin : Edge margin in pixels when x or y is auto-placed.

Examples

viewer.draw_text("SCORE 42", anchor="topleft") # top-left corner viewer.draw_text("PRESS ENTER", anchor="center") # screen centre viewer.draw_text("3 lives", anchor="topright") # top-right corner

set_camera

set_camera(cam: Any) -> None

Set the camera for subsequent :meth:draw calls.

Parameters

cam : :class:~forge3d.render.snapshot.CameraSnapshot or a camera controller (:class:~forge3d.camera.OrbitCamera etc.).

run

run(dt: float | None = None, max_frames: int | None = None, collect_frames: bool = False) -> list[np.ndarray] | None

Step + draw in a loop until the viewer closes.

Parameters

dt : Physics step size (default: 1/60 s). max_frames : Override the frame limit for this run. collect_frames: If True, return all rendered frames (headless only).

step_once

step_once(dt: float | None = None) -> np.ndarray | None

Advance one physics step and draw one frame (useful for debugging).

pause

pause() -> None

Pause rendering (no-op placeholder for compatibility).

resume

resume() -> None

Resume after :meth:pause.

close

close() -> None

Release renderer resources and mark the viewer as closed.


Windowed mode (v2.0)

Pass title to open a real OS window instead of rendering offscreen. Everything else — is_open, input, draw(), draw_text() — works identically between windowed and headless modes.

viewer = f3d.Viewer(world, width=1280, height=720, title="My Game")

Shadow resolution

The windowed renderer uses shadow_resolution=1024 by default (raised from 512 in v2.1). Pass a higher value for sharper close-up shadows:

viewer = f3d.Viewer(
    world,
    width=1920, height=1080,
    title="High Quality",
    shadow_resolution=2048,   # 2K shadow map
)
shadow_resolution Quality Cost
512 Low (old default) Fast
1024 Medium (default) Moderate
2048 High Noticeable on old GPUs
Without title With title
Offscreen FBO (headless, CI-safe) Real OS window via glfw
draw() returns (H,W,3) frame draw() returns None, flips to screen
dt = fixed 1/60 s dt = real measured wall time
is_open = False after max_frames (default 300) is_open = False when window closed or max_frames reached
input = always empty input = live keyboard + mouse

Game loop with windowed Viewer

import forge3d as f3d

world = f3d.World(gravity=(0, 0, -9.81))
world.add_ground()
car = world.add_box(size=(4, 2, 0.5), position=(0, 0, 0.3), mass=1200)
cam = f3d.FollowCamera(car, offset=(-10, 0, 3.5), frame="local")

viewer = f3d.Viewer(world, width=1280, height=720, title="Drive")
while viewer.is_open:
    inp = viewer.input                           # real keyboard / mouse
    if inp.key_held(f3d.Key.W):
        car.apply_force((8000, 0, 0))            # engine force
    world.step(dt=viewer.dt)                     # real wall-clock dt
    viewer.set_camera(cam.to_snapshot(dt=viewer.dt))
    viewer.draw()                                # render 3D + flip
    viewer.draw_text(f"Speed: {speed:.0f} km/h",
                     x=640, y=660, size=32, anchor="center")

ESC or the window close button automatically sets viewer.is_open = False.

draw_text caching

draw_text() caches GPU resources keyed by (text, x, y, size, color, anchor). Unchanged elements cost virtually nothing on repeated frames. Dynamic text (counters, timers) creates a new texture each frame — keep the number of changing lines small for maximum performance.


Usage examples

Simple render loop (headless)

world = f3d.World()
world.add_ground()
world.add_box(size=(1,1,1), position=(0,0,3), mass=1,
              material=f3d.Material(color="red"))

viewer = f3d.Viewer(world, width=1280, height=720)
while viewer.is_open:
    world.step(dt=1/60)
    viewer.draw()

HUD text overlay

while viewer.is_open:
    world.step()
    viewer.draw()                       # 3D scene first

    viewer.draw_text(f"Score: {score}", x=10, y=10, size=24)
    viewer.draw_text("PAUSED", x=640, y=360, size=48,
                     color=(1.0, 0.8, 0.2), anchor="center")
    viewer.draw_text(f"Speed: {speed:.0f} km/h",
                     x=1270, y=10, size=20, anchor="topright")

Terrain rendering

import numpy as np

heights = np.sin(np.linspace(0, 4*np.pi, 32)).reshape(32, 1) \
        * np.ones((1, 32)) * 2
heights = heights.astype(np.float32)

world.add_terrain(heights=heights, cell_size=2.0, origin=(-32,-32,0),
                  material=f3d.Material(color=(0.28, 0.45, 0.18)))

viewer = f3d.Viewer(world)
while viewer.is_open:
    world.step()
    viewer.draw()

Camera control

cam = f3d.OrbitCamera(target=(0,0,1), distance=10, elevation=30)

while viewer.is_open:
    inp = viewer.input
    if inp.mouse_button(1):
        dx, dy = inp.mouse_delta()
        cam.rotate(d_azimuth=dx*0.5, d_elevation=-dy*0.5)
    cam.zoom(inp.scroll_delta() * 0.5)
    viewer.set_camera(cam.to_snapshot())
    world.step()
    viewer.draw()

HQ video recording

world.set_camera(position=(4, -7, 3), target=(0, 0, 1))

rec = f3d.Recorder(
    world,
    mode="hq",
    resolution=(1920, 1080),
    samples=64,
    output="scene.mp4",
)
rec.run(duration=5.0, dt=1/240, fps=60)

Recorder — offline video capture

Recorder

Capture a World's simulation as video frames.

Parameters

world : World to record. May be None when only run_policy will be used (no free-simulation recording). mode : 'realtime' or 'hq'. resolution : (width, height) in pixels. output : Output file path (mp4 via imageio, or image sequence dir). samples : Ray samples per pixel for HQ mode (ignored in realtime).

run

run(duration: float, dt: float = 1.0 / 240.0, fps: float = 60.0) -> None

Simulate for duration seconds and save video.

Physics runs at dt timestep; frames are saved at fps rate. So phys_steps_per_frame = round(1/(fps*dt)).

Parameters

duration : Simulation duration in seconds. dt : Physics integration step. fps : Output video frame rate.

run_policy

run_policy(policy: Any, env: Any, duration: float = 5.0, fps: float = 24.0, deterministic: bool = True, seed: int = 0) -> None

Record a trained policy rollout from a Gymnasium-compatible env.

Parameters

policy : SB3 model or any object with predict(obs) method. env : Gymnasium env (must support render_mode="rgb_array"). duration : Recording duration in seconds. fps : Output video frame rate. deterministic : Use deterministic policy actions. seed : Env reset seed.