One of my goals for 2019 is to improve my existing pages. Yesterday I decided to work on my old 2D noise page. We normally use Perlin/Simplex noise to make terrain heightmaps, but on that page I used Fourier transforms instead. Perlin/Simplex noise are a fast approximation of the things you can get from Fourier transforms.

The 3D renderer on that page always bothered me. It was one of my early experiments with WebGL. I had never been able to figure out exactly what I didn't like or how to fix it.

I decided to improve the renderer.

I read through the code to and realized it was written so long ago that let and const weren't even widely available! I started by updating the code style to match the code I write today. I read through the rendering code and decided to switch from raw WebGL to regl.js, which lets me experiment and iterate much more quickly.

I wanted to compare the old and new output easily, so I put them side by side on the page. I wanted to try two techniques with the new renderer:

  1. Instead of building a new 3D quad mesh on the CPU every time the data changed, I built a single 2D triangle mesh with x,y, and then read the heightmap data from a texture on the GPU. Reading textures from vertex shaders is widely supported now. This way, I only have to update the texture on each render instead of rebuilding the mesh. Unfortunately I couldn't figure out why it wasn't interpolating pixels values; I had to put in my own interpolation.
  2. Instead of calculating normals and lighting on the CPU every frame, I calculated them in the fragment shader on the GPU. But I didn't want to use a standard lighting system; I wanted to apply outlines instead. I had learned how to do this in mapgen4 and wanted to try an even simpler approach here. This worked out really well.

In this comparison you can see how the dark/light spots in the noise are renderered in the two renderers. With the old renderer (green) the color changes but the shapes are all mushy. With the new renderer (gray) the dark/light matches the noise, and the mountain peaks are easier to see.

2D noise values Old renderer New renderer
Landscape: old (green) and new (gray) renderer

In smooth areas you can see how the outlines help show the shapes. You can also see that the old renderer flipped the elevation upside down (oops!).

2D noise values Old renderer New renderer
Smooth area: old (green) and new (gray) renderer

With blue noise (positive exponents) the new renderer looks much better:

2D noise values Old renderer New renderer
Blue noise: old (green) and new (gray) renderer

I'm really really happy with outlines! Compare:

No outlines Light outlines Medium outlines
Without and with outlines

The outlines are one line of code:

void main() {
  float light = 0.1 + z - fwidth(z) * u_outline;
  gl_FragColor = vec4(light, light, light, 1);

I darken the color based on fwidth(z). The GL extension OES_standard_derivatives calculates how much an expression changes from the current pixel(fragment) to adjacent pixels. When z changes a lot, that usually means it's changing from one mountain peak to another, so I darken the output color.

There are still more things I'd like to improve on this page, but the renderer was the thing that bothered me the most, and I'm now happy with it. The other changes will wait until another day.

Labels: ,