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.)