threlte logo

LavaLamp

A small MarchingCubes example using Threejs’s marching cubes addon.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Pane, Folder, List, Slider } from 'svelte-tweakpane-ui'
  import type { Axis } from './MarchingPlane'

  let ballCount = $state(15)
  let isolation = $state(80)
  let planeAxis: Axis = $state('y')
  let resolution = $state(35)

  type AxisOptions = {
    [Key in Axis]: Key
  }

  const axisOptions: AxisOptions = {
    x: 'x',
    y: 'y',
    z: 'z'
  }
</script>

<div>
  <Pane
    position="fixed"
    title="Lava Lamp"
  >
    <Slider
      label="ball count"
      bind:value={ballCount}
      min={3}
      max={25}
      step={1}
    />
    <Slider
      label="isolation"
      bind:value={isolation}
      min={40}
      max={100}
      step={1}
    />
    <Slider
      label="resolution"
      bind:value={resolution}
      min={10}
      max={50}
      step={1}
    />
    <Folder title="Plane">
      <List
        label="Axis"
        bind:value={planeAxis}
        options={axisOptions}
      />
    </Folder>
  </Pane>
  <Canvas>
    <Scene
      {ballCount}
      {planeAxis}
      {resolution}
      {isolation}
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import type { Props } from '@threlte/core'
  import { MarchingCube } from './MarchingCube'
  import { T } from '@threlte/core'

  type MarchingCubeProps = Props<MarchingCube>

  let { ref = $bindable(), children, ...props }: MarchingCubeProps = $props()
</script>

<T
  is={MarchingCube}
  bind:ref
  {...props}
>
  {@render children?.({ ref })}
</T>
import type { Color } from 'three'
import { Group } from 'three'

export class MarchingCube extends Group {
  constructor(
    public strength = 0.5,
    public subtract = 12,
    public color?: Color
  ) {
    super()
  }
}
<script
  lang="ts"
  context="module"
>
  import type { MarchingPlaneAxis } from './MarchingPlane'
  import { Vector3 } from 'three'

  type addAxisMap = {
    [Key in MarchingPlaneAxis]: `addPlane${Uppercase<Key>}`
  }

  const map: addAxisMap = {
    x: 'addPlaneX',
    y: 'addPlaneY',
    z: 'addPlaneZ'
  }

  // reusable for calculating world position of `<MarchingCube>`s
  const position = new Vector3()

  const defaultResolution = 50
</script>

<script lang="ts">
  import type { Props } from '@threlte/core'
  import { MarchingCube } from './MarchingCube'
  import { MarchingCubes } from 'three/examples/jsm/Addons.js'
  import { MarchingPlane } from './MarchingPlane'
  import { MeshBasicMaterial } from 'three'
  import { T, useTask } from '@threlte/core'

  type MarchingCubesProps = {
    resolution?: number
    enableUvs?: boolean
    enableColors?: boolean
    isolation?: number
  } & Props<MarchingCubes>

  let {
    resolution = defaultResolution,
    children,
    ref = $bindable(),
    ...props
  }: MarchingCubesProps = $props()

  const material = new MeshBasicMaterial()
  const marchingCubes = new MarchingCubes(defaultResolution, material, true, true, 20_000)

  $effect(() => {
    if (resolution !== marchingCubes.resolution) {
      marchingCubes.init(resolution)
    }
  })

  useTask(() => {
    marchingCubes.reset()
    for (const child of marchingCubes.children) {
      switch (true) {
        case child instanceof MarchingCube:
          child.getWorldPosition(position)
          position.addScalar(1).multiplyScalar(0.5) // center it
          marchingCubes.addBall(
            position.x,
            position.y,
            position.z,
            child.strength,
            child.subtract,
            child.color
          )
          break
        case child instanceof MarchingPlane:
          marchingCubes[map[child.axis]](child.strength, child.subtract)
          break
      }
    }
    marchingCubes.update()
  })

  // cleanup default material if marchingCubes.material has been set to something else
  $effect(() => {
    return () => {
      if (marchingCubes.material !== material) {
        material.dispose()
      }
    }
  })
</script>

<T
  is={marchingCubes}
  bind:ref
  {...props}
>
  {@render children?.({ ref: marchingCubes })}
</T>
<script lang="ts">
  import type { Props } from '@threlte/core'
  import { T } from '@threlte/core'
  import { MarchingPlane } from './MarchingPlane'

  type MarchingPlaneProps = Props<MarchingPlane>

  let { ref = $bindable(), children, ...props }: MarchingPlaneProps = $props()
</script>

<T
  is={MarchingPlane}
  bind:ref
  {...props}
>
  {@render children?.({ ref })}
</T>
import { Group } from 'three'

export type MarchingPlaneAxis = 'x' | 'y' | 'z'

export class MarchingPlane extends Group {
  constructor(
    public axis: MarchingPlaneAxis = 'x',
    public strength = 0.5,
    public subtract = 12
  ) {
    super()
  }
}
<script lang="ts">
  import MarchingCube from './MarchingCube.svelte'
  import MarchingCubes from './MarchingCubes.svelte'
  import MarchingPlane from './MarchingPlane.svelte'
  import type { MarchingPlaneAxis } from './MarchingPlane'
  import { Color, Vector2 } from 'three'
  import { OrbitControls } from '@threlte/extras'
  import { T, useTask } from '@threlte/core'

  type SceneProps = {
    ballCount?: number
    isolation?: number
    planeAxis: MarchingPlaneAxis
    resolution: number
  }

  let { ballCount = 5, isolation = 80, planeAxis = 'y', resolution = 50 }: SceneProps = $props()

  type Ball = {
    color: Color
    position: Vector2
  }

  /**
   * creates `count` randomly colored balls that are evenly distributed around a unit circle scaled by `scale`
   */
  const createBalls = (count: number, scale = 0.5): Ball[] => {
    const balls: Ball[] = []
    const m = (2 * Math.PI) / count
    for (let i = 0; i < count; i += 1) {
      const r = m * i
      const x = Math.cos(r)
      const y = Math.sin(r)
      const position = new Vector2(x, y).multiplyScalar(scale)
      const color = new Color().setRGB(Math.random(), Math.random(), Math.random())
      balls.push({ position, color })
    }
    return balls
  }

  let time = $state(0)
  useTask((delta) => {
    time += delta
  })

  const balls = $derived(createBalls(ballCount))
</script>

<T.PerspectiveCamera
  makeDefault
  position.z={5}
>
  <OrbitControls autoRotate />
</T.PerspectiveCamera>

<T.AmbientLight />

<MarchingCubes
  enableColors
  {resolution}
  {isolation}
>
  <T.MeshStandardMaterial vertexColors />
  {#each balls as { position, color }, i}
    <MarchingCube
      position.x={position.x}
      position.z={position.y}
      position.y={0.5 * Math.sin(time + i) - 0.5}
      {color}
    />
  {/each}
  <MarchingPlane axis={planeAxis} />
</MarchingCubes>

The addon is a little too limited to be ported into a component but this example shows how it might be incorporated into threlte.

Placement

MarchingCubes defines a space from -1 to 1 for all 3 axes.

MarchingPlane

The original example only allows for planes positioned at x = -1, y = -1, and z = -1.

MarchingCube

<MarchingCube>s can be placed anywhere in the MarchingCubes space. If they are placed outside this range they may be cutoff or not show altogether.

Even though MarchingCube appears as a ball, it is called Cube to be inline with drei’s MarchingCubes abstration.

Materials

The example above utilizes vertex coloring but the original threejs example has support for any kind of material. Vertex coloring requires a little more memory since each vertex now carries a color with it. If you’re not using vertex colors, you can leave enableColors turned off. The <MarchingCubes> component uses the same default material that Three.Meshes do. Setting different materials is the same process as setting different materials for <T.Mesh>.

<MarchingCubes>
  <T.MeshNormalMaterial />
  <MarchingCube />
</MarchingCubes>

or

<script>
  import { MeshNormalMaterial } from 'three'

  const material = new MeshNormalMaterial()
</script>

<MarchingCubes {material}>
  <MarchingCube />
</MarchingCubes>

If you’re using a material with a texture, you will need to set enableUvs to true.

<MarchingCubes enableUvs>
  <T.MeshNormalMaterial map={texture} />
  <MarchingCube />
</MarchingCubes>

Note that the example above enables both uvs and vertex coloring for demonstration purposes. You can set these to false in the constructor to save on space.

MarchingCubes.svelte
<script>
  // ...

  // don't allocate buffer space for vertex colors nor uvs
  const marchingCubes = new MarchingCubes(resolution, material, false, false, 20_000)

  // ...
</script>