GL Studio — the verve.gl capabilities showcase
GL Studio (/examples/gl-studio) is the flagship "what you can build with verve.gl" page. It is deliberately built from one declarative ctx.glScene(...) hero — the page's single GL island — followed by a pure-HTML tour that links every other GL feature to its own working demo.
Two rules shape the page:
- One GL island per page. A GL island owns a fixed static-data region, so only one may hydrate per page. The hero is that one island; the capability tour below it is plain links, not extra scenes.
- Build the hero on the declarative builder path.
ctx.glScene(...)+.build()is the proven, reliable way to put a scene on a page. GL Studio does not hand-roll a bespoke GL island.
The hero scene
The hero is a GPU-instanced floor-slab-and-pillars field (cubeshadow.vmesh under studio.venv) lit by three simultaneous shadow casters and sliced open by a live world-space cross-section. The slab is a guaranteed shadow receiver, so the multi-light shadows and the cross-section cutaway are always visible. GL Studio layers its multi-light rig on top of the instanced field. The full builder chain:
const studio_lights = [_]verve.GlLight{
// Key spot CASTER (type 2) — perspective cone shadow from above-forward.
.{ .kind = .spot, .pos = .{ 0.6, 6.5, 2.6 }, .dir = .{ -0.06, -1, -0.34 },
.color = .{ 1, 0.96, 0.9 }, .intensity = 58,
.inner_deg = 15, .outer_deg = 25, .range = 24, .casts_shadow = true },
// Fill directional CASTER (type 0) raking from the upper-left.
.{ .kind = .directional, .dir = .{ -0.55, -0.78, -0.30 },
.color = .{ 0.92, 0.96, 1.0 }, .intensity = 1.7, .casts_shadow = true },
// Cool rim directional (no shadow) — separates the model from the backdrop.
.{ .kind = .directional, .dir = .{ 0.5, -0.15, 0.6 },
.color = .{ 0.35, 0.5, 0.95 }, .intensity = 0.5, .casts_shadow = false },
};
const scene = ctx.glScene(.{ .src = "/gl/cubeshadow.vmesh", .env = "/gl/studio.venv", .poster = poster })
.camera(.{ .distance = 20, .pitch = 0.62, .yaw = 0.4 })
.lights(&studio_lights)
.areaLight(.{ .pos = .{ 0, 3.4, 0.4 }, .ex = .{ 0.7, 0, 0 }, .ey = .{ 0, 0, 0.7 },
.color = .{ 1, 0.98, 0.95 }, .intensity = 5, .casts_shadow = true })
.clipPlanes(&.{.{ .normal = .{ 1, 0, 0 }, .constant = 0 }})
.clipShadows(true)
.onPickExport("CubeShadowMesh", "verve:studio-pick")
.autoRotate(0.18)
.scrub(false)
.build();What is live in the hero
Every chained feature is verifiable at build time in two ways: zig build passes, and the server-rendered scene emits the out-of-band transport attribute that carries that feature to the client chunk. The builder keeps the hydration Props struct at a frozen 14 fields, so anything that would grow it travels as a data-gl* canvas attribute instead:
| Feature | Builder call | SSR attribute | Payload shape |
|---|---|---|---|
| Multi-light PBR | .lights(&studio_lights) | data-gllights | one CSV record per light; the casts_shadow flag is field 15 (0/1) |
| LTC area light | .areaLight(...) | data-glarealights | one record per rect area light (records ;-separated) |
| Cross-section | .clipPlanes(&.{ ... }) | data-glclip | nx,ny,nz,constant per plane (planes ;-separated) |
| Clip the shadow pass | .clipShadows(true) | data-glclipshadows="1" | present only when clipping is active |
| Auto-rotate | .autoRotate(0.18) | (in data-props) | rad/s spin; 0 = off |
| Click-to-inspect | .onPickExport("CubeShadowMesh", "verve:studio-pick") | (pick slot in data-props) | name → bubbling CustomEvent name |
The camera, image-based lighting (from studio.venv), orbit-drag, wheel-zoom, and click-pick are all built in — no extra wiring.
Interactivity
The control bar lives inside the island subtree so z-on-click resolves against the GlScene chunk's exports (it walks target.closest("verve-island")):
- Freeze / Unfreeze —
glscene_freeze/glscene_unfreezepin and release the auto-rotate. - Click a part — dispatches a bubbling
CustomEvent("verve:studio-pick", { detail: { name } })from the canvas, so a host page can react to which mesh was clicked.
Why no custom material or post-processing in the hero
Two capabilities are deliberately linked, not live, in the hero — because they do not compose with the multi-shadow + clip centerpiece:
- Custom materials. A custom material substitutes a single fixed shader handle for every submesh, which bypasses both the clip-discard shader variant and the 2D shadow-receiver bake. So a custom material would silently cancel the clip cutaway and the received shadows. The hero leads with those, so custom materials are showcased on their own page (
/examples/gl-material, with live tint buttons) instead. - Post-processing (tone-mapping, SSAO, SSR, depth-of-field, OIT) runs on the dedicated image-quality demos, not on the declarative scene path. See the Post-processing group in the tour.
Likewise, fog, a point-light shadow caster, and morph targets each silently disable clipping (there is no combined shader handle for those in v1), so the hero uses none of them — keeping the cross-section genuinely active. The one caster type it avoids is the point light; the spot, directional, and area casters all coexist with clipping.
The capability tour
Below the hero, a grouped set of links covers the whole surface. Every link points at a real, working demo:
- Geometry & primitives — points & sprites, fat lines, decals, wireframe, distance LOD, morph targets, 16-target morph.
- Lighting & shadows — shadow map, spot, point, area (LTC), multi-light shadows, cascaded shadow maps, distance fog.
- Camera & clipping — orthographic, orthographic CSM, clip planes, clip shadows.
- Materials & textures — custom shader materials, compressed textures, alpha-test cutout, mixed materials, double-sided.
- Instancing — GPU instancing (culling, multi-submesh, instanced shadows).
- Post-processing — tone-mapping, SSAO, SSR, depth of field, OIT.
- Scenes & product — declarative scene, product viewer, skinned + morph, two scenes on one page.
- Beyond GL — multi-instance graphs, the animation-engine landing page.
Open the WebGL guide for the builder API, or the GL feature matrix for the full support grid.