【问题标题】:Reading height value from the heightmap [closed]从高度图中读取高度值[关闭]
【发布时间】:2020-10-07 22:02:24
【问题描述】:

我正在尝试从 CPU 和 GPU 上的高度图读取高度,但我收到了两个不同的值。我将高度图加载为 CPU 上的图像数据和 GPU 上的纹理:

vd::img::IMGLoader imgLoader;
heightImg = imgLoader.loadFloatImage(tokens[0]);

heightMap = vd::model::TextureService::get(tokens[0]);
heightMap->bind();
heightMap->bilinearFilter();
heightMap->unbind();

其中 tokens[0] 是磁盘上高度图的路径,loadFloatImage 实现:

ImageFPtr stbiIMGLoader::loadFloatImage(const std::string& path)
{
    int x, y, n;
    int force_channels = 4;
    unsigned char* image_data = stbi_load(path.c_str(), &x, &y, &n, force_channels);
    if (!image_data)
    {
        vd::Logger::warn("Could not load " + path);
        return nullptr;
    }

    if ((x & (x - 1)) != 0 || (y & (y - 1)) != 0)
    {
        vd::Logger::log("Texture " + path + " is not power-of-2 dimension");
    }

    ImageF _img(x, y);

    size_t len = x * y * 4;
    for (size_t i = 0; i < len; i += 4)
    {
        float r = ((float)image_data[i] / 255.0f);
        float g = ((float)image_data[i + 1] / 255.0f);
        float b = ((float)image_data[i + 2] / 255.0f);
        float a = ((float)image_data[i + 3] / 255.0f);
        PixelF pixel(r, g, b, a);
        _img.expand(pixel);
    }
    stbi_image_free(image_data);

    ImageFPtr imagePtr = std::make_shared<ImageF>(_img);

    imagePtr->reverse();

    return imagePtr;
}

在这里,我在图像上创建了一个包装器,基本上,它有一个像素的 std::vector(4 个浮点数,值在 [0.0, 1.0] 内)。 expand 方法只是推回向量, reverse 方法是水平翻转图像。 TextureService 类做同样的事情,但加载一个字节图像(每个像素包含 4 个字节,其值在 {0, 1, ..., 255} 内)。加载图像后,它使用

创建纹理
glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_2D, id);

glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, &(imagePtr->data[0]));

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        
glBindTexture(GL_TEXTURE_2D, 0);

加载纹理后,bilinearFilter 只需将 MIN 和 MAG 滤镜切换到 GL_LINEAR。

在 CPU 上,我使用以下方法计算 (x, z) 对的高度值:

float TerrainConfig::getHeight(float x, float z) const {
    float upperBound = scaleXZ / 2.0f;
    float lowerBound = -upperBound;

    if (x < lowerBound || z < lowerBound || x >= upperBound || z >= upperBound)
        return 0.0f;

    // reverse transform
    float rx = (x + (scaleXZ / 2.0f)) / scaleXZ;
    float rz = (z + (scaleXZ / 2.0f)) / scaleXZ;

    const auto height = vd::img::ImageHelper::texture(*heightImg, glm::vec2(rz, rx)).r;

    return height * scaleY;
}

用纹理方法实现:

Pixel<T> ImageHelper::texture(const Image<T>& image, const glm::vec2& uv) {
    glm::vec2 uvCoords = uv;

    if (uvCoords.x < 0.0f || uvCoords.x > 1.0f || uvCoords.y < 0.0f || uvCoords.y    > 1.0f) {
       vd::Logger::warn("UV coords exceed image, down sampling to accepted values");

       uvCoords.x -= std::floor(uvCoords.x);
       uvCoords.y -= std::floor(uvCoords.y);
    }

    float uv_x = uvCoords.x * (image.width - 1);
    float uv_y = uvCoords.y * (image.height - 1);

    int uv_x_0 = int(std::floor(uv_x));
    int uv_y_0 = int(std::floor(uv_y));
    int uv_x_1 = uv_x_0 + 1;
    int uv_y_1 = uv_y_0 + 1;

    if ((std::abs(uv_x - std::floor(uv_x)) < std::numeric_limits<float>::epsilon() &&
         std::abs(uv_y - std::floor(uv_y)) < std::numeric_limits<float>::epsilon()) ||
         (uv_x_1 >= image.width || uv_y_1 >= image.height))
    {
        return image.at(uv_x_0, uv_y_0);
    }

    Pixel<T> v0 = image.at(uv_x_0, uv_y_0);
    Pixel<T> v1 = image.at(uv_x_0, uv_y_1);
    Pixel<T> v2 = image.at(uv_x_1, uv_y_0);
    Pixel<T> v3 = image.at(uv_x_1, uv_y_1);

    float frac_x = uv_x - float(uv_x_0);
    float frac_y = uv_y - float(uv_y_0);


    Pixel<T> v01(
        glm::mix(v0.r, v1.r, frac_x),
        glm::mix(v0.g, v1.g, frac_x),
        glm::mix(v0.b, v1.b, frac_x),
        glm::mix(v0.a, v1.a, frac_x)
    );

    Pixel<T> v23(
        glm::mix(v2.r, v3.r, frac_x),
        glm::mix(v2.g, v3.g, frac_x),
        glm::mix(v2.b, v3.b, frac_x),
        glm::mix(v2.a, v3.a, frac_x)
    );

    return Pixel<T>(
        glm::mix(v01.r, v23.r, frac_y),
        glm::mix(v01.g, v23.g, frac_y),
        glm::mix(v01.b, v23.b, frac_y),
        glm::mix(v01.a, v23.a, frac_y)
    );
}

在这里,我尝试实现相当于 GLSL 纹理的方法。我不确定这是最好的方法。

移动到地形,我使用四叉树结构,出于调试目的,我只保留了根节点(32x32)。每个根节点正在渲染一个 16 个顶点的补丁,对于每个节点,我计算一个模型矩阵以将补丁转换为地形内的根节点位置(获取值在 [0.0, 1.0] 内的顶点)和整个地形我应用模型矩阵将补丁转换为我想要的网格(我将其转换为 (-scaleXZ/2, 0.0f, -scaleXZ/2) 以使 (x: 0, z: 0) 坐标位于中心,然后按 (scaleXZ, scaleY, scaleXZ) 缩放它。

设置完成后,我实现了以下管道: VS:

#version 430

layout (location = 0) in vec2 vPosition;

out vec2 tcTexCoords;

uniform mat4 localModel;
uniform mat4 worldModel;

void main() {
    // Compute local coordinates
    vec2 localCoords = (localModel * vec4(vPosition.x, 0.0f, vPosition.y, 1.0f)).xz;

    // Pass texcoords
    tcTexCoords = localCoords;

    // Compute world coordinates
    vec4 worldCoords = worldModel * vec4(localCoords.x, 0.0f, localCoords.y, 1.0f);

    // Set vertex position
    gl_Position = worldCoords;
}

TCS:

#version 430

layout (vertices = 16) out;

in vec2 tcTexCoords[];
out vec2 teTexCoords[];

uniform float tessellationOuterLevel;
uniform float tessellationInnerLevel;

/*
               D        Outer[3] / CD      C
            (0, 1) -------------------- (1, 1)
               |                           |
               |                           |
               |                           |
 Outer[0] / DA |                           | Outer[2] / BC
               |                           |
               |                           |
               |                           |
            (0, 0) -------------------- (1, 0)
               A        Outer[1] / AB      B
*/

// patch edges indices
const int AB = 1;
const int BC = 2;
const int CD = 3;
const int DA = 0;

void main() {
    // control only the first call
    if (gl_InvocationID == 0) {
        gl_TessLevelOuter[AB] = tessellationOuterLevel;
        gl_TessLevelOuter[BC] = tessellationOuterLevel;
        gl_TessLevelOuter[CD] = tessellationOuterLevel;
        gl_TessLevelOuter[DA] = tessellationOuterLevel;

        gl_TessLevelInner[0] = tessellationInnerLevel;
        gl_TessLevelInner[1] = tessellationInnerLevel;
    }

    teTexCoords[gl_InvocationID] = tcTexCoords[gl_InvocationID];
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

测试:

#version 430

layout (quads, equal_spacing) in;

in vec2 teTexCoords[];
out vec2 gTexCoords;

uniform float patchHeights[16];

uniform sampler2D heightMap;
uniform float scaleY;

void main() {
    float u = gl_TessCoord.x;
    float v = gl_TessCoord.y;

    while (patchHeights[0] == 0.0f) break;

    // using corners of the patch, we can compute the position
    // 0 = bottom left, 3 = bottom right, 12 = top left, 15 = top right
    // vec4 v0 = vec4(gl_in[0].gl_Position.x, patchHeights[0], gl_in[0].gl_Position.z, 1.0f);
    // vec4 v3 = vec4(gl_in[3].gl_Position.x, patchHeights[3], gl_in[3].gl_Position.z, 1.0f);
    // vec4 v12 = vec4(gl_in[12].gl_Position.x, patchHeights[12], gl_in[12].gl_Position.z, 1.0f);
    // vec4 v15 = vec4(gl_in[15].gl_Position.x, patchHeights[15], gl_in[15].gl_Position.z, 1.0f);
    vec4 v0 = gl_in[0].gl_Position;
    vec4 v3 = gl_in[3].gl_Position;
    vec4 v12 = gl_in[12].gl_Position;
    vec4 v15 = gl_in[15].gl_Position;

    vec4 position = (
        (1 - u) * (1 - v) * v15 +
        u * (1 - v) * v3 +
        u * v * v0 +
        (1 - u) * v * v12
    );

    vec2 texCoords = (
        (1 - u) * (1 - v) * teTexCoords[15] +
        u * (1 - v) * teTexCoords[3] +
        u * v * teTexCoords[0] +
        (1 - u) * v * teTexCoords[12]
    );

    position.y = texture(heightMap, texCoords).r * scaleY;

    gTexCoords = texCoords;
    gl_Position = position;
}

在这里,我使用 patchHeights 通过在 CPU 上读取的统一高度值发送。它是这样工作的,但问题是插值高度时,地形看起来很丑,有非常尖锐的山峰和笔直的山丘。

在最后一步,我使用 GS 将 ProjectionView 矩阵应用到 gl_Position:

#version 430

layout(triangles) in;
layout(triangle_strip, max_vertices = 3) out;
....
void main() {
    for (int i = 0; i < gl_in.length(); ++i) {
        vec4 worldCoords = gl_in[i].gl_Position;
        vec4 eyeSpaceCoords = view * worldCoords;
        gl_Position = projection * eyeSpaceCoords;
        ....
    }
}

我尝试了很多更改,例如:在 texCoords 上将 XY 与 YX 交换,在 getHeight 方法上交换坐标,如通过统一发送高度,读取 VS 上的高度,并让 TES 对其进行插值,但似乎没有随心所欲地工作。

我发现的一个有趣的事实是,我使用相同的高度图来计算 splatmap。我正在使用计算着色器来执行此操作,因此我使用 GLSL 纹理从高度图中读取。我将 scaleY 设置为 160,并创建了一些间隔,[0, 7.5] - sand, [7.5, 45.0] - Grass1, [45.0, 95.0] - Grass2, [95.0, 143.0] - rock1, [143.0, 160.0] - rock2,splatmap 计算正确,但在应用程序中,我看不到最后一个材质,因为地形的最大海拔达到 ~111,但玩家对象 (它使用我在 CPU 上的纹理实现来读取完全相同位置的高度)达到 ~140。如果我和玩家一起移动,玩家似乎会跟随地形高程,但它向上移动了一些价值。当我最终达到 0.0 的高度时,CPU 和 GPU 都返回 0,所以从这里我假设它不是地形 Y 轴上的平移。

玩家海拔 ~140,地形 ~111

下山,跟随地形高程,但有一些偏移

在 0 高度,CPU 和 GPU 都返回 0

TL;DR:我正在尝试从 CPU 上的高度图读取高度值(使用我实现的方法)并返回与 GPU 不同的值(使用 GLSL 纹理方法)。

【问题讨论】:

  • 我敢打赌,您需要使用GL_NEAREST 过滤器来禁用高度纹理的插值,因为GL_LINEAR 仍然会进行插值,因此这些值与非插值 CPU 端的值不同。这种插值以及与网格的实际对齐位置也可能导致您描述的移位/偏移。这也解释了缺少的顶部高度,因为您很可能在它附近而不是在其确切位置上获取纹理像素,并且由于它是尖峰,即使非常靠近峰本身,高度也低得多。但这也可能完全不同,所以请尝试检查它是否有帮助
  • 我也在 CPU 上使用线性插值,使用 glm::mix,但我也尝试切换到 GL_NEAREST 并从 CPU 上的线性插值与最近邻插值 auto nearestNeighbour = [](float a, float b, float x) { return (x &lt; 0.5f) ? a : b; }; 交换。结果是完全一样的。没有任何改变。
  • 它是一个包装类,我在其中存储纹理,这就是构造函数。我还没有优化它(以及我读了两次图像)。一旦我解决了这个问题,我想优化事情。这会是个问题吗?
  • 是的,这绝对是个问题,因为使用 sRGB 格式会导致在采样时对纹理值进行非线性重新映射。在您的示例中,您声明在比例为 160 的纹理上,您在 CPU 上获得 ~140,在 GPU 上获得 ~111,这分别转换为 0.8750.69 的标准化值。如果 0.875 存储在纹理中,则 sRGB 转换将产生0.739(或大约 118)。这仍然有点偏离,所以我不确定你的数字是不精确还是有其他问题,但 sRGB 转换肯定是你的偏移量的一个重要因素。
  • @derhass 您应该将该评论转换为答案

标签: c++ opengl glsl


【解决方案1】:

来自@Rabbid76 的评论:

为什么高度图使用内部格式GL_SRGB_ALPHA

那是正确的。使用 sRGB 格式将导致在采样时对纹理值进行非线性重新映射。如definition of the sRGB color space,映射为:

        / x / 12.92                      if x <= 0.04045
f(x) = {
        \ ((x + 0.055) / 1.055)^2.4      otherwise

在您的示例中,您声明在比例为 160 的纹理上,您在 CPU 上获得 ~140,在 GPU 上获得 ~111,这分别转换为 0.8750.69 的标准化值。如果0.875 存储在纹理中,则上述sRGB 转换将产生0.739(或您的缩放高度约118)。这仍然有点偏离,所以我不确定您的数字是否不精确或有其他问题,但 sRGB 转换肯定是您的偏移量的一个重要因素。

当您想以 8 位精度存储“常规”RGB 彩色图像数据,但又想在物理线性颜色空间中进行照明计算等时,首先使用 sRGB 纹理格式非常有用。对于其他一些类型的数据,它也可能会派上用场,如果您基本上需要在 0 附近有更高的精度,而对 1 的精度要低得多,但我认为这对于标准高度场数据来说并不理想(即使您想使用为此,您需要在将线性高度值放入纹理之前对其进行反向预转换)。

【讨论】:

    猜你喜欢
    • 2023-01-25
    • 1970-01-01
    • 1970-01-01
    • 2020-05-14
    • 2012-08-15
    • 1970-01-01
    • 1970-01-01
    • 2015-05-11
    • 2011-02-04
    相关资源
    最近更新 更多