From what I understand, grey-scale bump maps are used to perturb normals on a surface such that the surface appears to have bumps. With a regular tangent normal map, it seems relatively intuitive enough, considering each pixel contains the 'normal' itself so to speak

A bump map only measures changes in height as far as I know. In a software like Blender, I can plug in a grey-scale bump map and get results similar to the bumps produced by a normal map (Considering they more or less produce the same end result - to perturb normals), so certainly, these ARE being converted to normals somehow

The question is how exactly. I've being cracking my head over this for so long and after dozens of searches, can't find exactly what I need

From what I know so far, since you only store height info in a bump map, the only meaningful way to 'extract' normals out of it is via finding the partial derivative along each axis of the bump map, and then.....I'm sorta blank in regards to the rest

From what I know, to find the partial derivative along each axis, you could use the central difference method (or forward difference method), to find how much the function f(u,v) [that returns a pixel representing a height value at the coordinates on the map (u,v)] changes as we move from left to right (for partial derivatives along the x axis or u), and up to down for the y/v axis

So we find how much the function changes as we move along either of the two axes. What do we do next to finally attain the normal? I am sorta lost. I would like some help with the intuition behind it as well rather than just plain equations since I really want to understand them thoroughly

I will also add that I am just delving in the theory now, I haven't done any computer graphics programming YET, so I dont need any API specific help


2 Answers 2


I have rewritten my answer to make it easier to follow and I also fixed some mistakes.

Let $S:[0,1]^2 \rightarrow \mathbb{R}^3$ be some parameterization of your surface (assume that the derivatives do not become zero anywhere, i.e. it's regular/an immersion). That is, the set of points of your surface is $\mathcal{M} = \{S(\beta, \gamma) \,:\, (\beta, \gamma) \in [0,1]^2\}$. Let $h:[0,1]^2\rightarrow \mathbb{R}$ be the heightmap. The unit normal $n:[0,1]^2\rightarrow \mathbb{R}^3$ is given by the normalized cross product of the partial derivatives: $n = \frac{\partial_\beta S \times \partial_\gamma S}{\|\partial_\beta S \times \partial_\gamma S\|}$. Then we displace the surface with the heightmap along the normal in order to construct a new surface $T(\beta, \gamma) = S(\beta, \gamma) + h(u(\beta, \gamma),v(\beta, \gamma))n(\beta, \gamma)$. We want to find the normal of $T$:

$$m = \partial_\beta T \times \partial_\gamma T$$ $$\partial_\beta T = \partial_\beta S+(\partial_\beta h)n + h(\partial_\beta n), \quad \partial_\gamma T = \partial_\gamma S+(\partial_\gamma h)n + h(\partial_\gamma n)$$ $$ \partial_\beta T \approx \partial_\beta S+(\partial_\beta h)n, \quad \partial_\gamma T \approx \partial_\gamma S+(\partial_\gamma h)n$$ $$m\approx (\partial_\beta S+(\partial_\beta h)n)\times(\partial_\gamma S+(\partial_\gamma h)n) = $$ $$\require{cancel}\partial_\beta S\times\partial_\gamma S + (\partial_\beta h)(n\times\partial_\gamma S) + (\partial_\gamma h)(\partial_\beta S\times n) + \cancel{(\partial_\beta h)(\partial_\gamma h)(n\times n)}$$

The above expression gives you an approximation of the (not necessarily unit length) normal of the displaced surface $T$. This is Blinn's approximation - it assumes that $h$ is small enough for $h(\partial_\beta n)$ and $h(\partial_\gamma n)$ to be ignored.

Let us consider how this can be applied in practice. There you usually have a triangle mesh. Then if we know how to modify the normal for every triangle, we know how to modify it for the whole mesh. Consider a specific triangle with vertices $p_0, p_1, p_2 \in \mathbb{R}^3$. We can parametrize the triangle through barycentric coordinates $\alpha, \beta, \gamma \geq 0, \alpha + \beta + \gamma = 1$:

$$S(\beta, \gamma) = \alpha p_0 + \beta p_1 + \gamma p_2 = \\ (1-\beta-\gamma) p_0 + \beta p_1 + \gamma p_2 = p_0 + \beta (p_1-p_0) + \gamma (p_2-p_0).$$

We can now compute the partial derivatives of $S$:

$$\partial_{\beta} S = p_1 - p_0, \, \partial_{\gamma} S = p_2 - p_0,$$

which turn out to be the edges of the triangle. Then the geometric normal is:

$$n_g = \partial_{\beta}S \times \partial_\gamma S, \, \hat{n}_g = \frac{n_g}{\|n_g\|}.$$

We also need $\partial_{\beta} h, \, \partial_\gamma h$ in order to be able to evaluate the perturbed normal. Let the texture coordinates at the 3 triangle vertices be $uv_0, uv_1, uv_2 \in \mathbb{R}^2$, then the reparametrization from barycentric coordinates to UV coordinates is given by:

$$uv(\beta, \gamma) = uv_0 + \beta (uv_1-uv_0) + \gamma (uv_2-uv_0).$$

We can now compute the partial derivatives:

$$\partial_\beta h(uv(\beta, \gamma)) = (uv_1-uv_0) \cdot (\partial_u h(uv(\beta, \gamma)),\partial_v h(uv(\beta, \gamma)))$$ $$\partial_\gamma h(uv(\beta, \gamma)) = (uv_2-uv_0) \cdot (\partial_u h(uv(\beta, \gamma)),\partial_v h(uv(\beta, \gamma)))$$

The finally the perturbed normal is:

$$m_g = \partial_{\beta}S\times\partial_{\gamma}S + (\partial_{\beta} h)\hat{n}_g\times\partial_{\gamma}S + (\partial_{\gamma}h)\partial_{\beta}S\times \hat{n}_g.$$

To compute $\partial_u h, \, \partial_v h$ you can use finite differences:

$$\partial_u h(u,v) \approx \frac{h(u+\epsilon,v) - h(u,v)}{\epsilon}, \, \partial_v h(u,v) \approx \frac{h(u, v+\epsilon) - h(u,v)}{\epsilon}$$

Here is some code to clarify how this would be implemented:

// Assume the following is given for a triangle
vec3 p0, p1, p2;    // positions of the 3 vertices
vec2 uv0, uv1, uv2; // texture coordinates at the 3 vertices
// epsilon for the finite differences
float eps;
// we want to compute the normal at bary coords (beta, gamma)
float beta, gamma;  // barycentric coordinates

// compute edges
vec3 e1 = p1-p0;
vec3 e2 = p2-p0;

// compute geometric normal
vec3 n_g = cross(e1,e2);
vec3 n_g_n = normalize(n_g);

// compute n x (d_gamma S)
vec3 t_g = -cross(n_g_n, e2);

// compute (d_beta S) x n
vec3 b_g = -cross(e1, n_g_n);

// compute partial derivatives of h
vec2 uv = uv0 + beta * (uv1-uv0) + gamma * (uv2-uv0);
vec2 h_uv = 1/eps * (vec2(h(uv+vec2(eps,0)), h(uv+vec2(0,eps))) - vec2(h(uv)));
float h_beta = dot(uv1-uv0, h_uv);
float h_gamma = dot(uv2-uv0, h_uv);

// compute coefficients
float coef_n = 1.0;
float coef_t = -h_beta;
float coef_b = -h_gamma;

// compute new geometric normal
vec3 m_g = coef_n * n_g + coef_t * t_g + coef_b * b_g;

Note that this matches how normal mapping is sometimes defined, with a sampled normal $(c_n, c_t, c_b) = (1, -\partial_\beta h, -\partial_\gamma h)$:

$$m_g = c_n (\partial_\beta S \times \partial_\gamma S) + c_t (\partial_\gamma S\times\hat{n}_g) + c_b (\hat{n}_g\times \partial_\beta S).$$

You may notice that the cross product arguments have been switched around in the last two term. This is because of how tangent space is defined when doing normal mapping. You will see I have done the same in the code (the minuses involved in $t_g, b_g$). The above doesn't take into account shading normals. When considering those, various approaches have been proposed. I can suggest looking into Simulation of Wrinkled Surfaces Revisited by Mikkelsen for more details on that.

  • $\begingroup$ Admittedly a LOT of the math here is going over my head so I am having trouble understanding it. I recall seeing a few of these equations on a site that explained bump maps : pbr-book.org/3ed-2018/Materials/Bump_Mapping I did have trouble understanding them though. Is there a way to understand this without heavy math notations? $\endgroup$
    – Hash
    Nov 15, 2021 at 9:53
  • $\begingroup$ @Hash The math is saying: deform every point of your original surface $S$ by moving along its normal by the corresponding value in the heightmap - this yields a displacement surface $T$. You want to find the normal of this displacement surface $T$, so that you can use this normal instead of the actual one. The key statement that I use here is that the normal of a surface is given by the cross product of its partial derivatives (e.g. for a triangle it's the cross product of its edges). For partial derivatives see: tutorial.math.lamar.edu/classes/calciii/PartialDerivsIntro.aspx $\endgroup$
    – lightxbulb
    Nov 15, 2021 at 10:39
  • $\begingroup$ Yes, I got that bit, but how do we obtain the partial derivatives exactly? To crossproduct, each of the 2 vectors involved in the crossproduct would have 3 components (x,y,z). How would we get these individual components? $\endgroup$
    – Hash
    Nov 15, 2021 at 11:32
  • $\begingroup$ @Hash Try the last formula for $m$ in the edit. I am not sure whether this is exactly what they do in practice though. You can look at some implementation to figure out what hack different people use to take into account shading normals. $\endgroup$
    – lightxbulb
    Nov 15, 2021 at 16:35
  • $\begingroup$ I find a lot of these math terms rather difficult to parse and understand (mostly due to my lack of familiarity with them). I'll try my level best to read up on the underlying math topics soon, and come back to this in the future to understanding and ask any further doubts $\endgroup$
    – Hash
    Nov 15, 2021 at 20:20

You are on the right track with your post...

Compute two slopes then somehow use those slopes to find a normal. The slopes are the result of finite difference (like central difference)

But why two slopes? Because we can use them to construct two independent vectors whose cross product will become our normal vector.

One vector is in the texture's $u$ direction and is simply $vec3(1,0,slopeU)$

The other vector is in the textures $v$ direction and is $vec3(0,1,slopeV)$

Where are these vectors coming from? Take a look at a 2D cartesian coordinate system such as this one. Now overlay the height map texture over the cartesian coordinate system with the point we are interested in located at the origin. The u direction of the texture lines up with the x direction of the coordinate system. Compute the u direction slope for the heightmap at that point using central differences. This samples a=(-1,0) and b=(1,0) on the cartesian coordinate system. These two points form a line on the x axis. This is the same direction the vector needs to point. Remember that vectors have direction but no location, so as long as we pick a vector that points in the same direction as the slope calculation then it doesn't matter what values we plug into the vector, just that it has the right direction. A natural choice for the vector in the same direction as the slope is (1,0) combined with the slope gives (1,0,slopeU).

With the two directions computed, and the two vectors created: Take the cross product and normalize, its really that simple.

Both of the vectors are tangent to the height, and they are independent of each other. They can be constructed as $(0,1,S_v)$ and $(1,0,S_u)$ because the slopes are very specifically taken in those directions. If you change the directions used to compute the slopes then you have to change the directions used in the vectors to match.

There are a few other minor caveats, like the calculations are assumed to always happen at the origin, and the height map, which usually contains values between 0 and 1 needs to be scaled to its final height.

Origin and direction choices: Yes, the choice to do the calculations at the origin is completely arbitrary. It just makes the math more convenient.

The choice of the (1,0) and (0,1) is not arbitrary. These are chosen because they lie in the exact same direction as the computed slopes. If we chose (12,243,slopeU) the question becomes where does this vector point? Well this vector points very heavily toward the y axis. (the y value of 243 ) so our calculation of the slope along the x (slopeU) axis will be used to compute a slope pointing nearly at the y axis and the end result will give a very skewed value. For this reason, most applications assume a grid spacing of 1 between height map points.

Theory is all nice, but where the rubber meets the road we need something that can be implemented, and runs in a timely manner. These functions can be implemented naively on the CPU or easily translated to a Compute Shader. They can also be used in real time to compute normals for height mapped geometry like water and terrain. Also the functions come straight out of the book FGED2 which gives brief but good explanation of the math here. This implementation of central difference can also be easily extended to 3 dimensions where it can be used to compute normals for isosurface extraction in real time.

slope in x direction: $d_x = \frac{\Delta z}{ \Delta x} = \frac{scale}{2}[h(i+1,j)-h(i-1,j)]$

slope in y direction: $d_y = \frac{\Delta z}{ \Delta y} = \frac{scale}{2}[h(i,j+1)-h(i,j-1)]$

x direction vector: $u_x = (1,0,d_x)$

y direction vector: $u_y = (0,1,d_y)$

Final Value: $m = normalize(u_x \times u_y ) = \frac{(-d_x,-d_y,1)}{\sqrt{d^2_x+d^2_y+1}}$

  • $\begingroup$ Yes I had seen this bit elsewhere but froze at the vectors themselves that you had mentioned. How and why exactly are the components of the vectors (1,0, slopeU) and (0,1, slopeV)? What is the intuitive reasoning as to how we got those components? I can tell, that for the first one , we get it by doing (dx/dx, dy/dx , df(u,v)/dx) which gives us (1, 0 , slopeU). But why exactly do we do that? Why does it give us the tangent vector? $\endgroup$
    – Hash
    Nov 15, 2021 at 12:50
  • $\begingroup$ I added an edit to the answer to help explain where the vectors come from. $\endgroup$
    – pmw1234
    Nov 15, 2021 at 13:40
  • $\begingroup$ After having gone through your edit, I have a few more questions. Why must the height map be centered at the origin? The choice seems arbitrary. Is there an actual reason? Also in " A natural choice for the vector in the same direction as the slope is (1,0) combined with the slope gives (1,0,slopeU).", why exactly do we combine (1,0) with the slope? I understand we just need the vector to point in the direction we want, but if it was say (12,243,slopeU) or something similar, it would point in a diff direction. Is there a reason we specifically took (1,0)? $\endgroup$
    – Hash
    Nov 15, 2021 at 19:22
  • $\begingroup$ I added a bit of explanation for your question above, and added some math showing a sample computation using central differences. $\endgroup$
    – pmw1234
    Nov 16, 2021 at 12:34
  • $\begingroup$ It is worth mentioning that this description isn't all that different from @lightxbulb. The major difference is this description uses several constants values which allows some of the math to be simplified. The reason I gave this particular description is because your question very specifically requested how to get normal maps for bump maps. The explanation above is pretty much industry standard with a few variations on the theme. $\endgroup$
    – pmw1234
    Nov 16, 2021 at 13:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.