# Calculating normals from heightmap in vertex shader

Hi! I’ve recently been trying to implement a simple heightmap by extending the phong shader in Three.js

Displacing the vertices was no big problem, but I’m having a lot of trouble with calculating the normals. I’m using pretty much a direct port of the approach presented in this paper, which simply looks at neighbouring pixels based on the current UV coordinate.

``````vec3 getNormal(vec2 uv) {

float u = texture2D(heightMap, uv + texelSize * vec2(0.0, -1.0)).r * texelMaxHeight;
float r = texture2D(heightMap, uv + texelSize * vec2(1.0, 0.0)).r * texelMaxHeight;
float l = texture2D(heightMap, uv + texelSize * vec2(-1.0, 0.0)).r * texelMaxHeight;
float d = texture2D(heightMap, uv + texelSize * vec2(0.0, 1.0)).r * texelMaxHeight;

vec3 n;
n.z = u - d;
n.x = l - r;
n.y = 2.0;
return normalize(n);
}
``````

Where `texelSize` should be the size of 1 pixel in UV coordinates, and `texelMaxHeight` is the maximum possible displacement of a vertex.

But it seems like there’s something wrong with my implementation, because the mesh comes out totally flat, as if all the normals are the same: By `flatShading: true`, you can get a feel for how it should look: I really can’t see what I’m doing wrong here, but I’ve noticed that I can stick a multiplier in there such as `uv + 8.0 * texelSize * vec2(0.0, -1.0)` which causes the slope to become at least a little bit visible, but it doesn’t feel right and I don’t want to be falling back on magic numbers.

Here’s a codepen in case anyone is able to help: https://codepen.io/geckojsc/pen/zYGNpgN

Thanks ^^

I’ve got the same unshaded result, using `.displacementMap` and `flatShading: false`

I got your approach working with a few modifications:

• If you calculate new normals before the `morphtarget`, `skinning & displacementmap` vertex shader code, your calculations will get overwritten. You should calculate normals AFTER those shader chunks, or remove them altogether if you’re not using them.
• I got rid of `uv + 8.0`, I’m not sure why you would want to shift your texel calculation 8/256ths in each direction
• I set `n.y` to equal 1/256 because your `n.z & n.x` values are in color steps of the same magnitude.
• I also replaced all your geometry creation code with a `THREE.PlaneBufferGeometry` for simplicity.

I’m not sure this method gives you truly accurate normals, but it’s a pretty close estimate.

5 Likes

TexelSize is definitely something like `1/x` where `x` is your heightMap size.

In my heightmap I use something like this:

``````vec2 texelSize = vec2(1.0 / WIDTH, 1.0 / WIDTH);

return normalize(vec3(
(texture2D(heightmap, uv + vec2(-texelSize.x, 0)).x - texture2D(heightmap, uv + vec2(texelSize.x, 0)).x),
(texture2D(heightmap, uv + vec2(0, -texelSize.y)).x - texture2D(heightmap, uv + vec2(0, texelSize.y)).x),
1.0));
``````

where `WIDTH` is texture size (in my case `#define WIDTH 512`).

But in fact this method should be equivalent to yours. You just need to set proper `texelSize`.

1 Like

Ah, wow thank you so much!

I think fortunately those unused shader chunks weren’t hurting anything because I never supplied the properties to make them work, but thanks for the heads up.

The + 8.0 was leftover by mistake, I noticed it did something while I was experimenting, but didn’t mean to share that, sorry. x)

And yep, `n.y = 2.0` is the key thing I was doing wrong, though I’m wondering if there’s a better value than 1/256, since the max height needs to be taken into account somewhere for sure.

I rolled my own plane because I was worried about the standard plane being oriented vertically, and whether that might mess things up if the normals are calculated in model space. Turns out I didn’t know that `geom.rotateX()` was a thing ^^

1 Like