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,
};| Field | Default | Notes |
|---|---|---|
mode | .perspective | .perspective / .orthographic |
ortho_height | 2 | Orthographic view half-height, world units; width = ortho_height × aspect |
fov_deg | 57.3 | Perspective half-fov (degrees); ignored when mode = .orthographic |
near | 0.1 | Near clip plane |
far | 100 | Far 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).
| Attribute | data-glcam |
| Format | "mode,ortho_height,fov_deg,near,far" (5 scalars) |
| Emitted | Only 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.
| Control | Export | Effect |
|---|---|---|
| Freeze | glscene_freeze | pins the orbit camera |
| Unfreeze | glscene_unfreeze | resumes 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.