3D Rendering — Phase 2
Canvas3D and @glyx-dev/three cover the foundation. Phase 2 is about closing the gap to Three.js parity.
Status: Phase 1 stable, Phase 2 in active design. The Canvas3D scene format is experimental — the API may change before v1.0.
What's shipped (Phase 1)
| Feature | Status |
|---|---|
| Box / sphere / plane primitives | ✅ |
| GLTF model loading — multi-mesh, multi-primitive | ✅ |
| GLTF textures (base-color, UVs) | ✅ |
| 32-bit indices (large models, >65k vertices) | ✅ |
| Phong shading | ✅ |
| Ambient + directional lights | ✅ |
| Point lights (range falloff) | ✅ |
| Spot lights (cone + falloff) | ✅ |
| Up to 8 dynamic lights | ✅ |
@glyx-dev/three — Three.js-shaped JS API | ✅ |
| Same-frame composite with 2D Vello scene | ✅ |
| Per-canvas dirty tracking (skip GPU pass when static) | ✅ |
Scene-graph hierarchy (<Group> / parenting) | ✅ |
| Raycasting / picking (click/select 3D objects) | ✅ |
Per-primitive material color (GLTF baseColorFactor) | ✅ |
| GLTF animations — keyframe + skeletal (CPU skinning) | ✅ |
GLTF animation playback is keyed by model path, not mesh instance — two
<Model>s pointing at the same GLTF file share one animation pose per
frame (the last-applied {clip, time} that frame wins). This is fine for
the common case of one animated character/prop per model file; per-instance
independently-animated clones of a single model is a possible future
refinement, not currently planned work.
Phase 2 — what's planned
Materials — PBR
Priority: High
Replace (or augment) Phong with a physically-based metallic/roughness model. GLTF files already carry PBR material data (metallicRoughnessFactor, baseColorTexture, normalTexture, occlusionTexture, emissiveTexture) — the loader reads them but the shader currently ignores everything except baseColorTexture.
Phase 2 adds a PBR shader path alongside the existing Phong path. Meshes with PBR material data use it automatically; plain-color meshes stay on Phong.
Shadows
Priority: High
Shadow maps for directional and spot lights. Planned approach: a single depth-pass per shadow-casting light into a shadow atlas, sampled in the main fragment pass with PCF filtering. Point light shadows (cube maps) are lower priority.
Triangle-precision GLTF raycasting
Priority: Low
Raycasting against GLTF models currently tests the model's AABB (axis-aligned bounding box), not individual triangles — good enough for "click to select a model," but a click inside a model's bounding box that misses its actual geometry (e.g. an L-shaped mesh) still registers as a hit. Built-in primitives (box/sphere/plane) already get exact analytic intersection.
Instanced rendering
Priority: Medium
Draw many copies of one mesh in a single GPU draw call — essential for particles, crowds, foliage, and repeated UI elements like icon grids. API target:
ctx.updateScene({
meshes: [
{
geometry: { type: 'sphere', radius: 0.1 },
instances: [
{ transform: [...], color: [1, 0.3, 0.3, 1] },
{ transform: [...], color: [0.3, 1, 0.3, 1] },
// ...thousands
],
}
]
})Environment maps / IBL
Priority: Low
Image-based lighting from an HDR cubemap or equirectangular panorama. Pairs with PBR for realistic reflections. Likely ships alongside or after PBR.
Post-processing
Priority: Low
A lightweight post-processing pass after the 3D scene but before 2D composite. Initial targets: bloom, tone mapping, FXAA. Implemented as a screen-space wgpu compute pass inserted between the 3D render and the Vello composite.
@glyx-dev/three API alignment
As each Phase 2 feature lands, @glyx-dev/three gets the matching Three.js-shaped API so migration paths from Three.js are straightforward:
// Today
new PointLight({ color: [1,1,1,1], intensity: 2, range: 10 })
// Shipped: GLTF animation — a prop-driven API, not a Three.js-shaped
// SkinnedMesh/AnimationMixer class pair. JS owns the clock (same
// "JS drives state, Rust is a dumb renderer" model as the rest of Glyx) and
// sends {clip, time} every frame; Rust does CPU skinning.
<Model src="rig.glb" animationClip="walk" animationTime={elapsed} />
// Phase 2 additions
new MeshStandardMaterial({ metalness: 0.8, roughness: 0.2 })
new InstancedMesh(geometry, material, count)Phase 2 order may shift based on what the community is building. Open an issue or start a discussion on GitHub (opens in a new tab) if a specific feature is blocking your project.