3
\$\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\$
  • 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 '18 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 '18 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 '18 at 15:48
3
\$\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.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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