diff --git a/source/04-rendering.md b/source/04-rendering.md index 1026926..6963047 100644 --- a/source/04-rendering.md +++ b/source/04-rendering.md @@ -24,7 +24,7 @@ OpenGL 2.0 引入了可编程管线(`Programmable Pipeline`)的概念。在 注意,显卡被设计成并行处理上述所有操作。输入数据可以并行处理以生成最终场景。 -让我们开始写我们的第一个着色器程序。着色器是使用基于ANSI C的GLSL(OpenGL着色器语言)编写的。首先,我们在`resources`目录下创建一个名为“`vertex.vs`”(扩展名为顶点着色器英文简写)的文件,内容如下: +让我们开始编写第一个着色器程序。着色器是使用基于ANSI C的GLSL(OpenGL着色器语言)编写的。首先,在`resources`目录下创建一个名为“`vertex.vs`”(扩展名为顶点着色器英文简写)的文件,内容如下: ```glsl #version 330 @@ -54,13 +54,13 @@ void main() | 4.40 | 4.4 | \#version 440 | | 4.50 | 4.5 | \#version 450 | -第二行指定此着色器的输入格式。OpenGL缓冲区中的数据可以是我们想要的任何东西,也就是说,语言不会强迫你传递预定义语义的任何特定数据结构。从着色器的角度来看,它希望接收带有数据的缓冲区。它可以是一个坐标,一个有一些附加信息的坐标,或者我们想要的任何东西。顶点着色器只接收浮点数组。当我们填充缓冲区时,我们定义要由着色器处理的缓冲块。 +第二行指定此着色器的输入格式。OpenGL缓冲区中的数据可以是我们想要的任何东西,也就是说,语言不会强迫你传递预定义语义的任何特定数据结构。从着色器的角度来看,它希望接收带有数据的缓冲区。它可以是一个坐标,一个有一些附加信息的坐标,或者我们想要的任何东西。顶点着色器只接收浮点数组。当填充缓冲区时,我们定义要由着色器处理的缓冲块。 -首先,我们需要把这些块变成对我们有意义的东西。现在我们规定,从位置0开始,我们期望接收由三个属性(X、Y、Z)组成的向量。 +首先,需要把这些块变成对我们有意义的东西。现在规定从位置0开始,我们期望接收由三个属性(X、Y、Z)组成的向量。 -着色器有个主代码块,就像任何C语言程序一样,这是示例是非常简单的。它只是将接收到的坐标不经任何变换返回到`gl_Position`。你现在可能想知道为什么三个属性的向量被转换成四个属性的向量(`vec4`)。这是因为`gl_Position`仅接受`vec4`类型的数据,因为它是齐次坐标(`Homogeneous Coordinates`)。也就是说,它希望接收到形似(X, Y, Z, W)的东西,其中W代表一个额外的维度。为什么还要添加另一个维度?在之后的章节中,你会看到我们需要做的大部分操作都是基于向量和矩阵的。如果没有额外的维度,一些操作不能组合。例如,我们不能把旋转和变换操作组合起来。(如果你想学习更多有关于这方面的知识,这个额外的维度允许我们组合仿射和线性变换。你可以通过阅读《3D Math Primer for Graphics and Game development》(作者是Fletcher Dunn 和 Ian Parberry)来更多地了解这一点。) +着色器有个主代码块,就像任何C语言程序一样,这是示例是非常简单的。它只是将接收到的坐标不经任何变换返回到`gl_Position`。你现在可能想知道为什么三个属性的向量被转换成四个属性的向量(`vec4`)。这是因为`gl_Position`仅接受`vec4`类型的数据,因为它是齐次坐标(`Homogeneous Coordinates`)。也就是说,它希望接收到形似(X, Y, Z, W)的东西,其中W代表一个额外的维度。为什么还要添加另一个维度?在之后的章节中,你会看到我们需要做的大部分操作都是基于向量和矩阵的。如果没有额外的维度,一些操作不能组合。例如,不能把旋转和变换操作组合起来。(如果你想学习更多有关于这方面的知识,这个额外的维度允许我们组合仿射和线性变换。你可以通过阅读《3D Math Primer for Graphics and Game development》(作者是Fletcher Dunn 和 Ian Parberry)来更多地了解这一点。) -现在让我们来看看我们的第一个片元着色器。我们将在`resources`目录下创建一个名为`fragment.fs`(扩展名片元着色器英文简写)的文件,内容如下: +现在来看看我们的第一个片元着色器。在`resources`目录下创建一个名为`fragment.fs`(扩展名片元着色器英文简写)的文件,内容如下: ```glsl #version 330 @@ -73,9 +73,9 @@ void main() } ``` -该结构与我们的顶点着色器非常相似。现在,我们将为每个片元设置固定的颜色。输出被定义为第二行的`vec4`类型的`fragColor`变量。 +该结构与我们的顶点着色器非常相似。现在,它将为每个片元设置固定的颜色。输出被定义为第二行的`vec4`类型的`fragColor`变量。 -现在我们已经创建了我们的着色器,我们该如何使用它们呢?以下我们要做的一系列步骤: +现在我们已经创建了着色器,该如何使用它们呢?以下是我们要做的一系列步骤: 1. 创建OpenGL程序 2. 载入顶点和片元着色器文件 3. 对每个着色器创建一个新的着色器程序并指定它的类型(顶点或片元) @@ -176,13 +176,13 @@ public class ShaderProgram { `ShaderProgram`类还提供了在渲染时激活该程序(绑定)和停止使用它(解绑)的方法。最后,它提供了一个`cleanup`方法,用于当它不再被需要时,释放所有资源。 -既然我们有一个清理方法,让我们更改我们的`IGameLogic`接口来添加一个`cleanup`方法: +既然有一个清理方法,让我们更改`IGameLogic`接口来添加一个`cleanup`方法: ```java void cleanup(); ``` -这个方法将在游戏循环结束时调用,所以我们需要修改`GameEngine`类的`run`方法: +这个方法将在游戏循环结束时调用,所以需要修改`GameEngine`类的`run`方法: ```java @Override @@ -198,7 +198,7 @@ public void run() { } ``` -现在我们可以使用着色器来显示一个三角形。我们将在我们的`Renderer`类的`init`方法中进行。首先,我们要创建着色器程序: +现在我们可以使用着色器来显示一个三角形。这将在`Renderer`类的`init`方法中进行。首先,我们要创建着色器程序: ```java public void init() throws Exception { @@ -211,7 +211,7 @@ public void init() throws Exception { 我们已经创建了一个工具类,它提供了一个从类路径中取得文件内容的方法。此方法用于取得着色器的内容。 -现在我们可以把我们的三角形定义为一组浮点数。我们创建一个一维浮点数组,它将定义三角形的顶点。正如你所看到的,数组中没有结构。就目前而言,OpenGL无法知道该数组的结构。这只是一组浮点数: +现在我们可以把我们的三角形定义为一组浮点数。我们创建一个一维浮点数组,它将定义三角形的顶点。正如你所看到的,数组中没有结构。就目前而言,OpenGL无法知道该数组的结构,这只是一组浮点数: ```java float[] vertices = new float[]{ @@ -225,41 +225,41 @@ float[] vertices = new float[]{ ![Coordinates Syste](_static/04/triangle_coordinates.png) -现在我们有了坐标,我们需要把它们储存到我们的显卡中,并告诉OpenGL它的结构。现在我们将介绍两个重要的概念,顶点数组对象(`Vertex Array Object`, VAO)和顶点缓冲对象(`Vertex Buffer Object`, VBO)。如果你对下一段代码感到疑惑,请记住我们所做的是我们将要绘制的模型对象数据传递到显卡的储存器中。当我们储存它的时候,我们将得到一个ID,稍后再绘制时我们会使用它。 +现在有了坐标,我们需要把它们储存到我们的显卡中,并告诉OpenGL它的结构。现在将介绍两个重要的概念,顶点数组对象(`Vertex Array Object`, VAO)和顶点缓冲对象(`Vertex Buffer Object`, VBO)。如果你对下一段代码感到疑惑,请记住现在所做的是把将要绘制的模型对象数据传递到显卡的储存器中。当储存它的时候,我们将得到一个ID,稍后再绘制时会使用它。 -让我们先介绍顶点缓冲对象(VBO)吧。VBO只是显卡储存器中存储顶点的内存缓冲区。这是我们用来暂存一组三角形模型的浮点数的地方。正如我们之前所说的,OpenGL对我们的数据结构一无所知。事实上,它不仅可以保存坐标,还可以保存其他信息,比如纹理、颜色等。 +先介绍顶点缓冲对象(VBO)吧。VBO只是显卡储存器中存储顶点的内存缓冲区。这是用来暂存一组三角形模型的浮点数的地方。正如之前所说的,OpenGL对我们的数据结构一无所知。事实上,它不仅可以保存坐标,还可以保存其他信息,比如纹理、颜色等。 顶点数组对象(VAO)是包含一个或多个VBO的对象,通常被称为属性列表。每个属性列表可以保存一种类型的数据:位置、颜色、纹理等。在每个渲染间隔中,你可以自由地储存所需的任何数据。 一个VAO就像是一个包装,它按一组定义对储存在显卡中的数据分组。当我们创建一个VAO是,我们得到一个ID。我们使用这个ID来渲染它和使用它在创建过程中按特定定义的数据。 -让我们继续编写我们的示例。我们必须做的第一件事就是把我们的浮点数储存在一个`FloatBuffer`中。这主要是因为我们必须使用基于C语言的OpenGL库的接口,所以我们必须把浮点数转换成可以由库管理的东西。 +让我们继续编写示例代码。必须做的第一件事就是把浮点数储存在一个`FloatBuffer`中。这主要是因为我们必须使用基于C语言的OpenGL库的接口,所以必须把浮点数转换成可以由库管理的东西。 ```java FloatBuffer verticesBuffer = MemoryUtil.memAllocFloat(vertices.length); verticesBuffer.put(vertices).flip(); ``` -我们使用`MemoryUtil`类来在堆内存中创建一个缓冲区,以便于OpenGL库访问。在我们储存了数据(调用`put`方法)之后,我们需要调用`flip`方法将缓冲区的位置重置为0(也就是说,我们已经完成了对它的写入)。记住,Java中的对象,被分配在一个叫堆(`Heap`)的内存空间。堆是JVM内存中保留的一大堆内存。储存在堆中的对象不能通过本地代码访问(JNI,这种机制使得Java不能直接调用本地代码)。Java代码和本地代码直接共享内存数据的唯一方法是直接在Java分配内存。 +我们使用`MemoryUtil`类来在堆内存中创建一个缓冲区,以便于OpenGL库访问。在储存了数据(调用`put`方法)之后,我们需要调用`flip`方法将缓冲区的位置重置为0(也就是说,我们已经完成了对它的写入)。记住,Java中的对象,被分配在一个叫堆(`Heap`)的内存空间。堆是JVM内存中保留的一大堆内存。储存在堆中的对象不能通过本地代码访问(JNI,这种机制使得Java不能直接调用本地代码)。Java代码和本地代码直接共享内存数据的唯一方法是直接在Java分配内存。 如果你来自LWJGL的旧版本,强调一些要点是很重要的。你可能注意到了,我们不使用工具类`BufferUtils`来创建缓冲区。相反,我们使用`MemoryUtil`类。这是由于`BufferUtils`不是非常有效的,并且仅被用于向后兼容。相反,LWJGL 3提供了两种缓冲区的管理方法: * 自动管理缓冲区,即由垃圾回收器自动回收的缓冲区。这些缓冲区主要用于短暂的操作,或者用于传递到GPU的数据,并且不需要存在于进程内存中。这是通过使用`org.lwjgl.system.MemoryStack`实现的。 -* 手动管理缓冲区。这种情况下,一旦我们完成操作,我们需要小心地释放它们。这些缓冲区用于长时间操作或者大量的数据。这是通过使用`MemoryUtil`类实现的。 +* 手动管理缓冲区。这种情况下,一旦完成操作,我们需要小心地释放它们。这些缓冲区用于长时间操作或者大量的数据。这是通过使用`MemoryUtil`类实现的。 你可以在此处查阅细节: [https://blog.lwjgl.org/memory-management-in-lwjgl-3/](https://blog.lwjgl.org/memory-management-in-lwjgl-3/ "here") -在这种情况下,我们的数据被发送到GPU,这样我们就可以使用自动管理的缓冲区。但是,稍后我们将使用他们来储存可能需要手动管理的大量数据。这就是为什么我们使用`MemoryUtil`类,因此,这就是为什么我们要在最后一个块中释放缓冲区资源。在下一章中,我们将学习如何使用自动管理缓冲区。 +在这种情况下,我们的数据被发送到GPU,这样就可以使用自动管理的缓冲区。但是,稍后我们将使用它们来储存可能需要手动管理的大量数据。这就是为什么我们使用`MemoryUtil`类,因此,这就是为什么我们要在最后一个块中释放缓冲区资源。在下一章中,我们将学习如何使用自动管理缓冲区。 -现在我们需要创建VAO然后绑定它。 +现在需要创建VAO然后绑定它。 ```java vaoId = glGenVertexArrays(); glBindVertexArray(vaoId); ``` -然后我们需要创建VBO,绑定它并将数据输入。 +然后需要创建VBO,绑定它并将数据输入。 ```java vboId = glGenBuffers(); @@ -283,7 +283,7 @@ glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0); * stride: 指定连续顶点数据之间的字节偏移量(稍后我们再解释)。 * offset: 指定缓冲区中第一个数据的偏移量。 -在我们完成了我们的VBO之后,我们可以解除它和VAO的绑定(绑定到0)。 +在完成了VBO操作之后,我们可以解除它和VAO的绑定(绑定到0)。 ```java // 解绑VBO @@ -293,7 +293,7 @@ glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); ``` -一旦完成,我们**必须**释放由`FloatBuffer`占用的堆内存,这是通过手动调用`memFree`方法完成的,因为Java垃圾回收不会清理分配的堆内存。 +一旦完成操作,我们**必须**释放由`FloatBuffer`占用的堆内存,这是通过手动调用`memFree`方法完成的,因为Java垃圾回收不会清理分配的堆内存。 ```java if (verticesBuffer != null) { @@ -301,7 +301,7 @@ if (verticesBuffer != null) { } ``` -这就是我们的`init`方法应该有的代码。我们的数据已经在显卡中,准备使用了。我们只需要修改我们的`render`方法在游戏循环中进行渲染。 +这就是`init`方法应该有的代码。我们的数据已经在显卡中,准备使用了。现在只需要修改我们的`render`方法在游戏循环中进行渲染。 ```java public void render(Window window) { @@ -331,7 +331,7 @@ public void render(Window window) { 正如你所看到的,我们只需要清理窗口,绑定着色器程序,绑定VAO,绘制储存在VAO关联的VBO中的顶点,然后还原状态。仅此而已。 -我们还在我们的`Renderer`类中添加了一个`cleanup`方法用于释放资源。 +我们还在`Renderer`类中添加了一个`cleanup`方法用于释放资源。 ```java public void cleanup() {