在 OpenGL 中重用纹理和顶点

Reuse texture and vertices in OpenGL

我正在尝试制作一个简单的 2D 游戏,我将世界存储在 Block 的 2D 数组中(枚举,每个值都有其纹理)。

因为这些都是简单的不透明图块,所以在渲染时我按纹理对它们进行排序,然后通过转换为它们的坐标来渲染它们。但是,我还需要为我绘制的每个图块指定纹理坐标和顶点,即使这些也是相同的。

这是我目前拥有的:

public static void render() {
    // Sorting...
    for(SolidBlock block : xValues.keySet()) {
        block.getTexture().bind();
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

        for(int coordinateIndex = 0; coordinateIndex < xValues.get(block).size(); coordinateIndex++) {
            int x = xValues.get(block).get(coordinateIndex);
            int y = yValues.get(block).get(coordinateIndex);
            glTranslatef(x, y, Integer.MIN_VALUE);
            // Here I use MIN_VALUE because I'll later have to do z sorting with other tiles
            glBegin(GL_QUADS);
            loadModel();
            glEnd();
            glLoadIdentity();
        }

        xValues.get(block).clear();
        yValues.get(block).clear();
    }
}

private static void loadModel() {
    glTexCoord2f(0, 0);
    glVertex2f(0, 0);
    glTexCoord2f(1, 0);
    glVertex2f(1, 0);
    glTexCoord2f(1, 1);
    glVertex2f(1, 1);
    glTexCoord2f(0, 1);
    glVertex2f(0, 1);
}

我想知道是否可以将 loadModel() 放在主循环之前,以避免使用相同的数据加载模型数千次,以及还可以移动什么来实现它越快越好!

一些快速优化:

  • glTexParameteri 每个纹理每个参数只需要调用一次。你应该把它放在你加载纹理的代码部分。
  • 只需添加更多顶点,您就可以在一对 glBegin/glEnd 中绘制多个四边形。但是,您不能在 glBeginglEnd 之间进行任何坐标更改(例如 glTranslatefglLoadIdentityglPushMatrix),因此您必须通过 xy 到您的 loadModel 函数(为了准确起见,实际上应该称为 addQuad )。也不允许在 glBegin/glEnd 之间重新绑定纹理,因此每个纹理必须使用一组 glBegin/glEnd
  • 次要,但不要多次调用 xValues.get(block),只需在外循环的开头说 List<Integer> blockXValues = xValues.get(block),然后从那里开始使用 blockXValues

一些更复杂的优化:

  • 旧版 OpenGL 有绘图列表,它们基本上是 OpenGL 的宏。您可以让 OpenGL 记录您在 glNewListglEndList 之间所做的所有 OpenGL 调用(有一些例外),并以某种方式存储它们。下次您想 运行 那些确切的 OpenGL 调用时,您可以使用 glCallList 让 OpenGL 为您做这些。将对抽奖列表进行一些优化,以加快后续抽奖。
  • 纹理切换相对昂贵,自从您按纹理对四边形进行排序后您可能已经意识到这一点,但是有比排序纹理更好的解决方案:将所有纹理放入一个 texture atlas。您需要将每个块的子纹理坐标存储在 SolidBlock 中,然后将 block 也传递给 addQuad,这样您就可以将适当的子纹理坐标传递给 glTexCoord2f.完成后,您不再需要按纹理排序,只需遍历 x 和 y 坐标即可。

良好做法:

  • 每帧仅在绘制过程开始时使用 glLoadIdentity 一次。然后使用 glPushMatrixglPopMatrix 配对来保存和恢复矩阵的状态。这样,代码的内部部分就不需要知道外部部分事先可能完成或未完成的矩阵变换。
  • 不要使用 Integer.MIN_VALUE 作为顶点坐标。使用您自己选择的常量,最好是不会使您的深度范围变大的常量(glOrtho 的最后两个参数,我假设您正在使用)。深度缓冲区精度有限,如果在将 Z 范围设置为 Integer.MIN_VALUEInteger.MAX_VALUE 后尝试使用 1 或 2 左右的 Z 坐标,您将 运行 陷入 Z 冲突问题。此外,您使用的是 float 坐标,因此 int 常量在这里毫无意义。

这是快速通过后的代码(没有纹理图集更改):

private static final float BLOCK_Z_DEPTH = -1; // change to whatever works for you
private int blockCallList;
private boolean regenerateBlockCallList; // set to true whenever you need to update some blocks

public static void init() {
    blockCallList = glGenLists(1);
    regenerateBlockCallList = true;
}

public static void render() {
    if (regenerateBlockCallList) {
        glNewList(blockCallList, GL_COMPILE_AND_EXECUTE);
        drawBlocks();
        glEndList();

        regenerateBlockCallList = false;
    } else {
        glCallList(blockCallList);
    }
}

private static void drawBlocks() {
    // Sorting...

    glPushMatrix();
    glTranslatef(0, 0, BLOCK_Z_DEPTH);

    for (SolidBlock block : xValues.keySet()) {
        List<Integer> blockXValues = xValues.get(block);
        List<Integer> blockYValues = yValues.get(block);

        block.getTexture().bind();
        glBegin(GL_QUADS);
        for(int coordinateIndex = 0; coordinateIndex < blockXValues.size(); coordinateIndex++) {
            int x = blockXValues.get(coordinateIndex);
            int y = blockYValues.get(coordinateIndex);
            addQuad(x,y);
        }
        glEnd();

        blockXValues.clear();
        blockYValues.clear();
    }

    glPopMatrix();

}

private static void addQuad(float x, float y) {
    glTexCoord2f(0, 0);
    glVertex2f(x, y);
    glTexCoord2f(1, 0);
    glVertex2f(x+1, y);
    glTexCoord2f(1, 1);
    glVertex2f(x+1, y+1);
    glTexCoord2f(0, 1);
    glVertex2f(x, y+1);
}

使用现代 OpenGL(顶点缓冲区、着色器和实例化而不是显示列表、矩阵变换和一个接一个地传递顶点)你会以非常不同的方式处理这个问题,但我会把它放在我的回答范围之外.