in development

another four-letter world to explore

As a followup on my last post regarding Rise, here’s a fine little demo that uses it.

mope

(Want to see it in action? Try the Live Demo. WSAD to move, Q for fullscreen.)

Mope uses the marching cubes algorithm to render an arbitrary surface at regular spatial intervals. A web worker performs the rendering, making the transition seamless. The isosurface function also permits simple collision detection.

As in an endless golden thread, the code is arranged around an object containing GL wrapper objects (e.g., texture, mesh, shader) and a worker thread that generates the vertex data, but with Rise, the separation of wrapper and buffer objects makes it easier.

Want to examine the code? There are several key pieces.

Defined in the world object (world.js), this function provides the isosurface. It’s the same one I pointed out a couple posts back. Since both the main thead and the worker thread require it, the function is statically defined in its own little object.

getAnchorField: function(x, y, z) {
	var a = 2 * Math.cos(1.2 * x);
	var b = 2 * Math.cos(0.7 * y);
	var c = 2 * Math.cos(1.3 * z);

	return (Math.sin(x + y + a) + Math.sin(y + z + b) + Math.sin(z + x + c)) / 3;
}

The “anchor” object (anchor.js) maintains the GL wrappers, communicating with the worker thread whenever it needs new vertex data. These two functions provide that communication: send out a request whenever the player has moved a set distance from her last position, and accept the data response when it’s available.

/**
	update the anchor state when necessary
	called for every frame

	@method update
	@param pp player position
**/

update: function(pp) {
	if (M.anchor.lastUpdate.distance(pp) > M.anchor.UPDATE_INTERVAL) {
		M.worker.postMessage( {
			cmd: "update-anchor",
			pos: pp
		} );
		M.anchor.lastUpdate.copy(pp);
	}
},

/**
	handle message event from worker thread
	build the mesh with vertex data

	@method handleWorker
	@param e message object from worker
**/

handleWorker: function(e) {
	if (e.data.cmd === "update-anchor") {
		M.anchor.mesh.build(e.data.vbuffer);
	}
},

Next is the heart of the worker thread (worker.js). It’s called on initialization of the thread to establish both the data buffer and the surface polygon generator.

/**
	initializes anchor objects

	@method initAnchor
**/

function initAnchor() {
	var vbuffer = RISE.createBuffer(Float32Array);

	var step = M.world.ANCHOR_STEP;
	var radius = M.world.ANCHOR_RADIUS;
	var threshold = M.world.ANCHOR_THRESHOLD;

	var source = function(x, y, z) {
		return M.world.getAnchorField(x, y, z);
	};

	var handle = function(v0, v1, v2, n) {
		var nx = n.x * n.x;
		var ny = n.y * n.y;
		var nz = n.z * n.z;		
		vbuffer.set(v0.x, v0.y, v0.z, nx, ny, nz);
		vbuffer.set(v1.x, v1.y, v1.z, nx, ny, nz);
		vbuffer.set(v2.x, v2.y, v2.z, nx, ny, nz);
	};

	var surf = RISE.createSurfacer(radius, step, threshold, source, handle);

	M.generate = function(p) {
		vbuffer.reset();
		surf.generate(p);

		// have to post fake vbuffer object back to main thread
		// as functions (among other types) can't be serialized
		postMessage( { 
			cmd: "update-anchor",
			vbuffer: {
				data: vbuffer.data,
				length: vbuffer.length
			}
		} );
	};
}

The viewer object (viewer.js) handles controls, motion, and collision detection. A smooth surface function makes collision a breeze. Here, I model a spring-like force that increases rapidly as the player approaches the surface, pushing her backward along the normal vector.

// use spring-like force to push camera away from surface
var s = Math.max(0, M.world.getAnchorField(pos.x, pos.y, pos.z)) - 0.05;
var f = Math.pow((1 - s), 128) * 2;
M.world.getAnchorNormal(normal, pos.x, pos.y, pos.z);
normal.mul(f);
direct.add(normal);

And there you have it. Any smooth function can replace getAnchorField above. I would also experiment with the threshold value (which defines where the polygonization is applied). Have fun!