Proofread chapter 9

This commit is contained in:
Mouse
2019-11-17 17:33:16 +08:00
parent 15da1b9e5f
commit 9c0958eb78
2 changed files with 76 additions and 72 deletions

View File

@@ -1,36 +1,34 @@
# 加载更复杂的模型Loading more complex models # 加载更复杂的模型Loading more complex models
本章中我们将学习加载在外部文件中定义的复杂模型。这些模型将3D建模工具(例如[Blender](https://www.blender.org/))创建。目前,我们是徒手创建模型,直接写代码定义它们的几何数组。在本章中我们将学习如何以OBJ格式定义的模型。 本章中我们将学习加载在外部文件中定义的复杂模型。这些模型将使用3D建模工具(例如[Blender](https://www.blender.org/))创建。目前为止,我们已经通过直接编码定义其几何图形的数组来创建模型。但在本章中,我们将学习如何加载以OBJ格式定义的模型。
OBJ(或者.obj)是Wavefront Technologies开发的一种几何定义开放文件格式现已被广泛使用。OBJ文件储存构成3D模型的顶点、纹理坐标和多边形。这是一种相对简单的格式,因为它基于文本格式的,每一行定义一个元素(顶点、纹理坐标等) OBJ(或.obj是Wavefront Technologies开发的一种几何定义开放文件格式现已被广泛用。OBJ文件定义构成三维模型的顶点、纹理坐标和多边形。这是一种相对容易解析的格式,因为它基于文本,每一行定义一个元素顶点、纹理坐标等
在.obj文件中每行一个标识符开始,标识元素类型: 在.obj文件中每行一个标元素类型的标识符开头
* 以"\#"开始的行是注释。 * 以"\#"开始的行是注释。
* 以"v"开始的行用坐标(x, y, z, w)定义一个几何顶点。例如:`v 0.155 0.211 0.32 1.0` * 以"v"开始的行用坐标(x, y, z, w)定义一个几何顶点。例如:`v 0.155 0.211 0.32 1.0`
* 以"vn"开始的行是用坐标(x, y, z)定义顶点法线(`Vertex normals`)。例如:`vn 0.71 0.21 0.82`。之后再讨论这个东西。 * 以"vn"开始的行是用坐标(x, y, z)定义顶点法线Normals。例如:`vn 0.71 0.21 0.82`。之后再讨论这个东西。
* 以"vt"开始的行定义纹理坐标。例如:`vt 0.500 1` * 以"vt"开始的行定义纹理坐标。例如:`vt 0.500 1`
* 以"f"开始的行定义了一个面。利用该行中的信息可以创建索引数组。我们只处理面三角形的情况。它可以有几种定义方式: * 以"f"开始的行定义了一个面。利用该行中的数据可以构造索引数组。我们只处理面导出为三角形的情况。它可以有几种定义方式:
* 它可以定义顶点位置(f v1 v2 v3)。例如:`f 6 3 1`。在这种情况下这个三角形是由位置为6、3和1的几何顶点定义的(顶点索引总是从1开始) * 它可以定义顶点位置`f v1 v2 v3`。例如:`f 6 3 1`。在这种情况下这个三角形是由位置为6、3和1的几何顶点定义的顶点索引总是从1开始
* 它可以定义顶点位置、纹理坐标和法线(f v1/t1/n1 v2/t2/n2 v3/t3/n3)。例如:`f 6/4/1 3/5/3 7/6/5`。第一部分`v1/t1/n1`分别定义了坐标、纹理坐标和顶点法线。看到这个部分可以想到选择几何顶点6、纹理坐标4和顶点法线1。 * 它可以定义顶点位置、纹理坐标和法线`f v1/t1/n1 v2/t2/n2 v3/t3/n3`。例如:`f 6/4/1 3/5/3 7/6/5`。第一部分`v1/t1/n1`,其定义了坐标、纹理坐标和顶点法线。看到部分可以说出选择几何顶点6、纹理坐标4和顶点法线1。
OBJ格式有更多的元素类型(如一对多多边形、定义材质等)。现在我们仅实现如上所述我们的OBJ加载器将忽略其他元素类型。 OBJ格式有更多的元素类型如一多边形、定义材质等。现在我们仅实现上述子集我们的OBJ加载器将忽略其他元素类型。
但是什么是法线呢?让我们先定义它。当你有一个平面时,它的法线是垂直于该平面的向量其长度等于1 但是什么是法线呢?让我们先定义它。一个平面的法线是一个垂直于该平面的长度为1的向量。
![法线](_static/09/normals.png) ![法线](_static/09/normals.png)
正如你在上图中看到的,一个平面可以有两法线,我们应该用哪一个呢?三维图形中的法线用于光照,所以我们应该选择面向光源的法线。换句话说,我们应该选择指向模型外的法线。 如上所见,一个平面可以有两法线,我们应该用哪一个呢?三维图形中的法线用于光照,所以我们应该选择面向光源的法线。换言之,我们应该选择指向模型外的法线。
我们有一个多边形三角形组成的3D模型每个三角形由三个顶点组成,三角形的法线向量是垂直于三角形表面的向量其长度等于1 我们有一个多边形三角形组成的3D模型每个三角形由三个顶点组成,三角形的法线向量是垂直于三角形表面的长度为1的向量。
顶点法线与特定顶点相关联,并且是周围三角形的法线的组合(当然它的长度等于1)。在这里你可以看到一个3D网格的顶点模型(取自[维基百科](https://en.wikipedia.org/wiki/Vertex_normal#/media/File:Vertex_normals.png)) 顶点法线与特定顶点相关联,并且是周围三角形的法线的组合当然它的长度等于1。在这里你可以看到一个3D网格的顶点模型取自[维基百科](https://en.wikipedia.org/wiki/Vertex_normal#/media/File:Vertex_normals.png)
![法线向量](_static/09/vertex_normals.png) ![顶点法线](_static/09/vertex_normals.png)
法线将会被用于光照 现在我们开始创建OBJ加载器。首先我们将修改`Mesh`因为现在必须使用纹理。我们可能加载一些没有定义纹理坐标的OBJ文件因此必须能够使用颜色而不是使用纹理渲染它们。在此情况下面的定义格式为`f v/n`
让我们开始创建OBJ加载器。首先我们将修改`Mesh`因为现在必须使用纹理。我们可能加载一些没有定义纹理坐标的OBJ文件我们必须能够使用颜色而不是纹理渲染它们。这种情况下面的定义就是这样的`f v/n`
`Mesh`类现在有一个名为`colour`的新属性。 `Mesh`类现在有一个名为`colour`的新属性。
@@ -44,10 +42,10 @@ private Vector3f colour;
public Mesh(float[] positions, float[] textCoords, float[] normals, int[] indices) { public Mesh(float[] positions, float[] textCoords, float[] normals, int[] indices) {
``` ```
当然,在`render``clear`方法中,在使用纹理之前,必须检查纹理是否为`null`。正如你在构造函数中看到的,现在需要一个名为`normals`的浮点数组。如何使用法线渲染?答案很简单,它只是VAO的另一个VBO所以我们需要添加如下代码 当然,在`render``clear`方法中,在使用纹理之前,必须检查纹理是否为`null`。正如你在构造函数中看到的,现在需要传递一个名为`normals`浮点数组。如何使用法线渲染答案很简单它只是VAO的另一个VBO所以我们需要添加如下代码
```java ```java
// Vertex normals VBO // 顶点法线VBO
vboId = glGenBuffers(); vboId = glGenBuffers();
vboIdList.add(vboId); vboIdList.add(vboId);
vecNormalsBuffer = MemoryUtil.memAllocFloat(normals.length); vecNormalsBuffer = MemoryUtil.memAllocFloat(normals.length);
@@ -57,10 +55,10 @@ glBufferData(GL_ARRAY_BUFFER, vecNormalsBuffer, GL_STATIC_DRAW);
glVertexAttribPointer(2, 3, GL_FLOAT, false, 0, 0); glVertexAttribPointer(2, 3, GL_FLOAT, false, 0, 0);
``` ```
`render`方法中,我们必须启用这个VBO并在后禁用它。 `render`方法中,我们必须在渲染之前启用此VBO并在完成后禁用它。
```java ```java
// Draw the mesh // 绘制网格
glBindVertexArray(getVaoId()); glBindVertexArray(getVaoId());
glEnableVertexAttribArray(0); glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1); glEnableVertexAttribArray(1);
@@ -68,7 +66,7 @@ glEnableVertexAttribArray(2);
glDrawElements(GL_TRIANGLES, getVertexCount(), GL_UNSIGNED_INT, 0); glDrawElements(GL_TRIANGLES, getVertexCount(), GL_UNSIGNED_INT, 0);
// Restore state // 恢复状态
glDisableVertexAttribArray(0); glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1); glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2); glDisableVertexAttribArray(2);
@@ -76,7 +74,7 @@ glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
``` ```
现在已经完成了`Mesh`类的修改,我们可以修改代码来使用纹理坐标或固定的颜色。因此,我们需要修改片元着色器,像这样 现在我们已经完成了`Mesh`类的修改,可以修改代码来使用纹理坐标或固定的颜色。因此,我们需要像这样修改片元着色器:
```glsl ```glsl
#version 330 #version 330
@@ -101,15 +99,15 @@ void main()
} }
``` ```
如你所看到的,我们已经创建了两个新Uniform 如你所我们创建了两个新Uniform
* `colour`: 将储存基颜色。 * `colour`: 将储存基颜色。
* `useColour`: 这是个标当你不想使用纹理时它将被设置为1。 * `useColour`: 这是个标当你不想使用纹理时它将被设置为1。
`Renderer`类中我们需要创建这两个Uniform。 `Renderer`类中我们需要创建这两个Uniform。
```java ```java
// Create uniform for default colour and the flag that controls it // 为默认颜色与控制它的标志创建Uniform
shaderProgram.createUniform("colour"); shaderProgram.createUniform("colour");
shaderProgram.createUniform("useColour"); shaderProgram.createUniform("useColour");
``` ```
@@ -119,25 +117,25 @@ shaderProgram.createUniform("useColour");
```java ```java
for(GameItem gameItem : gameItems) { for(GameItem gameItem : gameItems) {
Mesh mesh = gameItem.getMesh(); Mesh mesh = gameItem.getMesh();
// Set model view matrix for this item // 为该游戏项设置模型观察矩阵
Matrix4f modelViewMatrix = transformation.getModelViewMatrix(gameItem, viewMatrix); Matrix4f modelViewMatrix = transformation.getModelViewMatrix(gameItem, viewMatrix);
shaderProgram.setUniform("modelViewMatrix", modelViewMatrix); shaderProgram.setUniform("modelViewMatrix", modelViewMatrix);
// Render the mes for this game item // 为该游戏项渲染网格
shaderProgram.setUniform("colour", mesh.getColour()); shaderProgram.setUniform("colour", mesh.getColour());
shaderProgram.setUniform("useColour", mesh.isTextured() ? 0 : 1); shaderProgram.setUniform("useColour", mesh.isTextured() ? 0 : 1);
mesh.render(); mesh.render();
} }
``` ```
我们可以创建一个名为`OBJLoader`的新类,它将解析OBJ文件并用其中的数据创建一个`Mesh`实例。你可能会在网上发现一些其他实现可能比这更有效,但我认为这个方案更容易理解。这是一个工具类,将有一个静态方法: 现在我们可以创建一个名为`OBJLoader`的新类,该类解析OBJ文件并用其中包含的数据创建一个`Mesh`实例。你可能会在网上发现一些其他实现可能比这更有效,但我认为方案更容易理解。这是一个工具类,将有如下静态方法:
```java ```java
public static Mesh loadMesh(String fileName) throws Exception { public static Mesh loadMesh(String fileName) throws Exception {
``` ```
参数`fileName`指定文件名称,该文件必须包含在OBJ模型的类路径中。 参数`fileName`指定OBJ模型的文件名称,该文件必须包含在类路径中。
我们在该方法中做的第一件事是读取文件内容并存储数组中的所有行。然后创建几个列表来存顶点、纹理坐标、法线和面。 在该方法中我们首先要做的是读取文件内容并将所有行储存到一个数组中,然后创建几个列表来存顶点、纹理坐标、法线和面。
```java ```java
List<String> lines = Utils.readAllLines(fileName); List<String> lines = Utils.readAllLines(fileName);
@@ -148,14 +146,14 @@ List<Vector3f> normals = new ArrayList<>();
List<Face> faces = new ArrayList<>(); List<Face> faces = new ArrayList<>();
``` ```
然后解析每一行,并根据起始标识符得到顶点位置、纹理坐标、顶点法线或面定义最后重新整理这些数据。 然后解析每一行,并根据开头标识符得到顶点位置、纹理坐标、顶点法线或面定义最后重新排列这些数据。
```java ```java
for (String line : lines) { for (String line : lines) {
String[] tokens = line.split("\\s+"); String[] tokens = line.split("\\s+");
switch (tokens[0]) { switch (tokens[0]) {
case "v": case "v":
// Geometric vertex // 几何顶点
Vector3f vec3f = new Vector3f( Vector3f vec3f = new Vector3f(
Float.parseFloat(tokens[1]), Float.parseFloat(tokens[1]),
Float.parseFloat(tokens[2]), Float.parseFloat(tokens[2]),
@@ -163,14 +161,14 @@ for (String line : lines) {
vertices.add(vec3f); vertices.add(vec3f);
break; break;
case "vt": case "vt":
// Texture coordinate // 纹理坐标
Vector2f vec2f = new Vector2f( Vector2f vec2f = new Vector2f(
Float.parseFloat(tokens[1]), Float.parseFloat(tokens[1]),
Float.parseFloat(tokens[2])); Float.parseFloat(tokens[2]));
textures.add(vec2f); textures.add(vec2f);
break; break;
case "vn": case "vn":
// Vertex normal // 顶点法线
Vector3f vec3fNorm = new Vector3f( Vector3f vec3fNorm = new Vector3f(
Float.parseFloat(tokens[1]), Float.parseFloat(tokens[1]),
Float.parseFloat(tokens[2]), Float.parseFloat(tokens[2]),
@@ -182,18 +180,18 @@ for (String line : lines) {
faces.add(face); faces.add(face);
break; break;
default: default:
// Ignore other lines // 忽略其他行
break; break;
} }
} }
return reorderLists(vertices, textures, normals, faces); return reorderLists(vertices, textures, normals, faces);
``` ```
在重新排序之前,让我们看看如何解析面的定义。我们创建了一个名为`Face`的类,它负责解析一个面的定义。一个`Face`是由索引组列表组成的,现在,因为我们正在处理三角形,所以我们将有三个索引组。 讲解重新排序之前,让我们看看如何解析面的定义。我们创建了一个名为`Face`的类,它负责解析一个面的定义。一个`Face`是由一个索引组列表组成的,在本例中,由于我们处理的是三角形,所以我们将有三个索引组。
![面定义](_static/09/face_definition.png) ![面定义](_static/09/face_definition.png)
我们将创建另一个名为`IndexGroup`的内部类,它将索引组保存数据。 我们将创建另一个名为`IndexGroup`的内部类,它将储存索引组数据。
```java ```java
protected static class IdxGroup { protected static class IdxGroup {
@@ -214,19 +212,19 @@ protected static class IdxGroup {
} }
``` ```
`Face`看起来像这样。 `Face`如下所示:
```java ```java
protected static class Face { protected static class Face {
/** /**
* List of idxGroup groups for a face triangle (3 vertices per face). * 面三角形的索引组列表(每个面三个顶点)。
*/ */
private IdxGroup[] idxGroups = new IdxGroup[3]; private IdxGroup[] idxGroups = new IdxGroup[3];
public Face(String v1, String v2, String v3) { public Face(String v1, String v2, String v3) {
idxGroups = new IdxGroup[3]; idxGroups = new IdxGroup[3];
// Parse the lines // 解析行
idxGroups[0] = parseLine(v1); idxGroups[0] = parseLine(v1);
idxGroups[1] = parseLine(v2); idxGroups[1] = parseLine(v2);
idxGroups[2] = parseLine(v3); idxGroups[2] = parseLine(v3);
@@ -239,7 +237,7 @@ protected static class Face {
int length = lineTokens.length; int length = lineTokens.length;
idxGroup.idxPos = Integer.parseInt(lineTokens[0]) - 1; idxGroup.idxPos = Integer.parseInt(lineTokens[0]) - 1;
if (length > 1) { if (length > 1) {
// It can be empty if the obj does not define text coords // 如果OBJ不定义纹理坐标则可为null
String textCoord = lineTokens[1]; String textCoord = lineTokens[1];
idxGroup.idxTextCoord = textCoord.length() > 0 ? Integer.parseInt(textCoord) - 1 : IdxGroup.NO_VALUE; idxGroup.idxTextCoord = textCoord.length() > 0 ? Integer.parseInt(textCoord) - 1 : IdxGroup.NO_VALUE;
if (length > 2) { if (length > 2) {
@@ -256,11 +254,11 @@ protected static class Face {
} }
``` ```
当解析面时,我们可以看到没有纹理但有矢量法线的对象,在这种情况下,面定义可能像`f 11//1 17//1 13//1`这样,所以我们需要检这些情况。 当解析面时,我们可以看到没有纹理但有矢量法线的对象。在此情况下,面定义可能像`f 11//1 17//1 13//1`这样,所以我们需要检这些情况。
最后,我们需要重新整理这些数据。`Mesh`类需要四个数组,分别用于位置坐标、纹理坐标、法线矢量,一个用于索引。前三个数组应该具有相同数量的元素,因为索引数组是唯一的(注意,相同数量的元素并不意味着相同的长度。顶点坐标是三维的,由三个浮点数组成。纹理坐标是二维的,由两个浮点数组成)。OpenGL不允许我们对每个元素类型定义不同的索引数组(如果可以的话,我们就不需要在应用纹理时重复顶点) 最后,我们需要重新排列这些数据。`Mesh`类需要四个数组,分别用于位置坐标、纹理坐标、法线矢量索引。前三个数组应该具有相同数量的元素,因为索引数组是唯一的注意,相同数量的元素并不意味着相同的长度。顶点坐标是三维的,由三个浮点数组成。纹理坐标是二维的,由两个浮点数组成。OpenGL不允许我们对每个元素类型定义不同的索引数组如果可以的话,我们就不需要在应用纹理时重复顶点
当你打开一个OBJ文件时你首先可能会看到存顶点坐标的列表比保存纹理坐标的列表和顶点的数量多。这是我们需要解决的问题。举一个简单的例子,定义一个具有纹理高度的正方形(只是为了说明例子)其OBJ文件可能是这样的(不要太关注法线坐标,因为它只是为了说明例子) 当你打开一个OBJ文件时你首先可能会看到存顶点坐标的列表,比储存纹理坐标和顶点数量的列表的元素数量多。这是我们需要解决的问题。举一个简单的例子,定义一个具有像素高度的正方形只是为了演示)其OBJ文件可能是这样的不要太关注法线坐标,因为它只是为了演示)
```java ```java
v 0 0 0 v 0 0 0
@@ -277,46 +275,46 @@ f 1/2/1 2/1/1 3/2/1
f 1/2/1 3/2/1 4/1/1 f 1/2/1 3/2/1 4/1/1
``` ```
我们完成文件解析时,可以看到以下列表(每个元素的数字是它在文件中的位置顺序) 当完成文件解析时,我们得到如下所示列表每个元素的数字是它在文件中的位置顺序,按出现顺序排列)
![序列1](_static/09/ordering_i.png) ![序列1](_static/09/ordering_i.png)
现在我们将使用面定义来创建包索引在内的最终数组。要考虑的是,纹理坐标与法线向量的定义顺序与顶点的定义顺序不一样。如果列表的大小是相同的,并且它们是有序的,那么面定义就只需要个顶点一个数字 现在我们将使用面定义来创建包索引的最终数组。要考虑的是,纹理坐标与法线向量的定义顺序与顶点的定义顺序不对应。如果列表的大小是相同的有序的,那么面定义就只需要个顶点一个索引
因此,我们需要排列数据,并根据我们的需要进行设置。必须要做的第一件事是创建三个数组和一个列表,分别用于顶点、纹理坐标法线向量和索引列表。正如我们之前所说的,三个数组将拥有相同数量的元素(等于顶点的数量)。顶点数组将有一个顶点列表的副本。 因此,我们需要排列数据,并根据需要进行相应的设置。首先要做的是创建三个数组用于顶点、纹理坐标法线)和一个索引列表。如上所述,三个数组元素数量相同(等于顶点数),顶点数组将顶点列表的副本。
![序列2](_static/09/ordering_ii.png) ![序列2](_static/09/ordering_ii.png)
现在我们开始处理面。第一个面的第一个索引组是1/2/1。看到索引组中的第一个索引,即定义几何顶点来构造索引列表的所以,我们把它命名`posIndex` 现在我们开始处理这些面。第一个面的第一个索引组是1/2/1。我们使用索引组中的第一个索引定义几何顶点的索引)来构造索引列表,称之`posIndex`
面指定我们应该把占据第一个位置的元素的索引添加到索引列表中。因此,我们将`posIndex`减去1后放到`indicesList`(我们必须减1因为数组的起始是0而OBJ文件格式中是1) 面指定我们应该把占据第一个位置的元素的索引添加到索引列表中。因此,我们将`posIndex`减去1后放到`indicesList`必须减1因为数组的起始是0而OBJ文件格式中起始是1
![序列3](_static/09/ordering_iii.png) ![序列3](_static/09/ordering_iii.png)
然后,我们使用索引组的其他索引来设置`texturesArray``normalsArray`。索引组的第二个索引是2我们必须将第二个纹理坐标放在与顶点指定的`posIndex`位置(V1)相同的位置上。 然后,我们使用索引组的其他索引来设置`texturesArray``normalsArray`。索引组的第二个索引是2我们必须将第二个纹理坐标放在与所占顶点指定的`posIndex`位置V1相同的位置上。
![序列4](_static/09/ordering_iv.png) ![序列4](_static/09/ordering_iv.png)
然后我们选择第三个索引,它1所以我们要做的是将第一个法线向量坐标放在与顶点指定的`posIndex`位置(V1)相同的位置上。 然后我们看到第三个索引,它1所以要做的是将第一个法线向量坐标放在与所占顶点指定的`posIndex`位置V1相同的位置上。
![序列5](_static/09/ordering_v.png) ![序列5](_static/09/ordering_v.png)
我们处理了第一个面之后,数组和列表看起来就会像这样 在处理了第一个面之后,数组和列表如下所示
![序列6](_static/09/ordering_vi.png) ![序列6](_static/09/ordering_vi.png)
我们处理了第二个面之后,数组和列表看起来就会像这样 在处理了第二个面之后,数组和列表如下所示
![序列7](_static/09/ordering_vii.png) ![序列7](_static/09/ordering_vii.png)
第二个面也定义了已经被赋值的顶点,但是它们有相同的值,所以处理这个问题上很简单。我觉得这个过程已经讲解得清晰了,不过在你实现它之前可能会有些麻烦。下面是重新排列数据的方法。请记住,我们要得到的是浮点数组,所以必须把顶点、纹理和法线数组转换浮点数组。因此,顶点和法线数组的大小将是顶点列表的大小乘以,而纹理坐标数组的大小应该是顶点列表的大小乘以 第二个面也定义了已经被赋值的顶点,但是它们有相同的值,所以处理这个问题上很简单。我觉得这个过程已经讲解得足够清晰了,不过在你明白之前可能会有些棘手。重新排列数据的方法如下所示。请记住,我们要的是浮点数组,所以必须把顶点、纹理和法线数组转换浮点数组。因此,对于顶点和法线来说数组的长度是顶点列表的长度乘以3,而对于纹理坐标来说数组的长度是顶点列表的长度乘以2
```java ```java
private static Mesh reorderLists(List<Vector3f> posList, List<Vector2f> textCoordList, private static Mesh reorderLists(List<Vector3f> posList, List<Vector2f> textCoordList,
List<Vector3f> normList, List<Face> facesList) { List<Vector3f> normList, List<Face> facesList) {
List<Integer> indices = new ArrayList(); List<Integer> indices = new ArrayList<>();
// Create position array in the order it has been declared // 按声明的顺序创建位置数组
float[] posArr = new float[posList.size() * 3]; float[] posArr = new float[posList.size() * 3];
int i = 0; int i = 0;
for (Vector3f pos : posList) { for (Vector3f pos : posList) {
@@ -345,18 +343,18 @@ private static void processFaceVertex(IdxGroup indices, List<Vector2f> textCoord
List<Vector3f> normList, List<Integer> indicesList, List<Vector3f> normList, List<Integer> indicesList,
float[] texCoordArr, float[] normArr) { float[] texCoordArr, float[] normArr) {
// Set index for vertex coordinates // 设置顶点坐标的索引
int posIndex = indices.idxPos; int posIndex = indices.idxPos;
indicesList.add(posIndex); indicesList.add(posIndex);
// Reorder texture coordinates // 对纹理坐标重新排序
if (indices.idxTextCoord >= 0) { if (indices.idxTextCoord >= 0) {
Vector2f textCoord = textCoordList.get(indices.idxTextCoord); Vector2f textCoord = textCoordList.get(indices.idxTextCoord);
texCoordArr[posIndex * 2] = textCoord.x; texCoordArr[posIndex * 2] = textCoord.x;
texCoordArr[posIndex * 2 + 1] = 1 - textCoord.y; texCoordArr[posIndex * 2 + 1] = 1 - textCoord.y;
} }
if (indices.idxVecNormal >= 0) { if (indices.idxVecNormal >= 0) {
// Reorder vectornormals // 对法线向量重新排序
Vector3f vecNorm = normList.get(indices.idxVecNormal); Vector3f vecNorm = normList.get(indices.idxVecNormal);
normArr[posIndex * 3] = vecNorm.x; normArr[posIndex * 3] = vecNorm.x;
normArr[posIndex * 3 + 1] = vecNorm.y; normArr[posIndex * 3 + 1] = vecNorm.y;
@@ -365,9 +363,9 @@ private static void processFaceVertex(IdxGroup indices, List<Vector2f> textCoord
} }
``` ```
另一件值得注意的是纹理坐标是UV格式所以Y坐标需要将文件中取到的值减去一 此外需要注意的是纹理坐标是UV格式所以Y坐标为用一减去文件中取到的值。
最后我们可以渲染OBJ模型。我已经准备了一个OBJ文件其中是前章节中使用过的具有纹理的立方体。为了在`DummyGame`类的`init`方法中使用它,我们需要创建一个`GameItem`实例。 现在,我们终于可以渲染OBJ模型。我准备了一个OBJ文件其中是前章节中使用过的具有纹理的立方体。为了在`DummyGame`类的`init`方法中使用它,我们需要创建一个`GameItem`实例。
```java ```java
Texture texture = new Texture("/textures/grassblock.png"); Texture texture = new Texture("/textures/grassblock.png");
@@ -382,7 +380,7 @@ gameItems = new GameItem[]{gameItem};
![有纹理的立方体](_static/09/textured_cube.png) ![有纹理的立方体](_static/09/textured_cube.png)
我们可以尝试渲染其他模型例如可以使用著名的Standford Bunny模型(它可以自由地下载),将它放在resources文件夹。这个模型没有纹理所以我们可以这样做 我们可以尝试渲染其他模型例如可以使用著名的Stanford Bunny模型它可以免费下载),它放在resources文件夹。这个模型没有纹理,所以我们可以这样做:
```java ```java
Mesh mesh = OBJLoader.loadMesh("/models/bunny.obj"); Mesh mesh = OBJLoader.loadMesh("/models/bunny.obj");
@@ -392,7 +390,7 @@ gameItem.setPosition(0, 0, -2);
gameItems = new GameItem[]{gameItem}; gameItems = new GameItem[]{gameItem};
``` ```
![Standford Bunny](_static/09/standford_bunny.png) ![Stanford Bunny](_static/09/standford_bunny.png)
这个模型看起来有点奇怪,因为没有纹理也没有光,所以我们不能看到它的体积,但是你可以检查模型是否正确地加载。在`Window`类中设置OpenGL参数时添加这一行代码。 这个模型看起来有点奇怪,因为没有纹理也没有光,所以我们不能看到它的体积,但是你可以检查模型是否正确地加载。在`Window`类中设置OpenGL参数时添加这一行代码。
@@ -400,17 +398,17 @@ gameItems = new GameItem[]{gameItem};
glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
``` ```
当你放大的时候,你会看到类似的东西。 当你放大的时候,你会看到如下所示的东西。
![Standford Bunny的三角形](_static/09/standford_bunny_triangles.png) ![Stanford Bunny的三角形](_static/09/standford_bunny_triangles.png)
现在你可以看到成模型的所有三角形 现在你可以看到成模型的所有三角形。
使用OBJ载入类可以使用Blender创建模型。Blender是一个强大的工具但刚开始使用它有点困难它有很多选项很多组合第一次使用它的时候你需要做很多基本的事情。当使用Blender导出模型时务必将法线和面输出为三角形。 有了这个OBJ载入类你现在可以使用Blender创建模型。Blender是一个强大的工具但刚开始使用它有点困难它有很多选项很多关节组合,在次使用它时你需要花时间做很多基本的事情。当使用Blender导出模型时确保包含法线并将面导出为三角形。
![OBJ导出选项](_static/09/obj_export_options.png) ![OBJ导出选项](_static/09/obj_export_options.png)
还要记得在导出时切割边,因为我们不能给同一个顶点配方几个纹理坐标。此外,我们需要为每个三角形定义法线,而不是顶点。如果你遇到了光照问题(下一章),在使用模型时你应该验证一下法线。你可以在Blender中看到法线。 导出时请记得分割边,因为我们不能将多个纹理坐标指定给同一个顶点。此外,我们需要为每个三角形定义法线,而不是指定给顶点。如果你在某些模型中遇到了光照问题(在下一章中)你应该验证一下法线。你可以在Blender中看到法线。
![](_static/09/edge_split.png) ![](_static/09/edge_split.png)

View File

@@ -20,7 +20,7 @@
**着色器Shader** 用于控制图形管线不同阶段的特定程序。 **着色器Shader** 用于控制图形管线不同阶段的特定程序。
**顶点Vertex** 描述二维或者三维空间中的点的数据结构。 **顶点Vertex** 用于描述二维或者三维空间中的点的数据结构。
**顶点缓冲区Vertex Buffer** 使用顶点数组来包装所有需要渲染的顶点的数据结构,并使这些数据能够在图形管线的着色器中使用。 **顶点缓冲区Vertex Buffer** 使用顶点数组来包装所有需要渲染的顶点的数据结构,并使这些数据能够在图形管线的着色器中使用。
@@ -106,4 +106,10 @@
**观察矩阵View Matrix** 用于将顶点向摄像机所在位置的相反方向和旋转状态变换的矩阵。 **观察矩阵View Matrix** 用于将顶点向摄像机所在位置的相反方向和旋转状态变换的矩阵。
**模型观察矩阵Model View Matrix** 是观察矩阵与世界矩阵组合,可将模型空间的坐标变换到摄像机空间中的坐标,用于简化计算。 **模型观察矩阵Model View Matrix** 是观察矩阵与世界矩阵组合,可将模型空间的坐标变换到摄像机空间中的坐标,用于简化计算。
**OBJ.obj** Wavefront Technologies开发的一种几何定义开放文件格式现已被广泛采用。
**法线Normal** 一个平面的法线是一个垂直于该平面的长度为1的向量。
**顶点法线Vertex Normal** 一个顶点的法线是其参与组成的三角形的面的法线的组合其长度仍为1。