Skip to content

Marching Cubes

A small MarchingCubes example using Three.js’ marching cubes addon.

<script lang="ts">
import Scene from './Scene.svelte'
import { Canvas } from '@threlte/core'
import { Pane, Folder, List, Slider } from 'svelte-tweakpane-ui'
import type { MarchingPlaneAxis } from './types'
let ballCount = $state(15)
let isolation = $state(80)
let planeAxis = $state<MarchingPlaneAxis>('y')
let resolution = $state(35)
type AxisOptions = {
[Key in MarchingPlaneAxis]: 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>
import { Color, Group } from 'three'
export class MarchingCube extends Group {
constructor(
public color = new Color(),
public strength = 0.5,
public subtract = 12
) {
super()
}
}
<script
lang="ts"
module
>
import type { AddAxisMap } from './types'
import { Vector3 } from 'three'
const map: AddAxisMap = {
x: 'addPlaneX',
y: 'addPlaneY',
z: 'addPlaneZ'
} as const
// 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'
let { children, ...props }: Props<MarchingPlane> = $props()
const plane = new MarchingPlane()
</script>
<T
is={plane}
{...props}
>
{@render children?.({ ref: plane })}
</T>
import type { MarchingPlaneAxis } from './types'
import { Group } from 'three'
export class MarchingPlane extends Group {
constructor(
public axis: MarchingPlaneAxis = 'x',
public strength = 0.5,
public subtract = 12
) {
super()
}
}
<script lang="ts">
import MarchingCubes from './MarchingCubes.svelte'
import MarchingPlane from './MarchingPlane.svelte'
import type { MarchingPlaneAxis } from './types'
import { Color } from 'three'
import { Environment, OrbitControls } from '@threlte/extras'
import { MarchingCube } from './MarchingCube'
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()
const randomColor = (): Color => {
return new Color().setRGB(Math.random(), Math.random(), Math.random())
}
/**
* creates `count` randomly colored balls that are evenly distributed around a unit circle scaled by `scale`
*/
const createBalls = (count: number, scale = 0.5): MarchingCube[] => {
const balls: MarchingCube[] = []
const m = (2 * Math.PI) / count
for (let i = 0; i < count; i += 1) {
const ball = new MarchingCube(randomColor())
const r = m * i
const x = Math.cos(r)
const y = Math.sin(r)
ball.position.set(x, 0, y).multiplyScalar(scale)
balls.push(ball)
}
return balls
}
const balls = $derived(createBalls(ballCount))
let time = 0
useTask((delta) => {
time += delta
let i = 0
for (const ball of balls) {
ball.position.setY(0.5 * Math.sin(time + i) - 0.5)
i += 1
}
})
</script>
<T.PerspectiveCamera
makeDefault
position.z={5}
>
<OrbitControls autoRotate />
</T.PerspectiveCamera>
<Environment url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr" />
<MarchingCubes
enableColors
{resolution}
{isolation}
>
<T.MeshStandardMaterial vertexColors />
{#each balls as ball}
<T is={ball} />
{/each}
<MarchingPlane axis={planeAxis} />
</MarchingCubes>
import type { MarchingCubes } from 'three/examples/jsm/Addons.js'
export type AddAxisMap = {
[K in keyof MarchingCubes as K extends `addPlane${infer Axis}` ? Lowercase<Axis> : never]: K
}
export type MarchingPlaneAxis = keyof AddAxisMap

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

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

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

<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.

The example above utilizes vertex coloring but the original Three.js 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 has the same default material that a Mesh has. Setting different materials is the same process as setting different materials for <T.Mesh>.

<MarchingCubes>
<T.MeshNormalMaterial />
<T is={cube} />
</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 space for vertex colors nor uvs
const marchingCubes = new MarchingCubes(resolution, material, false, false, 20_000)
// ...
</script>