You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用texelFetch的OpenGL 2D瓦片渲染代码异常,求排查

2D瓦片地图渲染:texelFetch始终返回0导致瓦片显示异常

代码实现

C++渲染器代码

#define TILE_NUM_INDICES 6

inline static u32 GetRandomIntBetween(u32 min, u32 max) {
    return (u32)rand() % (max - min + 1) + min;
}

static void GetRandomTileMap(u32* map, u32 size) {
    for (int i = 0; i < size; i++) {
        u32 r = GetRandomIntBetween(0, 23);
        map[i] = r;
    }
}
NewRenderer::NewRenderer(const NewRendererInitialisationInfo& info)
    :m_tileShader("shaders\TilemapVert2.glsl", "shaders\TilemapFrag2.glsl"),
    m_worldMapSize(info.tilemapSizeX, info.tilemapSizeY),
    m_tilemapChunkSize(info.chunkSizeX, info.chunkSizeY),
    m_windowWidth(info.windowWidth),
    m_windowHeight(info.windowHeight)
{
    using namespace std;
    const u32 mapsize = info.tilemapSizeX * info.tilemapSizeY;
    m_worldTextureBytes = make_unique<u32[]>(mapsize);
    GetRandomTileMap(m_worldTextureBytes.get(), mapsize);
    glGenTextures(1, &m_worldTextureHandle);
    glBindTexture(GL_TEXTURE_2D, m_worldTextureHandle);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // GL_NEAREST is the better filtering option for this game
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, info.tilemapSizeX, info.tilemapSizeY, 0, GL_RED, GL_UNSIGNED_INT, m_worldTextureBytes.get());
    glGenerateMipmap(GL_TEXTURE_2D);

    glGenVertexArrays(1, &m_vao);
}

void NewRenderer::DrawChunk(
    const glm::ivec2& chunkWorldMapOffset,
    const glm::vec2& pos,
    const glm::vec2& scale,
    float rotation,
    ArrayTexture2DHandle texArray,
    const Camera2D& cam
) const
{
    m_tileShader.use();
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, glm::vec3(pos, 0.0f));
    model = glm::rotate(model, glm::radians(rotation), glm::vec3(0.0f, 0.0f, 1.0f));
    model = glm::scale(model, glm::vec3(scale, 1.0f));

    m_tileShader.setMat4("vpMatrix", cam.GetProjectionMatrix(m_windowWidth, m_windowHeight));
    m_tileShader.setMat4("modelMatrix", model);
    m_tileShader.SetIVec2("chunkOffset", chunkWorldMapOffset);
    m_tileShader.SetIVec2("chunkSize", m_tilemapChunkSize);
    m_tileShader.setInt("masterTileTexture", 0);
    m_tileShader.setInt("atlasSampler", 1);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_worldTextureHandle);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D_ARRAY, texArray);

    glBindVertexArray(m_vao);
    glDrawArrays(GL_TRIANGLES, 0, m_tilemapChunkSize.x * m_tilemapChunkSize.y * TILE_NUM_INDICES);

}

顶点着色器

#version 440 core
/*
cpp setup:
create a big index buffer
*/
layout (location = 0) in vec2 pos;
layout (location = 1) in vec2 uv;
out vec3 TexCoords;

uniform mat4 vpMatrix;
uniform mat4 modelMatrix;
uniform ivec2 chunkOffset;
uniform ivec2 chunkSize;
uniform sampler2D masterTileTexture;

#define TILE_NUM_VERTS 4
#define NUM_TILE_INDICES 6
void main()
{

    // vertices and indices that make up two triangles (a quad)
    // ie one tile in the map
    vec4 vertices[TILE_NUM_VERTS] = vec4[TILE_NUM_VERTS](
        vec4(0.5f,  0.5f,   1.0f, 1.0f),
        vec4(0.5f, -0.5f,   1.0f, 0.0f),
        vec4(-0.5f, -0.5f,  0.0f, 0.0f),
        vec4(-0.5f,  0.5f,   0.0f, 1.0f)
    );
    int indices[NUM_TILE_INDICES] = int[NUM_TILE_INDICES](
        0, 1, 3,   // first triangle
        1, 2, 3    // second triangle
    );

    // cycle through indicies
    int index = indices[int(gl_VertexID % NUM_TILE_INDICES)];

    // get base vertex
    vec4 baseVertex = vertices[index];

    // which tile in the map is being drawn?
    int whichTile = gl_VertexID / NUM_TILE_INDICES;

    // transfrom into x y coords of tile in the chunk
    ivec2 tilexy = ivec2(int(whichTile / chunkSize.y), int(whichTile % chunkSize.y));

    // translate base vertex by tilexy
    baseVertex.xy += vec2(tilexy);

    // set the z coord of the tex coords passed based on what tile is here
    // in the master tile map.

    // based on shader output all steps up to here are successful, a grid is drawn.
    // The problem is the texelFetch is not working, it's always the same tile drawn.
    TexCoords = vec3(
        baseVertex.zw,
        // changing this to different hard coded values does change what tile is drawn as expectd so sampler2DArray is setup correctly
        float(texelFetch(masterTileTexture, tilexy + chunkOffset, 0).r)); 

    gl_Position = vpMatrix * modelMatrix * vec4(baseVertex.xy, 0.0, 1.0);
}

片段着色器

#version 440 core

uniform sampler2DArray atlasSampler;
in vec3 TexCoords;
out vec4 FragColor;

void main()
{
    FragColor = texture(atlasSampler, TexCoords);
}

问题描述

代码目标是通过存储瓦片索引的2D纹理(每个像素对应一个瓦片ID),结合纹理数组渲染2D瓦片地图。目前网格渲染正常,但所有瓦片均显示索引为0的纹理,texelFetch始终返回0。

已确认:

  • 瓦片索引纹理的缓冲区已填充0-23的随机值,数据正确。
  • 手动硬编码索引值时,纹理数组能正确显示对应瓦片,说明sampler2DArray配置正常。
  • 此前成功使用过texelFetch实现类似功能。

问题排查与修复

1. 纹理格式不匹配(核心问题)

创建瓦片索引纹理时,内部格式使用GL_RED(默认8位无符号整数),但传入的是32位无符号整数数据:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, ..., GL_UNSIGNED_INT, ...);

这会导致数据被截断为8位,且texelFetch返回的r分量是归一化到0.0-1.0的浮点数(比如原ID为5,会变成5/255≈0.0196,而非5.0),纹理数组采样时会取索引0的纹理。

修复步骤:

  • 修改纹理创建代码,使用32位无符号整数内部格式:
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32UI, info.tilemapSizeX, info.tilemapSizeY, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, m_worldTextureBytes.get());
  • 顶点着色器中使用无符号整数采样器:
uniform usampler2D masterTileTexture;
  • 保持texelFetch的转换逻辑不变,因为usampler2D返回的是无符号整数,转float后得到正确的索引值:
float(texelFetch(masterTileTexture, tilexy + chunkOffset, 0).r)

2. 瓦片坐标计算错误

顶点着色器中tilexy的行列计算颠倒:

ivec2 tilexy = ivec2(int(whichTile / chunkSize.y), int(whichTile % chunkSize.y));

若块是按X列数×Y行数排列,正确计算应为:

ivec2 tilexy = ivec2(whichTile % chunkSize.x, whichTile / chunkSize.x);

错误的计算会导致访问纹理的错误区域,甚至超出范围触发GL_REPEAT回到0索引。

3. 移除不必要的Mipmap

瓦片索引纹理不需要Mipmap,建议删除glGenerateMipmap(GL_TEXTURE_2D);,避免采样时的潜在干扰。

内容的提问来源于stack exchange,提问作者Jim Marshall

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 18:01:25