渲染补充(More on Rendering)

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

  1. package org.lwjglb.engine.graph;
  2. import java.nio.FloatBuffer;
  3. import org.lwjgl.system.MemoryUtil;
  4. import static org.lwjgl.opengl.GL30.*;
  5. public class Mesh {
  6. private final int vaoId;
  7. private final int vboId;
  8. private final int vertexCount;
  9. public Mesh(float[] positions) {
  10. FloatBuffer verticesBuffer = null;
  11. try {
  12. verticesBuffer = MemoryUtil.memAllocFloat(positions.length);
  13. vertexCount = positions.length / 3;
  14. verticesBuffer.put(positions).flip();
  15. vaoId = glGenVertexArrays();
  16. glBindVertexArray(vaoId);
  17. vboId = glGenBuffers();
  18. glBindBuffer(GL_ARRAY_BUFFER, vboId);
  19. glBufferData(GL_ARRAY_BUFFER, verticesBuffer, GL_STATIC_DRAW);
  20. glEnableVertexAttribArray(0);
  21. glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
  22. glBindBuffer(GL_ARRAY_BUFFER, 0);
  23. glBindVertexArray(0);
  24. } finally {
  25. if (verticesBuffer != null) {
  26. MemoryUtil.memFree(verticesBuffer);
  27. }
  28. }
  29. }
  30. public int getVaoId() {
  31. return vaoId;
  32. }
  33. public int getVertexCount() {
  34. return vertexCount;
  35. }
  36. public void cleanUp() {
  37. glDisableVertexAttribArray(0);
  38. // 删除VBO
  39. glBindBuffer(GL_ARRAY_BUFFER, 0);
  40. glDeleteBuffers(vboId);
  41. // 删除VAO
  42. glBindVertexArray(0);
  43. glDeleteVertexArrays(vaoId);
  44. }
  45. }

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

  1. public void render(Mesh mesh) {
  2. clear();
  3. if ( window.isResized() ) {
  4. glViewport(0, 0, window.getWidth(), window.getHeight());
  5. window.setResized(false);
  6. }
  7. shaderProgram.bind();
  8. // 绘制
  9. glBindVertexArray(mesh.getVaoId());
  10. glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());
  11. // 还原状态
  12. glBindVertexArray(0);
  13. shaderProgram.unbind();
  14. }
  15. public void cleanup() {
  16. if (shaderProgram != null) {
  17. shaderProgram.cleanup();
  18. }
  19. }

值得注意的一点是:

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

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

正方形坐标

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

  1. @Override
  2. public void init() throws Exception {
  3. renderer.init();
  4. float[] positions = new float[]{
  5. -0.5f, 0.5f, 0.0f,
  6. -0.5f, -0.5f, 0.0f,
  7. 0.5f, 0.5f, 0.0f,
  8. 0.5f, 0.5f, 0.0f,
  9. -0.5f, -0.5f, 0.0f,
  10. 0.5f, -0.5f, 0.0f,
  11. };
  12. mesh = new Mesh(positions);
  13. }

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

正方形渲染

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

海豚

最后,我们需要更多的内存来储存重复的数据,这就是索引缓冲区(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类来接收另一个参数,一个索引数组,现在绘制的顶点数量是该索引数组的长度。

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

在创建了储存位置的VBO之后,我们需要创建另一个VBO来储存索引。因此,重命名储存位置的VBO的ID的变量名,并为索引VBO(idxVboId)创建一个ID。创建VBO的过程相似,但现在的类型是GL_ELEMENT_ARRAY_BUFFER

  1. idxVboId = glGenBuffers();
  2. indicesBuffer = MemoryUtil.memAllocInt(indices.length);
  3. indicesBuffer.put(indices).flip();
  4. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idxVboId);
  5. glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
  6. memFree(indicesBuffer);

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

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

  1. public void cleanUp() {
  2. glDisableVertexAttribArray(0);
  3. // 删除 VBO
  4. glBindBuffer(GL_ARRAY_BUFFER, 0);
  5. glDeleteBuffers(posVboId);
  6. glDeleteBuffers(idxVboId);
  7. // 删除 VAO
  8. glBindVertexArray(0);
  9. glDeleteVertexArrays(vaoId);
  10. }

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

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

改为调用glDrawElements方法:

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

方法的参数如下:

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

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

  1. public void init() throws Exception {
  2. renderer.init();
  3. float[] positions = new float[]{
  4. -0.5f, 0.5f, 0.0f,
  5. -0.5f, -0.5f, 0.0f,
  6. 0.5f, -0.5f, 0.0f,
  7. 0.5f, 0.5f, 0.0f,
  8. };
  9. int[] indices = new int[]{
  10. 0, 1, 3, 3, 1, 2,
  11. };
  12. mesh = new Mesh(positions, indices);
  13. }

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

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

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

  1. // 颜色 VBO
  2. colourVboId = glGenBuffers();
  3. FloatBuffer colourBuffer = memAllocFloat(colours.length);
  4. colourBuffer.put(colours).flip();
  5. glBindBuffer(GL_ARRAY_BUFFER, colourVboId);
  6. glBufferData(GL_ARRAY_BUFFER, colourBuffer, GL_STATIC_DRAW);
  7. glEnableVertexAttribArray(1);
  8. glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, 0);

请注意glVertexAttribPointer方法的调用,第一个参数现在是“1”,这是着色器期望数据的位置。(当然,由于增加了一个VBO,我们需要在cleanUp方法中释放它)。可以看到,我们需要在渲染期间启用位置1处的VAO属性。

接下来是修改着色器。顶点着色器现在需要两个参数,坐标(位置0)和颜色(位置1)。顶点着色器将只输出接收到的颜色,以便片元着色器可以对其进行处理。

  1. #version 330
  2. layout (location=0) in vec3 position;
  3. layout (location=1) in vec3 inColour;
  4. out vec3 exColour;
  5. void main()
  6. {
  7. gl_Position = vec4(position, 1.0);
  8. exColour = inColour;
  9. }

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

  1. #version 330
  2. in vec3 exColour;
  3. out vec4 fragColor;
  4. void main()
  5. {
  6. fragColor = vec4(exColour, 1.0);
  7. }

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

我们现在可以将如下所示的颜色数组传递给Mesh类,为正方形添加一些颜色。

  1. float[] colours = new float[]{
  2. 0.5f, 0.0f, 0.0f,
  3. 0.0f, 0.5f, 0.0f,
  4. 0.0f, 0.0f, 0.5f,
  5. 0.0f, 0.5f, 0.5f,
  6. };

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

色彩鲜艳的正方形