verve.gl
Guide: WebGL · Advanced WebGL · live: viewer, scene, mixed materials.
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) |
.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() |
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.
- GPU instancing:
EXT_mesh_gpu_instancing, onedraw_pbr_instancedcommand;variant_instanced. See gl-instanced. - 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 deltas, 8 active influences, runtime via
morph:<i>anim target. See gl-morph. - 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; no public builder surface.
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.