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.

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.

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.

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 ^^