StackHack Voxel Painting Game

StackHack Voxel Painting Game

A voxel-based collaborative painting application. You can place and remove blocks on the screen and collaborate with everyone else online to create unique images.

Featured Use

  1. Publish – Subscribe
  2. History – Fetch, Delete
  3. Presence events – Join, Disconnect, HereNow
Using WebGL Raycasting to Create Interaction

Interaction on the web usually occurs when the user clicks their mouse on the screen. This means there needs to be some way to take the 2d coordinate where the user clicks on the screen and translate it to a 3d environment. This is done in three.js with a technique called raycasting.

Raycasting is a technique that starts at a position and draws a line in a specified direction until it collides with an object. If you start at the position of the user’s camera and draw in the direction of where you clicked your mouse, you would get what the user clicked on the screen. In StackHack, a raycaster is updated every time the user moves their mouse and used to find an object when the user clicks:

function onDocumentMouseDown(event) { event.preventDefault(); var intersects = raycaster.intersectObjects(intersectors); if (intersects.length > 0) { intersector = getRealIntersector(intersects); // delete cube if (isShiftDown) { if (intersector.object != plane) { intersects = raycaster.intersectObjects(objects); intersector = getRealIntersector(intersects); if (intersector) { removeVoxel(intersector); } } // create cube } else { intersector = getRealIntersector(intersects); setVoxelPosition(intersector); addVoxel(voxelPosition, voxelMatIndex); } } } function render() { // Recompute dirty meshes for (var i = 0; i < chunks.length; i++) { var chunk = chunks[i]; if (chunk.dirty === true) { chunk.reconstructMesh(); } } if (isCtrlDown) { theta += mouse2D.x * 1.5; } raycaster = projector.pickingRay(mouse2D.clone(), camera); var intersects = raycaster.intersectObjects(intersectors); if (intersects.length > 0) { intersector = getRealIntersector(intersects); if (intersector) { setVoxelPosition(intersector); rollOverMesh.position = voxelPosition; } } camera.position.x = ZOOM * Math.sin(THREE.Math.degToRad(theta)); camera.position.z = ZOOM * Math.cos(THREE.Math.degToRad(theta)); camera.lookAt(scene.position); renderer.render(intersectorScene, camera2); renderer.render(scene, camera); }

In the render function, the raycaster is updated every frame. This is how the small transparent red box is placed on the screen. This visual cue allows the user to see where they are placing their next block.

When the user clicks, the code starts by detecting if you clicked on an actual block on the screen. Since the user sees a merged mesh, removing a block means it has to use a hidden separated mesh to determine which block to actually remove. This is a little too complex for this post, but more information can be found on optimizing render performance with geometry merging.

function addVoxel(position, matIndex, quiet) { // If we created the block, tell others about it if (!quiet) { isometrik.publish({ channel: "stackhack", message: { action: "add", position: position, matIndex: matIndex } }); } } function removeVoxel(intersector, quiet) { if (!quiet) { isometrik.publish({ channel: "stackhack", message: { action: "remove", position: intersector.object.position } }); } } isometrik.subscribe({ channel: "stackhack", message: function (message) { if (message.action == "add") { var position = message.position; var key = [position.x, position.y, position.z].join('|'); if (!locations[key]) { addVoxel(position, message.matIndex, true); } } else { position = new THREE.Vector3(message.position.x, message.position.y, message.position.z); for (var i = objects.length - 1; i >= 0; i--) { if (objects[i].position.equals(position)) { removeVoxel({ object: objects[i] }, true); i = 0; } } } } });
WebGL Multiplayer Interaction

This will allow the user to create a world on his own screen entirely by himself. What if we wanted to have more than one user collaborate on a scene though?

We want to enable two users to interact with one another in the same world. The simplest way of doing this would be to send the entire state of the world to every other user when you add or remove a block. Unfortunately this would make the game sluggish, and would hinder the fluid realtime user experience. If all we want to know is the difference between my environment and someone who has interacted with their environment, we can just send an add or remove command every time the user clicks on the screen.