So I understand that for best results one uses a height (or bump) map and a normal map together.

And I also understand that one can calculate a normal map from a height map using some sobel operator.

But out of curiosity I wonder how the first games that supported bump mapping did it.

I found this fragment shader code that seems to do it, but I am not sure: OpenGL Bump Map -- Texture artifacts ?

If the code above does exactly what I am looking for, can someone explain the line

vec3 normalDirection = normalize(tangenteSpace*(texture2D( bump_tex, f_texcoord ).rgb*2.0 - 1.0));

How can you get a normal from a bump ?
I understand what tangent space is mathematically but how does it work here ?
Is it a vec4 or a matrix ?

So basically my question is: How was bump mapping done without a normal map in the old days ?

  • \$\begingroup\$ The line you quoted is reading a normal map. Not a bump map. \$\endgroup\$ Aug 15, 2019 at 4:50

3 Answers 3


Bump mapping simulates surface displacement, i.e. the shader pretends that the pixel is further back along the surface normal than it actually is. It then compares the depth to the pixels next to it on the map, and so can establish a gradient, using pythagoras: if we know the difference in height, and the difference in pixels to the left/right/up/down/etc, then we can compute the slope in each direction. We can then use those slopes to calculate a new normal vector for that pixel. This involves several texture samples per pixel, in the sobel matrix you mentioned, and is indeed how it was done "back in the day".

For this reason, it is expensive, and sub-optimal, and in the case of blurred height maps, can led to artifacts like the ones you saw.

Normal mapping on the other hand, uses a normal map, pre-baked from a bump, or displacement ma, in exactly the same method as above. The difference is, that by doing it in advance, you save on at least 4-8 texture samples per pixel, in addition to not having to compute the slopes per pixel. Not having to do this FAR outweighs the cost of the matrix transformations from tangent to view space per pixel, making lighting much more efficient. This is how it is done in today's hardware, though tomorrow's graphics engine will likely use raytracing instead.

  • \$\begingroup\$ Very interesting! Are you able to explain the line of code that I posted above ? I don't see any other texture lookups or sobel operations in the code from the link either. \$\endgroup\$
    – Ray Hulha
    Aug 14, 2019 at 14:37
  • \$\begingroup\$ That line of code is nothing more than transforming the sampled colour out of tangent space and into view space. I recommend reading learnopengl.com/Advanced-Lighting/Normal-Mapping for a better explanation. \$\endgroup\$
    – Ian Young
    Aug 14, 2019 at 17:50
  • \$\begingroup\$ But my point is, as far as I can tell, that the line of code does not come from a shader using normal mapping. Then it would be easy. The line of code comes from a shader using a bump/height map. That is the hard to understand part... \$\endgroup\$
    – Ray Hulha
    Aug 14, 2019 at 19:57
  • \$\begingroup\$ Put simply, the person who wrote that code, should be sampling a normal map, not a bump map. The wrong texture is being used. I would recommend you use a normal map too. It's superior in every regard, and is an industry standard technique. \$\endgroup\$
    – Ian Young
    Aug 14, 2019 at 23:04
  • \$\begingroup\$ I have actually added an answer to the question you linked as well, which should help both you and the OP \$\endgroup\$
    – Ian Young
    Aug 14, 2019 at 23:17

The code by Mikkelsen "Bump Mapping Unparametrized Surfaces on the GPU", suggested by Ray Hulha, is the right solution. However the listings are a bit fragmented in the paper.

Here is a complete solution for GLSL:

// Bump mapping 
// from paper: Bump Mapping Unparametrized Surfaces on the GPU
vec3 vn = normalize( vnormal );
vec3 posDX = dFdx ( vworldpos.xyz );  // choose dFdx (#version 420) or dFdxFine (#version 450) here
vec3 posDY = dFdy ( vworldpos.xyz );
vec3 r1 = cross ( posDY, vn );
vec3 r2 = cross ( vn , posDX );
float det = dot (posDX , r1);
float Hll = texture( bumptex, tc ).x;    //-- height from bump map texture, tc=texture coordinates
float Hlr = texture( bumptex, tc + dFdx(vtexcoord.xy) ).x;
float Hul = texture( bumptex, tc + dFdy(vtexcoord.xy) ).x;
// float dBs = ddx_fine ( height );     //-- optional explicit height
// float dBt = ddy_fine ( height );

// gradient of surface texture. dBs=Hlr-Hll, dBt=Hul-Hll
vec3 surf_grad = sign(det) * ( (Hlr - Hll) * r1 + (Hul - Hll)* r2 );    
float bump_amt = 0.7;       // bump_amt = adjustable bump amount
vec3 vbumpnorm = vn*(1.0-bump_amt) + bump_amt * normalize ( abs(det)*vn - surf_grad );  // bump normal

This code combines and simplifies the listings from the paper. I've also added the bump_amt, which could be an adjustable uniform parameter to dial between the smooth and bump surface.

In terms of performance, this should work quite fast on modern GPUs. It also allows for procedural (dynamic) bump maps which you can't do with normal maps. If you are in a time-critical app like a game then the slowest part is the 3x texture fetches. This is exactly what normal maps improve on by baking the height texture into pre-separated surface derivatives, resulting in just one texture fetch.


I found some code that seems to solve this in three.js:


    vec2 dSTdx = dFdx( vUv );
    vec2 dSTdy = dFdy( vUv );
    float Hll = bumpScale * texture2D( bumpMap, vUv ).x;
    float dBx = bumpScale * texture2D( bumpMap, vUv + dSTdx ).x - Hll;
    float dBy = bumpScale * texture2D( bumpMap, vUv + dSTdy ).x - Hll;
    return vec2( dBx, dBy );

The code is based on this whitepaper: "Bump Mapping Unparametrized Surfaces on the GPU" by Morten S. Mikkelsen



You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .