in development

the infinite sandwich

My game Fissure takes place inside a cave, or at least that’s what I’d have you believe. It’s actually taking place inside a sandwich. An infinite sandwich. Imagine how much mustard was involved.

Fissure was my first foray into WebGL. I’d played around with OpenGL in C++ for a couple of years—nothing serious, just enough to accumulate a few useful routines and techniques—and when I discovered WebGL it didn’t take long to port everything I knew into JavaScript. Like many 3D hobbyists, I toyed with heightmaps and Perlin noise, and cobbled together a Noise2D object to create landscapes to explore. This object provides a get(x, y) method that returns a height value based on interpolation between adjacent random numbers. The random numbers are rolled up at object creation time. Here’s the relevant code from my Foam library.

FOAM.Noise2D = function(seed, ampl, xsz, xper, ysz, yper) {
	var x, xl;

	this.xSize = xsz;
	this.ySize = ysz || xsz;
	this.xPeriod = xper;
	this.yPeriod = yper || xper;
	this.amplitude = ampl;
	this.prng = new FOAM.Prng(seed);
	this.map = new Float32Array(this.xSize * this.ySize);

	for (x = 0, xl = this.map.length; x < xl; x++)
		this.map[x] = this.prng.get();
};
FOAM.Noise2D.prototype = {

	get: function(x, y) {
		var xf = this.xPeriod * Math.abs(x);
		var xi = Math.floor(xf);
		var mux = xf - xi;

		var yf = this.yPeriod * Math.abs(y);
		var yi = Math.floor(yf);
		var muy = yf - yi;

		var xi0 = xi % this.xSize;
		var yi0 = yi % this.ySize;
		var xi1 = (xi + 1) % this.xSize;
		var yi1 = (yi + 1) % this.ySize;

		var v1, v2, v3, v4;
		var i1, i2;

		v1 = this.map[xi0 + yi0 * this.xSize];
		v2 = this.map[xi0 + yi1 * this.xSize];
		i1 = FOAM.interpolate(v1, v2, muy);

		v3 = this.map[xi1 + yi0 * this.xSize];
		v4 = this.map[xi1 + yi1 * this.ySize];
		i2 = FOAM.interpolate(v3, v4, muy);

		return this.amplitude * FOAM.interpolate(i1, i2, mux);
	}
};

When I decided I wanted to set a game in a cave, I knew I’d have to find a clever way to create a realistic cave. I wanted it to look smooth, not blocky, so voxels were out. I’d need to create it as a continuous surface. It had to be a real 3D cave, twisting and turning in every direction. Lastly, it had to support a simple collision detection system.

After a while, I abandoned any pretense at cleverness, and cheated my way into something that I thought looked and worked well enough.

The sandwich model implements a cave using two surfaces, one above, and one below. I modulate the surfaces using three Noise2D objects as heightmaps: upperWall, lowerWall, and commonGap. Now, the function of the first two is obvious. They provide a nice bumpy-smoothness that simulates the rocky outcroppings of a real cave. But the third one, the commonGap, what’s that all about?

Well, commonGap is the secret sauce in this cave sandwich. It’s long period and large amplitude, as opposed to the short period and small amplitude of the other two. As I mentioned earlier, I wanted a twisty-turny cave, and that’s what commonGap provides. It creates large perturbations that are common to both the upper and lower surfaces.

	// arguments: seed, amplitude, source length, period
	var upperWallMap = new FOAM.Noise2D(3939592, 100, 256, 0.01);
	var lowerWallMap = new FOAM.Noise2D(2194828, 100, 256, 0.01);
	var commonGapMap = new FOAM.Noise2D(9147374, 500, 128, 0.002);

	this.upperWallHeight = function(x, z) { 
		return commonGapMap.get(x, z) + upperWallMap.get(x, z) + caveHeight; 
	};

	this.lowerWallHeight = function(x, z) { 
		return commonGapMap.get(x, z) - lowerWallMap.get(x, z); 
	};

The surfaces described by these heightmaps swoop and dive, creating what look like warrens and passages. However, it’s still a sandwich. The surfaces never meet. The illusion is the cheat.

The use of heightmaps also makes collison detection simple. If your player drops below the lower wall height or rises above the upper wall height, you have a collision.

Once I’d worked this out, I was pretty elated. The Noise2D object is addressable throughout the entire real number system, which meant that I’d not only created a cave sandwich, but an infinite one!

Why this wasn’t a good thing was something I had to learn later, and I’ll explain in the next post.