verve.gl orthographic projection

Guide: WebGL · Advanced WebGL · live: orthographic vs perspective, orthographic CSM.

GlSceneBuilder defaults to perspective projection. .projection(ProjectionOpts) switches a scene to orthographic (parallel) projection — OrthographicCamera parity, no foreshortening (v0.22.0).

.projection

ctx.glScene(.{ .src = "/gl/cubegrid.vmesh", .env = "/gl/studio.venv" })
    .projection(.{ .mode = .orthographic, .ortho_height = 12.0 })
    .build();
pub const Projection = enum(u32) { perspective = 0, orthographic = 1 };
pub const ProjectionOpts = struct {
    mode:         Projection = .perspective,
    ortho_height: f32        = 2,     // world-units half-height; width tracks aspect
    fov_deg:      f32        = 57.3,  // perspective half-fov (ignored in ortho mode)
    near:         f32        = 0.1,
    far:          f32        = 100,
};
FieldDefaultNotes
mode.perspective.perspective / .orthographic
ortho_height2Orthographic view half-height, world units; width = ortho_height × aspect
fov_deg57.3Perspective half-fov (degrees); ignored when mode = .orthographic
near0.1Near clip plane
far100Far clip plane

Wire facts

Non-default projection is serialized out-of-band as the data-glcam canvas attribute — a five-value CSV: mode,ortho_height,fov_deg,near,far (e.g. 1,12,57.3,0.1,100). The chunk reads it via refGetAttr("data-glcam") and builds the projection matrix before the first frame. Perspective (mode = 0, the default) emits no attribute — the builder only writes data-glcam when proj_opts.mode != .perspective (core/gl_scene.zig).

Attributedata-glcam
Format"mode,ortho_height,fov_deg,near,far" (5 scalars)
EmittedOnly when .projection() sets mode = .orthographic

Picking is ortho-aware

Pick rays are derived correctly under either projection. Under orthographic (mode == 1), the ray origin comes from the NDC-space canvas pointer position offset along the camera's right/up basis by ndc × ortho_height × aspect, and the direction is the camera's forward vector — parallel rays, no foreshortening (core/gl/ray.zig, rayFromCameraOrtho). Under perspective, rays still fan out from a single eye point as usual.

Demo (/gl-ortho)

A 7×7 cube grid (/gl/cubegrid.vmesh) rendered twice, side by side, with identical camera distance/pitch/yaw, identical light, and identical auto- rotate — the only difference between the two panels is the projection matrix. Under perspective (left) the far rows of cubes visibly shrink with distance; under orthographic (right, ortho_height = 12) every row renders at the same on-screen size, since projection rays are parallel rather than converging on the eye. Both panels are independent GlScene island instances (P7 multi-instance), each driving its own orbit camera. Drag to orbit, wheel to zoom, on either panel.

Orthographic-aware CSM

Directional cascaded shadow maps are projection-aware. Under orthographic projection, cascadeLightVp (client/islands/GlScene.zig) fits each cascade slice as a rectangular slab — constant half-extents (ortho_height × ortho_height·aspect) at every depth — instead of a perspective frustum wedge. A naive perspective-style cascade fit under an orthographic camera mis-sizes the light frustum per slice, smearing or dropping the shadow; the slab fit keeps the shadow attached to the caster across all cascade bands. This was shipped alongside orthographic projection in v0.22.0 and the cascade-fit bug was corrected in v0.23.1.

Demo (/gl-ortho-csm)

The same single directional caster + four-cascade setup as /gl-csm, on the same shadow.vmesh cube-and-floor asset, but the camera uses .projection(.{ .mode = .orthographic, .ortho_height = 8.0 }) instead of the default perspective. The floor stays the same width front-to-back (parallel projection, no foreshortening) and the cast shadow stays correctly attached to the cube across the cascade bands. Renders on both WebGL2 and WebGPU.

ControlExportEffect
Freezeglscene_freezepins the orbit camera
Unfreezeglscene_unfreezeresumes the orbit

glscene_freeze/glscene_unfreeze are the shared GlScene chunk exports used by every declarative scene that exposes freeze controls — no per-demo island or export is needed.