in development

how to thicken a tumbleweed (in space)

So, I’m working on a new game, and it takes place in an “open” world for a change. No more underground ruins, moon caves, or pastel factories for this cat.

We’re hanging some hundreds of kilometers above a gas giant world here. (Don’t worry, we can float.) To one side, we have a space tumbleweed.

I generate the weed using Brownian motion. I have a point and a direction vector. Inside a loop, I add the direction vector to the point, then apply a slight random error to the direction vector, and normalize it to keep the distance constant between iterations. I also constrain the point to remain within a spherical radius, and maintain a direction vector at right angles to the direction of motion. Some rough code is below.

// p is our point undergoing Brownian motion
var p = SOAR.vector.create(24, 0, 0);
// f is our direction vector
var f = SOAR.vector.create(0, 0, -1);
// r is a right-hand vector for generating the strip
var r = SOAR.vector.create(-1, 0, 0);
// d is a scratch vector
var d = SOAR.vector.create();

for (i = 0; i < 30000; i++) {

 	// this is a triangle strip, so generate two points on either side of the point
 	this.mesh.set(p.x - 0.05 * r.x, p.y - 0.05 * r.y, p.z - 0.05 * r.z, i % 2, 0);
 	this.mesh.set(p.x + 0.05 * r.x, p.y + 0.05 * r.y, p.z + 0.05 * r.z, i % 2, 1);

 	// move the point
 	p.x += f.x;
 	p.y += f.y;
 	p.z += f.z;

 	// add a random error to the direction
	f.x += 0.5 * (Math.random() - Math.random());
 	f.y += 0.5 * (Math.random() - Math.random());
 	f.z += 0.5 * (Math.random() - Math.random());
 	f.norm();

 	// add a smaller random error to the right angle vector
 	r.x += 0.01 * (Math.random() - Math.random());
 	r.y += 0.01 * (Math.random() - Math.random());
 	r.z += 0.01 * (Math.random() - Math.random());

 	// this is a trashy, hacky way of insuring that the
 	// right angle vector remains at right angles to the
 	// direction vector.
 	r.cross(f).cross(f).neg().norm();

 	// if the point strays outside the sphere, generate a vector
 	// that points back toward the center of the sphere, and add
 	// it to the direction vector. the point's path will gently
 	// curve back around.

 	if (p.length() > this.MAX_RADIUS) {
		d.copy(p).norm().neg().mul(0.5);
		f.add(d);
	}
}

However, if I draw the mesh, I get a rather anemic-looking thing.

Now, I could increase the number of iterations, and that’s what I tried at first, but it’s a problem for two reasons. First, I want to be able to generate this in real time when the game starts up, so too many iterations means the web page will be unresponsive for more than a few seconds. Not good. The second issue is the sheer size of the object in video memory. Running in a web browser is lovely, but it keeps me mindful of the fact that I’m sharing resources with loads of other programs. I want to keep GL objects as small as possible.

There’s also a slight aesthetic problem. It’s a random tangle of stuff. I’d prefer that it look more like a product of Nature and less like a discarded shoelace.

Enter symmetry. I generate my anemic tangle as specified, but at display time, I rotate the mesh through eight successive angles and draw each one.

// rotor is just a wrapper around a quaternion.
// see https://github.com/wordsaretoys/soar/blob/master/rotator.js
// and check out the freeRotor object
this.rotor.rotation.set(0, 0, 0, 1);
this.rotor.turn(0, 0, 0);

// SYMMETRY is set to 8
for (i = 0, r = SOAR.PIMUL2 / this.SYMMETRY; i < this.SYMMETRY; i++) {
	gl.uniformMatrix4fv(shader.rotations, false, this.rotor.matrix.rotations);
	this.mesh.draw();
	this.rotor.turn(0, r, 0);
}

The vertex shader only has to multiply one more matrix, and GPUs are great at that kind of stuff.

attribute vec3 position;
attribute vec2 texturec;

uniform mat4 projector;
uniform mat4 modelview;
uniform mat4 rotations;

varying vec2 uv;

void main(void) {
	gl_Position = projector * modelview * rotations * vec4(position, 1.0);
	uv = texturec;
}

Now, I have a thick tumbleweed. There’s a performance penalty, of course, but it doesn’t seem to have affected my frame rate. As a bonus, some lovely patterns arise from the chaos.

Symmetries occur all over the natural world, and they’re a useful way of giving the appearance of structure. (Not to mention the appearance of doing more work than you actually did.)