Heightmaps and Golf
Nostalgia
I'm a big fan of games like SimCity 2000 where you have editable terrain. Typically the world is basically a big grid mesh, and each vertex can have different heights on the z axis.
SimCity 2000
I was thinking about Sid Meier's SimGolf
Sid Meier's SimGolf
I like the way the different terrain tiles join, so I decided to to and re-create it. at least until I give up or get bored 🙃
Demo
Before we go any further, here's where I got up to
What its made of
- A dynamic VBO where we can update them
- A dynamic FBO for the tiles (link to the other post)
- A render pass that shows the x and y coordinates in the red and green channels
Building the heightmap
TODO: Excerpt of the code that actually builds the mesh
Triangle layout, dependent on the largest diagonal height difference
What I ended up calling the "triangle layout" is defined by the poopsing corners with the biggest height difference. The alternative is having all quads split in the same direction, but this means peaks (like the one show) won't be shown as pyramids, and will instead be directional. Sim City 2000 does the optional, adding the triangle edge (red line) between the most level opposing vertices (as seen in the screenshot).
The vertex buffer (VBO) itself is created with the DYNAMIC_DRAW
usage
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.DYNAMIC_DRAW);
And then later on I can update values within the buffer with something like:
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferSubData(gl.ARRAY_BUFFER, offset * Float32Array.BYTES_PER_ELEMENT, new Float32Array(newData));
Rendering tiles
This expands on the technique discussed in 3D textures for WebGL Tiles where a 2D image of tiles is loaded into a 3D texture, stacking each tile along the Z axis (known also as a layer). Here's an excerpt of the GLSL shader where it uses the 2D texture uTex
as a lookup for which tile layer to use uTex2
#version 300 es
precision mediump float;
precision mediump sampler3D;
uniform sampler2D uTex;
uniform sampler3D uTex2;
uniform vec2 uScale;
in vec2 uv;
in vec3 norm; // Used for lighting
out vec4 o_color;
void main(void)
{
float t = texture(uTex, uv / uScale).r;
vec4 rgba = texture(uTex2, vec3(mod(uv * 2.0, 1.0), t));
o_color = vec4(rgba.rgb, 1.0);
}
Here's the texture I'm working with:
The tiles themselves can be split into 4 sub-tiles and re-assembled for all combinations
These sub-tiles are re-assembled based on their neighbours
In the diagram above we have
- "S" - side tiles - where one side neighbour is different
- "in" - inner corners - when both side neighbours are different
- "out" - outer corners (this one is a the "green" type) where the diagonal neighbour is different
- "f" - full tiles, where all neighbours are the same
With these, we can build any transition. One beauty with this is we can have repeating patterns as (for example) the top-left "full" subtile is different to the top-right "full" subtile.
When changing the tile, we just update this 2d reference texture with code like
gl.bindTexture(gl.TEXTURE_2D, myFboTexture);
gl.texSubImage2D(gl.TEXTURE_2D, 0, min[0] * 2, min[1] * 2, w * 2, h * 2, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
gl.bindTexture(gl.TEXTURE_2D, null);
Note the multiplications by 2 are to deal with the subtiles. You get the idea
Picking
Picking is the technique used to figure our where you clicked. In a separate pass we render the whole scene (the important parts) in a way that renders there x, y and z coordinates as colours in red, green, and blue
In this sample, the pixel under the coursor is coloured 106,1,92
. The values range from 0 to 255, so it translates to 42%
on the X axis (red) and 0%
on the Y axis (green). The Z axis (blue) is on a different scale, and we don't use it here anyway.
To work out what vertex we're pointing at
vertX = Math.round( red / 255 * width)
(same for y)
To work out what tile we're pointing at
vertX = Math.floor( red / 255 * width)
(same for y)
The shader for this is as simple as you can imagine
precision mediump float;
varying vec3 vPos; // this comes from the vertex shader, already divided by vec2(width,height)
void main(void){
gl_FragColor = vec4(vPos, 1.0);
}
Gameplay
I want to add item placing, course building, path finding and playable/NPC golfers.