【发布时间】: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 轴上的平移。
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 < 0.5f) ? a : b; };交换。结果是完全一样的。没有任何改变。 -
它是一个包装类,我在其中存储纹理,这就是构造函数。我还没有优化它(以及我读了两次图像)。一旦我解决了这个问题,我想优化事情。这会是个问题吗?
-
是的,这绝对是个问题,因为使用 sRGB 格式会导致在采样时对纹理值进行非线性重新映射。在您的示例中,您声明在比例为 160 的纹理上,您在 CPU 上获得 ~140,在 GPU 上获得 ~111,这分别转换为
0.875和0.69的标准化值。如果 0.875 存储在纹理中,则 sRGB 转换将产生0.739(或大约 118)。这仍然有点偏离,所以我不确定你的数字是不精确还是有其他问题,但 sRGB 转换肯定是你的偏移量的一个重要因素。 -
@derhass 您应该将该评论转换为答案