This post is the continuation of the previous post. (link)
In this post, I will continue from where I finished the previous post. First, I will talk about how to compute the normal vectors for a surface defined by a height map, and how they get transformed when we transform the underlying height map. After that, I will derive to proper formula for the normal vector of a surface whose vertices are offset with a height map. I used this formula for computing the normal in the project I talked about in the previous post. (For more info, please check the previous post.)
Connection between the normal and the height map
Let’s take a height map, i.e. a function. Let’s assume that this function is smooth. The graph of this function,
,
defines a smooth surface in
.
What is the normal vector of this surface? Or in other words, what is the normal map, corresponding to the height map ? To calculate that, we can take the cross product of the tangent and bitangent vectors, and normalize the result. So, for the unnormalized normal vector (denoted by
) we have
and for the normal vector, we have . Now, what happens if we scale the height map by some number
, that is, we consider the height map
? The partial derivatives will get multiplied by
as well, so to compute the new normal from the old one, we have to do the following steps:
- Divide the old normal by its z coordinate to get the old unnormalized normal back.
- Multiply the first two coordinates of the old unnormalized normal by
to get the new unnormalized normal.
- Normalize the new unnormalized normal.
As you can see, the normal vector does not behave so nicely during the transformation of the height map, the partial derivatives (gradient) behave in a much simpler fashion. In fact, if we apply a more complicated transformation to the height map (for example multiply, add, divide two height maps together, take the exponential, logarithm, sine, square root of a height map, or whatever you like), the gradient of the new height map can be computed relatively easily and intuitively from the old gradient map, using the chain rule for derivation. Then the transformed normal map can be computed from the transformed gradient map.
In my opinion, it’s always more straightforward to work with the gradient map directly instead of the normal map. It’s not only that gradient maps can be transformed in a more naturally way, but in case of linear sampling and mip level generation they produce more correct results. This easily follows from the fact that differentiation is a linear operation, but the operation of taking the normal vector is not. For example, in this project, to compute the mip levels for a normal map I generate all mip levels of the underlying height map, and generate each level of the normal map from the corresponding level of the height map, instead of generating the biggest level of the normal map from the height map, and generating the mip levels of it by downsampling.
Also, as we will see below, if we want to apply normal mapping properly, what we need directly is not the normal map of the height map, it’s rather the gradient map of it.
Normal mapping
So let’s see, what we have and what we want. We have a surface . As I wrote at the end of the previous post, we can assume that the texture coordinates of a point
is
. We also have a height map
. Let’s define the function
in a way that
is the normal vector of the surface at the point
. (This map is called the Gauss map of the surface
.) From
and
we can define a new surface
by offsetting the surface
along its normal by the amount
, that is,
.
(This is the equation I use in the vertex shader to offset the vertices.) What we want is to find the normal vector of the surface . For that, we have to compute the tangent and bitangent vectors of this surface, and take the cross product of them. For the tangent vector, we have
. The terms look familiar, except for one: we haven’t seen the
term yet. This term represents how the normal vector changes if we move along the tangent direction on the surface. It can be viewed as a second order term for describing the surface (the first order terms are the tangent and bitangent vectors), and can be computed from the first and second partial derivatives of the surface
. Without going into too much details, I say that in differential geometry there is a thing called the shape operator (or Weingarten map) for a given surface. This operator tells us how the normal vector changes along a vector from the tangent plane on the surface, and this is exactly what we need. It can be represented by a 2×2 matrix
which for the surface
can be computed as
where denotes the usual scalar product on
.
I mention that the shape operator is related to the curvature of the surface: its eigenvalues are the principal curvatures, its determinant (the product of the eigenvalues) are the Gaussian curvature, the half of its trace (the sum of the element in the main diagonal, or the sum of the eigenvalues) is the mean curvature of the surface.
For us, the important thing is that we have . (Don’t care about the negative sign, let’s just accept it as a convention.) So for the tangent vector of the offset surface we have
,
where is the usual (but not orthogonalized!) TBN matrix. Similarly, for the bitangent vector of the offset surface we have
.
And actually, that’s it. As usually, the cross product of the tangent and bitangent vectors gives the unnormalized normal , and normalizing that we get the required normal vector
.
Computation in practice
In order to do the above computation in the pixel shader, we need the following data
- TBN matrix (at least the tangent and bitangent vectors),
- the height map,
- the gradient map (or indirectly, the normal map) of the height map,
- the shape operator of the mesh.
The shape operator can be placed in the vertex data, next to the TBN matrix. Note, that even if the TBN matrix is not orthogonal, the normal vector is always perpendicular to the tangent vectors, and has unit length, so it can be computed on the fly, doesn’t have to be stored.
The shape operator is not large, it’s four floats. But anyway, is this the shape operator really necessary? As I said above, it can be seen as the second order terms of the surface, so in some way, they are less significant as the first order terms (the tangent and bitangent vectors). For the shapes in the video in the previous post I didn’t see too big difference between using the shape operator in the computation, or assuming it is zero. I saw only a little change in the rendered image when I switched on and off the shape operator in the shader (and hot-reloaded it). I want to do more experiments with it in the future, but it’s obvious from the above calculation, that the bigger the vertex offset (the values in the height map), the more significantly the shape operator influences the normal. Also, of course, the matrix of the shape operator depends on the actual surface, the more it curves, the bigger the values in the matrix are. For example, the shape operator of the plane (not surprisingly) is the zero matrix, and the shape operator of a sphere with radius is
.
The case when TBN is orthogonal
Let’s finish with assuming that the TBN matrix is orthogonal, the shape operator is zero, and compute the normal in this case. Hopefully, we will get back the formula that is usually used for normal mapping.
If the shape operator is zero, we have
, and
.
And if TBN is orthogonal, we have
.
Does this look familiar? I hope it does. The last term in the above equation is the unnormalized normal of the height map that we’ve seen before. That is what is usually stored in the normal map, and that’s what we multiply by the TBN matrix to get the normal vector of the normal mapped surface.