I try to avoid big software rewrites. But sometimes the rewrites are just an excuse to re-familiarize myself with the code. I rationalized rewriting Mapgen4’s renderer by saying I wanted to use WebGL2. And I did use WebGL2, but the improvements turned out to be minor:
- Vertex Array Objects to simplify and speed up code — but OES_vertex_array_object[1] makes this available in WebGL1 (good support on all platforms[2])
-
gl_VertexID
to simplify and speed up code — only available in WebGL2 -
R16F
texture format for elevation and depth — needing EXT_color_buffer_half_float[3] in WebGL1 (poor support on Android[4]) or EXT_color_buffer_float[5] in WebGL2 (good support on all platforms[6]) - Linear filtering of elevation and depth — needing the
R16F
format first -
SRGB8
texture format for linear rgb color handling — supported in WebGL2, but could be emulated in WebGL1
I did not need to switch to WebGL2 for features, but implementing those features would make the code more complex and error prone. I decided to switch to WebGL2 so that I could simplify my code.
The Vertex Array Objects and gl_VertexID
didn’t change the rendering output, but the texture format did, both good and bad, and I wanted to share some screenshots.
Let’s start with a screenshot comparison I showed last time:


GL_NEAREST
vs GL_LINEAR
filteringWhy do I get the blue blotches when I use GL_LINEAR
filtering?
Elevation is slightly too big for an 8 bit color channel, so I encode the elevation in the red and green channels (8 bits each), [fract(elevation)
, floor(elevation)/256.0
]. For example an elevation of 32.5 would be encoded as [32.5%1.0
, 32.0/256.0
], or [0.5
, 0.125
]. In the fragment shader I decode it with with green*256.0
+ red
, in this case 0.125*256.0
+ 0.5
= 32.5.
GL_LINEAR
texture filtering will blend each channel independently, then combine the channels into one value:
(a.hi
+ b.hi
) / 2.0 + ((a.lo
+ b.lo
) / 2.0) / 256.0
But that produces a different value than correctly combining the channels first and then blending:
((a.hi
+ a.lo
/ 256.0) + (b.hi
+ b.lo
/ 256.0)) / 2.0
Near sea level, some of the below-sea-level elevation must be getting blended incorrectly into the above-sea-level ground.
If I could store a single 16-bit value instead of two 8-bit values, blending would work correctly. And I wouldn’t have to split the elevation into two channels, and I wouldn’t have to combine them together again. It’d be simpler code and it would look better.
Or so I thought. Here’s the comparison:


RGBA8
vs R16F
texture, with GL_NEAREST
filteringIt looks the same, right? Well, no, it’s slightly different, especially in the mountains:


RGBA8
vs R16F
texture, zoomed in on mountainsThe R16F
format makes the mountains smoother! It also makes the valleys smoother but that difference is harder to see.
The outputs should have been the same. Why aren’t they?
It turns out mapgen4 had a bug in it. I didn’t actually encode the elevation the way I described above. I encoded with vec4(fract(256.0*e), e, 0, 1) and decoded with dot(color.xy, vec2(1.0/256.0, 1.0)). That doesn’t preserve the value. I should have used vec4(fract(256.0*e), floor(256.0*e)/256.0, 0, 1). I used ObservableHQ Plot[7] to plot the difference in these two calculations. The black dots are the incorrect encoding, and we can see that half of them are a little bit off:

This bug adds a “texture” to the sides of mountains. I like the old look better. But don’t want to keep the bug. I would prefer adding a texture intentionally.
After I got R16F
working, I decided to compare GL_NEAREST
to GL_LINEAR
. I couldn’t use GL_LINEAR
because it interpolated each channel separately, but I can use it now that I have only one channel. I think the coastlines look better, but the difference is small:


GL_NEAREST
vs GL_LINEAR
, smoother coastlinesTo compensate for the mountains looking smooth, I added a slider mountain_folds
that adds geometry to make the sides of mountains look more interesting:


mountain_folds
slider to add geometric interestingnessI enjoyed the graphics rewrite. I hadn’t looked at the graphics code in a long time, and I found lots of places for improvement. I found and understood a bug that I had long suspected. All of this could have been done without a rewrite, but the rewrite got me to actually do it.