5
\$\begingroup\$

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:

<code>height map</code>

Here is my generated normal map:

generated normal map

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

reference normal map

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?

\$\endgroup\$
3
  • 2
    \$\begingroup\$ 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? \$\endgroup\$
    – DMGregory
    Nov 23, 2018 at 2:13
  • \$\begingroup\$ 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. \$\endgroup\$
    – Jarrett
    Nov 23, 2018 at 5:21
  • \$\begingroup\$ If you've solved your problem, please share your solution as an answer so it can help others too. :) \$\endgroup\$
    – DMGregory
    Nov 23, 2018 at 15:48

1 Answer 1

4
\$\begingroup\$

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.

Thanks to @DMGregory for pointing me in the right direction.

\$\endgroup\$

You must log in to answer this question.

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