🚧 VeloxKit is pre-release software. APIs may change before v1.0. Get started →
Documentation
Components
Canvas (3D)

Canvas (3D) Experimental WINMACLNX

⚠️

Experimental — the 3D scene format may change before v1.0.

A hardware-accelerated 3D surface rendered via wgpu, composited on top of the 2D Vello scene. Describe your scene as a JSON object and call updateScene() — no shader code required for common geometry.

Import

import { Canvas3D } from 'veloxkit'

Basic usage

import { Canvas3D } from 'veloxkit'
import { useRef, useEffect } from 'react'
 
function RotatingCube() {
  const c3dRef = useRef(null)
 
  useEffect(() => {
    const ctx = c3dRef.current
    if (!ctx) return
 
    let angle = 0
    const id = setInterval(() => {
      angle += 0.02
      const cos = Math.cos(angle), sin = Math.sin(angle)
 
      // Row-major 4×4 rotation matrix (Y axis)
      const transform = [
         cos, 0, sin, 0,
           0, 1,   0, 0,
        -sin, 0, cos, 0,
           0, 0,   0, 1,
      ]
 
      ctx.updateScene({
        background: [0.05, 0.05, 0.08, 1.0],
        camera: {
          position: [0, 1, 3],
          target:   [0, 0, 0],
          fovDeg:   60,
        },
        lights: [
          { type: 'ambient',     color: [1,1,1,1], intensity: 0.3 },
          { type: 'directional', color: [1,1,1,1], intensity: 1.0,
            direction: [0.5, -1, -0.5] },
        ],
        meshes: [
          {
            geometry:  { type: 'box', width: 1, height: 1, depth: 1 },
            transform,
            color:     [0.48, 0.64, 0.97, 1.0],
          },
        ],
      })
    }, 16)  // ~60 fps
 
    return () => clearInterval(id)
  }, [])
 
  return <Canvas3D ref={c3dRef} style={{ width: 400, height: 300 }} />
}

Props

PropTypeDescription
refReact.Ref<Canvas3DContext>Attach to access updateScene() and loadGltf()
styleViewStyleWidth and height (required)

ctx.updateScene(scene)

Push a new scene description. The GPU renders it on the next frame. Calling updateScene() every frame is the standard pattern for animation.

Scene object

All fields are optional. Omit what you don't need.

type Scene = {
  background?: [r, g, b, a]    // RGBA 0.0–1.0, default black
 
  camera?: {
    position?: [x, y, z]       // default [0, 0, 5]
    target?:   [x, y, z]       // default [0, 0, 0]
    up?:       [x, y, z]       // default [0, 1, 0]
    fovDeg?:   number          // vertical field of view, default 60
    near?:     number          // near clip plane, default 0.1
    far?:      number          // far clip plane, default 1000
  }
 
  lights?: Light[]
 
  meshes?: Mesh[]
}
 
type Light =
  | { type: 'ambient',     color: [r,g,b,a], intensity: number }
  | { type: 'directional', color: [r,g,b,a], intensity: number, direction: [x,y,z] }
 
type Mesh = {
  geometry:   BoxGeometry | SphereGeometry | PlaneGeometry | GltfGeometry
  transform?: number[]   // 16 floats, row-major 4×4 matrix; identity if omitted
  color?:     [r, g, b, a]  // RGBA 0.0–1.0
}
 
type BoxGeometry    = { type: 'box',    width: number, height: number, depth: number }
type SphereGeometry = { type: 'sphere', radius: number, rings?: number, sectors?: number }
type PlaneGeometry  = { type: 'plane',  width: number, depth: number }
type GltfGeometry   = { type: 'gltf',  path: string }

ctx.loadGltf(path)

Pre-load a .glb / .gltf file so it's ready when referenced by geometry.type = 'gltf'.

// Preload at mount time
useEffect(() => {
  c3dRef.current?.loadGltf('/assets/models/spaceship.glb')
}, [])
 
// Then reference in updateScene:
ctx.updateScene({
  meshes: [{
    geometry: { type: 'gltf', path: '/assets/models/spaceship.glb' },
    transform: identityMatrix(),
    color:     [1, 1, 1, 1],
  }],
})

loadGltf is a fire-and-forget pre-cache call. The model is loaded asynchronously on the Rust side. If updateScene references a path that isn't loaded yet, it is silently skipped until the load completes.

Examples

Multiple objects

ctx.updateScene({
  background: [0.02, 0.02, 0.05, 1],
  camera: { position: [0, 2, 6], target: [0, 0, 0] },
  lights: [
    { type: 'ambient',     color: [1,1,1,1], intensity: 0.2 },
    { type: 'directional', color: [1,0.9,0.8,1], intensity: 1.2,
      direction: [1, -2, -1] },
  ],
  meshes: [
    // Rotating box
    {
      geometry:  { type: 'box', width: 1, height: 1, depth: 1 },
      transform: rotateY(angle),
      color:     [0.48, 0.64, 0.97, 1],
    },
    // Static ground plane
    {
      geometry:  { type: 'plane', width: 10, depth: 10 },
      transform: translateY(-0.5),
      color:     [0.15, 0.15, 0.2, 1],
    },
    // Orbiting sphere
    {
      geometry:  { type: 'sphere', radius: 0.3, rings: 16, sectors: 16 },
      transform: translate(Math.sin(angle * 2) * 2, 0.3, Math.cos(angle * 2) * 2),
      color:     [0.97, 0.48, 0.48, 1],
    },
  ],
})

Matrix utilities

VeloxKit doesn't ship a math library. Add gl-matrix or write helpers:

// Identity matrix
const identity = (): number[] => [
  1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1,
]
 
// Y-axis rotation (radians)
function rotateY(rad: number): number[] {
  const c = Math.cos(rad), s = Math.sin(rad)
  return [c,0,s,0, 0,1,0,0, -s,0,c,0, 0,0,0,1]
}
 
// Translation
function translate(x: number, y: number, z: number): number[] {
  return [1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1]
}
 
// Translate + rotate Y
function translateRotateY(tx: number, ty: number, tz: number, rad: number): number[] {
  const c = Math.cos(rad), s = Math.sin(rad)
  return [c,0,s,0, 0,1,0,0, -s,0,c,0, tx,ty,tz,1]
}

See also