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 AddAxisMapThe addon is a little too limited to be ported into a component but this example shows how it might be incorporated into Threlte.
Placement
Section titled “Placement”MarchingCubes defines a space from -1 to 1 for all 3 axes.
MarchingPlane
Section titled “MarchingPlane”The original example only allows for planes positioned at x = -1, y = -1, and z = -1.
MarchingCube
Section titled “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
Section titled “Materials”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.
<script> // ...
// don't allocate space for vertex colors nor uvs const marchingCubes = new MarchingCubes(resolution, material, false, false, 20_000)
// ...</script>