3D Examples
Everything at once
A single script exercising most of the 3D surface: all seven primitives, the transform stack, all three light kinds, wireframe, billboarded sprites, additive blending, and an instanced ring driven by the spectrum.
It works in silence — audio::detect::get_spectrum() contains zeros with no audio playing, so the
ring sits at its minimum height rather than disappearing.
context 3d
prop bars = 64
prop spin = 0.0
prop bob = 0.0
prop pulse = 0.0
on_frame {
spin = $TIME_SEC * 12.0
bob = math::sin(rad: $TIME_SEC * 1.5)
// audio::detect::get_beat() is true for a single frame, so decay it into something visible
pulse = pulse * 0.92
if audio::detect::get_beat() {
pulse = 1.0
}
}
render {
gfx::clear(color: color::rgb(r: 0.02, g: 0.02, b: 0.05))
camera::orbit(distance: 20.0, yaw_deg: spin, pitch_deg: 20.0 + bob * 8.0)
camera::perspective(fov_deg: 55.0, near: 0.1, far: 200.0)
light::ambient(color: color::rgb(r: 0.08, g: 0.08, b: 0.12))
light::directional(x: -1.0, y: -2.0, z: -0.6, intensity: 0.85)
light::point(
x: 0.0, y: 4.0, z: 0.0,
color: color::rgb(r: 1.0, g: 0.45, b: 0.2),
intensity: 2.5,
range: 16.0
)
// Floor
draw::plane(y: -2.5, width: 44.0, depth: 44.0, color: color::rgb(r: 0.06, g: 0.06, b: 0.1))
// Centre sphere, swelling on the beat
draw::sphere(
radius: 1.2 + pulse * 0.6,
resolution: 32,
color: color::hsl(h: 0.55, s: 0.8, l: 0.55)
)
// Wireframe shell around it
draw::sphere(
radius: 2.3,
resolution: 14,
wireframe: true,
opacity: 0.35,
color: color::rgb(r: 0.25, g: 0.9, b: 0.8)
)
// One of each solid, sharing a transform
transform::push()
transform::translate(y: -1.5)
draw::cube(x: -8.0, size: 1.6, rotation_y_deg: spin, color: color::rgb(r: 0.9, g: 0.25, b: 0.35))
draw::cylinder(x: -4.0, radius: 0.7, height: 2.2, color: color::rgb(r: 0.95, g: 0.6, b: 0.2))
draw::cone(x: 4.0, radius: 0.85, height: 2.2, color: color::rgb(r: 0.4, g: 0.85, b: 0.45))
draw::torus(x: 8.0, radius: 1.0, tube_radius: 0.3, rotation_x_deg: 70.0, color: color::rgb(r: 0.8, g: 0.4, b: 0.95))
transform::pop()
// Billboarded sprites: always face the camera, whatever it does
gfx::blend(mode: "additive")
gfx::depth(enabled: true, write: false)
for i in 0..24 {
transform::push()
transform::rotate_y(deg: i / 24.0 * 360.0 + spin * 2.0)
draw::sprite(
x: 5.0,
y: math::sin(rad: i / 3.0 + $TIME_SEC) * 2.0 + 1.0,
size: 0.5,
color: color::hsl(h: i / 24.0 + 0.5, s: 0.9, l: 0.6),
opacity: 0.8
)
transform::pop()
}
// Spectrum ring: one instanced draw call however many bars
gfx::depth(enabled: true, write: false)
for i in 0..bars {
let level = audio::detect::get_spectrum()[i * 4]
let height = 0.25 + level * 5.0
transform::push()
transform::rotate_y(deg: i / bars * 360.0)
draw::cube(
x: 13.0,
y: height / 2.0 - 2.4,
width: 0.5,
height: height,
depth: 0.5,
color: color::hsl(h: i / bars, s: 0.9, l: 0.5 + level * 0.3)
)
transform::pop()
}
}
What each part is testing
| Part | Exercises |
|---|---|
camera::orbit + camera::perspective | Spherical camera placement, and a later call overriding an earlier one |
light::ambient / directional / point | All three light kinds, with the point light’s range falloff visible on the floor |
draw::plane | The XZ ground plane |
draw::sphere(radius: 1.2 + pulse * 0.6) | Beat reactivity, decayed into something the eye can follow |
wireframe: true | The edge list, which is a separate index buffer from the triangles |
The transform::push / pop row | Stack discipline — the row’s shared offset applies to all four solids |
rotation_x_deg / rotation_y_deg | Per-primitive rotation, in degrees |
draw::sprite | Billboarding: the quads stay square-on however far the camera swings |
The for ring | Instancing — 64 bars is one draw call, and so would 4096 be |
gfx::blend(mode: "additive") | Blend state, and the batch split it causes |
Smaller pieces
A bare primitive, with no camera or lights at all — everything is unit-sized at the origin and the default camera is already looking at it:
context 3d
render {
draw::cube()
}
Degrees and rad are interchangeable, as long as you only give one:
context 3d
render {
transform::rotate_y(deg: $TIME_SEC * 30.0)
draw::torus(radius: 1.5, tube_radius: 0.4)
}
A floor and a shadowless object, showing why culling defaults to none — swing
the camera under the plane and it is still there:
context 3d
render {
gfx::clear(color: color::rgb(r: 0.05, g: 0.05, b: 0.08))
camera::orbit(distance: 8.0, yaw_deg: $TIME_SEC * 20.0, pitch_deg: 10.0)
light::directional(x: -1.0, y: -1.0, z: -0.5)
draw::plane(y: -1.5, width: 10.0, depth: 10.0, color: color::rgb(r: 0.3, g: 0.3, b: 0.4))
draw::cone(radius: 1.0, height: 2.0, color: color::rgb(r: 0.9, g: 0.5, b: 0.2))
}
Custom geometry
draw::mesh takes nested arrays. Without indices the vertices are read as a
triangle list, and without normals one is computed per face:
context 3d
render {
camera::orbit(distance: 5.0, yaw_deg: $TIME_SEC * 40.0)
light::directional(x: -1.0, y: -1.0, z: -1.0)
draw::mesh(
vertices: [
[0.0, 1.0, 0.0], [-1.0, -1.0, 1.0], [1.0, -1.0, 1.0],
[0.0, 1.0, 0.0], [1.0, -1.0, 1.0], [1.0, -1.0, -1.0],
[0.0, 1.0, 0.0], [1.0, -1.0, -1.0], [-1.0, -1.0, -1.0],
[0.0, 1.0, 0.0], [-1.0, -1.0, -1.0], [-1.0, -1.0, 1.0]
],
color: color::rgb(r: 0.4, g: 0.8, b: 0.9)
)
}