Summary
Lights are split across two layers: the node (pure data, lives in vgpu/scene, has parenting
value like any other node) and its interpretation (a SceneSystem that lives in the optional
vgpu/shading pack). Core vgpu/scene never depends on the pack; the pack depends on core.
See the parent issue for the full boundary/context.
Light nodes stay in core (data only)
import { group, mesh } from "vgpu/scene";
import { pointLight, directionalLight } from "vgpu/scene"; // nodes: no shading logic attached
const world = group();
world.add(pointLight({ position: [2, 3, 1], intensity: 2, color: "#fff" }));
world.add(directionalLight({ rotation: [-Math.PI / 4, 0, 0], intensity: 1 }));A light node with no SceneSystem registered to interpret it is inert — it carries data and
participates in the tree/transform graph like any node, but contributes nothing to rendering.
Interpretation lives in the pack, via SceneSystem
import { lighting, lambertProgram, phongProgram } from "vgpu/shading";
const lightingSystem = lighting(); // SceneSystem
world.add(mesh(box(), lambertProgram({ color: "orange" })));
renderTree(gpu, world, cam, { target: screen, systems: [lightingSystem] });
// or: auto-registration from inside a dynamic program callback
const myProgram = program((attrs, ctx) => {
ctx.use(lightingSystem); // registers once, ctx.use is idempotent per view/root
return { shader: myShader, bindings: { lights: lightingSystem.buffer } };
});SceneSystem contract:
const SCENE_SYSTEM = Symbol.for("vgpu.scene.system");
interface SceneSystem {
[SCENE_SYSTEM]: true;
beginFrame(ctx: { visible: SceneNode[]; camera: SceneCamera; frame?: Frame }): void;
dispose?(): void;
}ctx.use(system)inside a program callback auto-registers the system against the current retained state — no separate setup call required for the common case.beginFramereceives the visible-node list already produced by the walk — the system never re-traverses the tree, and gets subtree-pruning for free (an invisible light's subtree, and thus the light itself, never reachesbeginFrame).- Systems are disposed together with the retained state they're attached to (root disposal /
gpu.dispose()cascade — see therenderTree()sub-issue).
// edge case: a light under an invisible parent is fully pruned — it contributes
// zero light, not just "zero visible geometry":
const rig = group({ visible: false }).add(pointLight({ intensity: 100 }));
world.add(rig); // this light never reaches lightingSystem.beginFrame's `visible` listABI split (companion to the core-ABI sub-issue)
SceneLights (tagged, count-prefixed, capacity 8, 672 bytes) and vgpuSceneLighting move out of
group(0) entirely and become the vgpu/shading pack's own @group(1). VGPU-SCENE-LIGHTS-CAPACITY
(thrown past 8 lights) moves with them. Core's group(0) only ever contains SceneCamera/SceneModel
(see the program-protocol sub-issue for that ABI).
Built-in programs kept in core
normalMaterial (the default mesh() falls back to when no program is given, ~150 B) and
shaderMaterial (thin wrapper over a raw program) stay in core, not the pack — they have no
lighting dependency. Their names intentionally keep the pre-rename "material" spelling; see the
program-protocol sub-issue's note about this.
CI fixtures
Two fixtures gate this boundary mechanically:
scene-renderer— batteries-included: importsvgpu/shadingand its built-in lit programs (lambertProgram/phongProgram/lighting()). This is what finally satisfies the still-pending acceptance item from #204 (bundle-budget CI needs a full-featured baseline to measure tree-shaken bundles against, alongside the minimal ones).scene-view-custom-material— imports onlyvgpu/sceneand a hand-writtenprogram(), nevervgpu/shading. CI asserts its bundle has zero references to the shading pack — the mechanical gate proving the promise that programs are optional and the pack tree-shakes away completely when unused.
// fixtures/scene-renderer: batteries-included
import { lighting, lambertProgram } from "vgpu/shading";
world.add(mesh(box(), lambertProgram({ color: "red" })));
// fixtures/scene-view-custom-material: MUST NOT import vgpu/shading
import { program } from "vgpu/scene";
world.add(mesh(box(), program(customShader))); // CI fails the build if this pulls in the packExplicitly out of scope
Shadows / multipass shadow rendering are not part of this issue or of program. They're a
future additive capability of SceneSystem (e.g. a hypothetical future shadows() system that
also consumes the visible-node list), tracked in the follow-ups sub-issue, not designed here.
Acceptance
- Light nodes (
pointLight,directionalLight, ...) exist invgpu/scenewith zero shading dependency and zero effect unless aSceneSysteminterprets them. -
lighting()SceneSysteminvgpu/shading, registered viasystems: []option orctx.use(), consuming the walk's visible-node list with no extra traversal. -
SceneLights/VGPU-SCENE-LIGHTS-CAPACITYlive in the pack's own@group(1), not core'sgroup(0). - Invisible light subtree contributes zero light (verified by a GPU test, not just CPU pruning).
-
normalMaterial/shaderMaterialremain in core with no pack dependency. -
scene-renderer(batteries-included) andscene-view-custom-material(structural exclusion of the pack) CI fixtures both pass, closing the pending #204 acceptance item. - Shadows/multipass explicitly documented as out of scope, tracked only as a follow-up idea.
Activity
0No activity yet.