# Calculating normal map from height map using Sobel operator

So I'm trying to generate a normal map from a greyscale height map. Unfortunately the result I'm getting doesn't seem quite right.

Here is the height map I'm using to generate the normal map:

Here is my generated normal map:

For reference, here is the normal map that gets generated from http://cpetry.github.io/NormalMap-Online/:

One thing I notice in my normal map is that there seems to be edges. I'm not really sure why I am getting these edges - does anyone know why?

Here's the code that I'm using to generate the normal map:

// Source:
// https://stackoverflow.com/questions/2368728/can-normal-maps-be-generated-from-a-texture
// and
// http://www.catalinzima.com/2008/01/converting-displacement-maps-into-normal-maps/
float32 height(const std::vector<char>& data, const int width, const int height, uint32 x, uint32 z)
{
if (x >= width)  x %= width;
while (x < 0)    x += width;
if (z >= height) z %= height;
while (z < 0)    z += height;

return static_cast<float32>(data[z*width*4 + x*4 + 3]) / 255.0f;
}

uint8 textureCoordinateToRgb(const float32 value)
{
return (value + 1.0) * (255.0 / 2.0);
}

glm::vec3 calculateNormal(const std::vector<char>& data, const int width, const int height, uint32 x, uint32 z)
{
const float32 strength = 8.0f;

// surrounding pixels
float32 tl = height(data, width, height, x-1, z-1); // top left
float32  l = height(data, width, height, x-1, z);   // left
float32 bl = height(data, width, height, x-1, z+1); // bottom left
float32  t = height(data, width, height, x, z-1);   // top
float32  b = height(data, width, height, x, z+1);   // bottom
float32 tr = height(data, width, height, x+1, z-1); // top right
float32  r = height(data, width, height, x+1, z);   // right
float32 br = height(data, width, height, x+1, z+1); // bottom right

// sobel filter
const float32 dX = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl);
const float32 dY = (bl + 2.0 * b + br) - (tl + 2.0 * t + tr);
const float32 dZ = 1.0 / strength;

glm::vec3 n(dX, dY, dZ);
n = glm::normalize(n);

return n;
}

void calculateNormals(std::vector<char>& data, const int width, const int height)
{
for (int i = 0; i < width; ++i)
{
for (int j = 0; j < height; ++j)
{
const glm::vec3 n = calculateNormal(data, width, height, i, j);

// convert to rgb
data[j*width*4 + i*4 + 0] = textureCoordinateToRgb(n.x);
data[j*width*4 + i*4 + 1] = textureCoordinateToRgb(n.y);
data[j*width*4 + i*4 + 2] = textureCoordinateToRgb(n.z);
}
}
}

std::unique_ptr<Image> generateFormattedHeightmap(const Image& image)
{
std::vector<char> data;
int width = image.width();
int height = image.height();

data.resize(width*height*4);

if (image.format() == Image::Format::FORMAT_RGB)
{
int j=0;
for (int i=0; i < data.size(); i+=4)
{
data[i] = image.data()[j];
data[i+1] = image.data()[j+1];
data[i+2] = image.data()[j+2];
data[i+3] = (image.data()[j] + image.data()[j+1] + image.data()[j+2]) / 3;

j+=3;
}
}
else if (image.format() == Image::Format::FORMAT_RGBA)
{
for (int i=0; i < image.data().size(); i+=4)
{
data[i] = image.data()[i];
data[i+1] = image.data()[i+1];
data[i+2] = image.data()[i+2];
data[i+3] = (image.data()[i] + image.data()[i+1] + image.data()[i+2]) / 3;
}
}

calculateNormals(data, width, height);

return std::make_unique<Image>(data, width, height, Image::Format::FORMAT_RGBA);
}


Does anyone have any ideas?

• The sharp edge seems to fall around the halfway point of your height gradient, separating heights <= 127 from those >= 128. That makes me thing the most significant bit of your heights is landing somewhere inopportune - could it be getting read as the sign bit of the float you're casting to, effectively inverting your tallest peaks into the deepest valleys halfway up? Nov 23, 2018 at 2:13
• Agh, it looks like you were right @DMGregory, thanks! My problem was in the float32 height(const std::vector<char>& data, const int width, const int height, uint32 x, uint32 z) function - casting the char to a float32 would cause the value to be negative. Nov 23, 2018 at 5:21
• If you've solved your problem, please share your solution as an answer so it can help others too. :) Nov 23, 2018 at 15:48

So it looks like my problem was in the float32 height(const std::vector<char>& data, const int width, const int height, uint32 x, uint32 z) function.
Casting the char to an unsigned char before casting it to a float32 fixes the issue.