verve.gl

Guide: WebGL · Advanced WebGL · live: viewer, scene, mixed materials.

Declarative builder (ctx.glScene)

MemberSignature
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
};
FieldDefaultNotes
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
intensity1Multiplied with color
inner_deg20Spot: angle where attenuation starts
outer_deg30Spot: angle where attenuation reaches 0
range0Max distance; 0 = no cutoff
casts_shadowfalseOne 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 };
FieldDefaultNotes
mode.none.none / .linear / .exp / .exp2
color{0.5,0.6,0.7}Linear RGB of the fog
near1Linear mode: start distance
far50Linear mode: end distance
density0.05Exp/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,
};
FieldDefaultNotes
posRequired; 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
intensity1Multiplied with color
two_sidedfalseReserved; LTC eval is single-sided
casts_shadowfalsePerspective 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.

AttributeFormatFeature
data-glfog"mode,r,g,b,near,far,density" (7 scalars).fog() when mode ≠ .none
data-gllights15 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-glarealights15 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) picks variant_pbr | normal_map? | emissive? per submesh; GlScene dedupes one shader per variant and switches SET_PIPELINE per 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_shadow lights. See gl-multishadow.
  • CSM: cascaded directional shadows. See gl-csm.
  • GPU instancing: EXT_mesh_gpu_instancing, one draw_pbr_instanced command; variant_instanced. See gl-instanced.
  • Alpha-test cutout: glTF MASK alphaMode; alpha_cutoff discard 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.bin LUT. 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-shader pow(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

FormatProducer
.vmeshtools/gl_asset_gen.zig (from glTF)
.venvprefiltered 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.