verve.gl clip planes & CSM

Guide: WebGL · Advanced WebGL · live: clip planes, cascaded shadow maps.

GlSceneBuilder supports up to 4 world-space clip planes (v0.23.0, three.js clippingPlanes parity) and ONE directional-light caster split into 4 automatic cascades (CSM). The two features compose: .clipPlanes() can also clip the CSM/shadow depth pass via .clipShadows(true), and both are projection-aware — see orthographic-aware CSM for the v0.23.1 cascade-fit fix.

.clipPlanes

ctx.glScene(.{ .src = "/gl/shadow.vmesh", .env = "/gl/studio.venv" })
    .clipPlanes(&.{
        .{ .normal = .{ 1, 0.4, 0 }, .constant = 0 },
    })
    .build();
pub const ClipPlane = struct {
    normal:   [3]f32 = .{ 0, 1, 0 },
    constant: f32    = 0,
};
pub const max_clip = 4;
FieldDefaultNotes
normal{0,1,0}Plane normal; normalized at .build() time
constant0Signed distance from the origin

Plane convention: a fragment at world position P is kept when dot(normal, P) + constant ≥ 0 for every active plane — intersection of half-spaces, not union. Up to max_clip = 4 planes; extra entries passed to .clipPlanes() beyond the cap are dropped.

Demo (/gl-clip): a single diagonal plane (normal = (1, 0.4, 0), constant = 0) slices the shadow-demo cube open, revealing its solid interior. Drag to orbit and inspect the cross-section from any angle.

v1 limitations

  • Mutually exclusive with fog, morph-targets, and a point-shadow caster. Those combos have no compiled shader handle in v1 — clipActive() (client/islands/GlScene.zig) silently disables clipping whenever fog, morph, or a point-light caster is active, rather than erroring.
  • Union (clipIntersection) mode is not supported — only intersection.
  • Per-material clip-plane sets are not supported (scene-global only).

Wire facts

Clip planes travel outside Props as the data-glclip canvas attribute — a semicolon-separated list of nx,ny,nz,constant records (e.g. data-glclip="1,0.4,0,0"). Emitted only when at least one plane is set (core/gl_scene.zig). The chunk reads it via refGetAttr("data-glclip") before the first frame and injects the clip test into both the forward-lit and PBR shader paths using the already-interpolated v_world_pos varying — no extra vertex pass required. Both WebGL2 and WebGPU backends.

Attributedata-glclip
Format"nx,ny,nz,constant" records, ;-separated (up to 4)
EmittedOnly when .clipPlanes() sets at least one plane

.clipShadows

ctx.glScene(.{ .src = "/gl/cubeshadow.vmesh", .env = "/gl/studio.venv" })
    .clipPlanes(&.{.{ .normal = .{ 1, 0, 0 }, .constant = 0 }})
    .clipShadows(true)
    .build();

When clipping is active, .clipShadows(true) also clips the 2D shadow-map depth pass, so geometry on the clipped-away side of the plane casts no shadow. No-op when no clip planes are set. Emits data-glclipshadows="1" on the canvas; the chunk decodes it during hydrate (before resources are sent) into inst.clip_shadows.

Attributedata-glclipshadows
Format"1" (boolean flag)
EmittedOnly when .clipShadows(true)

The clip-depth pass is a distinct shader variant from the ordinary depth pass — depthClipVertexSrc / depthClipFragmentSrc, plus an instanced variant depthClipInstancedVertexSrc for meshes with GPU instances, and a MASK/alpha-test variant depthAtClipVertexSrc / depthAtClipFragmentSrc for cutout submeshes (src/core/gl/command.zig). These are only compiled when clipShadows is on and clipping is active (clipActive()), so scenes that never use the clip-depth path pay no extra pipeline-creation cost.

clipShadows(true) is used in the GL Studio hero, where the world-space cross-section also clips the instanced shadow-depth pass so the cut-away pillars cast no shadow.

Cascaded shadow maps (CSM)

ONE directional light with casts_shadow = true is automatically split into FOUR cascades: the view frustum is sliced near→far (practical split λ = 0.5), each slice gets its own tight depth pass packed into the shadow atlas, and the fragment shader picks the right cascade per pixel and blends across cascade boundaries. No extra builder call — CSM activates whenever a directional caster is the scene's shadow-emitting light. Both WebGL2 and WebGPU decode all four cascades from the same shadow atlas used by the single-caster path.

Demo (/gl-csm): the shadow falls across a flat floor plane that recedes into the distance — near-crisp vs far-smooth cascade quality is directly observable on the ground receiver.

ControlExportEffect
Freezeglscene_freezepins the orbit camera
Unfreezeglscene_unfreezeresumes the orbit

Orthographic-aware CSM (v0.23.1)

CSM is projection-aware. Under orthographic projection (.projection(.{ .mode = .orthographic, ... })), cascadeLightVp fits each cascade slice as a rectangular slab — constant half-extents 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. Shipped alongside orthographic projection in v0.22.0; the cascade-fit bug itself was corrected in v0.23.1. See verve.gl orthographic projection and the /gl-ortho-csm demo for the full write-up.