惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
小众软件
小众软件
J
Java Code Geeks
I
InfoQ
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
L
LangChain Blog
博客园 - 司徒正美
量子位
Y
Y Combinator Blog
C
Check Point Blog
T
Tailwind CSS Blog
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
N
Netflix TechBlog - Medium
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Fortinet All Blogs
云风的 BLOG
云风的 BLOG
A
About on SuperTechFans
B
Blog RSS Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
V
V2EX
阮一峰的网络日志
阮一峰的网络日志

Red Blob Games: latest blog posts

Red Blob Games: Differential heuristics Red Blob Games: Responsive design calculator Red Blob Games: Academic citations Red Blob Games: Highlighting interactive code blocks Red Blob Games: Optimizing page size Red Blob Games: Writing a guide to SDF fonts Red Blob Games: URLs with trailing punctuation Red Blob Games: What I did in 2025 Red Blob Games: Goodbye Sass Goodbye Sass Red Blob Games: RotMG map seeds Mapgen4's use of WebGL2 Red Blob Games: Mapgen4 river shader Red Blob Games: Mapgen4 renderer Red Blob Games: Harnessing ChatGPT hallucinations Red Blob Games: Let Let's write a search engine, part 2 of 2 Red Blob Games: Let Let's write a search engine, part 1 of 2 Red Blob Games: Hexagon conversions Red Blob Games: Mapgen4 trade routes Red Blob Games: De-optimizing mapgen4 Red Blob Games: Hexagon spiral coordinates Red Blob Games: Thoughts on Flash Red Blob Games: What I did in 2024 Red Blob Games: Hexagon page animations Red Blob Games: SDF Halos SDF Halos Red Blob Games: SDF headless tests, part 2 Red Blob Games: SDF headless tests, part 1
Red Blob Games: Mapgen4
2025-10-14 · via Red Blob Games: latest blog posts
Blog post: 13 Oct 2025

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:

  1. 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])
  2. gl_VertexID to simplify and speed up code — only available in WebGL2
  3. 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])
  4. Linear filtering of elevation and depth — needing the R16F format first
  5. 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 filtering

The differences are slight — there are blue blotches on the ground when I use GL_LINEAR filtering. But why?

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.032.0/256.0], or [0.50.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. So I can’t use GL_LINEAR filtering with this encoding.

If I could store a single 16-bit value instead of two 8-bit values, GL_LINEAR 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 filtering

It looks the same, right? Well, no, it’s slightly different, especially in the mountains:

RGBA8 vs R16F texture, zoomed in on mountains

The 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:

Correct vs incorrect encoding

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:

GL_NEAREST and GL_LINEAR looks almost the same

It looks almost the same. Looking closer, the coastlines look slightly smoother:

GL_NEAREST vs GL_LINEAR, coastlines are smoother

So that wasn’t as much of a win as I thought it would be.

To 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 interestingness

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