Voxel-destructible

This sketch creates a fully destructible 3D voxel world built with three.js, where each tiny cube can be clicked or tapped to explode into falling physics-based pieces. The scene supports free camera rotation and zooming, and plays satisfying destruction sounds via tone.js.

🧪 Try This!

Experiment with the code by making these changes:

  1. Double the voxel grid size — Change gridSize from 10 to 20, creating an 8000-voxel cube instead of 1000. The structure will be 2× larger, denser, and heavier to simulate.
  2. Paint all voxels one color — Replace random color generation with a single color like pure red or cyan, making the destruction feel more unified.
  3. Reverse gravity (voxels float up) — Change the gravity Y value from -9.82 to positive, making destroyed voxels float upward instead of falling down.
  4. Make destruction sound longer — Change the synth decay envelope from 0.2 to 0.8 seconds, making each destruction sound linger and echo more.
  5. Darken the scene background — Change background color from 0x333333 (gray) to 0x000000 (black), creating a deeper, more dramatic space.
  6. Disable camera damping for snappy rotation — Change dampingFactor from 0.05 to 0, making the camera respond immediately to mouse movement without smooth deceleration.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a floating cubic grid made of individually destructible voxels that you can blast apart one by one. The visual appeal comes from the vibrant random colors of each block, the smooth physics simulation that makes destroyed voxels tumble away under gravity, and the ability to freely orbit the scene to watch destruction from every angle. It combines three powerful libraries: three.js for 3D rendering, cannon.js for physics simulation, and tone.js for audio feedback.

The code is organized into five major setup functions (for audio, three.js, cannon.js, voxel generation, and event listeners) plus core game loops and interaction handlers. By studying it you will learn how to integrate three 3D libraries together, how raycasting detects which object you clicked, how to synchronize mesh graphics with physics bodies each frame, and how to manage dynamic object destruction while keeping performance smooth.

⚙️ How It Works

  1. When the sketch loads, setup() initializes three.js with a scene, camera, and renderer; creates a physics world in cannon.js with gravity; generates 1000 voxels in a 10×10×10 grid, each with both a colored three.js mesh and a physics body; and sets up tone.js audio to play on user interaction.
  2. The draw loop runs 60 times per second: it updates the OrbitControls camera so you can rotate and zoom, steps the physics simulation forward, then syncs each voxel's mesh position and rotation to match its physics body, and finally renders the scene to the screen.
  3. When you click or tap the canvas, a raycaster shoots a ray from the camera through the mouse position to detect which voxel mesh you hit (if any).
  4. The hit voxel's physics body instantly changes from static (mass 0) to dynamic (mass 1), allowing it to fall under gravity and collide with other objects.
  5. The voxel is then removed from the three.js scene (disposing its geometry and material), removed from the cannon.js world, and a short pink noise burst plays from the destruction synth as audio feedback.
  6. A live counter updates to show remaining voxels; when all 1000 are destroyed, a congratulations alert appears.

🎓 Concepts You'll Learn

3D rendering with three.jsPhysics simulation with cannon.jsRaycasting and click detectionDynamic object destructionAudio synthesis with tone.jsCamera controls and OrbitControlsMesh-body synchronizationEvent handling for mouse and touch

📝 Code Breakdown

setup()

setup() is called once when the sketch starts. It initializes all the major systems (audio, 3D rendering, physics, voxel grid, and event listeners) so the game is ready to run. Notice how it delegates most work to helper functions—this keeps the code organized.

function setup() {
  p5Canvas = createCanvas(windowWidth, windowHeight);
  p5Canvas.style('display', 'none');
  voxelCountSpan = select('#voxel-count');
  voxelCountSpan.html(totalVoxels);
  initializeAudio();
  initializeThreeJS();
  initializeCannonJS();
  generateVoxelWorld();
  renderer.domElement.addEventListener('mousedown', onMouseDown, false);
  renderer.domElement.addEventListener('mouseup', onMouseUp, false);
  renderer.domElement.addEventListener('mousemove', onMouseMove, false);
  renderer.domElement.addEventListener('touchstart', onTouchStart, false);
  renderer.domElement.addEventListener('touchend', onTouchEnd, false);
  renderer.domElement.addEventListener('touchmove', onTouchMove, false);
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

initialization Canvas and UI Setup p5Canvas = createCanvas(windowWidth, windowHeight); p5Canvas.style('display', 'none');

Creates a minimal p5.js canvas but hides it since three.js handles all rendering

initialization Voxel Counter Display voxelCountSpan = select('#voxel-count'); voxelCountSpan.html(totalVoxels);

Grabs the HTML element that displays remaining voxel count and initializes it to the total

initialization Library Setup Functions initializeAudio(); initializeThreeJS(); initializeCannonJS(); generateVoxelWorld();

Calls the four major initialization functions that set up audio, rendering, physics, and the voxel grid

initialization Event Listener Attachment renderer.domElement.addEventListener('mousedown', onMouseDown, false); renderer.domElement.addEventListener('mouseup', onMouseUp, false); renderer.domElement.addEventListener('mousemove', onMouseMove, false); renderer.domElement.addEventListener('touchstart', onTouchStart, false); renderer.domElement.addEventListener('touchend', onTouchEnd, false); renderer.domElement.addEventListener('touchmove', onTouchMove, false);

Registers all mouse and touch event handlers so clicks/taps detect voxel destruction

p5Canvas = createCanvas(windowWidth, windowHeight);
Creates a minimal p5.js canvas that fills the window. We store a reference in p5Canvas but won't draw on it.
p5Canvas.style('display', 'none');
Hides the p5.js canvas completely so only the three.js canvas (which three.js appends to the body) is visible.
voxelCountSpan = select('#voxel-count');
Uses p5.js's select() function to grab the HTML element with id 'voxel-count' so we can update the display later.
voxelCountSpan.html(totalVoxels);
Sets the initial text content of the counter to show all 1000 voxels at the start.
initializeAudio();
Sets up tone.js and waits for the first user interaction to start the audio context (required by browsers).
initializeThreeJS();
Creates the three.js scene, camera, renderer, lights, and OrbitControls for 3D viewing and interaction.
initializeCannonJS();
Sets up the cannon.js physics world with gravity and collision detection optimization.
generateVoxelWorld();
Loops through all 1000 grid positions and creates a colored mesh and physics body for each voxel.
renderer.domElement.addEventListener('mousedown', onMouseDown, false);
Tells the browser to call onMouseDown() when the user presses the mouse button, to detect the start of a click or drag.
renderer.domElement.addEventListener('mouseup', onMouseUp, false);
Tells the browser to call onMouseUp() when the user releases the mouse button, to detect the end of interaction and perform destruction.
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
Tells the browser to call onMouseMove() when the user moves the mouse while a button is held, to distinguish drags from clicks.
renderer.domElement.addEventListener('touchstart', onTouchStart, false);
Registers the mobile touch start event, marking the beginning of a potential tap or drag.
renderer.domElement.addEventListener('touchend', onTouchEnd, false);
Registers the mobile touch end event, performing destruction if it was a tap and not a drag.
renderer.domElement.addEventListener('touchmove', onTouchMove, false);
Registers the mobile touch move event to detect if the user is dragging the camera rather than tapping.

draw()

draw() is the animation loop that runs 60 times per second. Each frame, it updates the camera, steps physics, synchronizes graphics with physics, and renders the scene. The magic is that the physics world and the visual world are separate—we must manually sync them every frame by copying position and rotation data. This pattern is core to all 3D games.

function draw() {
  controls.update();
  if (gameStarted) {
    world.step(timeStep);
    for (let voxel of voxels) {
      if (voxel.mesh && voxel.body) {
        voxel.mesh.position.copy(voxel.body.position);
        voxel.mesh.quaternion.copy(voxel.body.quaternion);
      }
    }
  }
  renderer.render(scene, camera);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

initialization Camera Control Update controls.update();

Updates OrbitControls to apply mouse/touch input from the previous frame, allowing camera rotation and zoom

conditional Audio Context Check if (gameStarted) {

Only runs physics if the audio context has been started (required by browsers for sound to work)

calculation Physics Simulation Step world.step(timeStep);

Advances the cannon.js physics world by 1/60th of a second, calculating gravity, collisions, and motion

for-loop Mesh-Body Synchronization for (let voxel of voxels) { if (voxel.mesh && voxel.body) { voxel.mesh.position.copy(voxel.body.position); voxel.mesh.quaternion.copy(voxel.body.quaternion); } }

Copies the position and rotation from each physics body to its corresponding three.js mesh so they stay visually aligned

calculation Scene Rendering renderer.render(scene, camera);

Draws the entire three.js scene to the canvas using the current camera view

controls.update();
Tells OrbitControls to process any mouse or touch events and update the camera position/rotation accordingly. Runs even before gameStarted so you can rotate the scene immediately.
if (gameStarted) {
Checks whether the audio context has been activated by the first user interaction. We only step physics after audio starts because most browsers require user input to enable Web Audio.
world.step(timeStep);
Tells the cannon.js physics engine to simulate 1/60th of a second of physics (gravity, collisions, motion). This happens once per frame to keep animation smooth.
for (let voxel of voxels) {
Loops through the entire voxels array (which contains up to 1000 objects with mesh and body properties).
if (voxel.mesh && voxel.body) {
Checks that the voxel hasn't been destroyed yet. When a voxel is destroyed, its mesh and body are set to null, so this condition skips null entries.
voxel.mesh.position.copy(voxel.body.position);
Copies the physics body's current position (updated by world.step()) to the mesh's position, so the visual cube appears where the physics object is.
voxel.mesh.quaternion.copy(voxel.body.quaternion);
Copies the physics body's current rotation (as a quaternion) to the mesh's rotation. This makes falling voxels visually tumble as physics expects.
renderer.render(scene, camera);
Draws the entire scene to the canvas from the perspective of the current camera, showing all lit meshes, the background, and any remaining voxels and the ground.

initializeAudio()

initializeAudio() creates the tone.js synth that plays when you destroy a voxel, and listens for the first user interaction (click or touch) to start the Web Audio context. Modern browsers require user input to activate audio for security reasons, so we can't play sound until the user touches the page.

🔬 This envelope shape controls how the destruction sound fades in and out. What happens if you change decay from 0.2 to 1.0 (making the sound 5 times longer)? What if you change attack to 0.1 (making it rise more slowly)?

    envelope: {
      attack: 0.001,
      decay: 0.2,
      sustain: 0,
      release: 0.2
    }
function initializeAudio() {
  destructionSynth = new Tone.NoiseSynth({
    noise: { type: 'pink' },
    envelope: {
      attack: 0.001,
      decay: 0.2,
      sustain: 0,
      release: 0.2
    }
  }).toDestination();
  document.documentElement.addEventListener('mousedown', () => {
    if (Tone.context.state !== 'running') {
      Tone.start();
      console.log('Audio context started.');
      gameStarted = true;
    }
  }, { once: true });
  document.documentElement.addEventListener('touchstart', () => {
    if (Tone.context.state !== 'running') {
      Tone.start();
      console.log('Audio context started.');
      gameStarted = true;
    }
  }, { once: true });
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

initialization Noise Synth Creation destructionSynth = new Tone.NoiseSynth({ noise: { type: 'pink' }, envelope: { attack: 0.001, decay: 0.2, sustain: 0, release: 0.2 } }).toDestination();

Creates a tone.js NoiseSynth that plays pink noise with a quick attack and decay, making the destruction sound effect punchy

conditional Mouse Audio Context Activation document.documentElement.addEventListener('mousedown', () => { if (Tone.context.state !== 'running') { Tone.start(); console.log('Audio context started.'); gameStarted = true; } }, { once: true });

On first mouse click anywhere on the page, starts the Web Audio context (required by browser policy) and sets gameStarted to true

conditional Touch Audio Context Activation document.documentElement.addEventListener('touchstart', () => { if (Tone.context.state !== 'running') { Tone.start(); console.log('Audio context started.'); gameStarted = true; } }, { once: true });

On first touch anywhere on the page, starts the Web Audio context for mobile devices and enables physics

destructionSynth = new Tone.NoiseSynth({
Starts creating a tone.js NoiseSynth object, which generates a noise sound (as opposed to a pitched tone). We store it in a global variable so destroyVoxel() can trigger it later.
noise: { type: 'pink' },
Specifies that the noise should be 'pink' noise (a softer, lower-frequency noise good for impact sounds), not white noise (which sounds like static).
attack: 0.001,
The sound reaches full volume in 0.001 seconds (1 millisecond), making it sound immediate and snappy when a voxel is destroyed.
decay: 0.2,
After the attack, the volume fades over 0.2 seconds, creating a brief whoosh sound that's punchy but not too long.
sustain: 0,
The sound does not sustain at any volume level after decay; it just keeps fading to silence. Set to 0 so each destruction sound is a short burst.
release: 0.2
If the sound is stopped before it finishes decaying, it takes 0.2 seconds to fade to silence. Not used in this sketch but part of standard envelope design.
.toDestination()
Connects the synth to the audio output (speakers), so you can actually hear the sound when it plays.
document.documentElement.addEventListener('mousedown', () => {
Listens for the first mouse click anywhere on the page. This event will trigger the audio context startup.
if (Tone.context.state !== 'running') {
Checks whether tone.js's audio context is already running. If it's not, we start it. This prevents multiple startup attempts.
Tone.start();
Starts the tone.js audio context, which activates Web Audio in the browser. Without this call, the synth cannot make sound (browser security policy).
gameStarted = true;
Sets the global gameStarted flag to true, which tells the draw() function to begin stepping physics and updating voxel positions.
}, { once: true });
The { once: true } option means this listener only runs once—on the first click. After that it automatically removes itself, so gameStarted is set exactly once.

initializeThreeJS()

initializeThreeJS() builds the core of the 3D visualization: a scene to hold objects, a camera to view them, a renderer to draw them, lights to illuminate them, controls to let you interact with them, and a raycaster to detect clicks. These are the essential pieces of any three.js application.

🔬 These two lines set where the camera is and what it looks at. What happens if you change the position to set(0, 0, 50) so the camera is directly in front instead of diagonally above?

  camera.position.set(gridSize * voxelSize * 1.5, gridSize * voxelSize * 1.5, gridSize * voxelSize * 1.5);
  camera.lookAt(0, 0, 0);
function initializeThreeJS() {
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x333333);
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(gridSize * voxelSize * 1.5, gridSize * voxelSize * 1.5, gridSize * voxelSize * 1.5);
  camera.lookAt(0, 0, 0);
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(windowWidth, windowHeight);
  document.body.appendChild(renderer.domElement);
  raycaster = new THREE.Raycaster();
  controls = new THREE.OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.05;
  controls.screenSpacePanning = false;
  controls.minDistance = voxelSize * 2;
  controls.maxDistance = gridSize * voxelSize * 5;
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
  scene.add(ambientLight);
  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
  directionalLight.position.set(10, 15, 10);
  scene.add(directionalLight);
}
Line-by-line explanation (20 lines)

🔧 Subcomponents:

initialization Scene and Background scene = new THREE.Scene(); scene.background = new THREE.Color(0x333333);

Creates an empty 3D scene and sets its background color to dark gray

initialization Camera Creation and Positioning camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(gridSize * voxelSize * 1.5, gridSize * voxelSize * 1.5, gridSize * voxelSize * 1.5); camera.lookAt(0, 0, 0);

Creates a camera with 75-degree field-of-view positioned diagonally above the voxel grid, looking at its center

initialization Renderer Creation and Attachment renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(windowWidth, windowHeight); document.body.appendChild(renderer.domElement);

Creates a WebGL renderer, sizes it to fill the window, and attaches its canvas element to the HTML body so users see it

initialization Raycaster for Click Detection raycaster = new THREE.Raycaster();

Creates a raycaster that will shoot rays from the camera through the mouse position to detect which voxel was clicked

initialization OrbitControls Configuration controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.screenSpacePanning = false; controls.minDistance = voxelSize * 2; controls.maxDistance = gridSize * voxelSize * 5;

Creates camera controls that let you rotate around the voxel grid with mouse/touch, with smooth damping and distance limits

initialization Ambient and Directional Lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 15, 10); scene.add(directionalLight);

Adds two lights: soft ambient light that illuminates everything, and a directional light (like the sun) that casts shadows and highlights

scene = new THREE.Scene();
Creates an empty three.js scene—a container that will hold all objects, lights, and cameras.
scene.background = new THREE.Color(0x333333);
Sets the scene's background to a dark gray color (hex code 0x333333 is RGB(51, 51, 51)). Everything drawn will show against this background.
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
Creates a camera with a 75-degree vertical field-of-view. The aspect ratio is calculated from the window size. 0.1 and 1000 are the near and far clipping planes (things closer than 0.1 or farther than 1000 units won't render).
camera.position.set(gridSize * voxelSize * 1.5, gridSize * voxelSize * 1.5, gridSize * voxelSize * 1.5);
Positions the camera at (30, 30, 30) assuming gridSize=10 and voxelSize=2. This puts it diagonally above the voxel grid so you can see the whole structure.
camera.lookAt(0, 0, 0);
Points the camera at the origin (0, 0, 0), which is the center of the voxel grid, so the grid appears centered in the view.
renderer = new THREE.WebGLRenderer({ antialias: true });
Creates a WebGL renderer that will draw the three.js scene. The { antialias: true } option smooths jagged edges on shapes.
renderer.setSize(windowWidth, windowHeight);
Tells the renderer to fill the entire window width and height. The renderer creates an HTML canvas at this size.
document.body.appendChild(renderer.domElement);
Adds the renderer's canvas element to the HTML body so it becomes visible on the page.
raycaster = new THREE.Raycaster();
Creates a raycaster, a tool that shoots invisible rays from the camera through the scene to detect which object is under the mouse pointer.
controls = new THREE.OrbitControls(camera, renderer.domElement);
Creates OrbitControls, which ties mouse and touch input to camera movement, letting you rotate around the scene's center by dragging.
controls.enableDamping = true;
Enables 'damping' (friction), which makes camera rotation feel smooth and weighty instead of instant and snappy.
controls.dampingFactor = 0.05;
Sets how much damping to apply. 0.05 means the camera movement decays at 5% per frame, creating a natural, momentum-like feel.
controls.screenSpacePanning = false;
Disables 'screen space' panning, so middle-mouse-drag moves the camera in 3D space relative to the scene, not relative to your screen.
controls.minDistance = voxelSize * 2;
Sets the closest you can zoom in: 4 units (if voxelSize=2). This prevents you from zooming so close the camera goes inside a voxel.
controls.maxDistance = gridSize * voxelSize * 5;
Sets the farthest you can zoom out: 100 units (if gridSize=10 and voxelSize=2). This keeps the voxel grid visible even when zoomed far away.
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
Creates a soft white ambient light with intensity 0.6. Ambient light illuminates all objects equally, preventing the scene from having stark black shadows.
scene.add(ambientLight);
Adds the ambient light to the scene so it takes effect.
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
Creates a white directional light (like sunlight) with intensity 0.8. Directional lights cast parallel rays, creating shadows and highlights.
directionalLight.position.set(10, 15, 10);
Positions the directional light above and to the side of the grid, so it illuminates the voxels from a natural angle.
scene.add(directionalLight);
Adds the directional light to the scene so its shadows and highlights appear on the voxels.

initializeCannonJS()

initializeCannonJS() sets up the physics engine that simulates gravity, collisions, and motion. The world object holds all physics bodies and controls the simulation. Gravity makes destroyed voxels fall, broad-phase detection keeps it fast, and sleep mode prevents wasted computation.

function initializeCannonJS() {
  world = new CANNON.World();
  world.gravity.set(0, -9.82, 0);
  world.broadphase = new CANNON.SAPBroadphase(world);
  world.allowSleep = true;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

initialization Physics World Creation world = new CANNON.World();

Creates an empty cannon.js physics world that will simulate gravity, collisions, and motion

initialization Gravity Configuration world.gravity.set(0, -9.82, 0);

Sets gravity pointing downward at 9.82 m/s² (Earth-like), so destroyed voxels fall

initialization Collision Detection Optimization world.broadphase = new CANNON.SAPBroadphase(world);

Uses Sweep-And-Prune broad-phase collision detection to efficiently find which objects might collide without checking all pairs

initialization Sleep Mode Enablement world.allowSleep = true;

Allows stationary bodies to 'sleep' (stop being simulated), saving CPU time when voxels are resting on the ground

world = new CANNON.World();
Creates a new cannon.js physics world—an empty universe where physics bodies will live and interact.
world.gravity.set(0, -9.82, 0);
Sets the gravity vector to (0, -9.82, 0). The negative Y means down. 9.82 is Earth's gravity in m/s². Objects in this world will accelerate downward at this rate.
world.broadphase = new CANNON.SAPBroadphase(world);
Assigns a Sweep-And-Prune broad-phase collision detector. This is an optimization: instead of checking every pair of bodies for collision (which is O(n²)), SAP maintains a sorted list and only checks pairs that overlap in space. Much faster for many bodies.
world.allowSleep = true;
Enables 'sleeping' for physics bodies. When a body stops moving and nothing affects it, cannon.js puts it to sleep—stops simulating it. This saves CPU time because stationary voxels on the ground don't need to be recalculated every frame.

generateVoxelWorld()

generateVoxelWorld() creates all 1000 voxels by triple-looping through the grid, creating both a visible mesh and a physics body for each one. The key optimization is using a single shared geometry for all meshes, which saves huge amounts of memory. It also creates the ground plane so destroyed voxels don't fall forever.

🔬 These three lines center the grid. What happens if you remove the '- halfGrid' part from posX, so it becomes just '(x + 0.5) * voxelSize'? The grid will shift to one side.

        const posX = (x - halfGrid + 0.5) * voxelSize;
        const posY = (y - halfGrid + 0.5) * voxelSize;
        const posZ = (z - halfGrid + 0.5) * voxelSize;
function generateVoxelWorld() {
  const halfGrid = gridSize / 2;
  const halfVoxel = voxelSize / 2;
  const boxGeometry = new THREE.BoxGeometry(voxelSize, voxelSize, voxelSize);
  for (let x = 0; x < gridSize; x++) {
    for (let y = 0; y < gridSize; y++) {
      for (let z = 0; z < gridSize; z++) {
        const posX = (x - halfGrid + 0.5) * voxelSize;
        const posY = (y - halfGrid + 0.5) * voxelSize;
        const posZ = (z - halfGrid + 0.5) * voxelSize;
        const color = new THREE.Color(Math.random(), Math.random(), Math.random());
        const material = new THREE.MeshStandardMaterial({ color: color, roughness: 0.7, metalness: 0.3 });
        const mesh = new THREE.Mesh(boxGeometry, material);
        mesh.position.set(posX, posY, posZ);
        scene.add(mesh);
        const boxShape = new CANNON.Box(new CANNON.Vec3(halfVoxel, halfVoxel, halfVoxel));
        const body = new CANNON.Body({ mass: 0, shape: boxShape });
        body.position.set(posX, posY, posZ);
        world.addBody(body);
        voxels.push({ mesh: mesh, body: body, id: `${x}-${y}-${z}` });
      }
    }
  }
  const groundShape = new CANNON.Plane();
  const groundBody = new CANNON.Body({ mass: 0, shape: groundShape });
  groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
  groundBody.position.set(0, -halfGrid * voxelSize - halfVoxel, 0);
  world.addBody(groundBody);
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

initialization Shared Geometry Optimization const boxGeometry = new THREE.BoxGeometry(voxelSize, voxelSize, voxelSize);

Creates one box geometry object that all 1000 voxels will share, saving memory instead of making 1000 separate geometries

for-loop Triple Nested Loop for Voxel Grid for (let x = 0; x < gridSize; x++) { for (let y = 0; y < gridSize; y++) { for (let z = 0; z < gridSize; z++) {

Iterates through all 1000 grid positions (10×10×10) in x, y, z order

calculation Center Grid Calculation const posX = (x - halfGrid + 0.5) * voxelSize; const posY = (y - halfGrid + 0.5) * voxelSize; const posZ = (z - halfGrid + 0.5) * voxelSize;

Converts grid coordinates (0-9) into world positions centered at (0,0,0) so the grid doesn't appear off to one side

calculation Three.js Mesh Creation and Addition const color = new THREE.Color(Math.random(), Math.random(), Math.random()); const material = new THREE.MeshStandardMaterial({ color: color, roughness: 0.7, metalness: 0.3 }); const mesh = new THREE.Mesh(boxGeometry, material); mesh.position.set(posX, posY, posZ); scene.add(mesh);

Creates a random-colored mesh from the shared geometry, positions it, and adds it to the three.js scene for rendering

calculation Cannon.js Body Creation and Addition const boxShape = new CANNON.Box(new CANNON.Vec3(halfVoxel, halfVoxel, halfVoxel)); const body = new CANNON.Body({ mass: 0, shape: boxShape }); body.position.set(posX, posY, posZ); world.addBody(body);

Creates a physics body with the same position as the mesh but starts with mass 0 (static). When clicked, mass will be set to 1 to make it dynamic and fall.

calculation Voxel Object Storage voxels.push({ mesh: mesh, body: body, id: `${x}-${y}-${z}` });

Stores the mesh and body together in the global voxels array so destroyVoxel() can find and remove them later

initialization Ground Plane Creation const groundShape = new CANNON.Plane(); const groundBody = new CANNON.Body({ mass: 0, shape: groundShape }); groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); groundBody.position.set(0, -halfGrid * voxelSize - halfVoxel, 0); world.addBody(groundBody);

Creates a static invisible ground plane below the voxel grid so destroyed voxels don't fall through the floor forever

const halfGrid = gridSize / 2;
Calculates half the grid size (5 if gridSize is 10). Used to center the voxel grid at the origin.
const halfVoxel = voxelSize / 2;
Calculates half a voxel's size (1 if voxelSize is 2). Needed for both mesh geometry and physics shape half-extents.
const boxGeometry = new THREE.BoxGeometry(voxelSize, voxelSize, voxelSize);
Creates one box-shaped geometry of the voxel's size. Creating it once and reusing it 1000 times saves enormous amounts of memory.
for (let x = 0; x < gridSize; x++) {
Outer loop iterates x from 0 to 9 (if gridSize is 10).
for (let y = 0; y < gridSize; y++) {
Middle loop iterates y from 0 to 9, so for each x we loop through all 10 y values.
for (let z = 0; z < gridSize; z++) {
Inner loop iterates z from 0 to 9, so for each (x,y) pair we loop through all 10 z values. The inner loop runs 1000 times total (10×10×10).
const posX = (x - halfGrid + 0.5) * voxelSize;
Converts the grid coordinate x (0-9) into world space. (x - 5 + 0.5) * 2 maps x=0→-9, x=9→9, centering the grid at x=0.
const posY = (y - halfGrid + 0.5) * voxelSize;
Same conversion for y, so the grid is centered vertically at y=0.
const posZ = (z - halfGrid + 0.5) * voxelSize;
Same conversion for z, so the grid is centered in depth at z=0.
const color = new THREE.Color(Math.random(), Math.random(), Math.random());
Creates a random RGB color. Math.random() returns a value 0-1 for each channel, giving a random hue.
const material = new THREE.MeshStandardMaterial({ color: color, roughness: 0.7, metalness: 0.3 });
Creates a material with the random color. MeshStandardMaterial reacts to light, and the roughness (0.7) and metalness (0.3) settings make voxels look matte and slightly reflective.
const mesh = new THREE.Mesh(boxGeometry, material);
Combines the shared box geometry with the material to create a visible mesh object.
mesh.position.set(posX, posY, posZ);
Positions the mesh at the calculated world-space coordinates.
scene.add(mesh);
Adds the mesh to the three.js scene so it will be rendered each frame.
const boxShape = new CANNON.Box(new CANNON.Vec3(halfVoxel, halfVoxel, halfVoxel));
Creates a cannon.js box shape matching the voxel's size. CANNON.Vec3 specifies half-extents (radius), not full dimensions.
const body = new CANNON.Body({ mass: 0, shape: boxShape });
Creates a physics body with mass 0, making it static (immovable). When the voxel is destroyed (clicked), its mass becomes 1 to let it fall.
body.position.set(posX, posY, posZ);
Positions the physics body at the same location as the mesh so they start aligned.
world.addBody(body);
Adds the physics body to the cannon.js world so gravity, collisions, and motion affect it.
voxels.push({ mesh: mesh, body: body, id: `${x}-${y}-${z}` });
Stores the mesh and body together in the global voxels array with a unique ID based on grid coordinates. Later, destroyVoxel() uses this to find and remove voxels.
const groundShape = new CANNON.Plane();
Creates an infinite plane shape for the ground.
const groundBody = new CANNON.Body({ mass: 0, shape: groundShape });
Creates a static body (mass 0) for the ground so it doesn't move.
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
Rotates the ground plane 90 degrees around the X-axis so it lies flat (normally a cannon plane faces Z). -Math.PI/2 is -90 degrees.
groundBody.position.set(0, -halfGrid * voxelSize - halfVoxel, 0);
Positions the ground below the voxel grid at y = -(5*2 + 1) = -11 (if halfGrid=5 and halfVoxel=1). Destroyed voxels fall and land on this.
world.addBody(groundBody);
Adds the ground body to the physics world so it collides with falling voxels.

destroyVoxel()

destroyVoxel() handles the complete lifecycle of removing a voxel: it removes the mesh from the scene and frees its memory to prevent leaks, removes the physics body from the world, plays audio feedback, updates the counter, and checks for the win condition. This function is called whenever the player clicks/taps a voxel.

function destroyVoxel(voxel) {
  scene.remove(voxel.mesh);
  voxel.mesh.geometry.dispose();
  voxel.mesh.material.dispose();
  voxel.mesh = null;
  world.removeBody(voxel.body);
  voxel.body = null;
  if (gameStarted) {
    destructionSynth.triggerAttackRelease('C4', '8n');
  }
  const remainingVoxels = voxels.filter(v => v.mesh !== null).length;
  voxelCountSpan.html(remainingVoxels);
  if (remainingVoxels === 0) {
    alert('Congratulations! You destroyed all voxels!');
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Three.js Scene Cleanup scene.remove(voxel.mesh); voxel.mesh.geometry.dispose(); voxel.mesh.material.dispose(); voxel.mesh = null;

Removes the mesh from the scene, frees its memory by disposing geometry and material, and clears the reference

calculation Physics Body Cleanup world.removeBody(voxel.body); voxel.body = null;

Removes the physics body from the world and clears the reference

conditional Sound Playback if (gameStarted) { destructionSynth.triggerAttackRelease('C4', '8n'); }

Plays a short destruction sound if the audio context is running

calculation Voxel Count Update const remainingVoxels = voxels.filter(v => v.mesh !== null).length; voxelCountSpan.html(remainingVoxels);

Counts how many voxels still exist and updates the on-screen counter

conditional Win State Check if (remainingVoxels === 0) { alert('Congratulations! You destroyed all voxels!'); }

Shows a congratulations message if all voxels have been destroyed

scene.remove(voxel.mesh);
Removes the mesh from the three.js scene, so it won't be drawn anymore. The mesh object still exists in memory but is invisible.
voxel.mesh.geometry.dispose();
Frees the GPU memory used by the mesh's geometry (the box shape). Without this, memory accumulates and leaks.
voxel.mesh.material.dispose();
Frees the GPU memory used by the mesh's material (the color and surface properties). Another potential memory leak if skipped.
voxel.mesh = null;
Sets the mesh reference to null, clearing the reference so JavaScript's garbage collector can free its memory.
world.removeBody(voxel.body);
Removes the physics body from the cannon.js world. It won't collide with other objects or fall anymore.
voxel.body = null;
Sets the body reference to null, allowing garbage collection to free its memory.
if (gameStarted) {
Checks whether the audio context is active. Only play sound if it is, preventing errors if the user hasn't clicked yet.
destructionSynth.triggerAttackRelease('C4', '8n');
Plays a short note on the destruction synth. 'C4' is the pitch (middle C, but this is a noise synth so pitch doesn't matter much), and '8n' means an eighth note (about 250ms at normal tempo).
const remainingVoxels = voxels.filter(v => v.mesh !== null).length;
Counts how many voxels still have a mesh (haven't been destroyed). voxels.filter() returns only the voxels where v.mesh !== null, and .length gives the count.
voxelCountSpan.html(remainingVoxels);
Updates the on-screen voxel counter to show the new count.
if (remainingVoxels === 0) {
Checks whether all voxels have been destroyed (remaining count is 0).
alert('Congratulations! You destroyed all voxels!');
Shows a browser alert dialog with a congratulations message when the player wins.

updateMouse()

updateMouse() is a helper function that converts browser mouse event coordinates into three.js's normalized device coordinate system, which is required by the raycaster to determine which voxel was clicked. It's called by mouse event handlers.

function updateMouse(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation X Coordinate Normalization mouse.x = (event.clientX / window.innerWidth) * 2 - 1;

Converts pixel X (0 to window width) to normalized device coordinates (-1 to 1) for raycasting

calculation Y Coordinate Normalization mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;

Converts pixel Y (0 to window height) to normalized device coordinates (-1 to 1), inverted because HTML Y increases downward but 3D Y increases upward

mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
Converts the mouse's pixel X coordinate (0 to width) into normalized device coordinates (-1 to 1, where -1 is left edge and 1 is right edge). This is required by three.js raycaster.
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
Converts pixel Y (0 to height) to normalized coordinates (-1 to 1), but with negation because HTML measures Y from top (increasing downward) while 3D space measures Y upward.

onMouseDown()

onMouseDown() is called when the user presses the mouse button. It records the position and assumes it's a click until mousemove proves otherwise.

function onMouseDown(event) {
  updateMouse(event);
  isDragging = false;
}
Line-by-line explanation (2 lines)
updateMouse(event);
Records the current mouse position in normalized device coordinates.
isDragging = false;
Assumes the user is clicking (not dragging) unless the mouse moves significantly while held down.

onMouseMove()

onMouseMove() detects when the user is dragging the mouse button. If isDragging becomes true, the upcoming mouseup event will know not to destroy a voxel (it's a camera rotation instead).

function onMouseMove(event) {
  if (event.buttons === 1) {
    isDragging = true;
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Drag Detection if (event.buttons === 1) { isDragging = true; }

Sets isDragging to true if the left mouse button is held down while moving, distinguishing camera rotation from voxel destruction

if (event.buttons === 1) {
Checks if the left mouse button (button 1) is currently held down.
isDragging = true;
Sets the isDragging flag to true, marking this as a drag event rather than a click. When the mouse is released, onMouseUp() will see this flag and not destroy a voxel.

onMouseUp()

onMouseUp() is the main click-destruction handler. It checks if the mouse moved (wasn't a drag), raycasts to find which voxel was clicked, activates its physics to make it fall, and calls destroyVoxel() to remove it from the scene.

function onMouseUp(event) {
  if (!isDragging) {
    updateMouse(event);
    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children);
    if (intersects.length > 0) {
      const hitMesh = intersects[0].object;
      const voxelToDestroy = voxels.find(v => v.mesh === hitMesh);
      if (voxelToDestroy) {
        voxelToDestroy.body.mass = 1;
        voxelToDestroy.body.updateMassProperties();
        voxelToDestroy.body.allowSleep = false;
        destroyVoxel(voxelToDestroy);
      }
    }
  }
  isDragging = false;
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional Click vs. Drag Differentiation if (!isDragging) {

Only destroys a voxel if this wasn't a camera rotation drag

calculation Raycaster Initialization updateMouse(event); raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children);

Sets up a ray from the camera through the mouse position and finds all scene objects it hits

conditional Hit Detection and Voxel Lookup if (intersects.length > 0) { const hitMesh = intersects[0].object; const voxelToDestroy = voxels.find(v => v.mesh === hitMesh);

Finds the first (closest) object hit by the ray and locates the corresponding voxel in the array

calculation Physics Body Activation voxelToDestroy.body.mass = 1; voxelToDestroy.body.updateMassProperties(); voxelToDestroy.body.allowSleep = false;

Changes the voxel's physics body from static (mass 0) to dynamic (mass 1) so it falls under gravity

if (!isDragging) {
Only proceeds if the mouse moved without dragging (isDragging is false). If the user dragged the camera, this block is skipped.
updateMouse(event);
Updates the mouse coordinates to the current mouse-up position.
raycaster.setFromCamera(mouse, camera);
Tells the raycaster to shoot a ray from the camera position through the normalized mouse coordinates into the 3D scene.
const intersects = raycaster.intersectObjects(scene.children);
Performs the raycast, testing which objects in the scene the ray intersects. Returns an array sorted by distance (closest first). This is the click detection.
if (intersects.length > 0) {
Checks if the ray hit anything. If the array is empty, the user clicked on empty space.
const hitMesh = intersects[0].object;
Gets the first (closest) object that was hit by the ray. intersects[0] is the nearest intersection object.
const voxelToDestroy = voxels.find(v => v.mesh === hitMesh);
Searches the voxels array for the voxel object whose mesh matches the hit mesh. .find() returns the first matching object.
if (voxelToDestroy) {
Checks that the voxel was found. It should always be found if the mesh came from scene.children and is a voxel mesh.
voxelToDestroy.body.mass = 1;
Changes the physics body's mass from 0 (static/immovable) to 1 (dynamic/moveable). Now gravity affects it.
voxelToDestroy.body.updateMassProperties();
Tells cannon.js to recalculate the body's inertia and other mass-dependent properties based on the new mass value.
voxelToDestroy.body.allowSleep = false;
Prevents the body from immediately 'sleeping' (stopping simulation). Without this, a freshly activated body might not move if it's not touching anything.
destroyVoxel(voxelToDestroy);
Calls the destruction function to remove the voxel from the scene, play audio, and update the counter.
isDragging = false;
Resets the drag flag for the next mouse interaction.

updateTouch()

updateTouch() is the mobile equivalent of updateMouse(), converting touch coordinates to normalized device coordinates for raycasting.

function updateTouch(event) {
  if (event.touches.length === 1) {
    mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1;
    mouse.y = - (event.touches[0].clientY / window.innerHeight) * 2 + 1;
  }
}
Line-by-line explanation (3 lines)
if (event.touches.length === 1) {
Only processes if exactly one finger is touching the screen. Multi-touch events are ignored to avoid confusion.
mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1;
Gets the X position of the first (and only) touch point and converts it to normalized device coordinates, same as mouse events.
mouse.y = - (event.touches[0].clientY / window.innerHeight) * 2 + 1;
Gets the Y position of the first touch point and converts it to normalized coordinates with the same Y-inversion as mouse events.

onTouchStart()

onTouchStart() is the mobile equivalent of onMouseDown(), recording the initial touch position and assuming it's a tap.

function onTouchStart(event) {
  if (event.touches.length === 1) {
    updateTouch(event);
    isDragging = false;
  }
}
Line-by-line explanation (3 lines)
if (event.touches.length === 1) {
Only handles single-touch events. Multi-touch (pinch, etc.) is ignored.
updateTouch(event);
Records the touch position in normalized device coordinates.
isDragging = false;
Assumes the touch is a tap (not a drag) until the touch moves significantly.

onTouchMove()

onTouchMove() is the mobile equivalent of onMouseMove(), detecting when the user is dragging the camera rather than tapping a voxel.

function onTouchMove(event) {
  if (event.touches.length === 1) {
    isDragging = true;
  }
}
Line-by-line explanation (2 lines)
if (event.touches.length === 1) {
Only responds to single-touch events.
isDragging = true;
Sets the drag flag to true, so onTouchEnd() will know this was a camera rotation, not a tap to destroy a voxel.

onTouchEnd()

onTouchEnd() is the mobile equivalent of onMouseUp(), handling tap-to-destroy interactions on touch devices with the same click-detection logic as mouse clicks.

function onTouchEnd(event) {
  if (!isDragging && event.touches.length === 0) {
    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children);
    if (intersects.length > 0) {
      const hitMesh = intersects[0].object;
      const voxelToDestroy = voxels.find(v => v.mesh === hitMesh);
      if (voxelToDestroy) {
        voxelToDestroy.body.mass = 1;
        voxelToDestroy.body.updateMassProperties();
        voxelToDestroy.body.allowSleep = false;
        destroyVoxel(voxelToDestroy);
      }
    }
  }
  isDragging = false;
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Tap vs. Drag Check if (!isDragging && event.touches.length === 0) {

Only destroys a voxel if it was a tap (not a drag) and all fingers have been lifted

calculation Tap Hit Detection and Destruction raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children); if (intersects.length > 0) { ... destroyVoxel(voxelToDestroy);

Identical logic to onMouseUp: raycasts from the tap position, finds the voxel, activates its physics, and destroys it

if (!isDragging && event.touches.length === 0) {
Checks two conditions: isDragging is false (it was a tap, not a drag) AND event.touches.length === 0 (all fingers have been lifted). Only then does it destroy a voxel.
raycaster.setFromCamera(mouse, camera);
Shoots a ray from the camera through the last recorded touch position.
const intersects = raycaster.intersectObjects(scene.children);
Tests which objects were hit by the ray, sorted by distance.
if (intersects.length > 0) {
Checks if the ray hit anything.
const hitMesh = intersects[0].object;
Gets the closest object hit by the ray.
const voxelToDestroy = voxels.find(v => v.mesh === hitMesh);
Finds the voxel object whose mesh was hit.
if (voxelToDestroy) {
Confirms the voxel was found.
voxelToDestroy.body.mass = 1;
Activates the physics body so it falls.
voxelToDestroy.body.updateMassProperties();
Recalculates physics properties based on the new mass.
voxelToDestroy.body.allowSleep = false;
Ensures the body doesn't sleep immediately.
destroyVoxel(voxelToDestroy);
Removes the voxel, plays audio, and updates the counter.
isDragging = false;
Resets the drag flag for the next touch.

windowResized()

windowResized() is called automatically by p5.js whenever the browser window is resized. It updates both the camera and renderer to maintain the correct 3D perspective and canvas size as the window changes.

function windowResized() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Camera Aspect Ratio Update camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix();

Recalculates the camera's perspective based on the new window dimensions

calculation Renderer Size Update renderer.setSize(window.innerWidth, window.innerHeight);

Resizes the rendering canvas to fill the new window dimensions

camera.aspect = window.innerWidth / window.innerHeight;
Recalculates the camera's aspect ratio (width divided by height) based on the new window size.
camera.updateProjectionMatrix();
Tells the camera to recompute its internal projection matrix based on the new aspect ratio. Without this call, the camera wouldn't know about the window resize.
renderer.setSize(window.innerWidth, window.innerHeight);
Resizes the three.js renderer's canvas to match the new window dimensions, so the 3D scene fills the entire window.

📦 Key Variables

p5Canvas object

Reference to the p5.js canvas element, kept minimal and hidden since three.js handles all rendering

let p5Canvas;
scene object

The three.js Scene object that contains all 3D objects, lights, and cameras

let scene;
camera object

The three.js camera that defines the viewpoint into the 3D scene

let camera;
renderer object

The three.js WebGL renderer that draws the scene to an HTML canvas

let renderer;
controls object

Three.js OrbitControls for rotating, panning, and zooming the camera with mouse/touch

let controls;
raycaster object

Three.js Raycaster for detecting which voxel mesh the user clicked on

let raycaster;
mouse object

THREE.Vector2 storing the normalized mouse/touch coordinates (-1 to 1) for raycasting

let mouse = new THREE.Vector2();
world object

The cannon.js physics world that simulates gravity, collisions, and motion

let world;
timeStep number

Physics simulation timestep in seconds (1/60 = 0.0167 seconds per frame for 60 FPS)

const timeStep = 1 / 60;
voxelSize number

Width, height, and depth of each individual voxel cube in units

const voxelSize = 2;
gridSize number

Number of voxels along each dimension (10 means 10×10×10 = 1000 total voxels)

const gridSize = 10;
totalVoxels number

Total number of voxels in the grid (gridSize³)

const totalVoxels = gridSize * gridSize * gridSize;
voxels array

Array storing objects with {mesh, body, id} for each voxel; voxel removal updates mesh to null

let voxels = [];
voxelCountSpan object

Reference to the HTML span element that displays the remaining voxel count

let voxelCountSpan;
destructionSynth object

Tone.js NoiseSynth that plays a sound effect when a voxel is destroyed

let destructionSynth;
gameStarted boolean

Flag indicating whether the audio context has been activated (set to true on first user interaction)

let gameStarted = false;
isDragging boolean

Flag to distinguish between click (destroy voxel) and drag (rotate camera) events

let isDragging = false;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE generateVoxelWorld()

Creating a new MeshStandardMaterial for each voxel wastes GPU memory. With 1000 materials for nearly identical cubes, this is inefficient.

💡 Create one material per unique color (or a few shared materials) and reuse them. Example: pre-create 10-20 random-colored materials and assign them randomly to voxels instead of creating 1000 unique ones.

BUG destroyVoxel()

When a voxel is destroyed, only its mesh is removed from the scene, but its references remain in the voxels array. Over many destructions, this array grows with null entries, wasting memory.

💡 Option 1: Remove the destroyed voxel from the voxels array using splice(). Option 2: Keep the current approach but occasionally clean up nulls. Without cleanup, the array accumulates dead references.

FEATURE onMouseUp() and onTouchEnd()

The sketch doesn't provide visual feedback when the user clicks empty space. It's unclear whether a click registered or hit something.

💡 Add a visual cursor indicator or brief highlight effect at the click position. Or play a different sound/effect for miss vs. hit to give feedback.

BUG generateVoxelWorld()

If gridSize is very large (e.g., 50), the loop creates 125,000 voxels instantly, which may freeze the browser or crash due to memory limits.

💡 For large grids, spread voxel creation across multiple frames using requestAnimationFrame or a time-based generator, showing a loading bar to the user.

STYLE Event handlers (onMouseDown, onMouseUp, onTouchStart, onTouchEnd)

Mouse and touch event handling code is almost duplicated (four functions with very similar logic), making maintenance error-prone.

💡 Refactor into a single unified event handler that works for both mouse and touch, or extract the common raycast-and-destroy logic into a shared function.

FEATURE draw()

Physics bodies that are resting never wake up unless moved. If a voxel falls onto the ground and sleeps, clicking nearby voxels won't disturb it.

💡 When destroying a voxel, apply a small impulse to nearby voxels to create a chain-reaction effect, or temporarily wake sleeping neighbors using body.wakeUp().

🔄 Code Flow

Code flow showing setup, draw, initializeaudio, initializethreejs, initializecannonjs, generatevoxelworld, destroyvoxel, updatemouse, onmousedown, onmousemove, onmouseup, updatetouch, ontouchstart, ontouchmove, ontouchend, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> canvas-setup[Canvas and UI Setup] setup --> voxel-counter[Voxel Counter Display] setup --> library-initialization[Library Setup Functions] setup --> event-listeners[Event Listener Attachment] setup --> camera-update[Camera Control Update] setup --> initializeaudio[initializeAudio] setup --> initializethreejs[initializeThreeJS] setup --> initializecannonjs[initializeCannonJS] setup --> generatevoxelworld[generateVoxelWorld] draw[draw loop] --> camera-update draw --> physics-check[Physics Check] physics-check -->|if audio context started| physics-step[Physics Simulation Step] physics-step --> mesh-sync-loop[Mesh-Body Synchronization] mesh-sync-loop --> render[Scene Rendering] draw -->|60 times per second| draw click setup href "#fn-setup" click draw href "#fn-draw" click canvas-setup href "#sub-canvas-setup" click voxel-counter href "#sub-voxel-counter" click library-initialization href "#sub-library-initialization" click event-listeners href "#sub-event-listeners" click camera-update href "#sub-camera-update" click initializeaudio href "#fn-initializeaudio" click initializethreejs href "#fn-initializethreejs" click initializecannonjs href "#fn-initializecannonjs" click generatevoxelworld href "#fn-generatevoxelworld" click physics-check href "#sub-physics-check" click physics-step href "#sub-physics-step" click mesh-sync-loop href "#sub-mesh-sync-loop" click render href "#sub-render" initializeaudio --> synth-creation[Noise Synth Creation] synth-creation --> mouse-audio-init[Mouse Audio Context Activation] mouse-audio-init --> touch-audio-init[Touch Audio Context Activation] click synth-creation href "#sub-synth-creation" click mouse-audio-init href "#sub-mouse-audio-init" click touch-audio-init href "#sub-touch-audio-init" initializethreejs --> scene-setup[Scene and Background] scene-setup --> camera-setup[Camera Creation and Positioning] scene-setup --> renderer-setup[Renderer Creation and Attachment] scene-setup --> raycaster-setup[Raycaster for Click Detection] scene-setup --> controls-setup[OrbitControls Configuration] scene-setup --> lighting-setup[Ambient and Directional Lighting] click scene-setup href "#sub-scene-setup" click camera-setup href "#sub-camera-setup" click renderer-setup href "#sub-renderer-setup" click raycaster-setup href "#sub-raycaster-setup" click controls-setup href "#sub-controls-setup" click lighting-setup href "#sub-lighting-setup" initializecannonjs --> world-creation[Physics World Creation] world-creation --> gravity-setup[Gravity Configuration] world-creation --> broadphase-setup[Collision Detection Optimization] world-creation --> sleep-setup[Sleep Mode Enablement] click world-creation href "#sub-world-creation" click gravity-setup href "#sub-gravity-setup" click broadphase-setup href "#sub-broadphase-setup" click sleep-setup href "#sub-sleep-setup" generatevoxelworld --> voxel-generation-loop[Voxel Generation Loop] voxel-generation-loop --> position-calculation[Center Grid Calculation] voxel-generation-loop --> mesh-creation[Three.js Mesh Creation and Addition] voxel-generation-loop --> body-creation[Cannon.js Body Creation and Addition] voxel-generation-loop --> voxel-storage[Voxel Object Storage] voxel-generation-loop --> ground-creation[Ground Plane Creation] click voxel-generation-loop href "#sub-voxel-generation-loop" click position-calculation href "#sub-position-calculation" click mesh-creation href "#sub-mesh-creation" click body-creation href "#sub-body-creation" click voxel-storage href "#sub-voxel-storage" click ground-creation href "#sub-ground-creation" onmousedown --> x-conversion[X Coordinate Normalization] onmousedown --> y-conversion[Y Coordinate Normalization] x-conversion --> raycast-setup[Raycaster Initialization] raycast-setup --> intersection-handling[Hit Detection and Voxel Lookup] intersection-handling --> click-check[Click vs. Drag Differentiation] click-check -->|if click| onmouseup[onMouseUp] click-check -->|if drag| drag-detection[Drag Detection] click-check -->|if tap| ontouchend[onTouchEnd] click-check -->|if drag| ontouchmove[onTouchMove] click-check -->|if tap| tap-check[Tap vs. Drag Check] tap-check --> raycast-and-destroy[Tap Hit Detection and Destruction] raycast-and-destroy --> physics-activation[Physics Body Activation] physics-activation --> destroyvoxel[destroyVoxel] destroyvoxel --> audio-feedback[Sound Playback] audio-feedback --> counter-update[Voxel Count Update] counter-update --> win-condition[Win State Check] click onmousedown href "#fn-onmousedown" click onmouseup href "#fn-onmouseup" click drag-detection href "#sub-drag-detection" click tap-check href "#sub-tap-check" click raycast-and-destroy href "#sub-raycast-and-destroy" click physics-activation href "#sub-physics-activation" click audio-feedback href "#sub-audio-feedback" click counter-update href "#sub-counter-update" click win-condition href "#sub-win-condition" windowresized --> aspect-update[Camera Aspect Ratio Update] windowresized --> renderer-resize[Renderer Size Update] click windowresized href "#fn-windowresized" click aspect-update href "#sub-aspect-update" click renderer-resize href "#sub-renderer-resize"

❓ Frequently Asked Questions

What kind of visual experience does the Voxel-destructible sketch provide?

The Voxel-destructible sketch creates a visually engaging 3D environment featuring a floating cube world made of tiny voxels that users can explore and watch crumble.

How can users interact with the Voxel-destructible sketch?

Users can interact with the sketch by clicking or tapping on the voxels to blast them apart, while also rotating and zooming the scene for a closer look.

What creative coding techniques are demonstrated in this voxel destruction sketch?

This sketch demonstrates the use of 3D rendering with three.js, physics simulations with cannon.js, and sound integration using tone.js to create an immersive experience.

Preview

Voxel-destructible - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Voxel-destructible - Code flow showing setup, draw, initializeaudio, initializethreejs, initializecannonjs, generatevoxelworld, destroyvoxel, updatemouse, onmousedown, onmousemove, onmouseup, updatetouch, ontouchstart, ontouchmove, ontouchend, windowresized
Code Flow Diagram