February 18, 2014

Fun with Three.js

My current Hacker School project is building a programming competition server for people to compete AIs against each other. The server sits in the middle between two AIs, manages turns and game state, and makes sure nobody cheats. I’ve already written server code for the first game — Mancala.

To let humans observe the bots battling it out, I’m building a Mancala game viewer with Three.js. I’ve never played with Three.js, so it should be an interesting project to work on.

You can see the first prototype on JSFiddle. Drag with the mouse to pan around, and use the scroll wheel to zoom in and out. Right now it just sits there doing nothing, but hopefully I’ll be able to hook it up to actual Mancala games tomorrow. The code definitely needs some refactoring, but it’s a good start!

Drawing 2D Text in 3D Space

To generate the score numbers at either end, I’m drawing the number to an invisible 2d canvas element, and then using Three.js’s Texture function to turn the canvas into a texture:

// create canvas
var canvas = document.createElement('canvas');

// the larger these numbers, the larger the canvas, and
// the smoother your final image can be. If your final
// texture is blurry or pixelated, try increasing these
// numbers, and drawing on the canvas in a larger font.
canvas.width = 1000;
canvas.height = 1000;

// draw the score of "50" to the canvas
var context = canvas1.getContext('2d');
context.font = "Bold 400px Helvetica";
context.fillStyle = "rgba(255,0,0,0.95)";
context.fillText('50', 0, 300);

// use canvas contents as a texture
var texture = new THREE.Texture(canvas)
texture.needsUpdate = true;

Once the canvas is in a texture, it can be used as a map in any material, just like a normal texture can:

var material = new THREE.MeshBasicMaterial( {
  map: texture1, side:THREE.DoubleSide
} );

And that material can be used when building meshes, just like normal materials.

The ability to easily draw on a 2D canvas in 3D space opens up a lot of possibilities, but I’m not yet sure how fast WebGL can create a new texture and material from a canvas. Probably fast enough to update a score once every couple seconds, but would it be fast enough to do every frame? I’ll have to do tests to see.

Shadows

Getting the slowly rotating dynamic shadows was probably the hardest part of the example above. For future reference, if shadows in a Three.js project aren’t working, double check the following:

  1. Make sure the light source you want to cast shadows is either a DirectionalLight or a SpotLight. A PointLight can’t cast shadows.
  2. Make sure you’ve set the light’s light.castShadow property to true.
  3. Make sure all the objects that are casting and receiving shadows are within the light’s shadow camera…shadows won’t appear outside the light’s shadow camera. You can see the area of this camera by setting light.shadowCameraVisible = true — just make sure the objects are within the yellow box.
  4. Make sure your objects and camera are at a proper scale. I had a problem where my objects were only a couple tenths of a unit large, and the directional light was thousands of units away. This caused bizarrely shaped shadows, even though the objects were technically within the light’s shadow camera. Scaling up all the objects resolved the problem.
  5. Make sure you have an object to receive shadows, and that its object.receiveShadow property has been set to true.
  6. Make sure you have an object to cast shadows on to, and that its object.castShadow property has been set to true.

Although you can’t tell in the example above, there actually is an object receiving the boxes’ shadow…it’s just a giant plane colored to be exactly the same shade of gray as the sky, so you can’t see where it ends. This is necessary, because without it, there would be nowhere to cast shadows onto!

Tomorrow, I want to attempt to get this 3D board hooked up to actual game data, so you can actually see the game being played with little marbles on top of the cubes. I also think it might be interesting to mess some more with this invisible plane that I’ve made…what if the plane had holes in it, instead of blocks protruding up?