Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Visamp Docs

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

PartExercises
camera::orbit + camera::perspectiveSpherical camera placement, and a later call overriding an earlier one
light::ambient / directional / pointAll three light kinds, with the point light’s range falloff visible on the floor
draw::planeThe XZ ground plane
draw::sphere(radius: 1.2 + pulse * 0.6)Beat reactivity, decayed into something the eye can follow
wireframe: trueThe edge list, which is a separate index buffer from the triangles
The transform::push / pop rowStack discipline — the row’s shared offset applies to all four solids
rotation_x_deg / rotation_y_degPer-primitive rotation, in degrees
draw::spriteBillboarding: the quads stay square-on however far the camera swings
The for ringInstancing — 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)
  )
}