verve.gl
Guide: WebGL · Advanced WebGL · Image quality & post-processing · live: viewer, scene, mixed materials.
Looking for a quick "does verve.gl support X, and where's the demo" answer? See the GL feature matrix — every shipped capability, its version, and its live demo in one table.
The post-processing API (tone-mapping, vignette, SSAO, SSR, DOF, OIT) is a low-level gl core surface documented separately in verve.gl post-processing.
Declarative builder (ctx.glScene)
| Member | Signature |
|---|---|
ctx.glScene | (GlSceneOpts) *GlSceneBuilder |
GlSceneOpts | { src: vmesh URL, env: venv URL, poster: ?data-URI } |
.camera | (.{ .distance = 4, .pitch = 0.3, .yaw = 0 }) |
.light | (.{ .dir = [3]f32, .intensity = 3 }) — single directional |
.lights | ([]const Light) — up to 4 mixed lights |
.spotLight | (Light) — append one spot (forces kind = .spot) |
.pointLight | (Light) — append one point (forces kind = .point) |
.areaLight | (AreaLight) — append one LTC rect area light (max 4) |
.fog | (FogOpts) — distance fog (default: .none) |
.projection | (ProjectionOpts) — perspective (default) or orthographic camera |
.morphWeights | ([]const f32) — initial morph-target blend weights |
.autoRotate | (rad_per_s) |
.scrub | (bool) — scroll-driven yaw over a 300vh sticky section; zeroes autoRotate |
.onPick | (mesh_name, event_id) — up to 4 pickable meshes |
.onPickExport | (mesh_name, event_name) — bubbling CustomEvent on pick (P8) |
.build | () *Node — GlScene island with frozen positional props |
Props wire contract (positional; mirrored in core/gl_scene.zig and the chunk): src env orbit_distance orbit_pitch orbit_yaw auto_rotate
light_dir_x/y/z light_intensity pick_names[] pick_event_ids[] scrub
pick_export_names[].
Out-of-band attributes transport non-positional data (see Transport attributes).
Light struct
pub const Light = struct {
kind: LightKind = .directional, // .directional | .point | .spot
pos: [3]f32 = .{ 0, 0, 0 },
dir: [3]f32 = .{ 0, -1, 0 }, // ignored for point lights
color: [3]f32 = .{ 1, 1, 1 },
intensity: f32 = 1,
inner_deg: f32 = 20, // spot inner cone (degrees)
outer_deg: f32 = 30, // spot outer cone (degrees)
range: f32 = 0, // 0 = no distance cutoff
casts_shadow: bool = false, // one caster per scene
};| Field | Default | Notes |
|---|---|---|
kind | .directional | .directional / .point / .spot |
pos | {0,0,0} | Ignored for directional lights |
dir | {0,-1,0} | Ignored for point lights |
color | {1,1,1} | Linear RGB |
intensity | 1 | Multiplied with color |
inner_deg | 20 | Spot: angle where attenuation starts |
outer_deg | 30 | Spot: angle where attenuation reaches 0 |
range | 0 | Max distance; 0 = no cutoff |
casts_shadow | false | One shadow-casting light per scene |
max_lights = 4.
FogOpts struct
pub const FogOpts = struct {
mode: FogMode = .none,
color: [3]f32 = .{ 0.5, 0.6, 0.7 },
near: f32 = 1,
far: f32 = 50,
density: f32 = 0.05,
};
pub const FogMode = enum(u32) { none = 0, linear = 1, exp = 2, exp2 = 3 };| Field | Default | Notes |
|---|---|---|
mode | .none | .none / .linear / .exp / .exp2 |
color | {0.5,0.6,0.7} | Linear RGB of the fog |
near | 1 | Linear mode: start distance |
far | 50 | Linear mode: end distance |
density | 0.05 | Exp/exp2 density coefficient |
Applied after PBR, before ACES tonemap. Radial distance (eye space).
AreaLight struct
pub const AreaLight = struct {
pos: [3]f32 = undefined,
ex: [3]f32 = .{ 0.5, 0, 0 }, // half-width edge
ey: [3]f32 = .{ 0, 0.5, 0 }, // half-height edge
color: [3]f32 = .{ 1, 1, 1 },
intensity: f32 = 1,
two_sided: bool = false, // reserved
casts_shadow: bool = false,
};| Field | Default | Notes |
|---|---|---|
pos | — | Required; light center |
ex | {0.5,0,0} | Half-width edge vector |
ey | {0,0.5,0} | Half-height edge vector |
color | {1,1,1} | Linear RGB |
intensity | 1 | Multiplied with color |
two_sided | false | Reserved; LTC eval is single-sided |
casts_shadow | false | Perspective depth pass from pos |
Rect normal = normalize(cross(ex, ey)). Requires /gl/ltc.bin. max_area_lights = 4.
Transport attributes
Out-of-band canvas attributes carry data that doesn't fit the positional Props struct. Emitted by .build() automatically when the feature is active.
| Attribute | Format | Feature |
|---|---|---|
data-glfog | "mode,r,g,b,near,far,density" (7 scalars) | .fog() when mode ≠ .none |
data-gllights | 15 f32 per light (CSV): type,intensity,pos(3),dir(3),color(3),range,cosInner,cosOuter,castsShadow | .lights() / .spotLight() / .pointLight() |
data-glmorph | "w0,w1,…" (comma-sep float weights) | .morphWeights() |
data-glarealights | 15 f32 per light (CSV): pos(3),ex(3),ey(3),color(3),intensity,two_sided,castsShadow | .areaLight() |
data-glcam | "mode,ortho_height,fov_deg,near,far" (5 scalars) | .projection() when mode ≠ .perspective |
data-glclip | "nx,ny,nz,constant" records, ;-separated (up to 4) | .clipPlanes() when ≥1 plane is set |
data-glclipshadows | "1" (boolean flag) | .clipShadows(true) |
Render quality (P8 / P9)
- Per-submesh shader variants (P9):
vmesh.Reader.submeshVariant(s)picksvariant_pbr | normal_map? | emissive?per submesh;GlScenededupes one shader per variant and switchesSET_PIPELINEper group. See mixed materials. - Directional shadow map (P9): a depth pass renders the scene from the single directional light; receivers sample it with 3×3 PCF. Automatic — no builder opt. See shadow map.
- Spot shadow: perspective depth pass (fov = 2 ×
outer_deg). See gl-spot. - Point shadow: 6-face distance atlas 1536×4096 (3×8 of 512² tiles, 6 faces × up to 4 casters), PCF. See gl-point.
- Multi-shadow: multiple
casts_shadowlights. See gl-multishadow. - CSM: cascaded directional shadows. See gl-csm.
- Orthographic projection (v0.22.0):
.projection(.{ .mode = .orthographic, .ortho_height = N })— parallel rays, no foreshortening; picking rays are ortho-aware. CSM cascades fit a rectangular slab (not a frustum wedge) under ortho (v0.23.1 fix). See verve.gl orthographic projection, gl-ortho, gl-ortho-csm. - Clip planes (v0.23.0): up to 4 world-space half-spaces via
.clipPlanes(&.{ ClipPlane{ .normal, .constant } });.clipShadows(true)also clips the shadow-map depth pass. Mutually exclusive with fog / morph / point-shadow in v1. See verve.gl clip planes & CSM, gl-clip. - GPU instancing:
EXT_mesh_gpu_instancing(vmesh v12), onedraw_pbr_instancedcommand per submesh;variant_instanced. Also: per-instance frustum culling, multi-submesh materials, and instanced shadow cast+receive (draw_depth_instanced). See verve.gl GPU instancing, gl-instanced, gl-instanced-cull, gl-instanced-multi, gl-instanced-shadow. - Alpha-test cutout: glTF
MASKalphaMode;alpha_cutoffdiscard in opaque pass. See gl-cutout. - Double-sided: glTF
doubleSided: true; two-pass for BLEND. See gl-double. - Morph targets: texture-delivered POSITION+NORMAL+TANGENT deltas (vmesh v14), up to 32 active influences, cubic-Hermite weight tracks, runtime via
morph:<i>anim target. See gl-morph. - Combined skin + morph: rigged meshes can also morph; morph deltas apply before the skin matrix (glTF order). Both backends.
- Distance LOD (vmesh v15): one vmesh packs N LOD levels (
_lodNglTF mesh-name grouping, squared distance thresholds); the runtime picks the level per object by camera distance across opaque/transparent/shadow passes. Automatic — no builder opt.glscene_lod_statsHUD export. - Area lights (LTC): rect analytical integration;
/gl/ltc.binLUT. See gl-area. - Distance fog: after PBR, before tonemap. See gl-fog.
- Per-node frustum culling (P9): nodes outside frustum skipped (
src/core/gl/cull.zig). - Node transforms baked (P8): glTF node TRS/matrix composed into geometry at parse; out-of-range tex indices rejected (
error.BadTexIndex). - sRGB textures (P8): base/emissive sampled sRGB→linear via
CREATE_TEXTURE_SRGB; no in-shaderpow(2.2). - G-buffer prepass (v0.12.0): internal depth+normal prepass (
h_gbuffer); no public builder surface. Consumed by SSAO/SSR/DOF — see post-processing. - Post-processing (v0.13.0+): 6 tone-mappers + vignette, SSAO, SSR, DOF, weighted-blended OIT. Low-level
glcore; see post-processing reference.
Low-level core
Scene, Mesh, Material, command encoding, math (vec/mat), glTF reader, PNG textures. The chunk builds a binary draw-command stream in shared linear memory; bridge JS replays it on WebGL2. Rendering is Cook-Torrance PBR with image-based lighting plus direct lights; software fallback renders server-side for tests.
Assets
| Format | Producer |
|---|---|
.vmesh | tools/gl_asset_gen.zig (from glTF) |
.venv | prefiltered environment maps (tools/gen_demo_hdr.zig demo) |
Chunks fetch via gl_load into a page-scoped GPU asset region (verve_asset_reset clears between pages).
Multi-instance (P7)
Multiple GlScene (or any same-name) islands can coexist on one page — each <verve-island> gets a per-instance state slot keyed by its vid, and the bridge selects the right instance before each frame / event. The same vid routing carries pushed SSE frames to the subscribing instance (see multi-instance push).
Constraint
A GL chunk is stateful, but P7 lifts the one-per-page limit for distinct island instances. A single island that drives multiple canvases still merges them into one chunk (see GlDemo); the rule is one stateful chunk module, instanced per vid.