Files
lwjglbook-CN-Translation/docs/05-more-on-rendering.md
2019-08-16 17:58:10 +08:00

9.5 KiB
Raw Blame History

渲染补充More on Rendering

本章中我们将继续讨论OpenGL如何渲染物体。为了整理代码我们要创建一个名为Mesh的新类把一个坐标数组作为输入创建VBO和VAO对象把VBO和VAO对象加载到显卡中。

package org.lwjglb.engine.graph;

import java.nio.FloatBuffer;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;
import org.lwjgl.system.MemoryUtil;

public class Mesh {

    private final int vaoId;

    private final int vboId;

    private final int vertexCount;

    public Mesh(float[] positions) {
        FloatBuffer verticesBuffer = null;
        try {
            verticesBuffer = MemoryUtil.memAllocFloat(positions.length);
            vertexCount = positions.length / 3;
            verticesBuffer.put(positions).flip();

            vaoId = glGenVertexArrays();
            glBindVertexArray(vaoId);

            vboId = glGenBuffers();
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, verticesBuffer, GL_STATIC_DRAW);            
            glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
            glBindBuffer(GL_ARRAY_BUFFER, 0);

            glBindVertexArray(0);         
        } finally {
            if (verticesBuffer  != null) {
                MemoryUtil.memFree(verticesBuffer);
            }
        }
    }

    public int getVaoId() {
        return vaoId;
    }

    public int getVertexCount() {
        return vertexCount;
    }

    public void cleanUp() {
        glDisableVertexAttribArray(0);

        // 删除VBO
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glDeleteBuffers(vboId);

        // 删除VAO
        glBindVertexArray(0);
        glDeleteVertexArrays(vaoId);
    }
}

我们将在DummyGame类中实例化Mesh,从Rendererinit方法中删除VAO和VBO代码。在Renderer类的渲染方法中也将接收一个Mesh对象来渲染。cleanup方法也被简化,因为Mesh类已经提供了一个释放VAO和VBO资源的方法。

public void render(Mesh mesh) {
    clear();

    if ( window.isResized() ) {
        glViewport(0, 0, window.getWidth(), window.getHeight());
        window.setResized(false);
    }

    shaderProgram.bind();

    // 绘制
    glBindVertexArray(mesh.getVaoId());
    glEnableVertexAttribArray(0);
    glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());

    // 还原状态
    glDisableVertexAttribArray(0);
    glBindVertexArray(0);

    shaderProgram.unbind();
}

public void cleanup() {
    if (shaderProgram != null) {
        shaderProgram.cleanup();
    }
}

值得注意的一点是:

glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());

Mesh类通过将坐标数组除以3来计算顶点的数目因为我们使用XY和Z坐标。现在我们可以渲染更复杂的形状。来渲染一个正方形吧。一个正方形可以用两个三角形来组成如图所示。

Quad coordinates

正如你所看到的这两个三角形中的每一个都由三个顶点组成。第一个三角形由顶点V1、V2和V4橙色的点组成第二个三角形由顶点V4V2和V3绿色的点组成。顶点以逆时针顺序连接因此要传递的浮点数数组应该是[V1, V2, V4, V4, V2, V3]。因此,DummyGameinit方法将是这样的:

@Override
public void init() throws Exception {
    renderer.init();
    float[] positions = new float[]{
        -0.5f,  0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
         0.5f,  0.5f, 0.0f,
         0.5f,  0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
    };
    mesh = new Mesh(positions);
}

现在你应该可以看到这样一个正方形:

Quad rendered

我们完成了吗没有。上面的代码仍然存在一些问题。我们使用了重复的坐标来表示正方形。我们传递了两次V2和V4坐标。这是个小形状它可能不是什么大问题但想象一个更复杂的3D模型我们会多次重复传递坐标。记住我们使用三个浮点数表示顶点的位置但稍后我们将需要更多的数据来表示纹理等。考虑到在更复杂的形状中三角形直接共享的顶点数量可以更高如图所示其中顶点可以在六个三角形之间共享

Dolphin

最后,我们需要更多的内存来储存重复的数据,这就是索引缓冲区(Index Buffer)发挥作用的地方。为了绘制正方形我们只需要以这样的方式指定每个顶点V1, V2, V3, V4。每个顶点在数组中都有一个位置。V1在位置0上V2在位置1上等。

V1 V2 V3 V4
0 1 2 3

然后,我们通过引用它们的位置来指定这些顶点的顺序:

0 1 3 3 1 2
V1 V2 V4 V4 V2 V3

因此,我们需要修改Mesh类来接收另一个参数,一个索引数组,现在绘制的顶点数量是该索引数组的长度。

public Mesh(float[] positions, int[] indices) {
    vertexCount = indices.length;

在创建了储存坐标的VBO之后我们需要创建另一个VBO来保存索引。因此重命名持有坐标的VBO的ID的变量名并为索引VBOidxVboId创建一个ID。创建VBO的过程相似但现在的类型是GL_ELEMENT_ARRAY_BUFFER

idxVboId = glGenBuffers();
indicesBuffer = MemoryUtil.memAllocInt(indices.length);
indicesBuffer.put(indices).flip();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idxVboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
memFree(indicesBuffer);

因为我们是在处理整数,所有需要创建一个IntBuffer而不是一个FloatBuffer

就是这样。现在VAO包含两个VBO一个储存坐标另一个储存索引。Mesh类的cleanUp方法也必须考虑到要释放另一个VBO。

public void cleanUp() {
    glDisableVertexAttribArray(0);

    // 删除 VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glDeleteBuffers(posVboId);
    glDeleteBuffers(idxVboId);

    // 删除 VAO
    glBindVertexArray(0);
    glDeleteVertexArrays(vaoId);
}

最后,我们需要修改glDrawArrays调用的方法:

glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());

改为调用glDrawElements方法:

glDrawElements(GL_TRIANGLES, mesh.getVertexCount(), GL_UNSIGNED_INT, 0);

方法的参数如下:

  • mode: 指定渲染的图元类型,现在是三角形。没有什么变化。
  • count: 指定要渲染的顶点数。
  • type: 指定索引数据的类型。现在是无符号整数型。
  • indices: 指定要开始使用索引渲染的数据偏移量。

现在可以使用新的和更有效的方法来绘制复杂的模型了,仅需指定索引。

public void init() throws Exception {
    renderer.init();
    float[] positions = new float[]{
        -0.5f,  0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.5f,  0.5f, 0.0f,
    };
    int[] indices = new int[]{
        0, 1, 3, 3, 1, 2,
    };
    mesh = new Mesh(positions, indices);
}

现在给示例代码增加颜色吧。我们把另一组浮点数传递给Mesh类,它储存了正方形中每个顶点的颜色。

public Mesh(float[] positions, float[] colours, int[] indices) {

为了使用该数组我们需要创建另一个VBO它将与我们的VAO相关联。

// 颜色 VBO
colourVboId = glGenBuffers();
FloatBuffer colourBuffer = memAllocFloat(colours.length);
colourBuffer.put(colours).flip();
glBindBuffer(GL_ARRAY_BUFFER, colourVboId);
glBufferData(GL_ARRAY_BUFFER, colourBuffer, GL_STATIC_DRAW);
memFree(colourBuffer);
glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, 0);

请注意glVertexAttribPointer方法的调用第一个参数现在是“1”。这是着色器希望数据的位置。当然因为增加了一个VBO所以我们需要在cleanUp方法中释放它。

下一步是修改着色器。顶点着色器现在需要两个参数坐标位置0和颜色位置1。顶点着色器将只输出接收到的颜色因此这可以由片元着色器处理。

#version 330

layout (location=0) in vec3 position;
layout (location=1) in vec3 inColour;

out vec3 exColour;

void main()
{
    gl_Position = vec4(position, 1.0);
    exColour = inColour;
}

现在,片元着色器输入由顶点着色器处理的颜色,并使用它来生成颜色。

#version 330

in  vec3 exColour;
out vec4 fragColor;

void main()
{
    fragColor = vec4(exColour, 1.0);
}

最后要做的是修改渲染代码,使其使用第二个数据数组:

public void render(Window window, Mesh mesh) {
    clear();

    if ( window.isResized() ) {
        glViewport(0, 0, window.getWidth(), window.getHeight());
        window.setResized(false);
    }

    shaderProgram.bind();

    // Draw the mesh
    glBindVertexArray(mesh.getVaoId());
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glDrawElements(GL_TRIANGLES, mesh.getVertexCount(), GL_UNSIGNED_INT, 0);
    // ...

你可以看到在渲染过程中我们需要启用位于位置1的VAO属性。现在可以把颜色传递给Mesh类,以便给正方形添加颜色。

float[] colours = new float[]{
    0.5f, 0.0f, 0.0f,
    0.0f, 0.5f, 0.0f,
    0.0f, 0.0f, 0.5f,
    0.0f, 0.5f, 0.5f,
};

然后会得到这样一个色彩鲜艳的正方形。

Coloured quad