精通
英语
和
开源
,
擅长
开发
与
培训
,
胸怀四海
第一信赖
锐英源精品原创,禁止转载和任何形式的非法内容使用,违者必究
最近研究三维图形库,原本想用图形库加载assimp的数据显示,后来发现用OpenGL就可以显示,用图形库有方便的环境,但环境可能比较大。本文列出用OpenGL显示的一些思路,来源于国外网页
With Assimp we can load many different models into the application, but once loaded they're all stored in Assimp's data structures. What we eventually want is to transform that data to a format that OpenGL understands so that we can render the objects. We learned from the previous chapter that a mesh represents a single drawable entity, so let's start by defining a mesh class of our own.
Let's review a bit of what we've learned so far to think about what a mesh should minimally have as its data. A mesh should at least need a set of vertices, where each vertex contains a position vector, a normal vector, and a texture coordinate vector. A mesh should also contain indices for indexed drawing, and material data in the form of textures (diffuse/specular maps).
Now that we set the minimal requirements for a mesh class we can define a vertex in OpenGL:
使用 Assimp,我们可以将许多不同的模型加载到应用程序中,但是一旦加载,它们就会全部存储在 Assimp 的数据结构中。我们最终想要的是将数据转换为 OpenGL 能够理解的格式,以便我们可以渲染对象。我们从上一章中了解到,一个网格代表一个单一的可绘制实体,所以让我们从定义我们自己的网格类开始。
让我们回顾一下到目前为止我们学到的一些知识,以思考网格应该作为其数据的最低限度。一个网格至少应该需要一组顶点,其中每个顶点包含一个位置向量、一个法向量和一个纹理坐标向量。网格还应包含索引绘图的索引和纹理形式的材质数据(漫反射/镜面反射贴图)。
现在我们为网格类设置了最低要求,我们可以在 OpenGL 中定义一个顶点:
struct Vertex { glm::vec3 Position; glm::vec3 Normal; glm::vec2 TexCoords; };
We store each of the required vertex attributes in a struct called Vertex. Next to a Vertex struct we also want to organize the texture data in a Texture struct:我们将每个必需的顶点属性存储在一个名为 Vertex. Vertex struct后面 我们还想将纹理数据组织在一个 Texture 结构:
struct Texture { unsigned int id; string type; };
We store the id of the texture and its type e.g. a diffuse or specular texture.
Knowing the actual representation of a vertex and a texture we can start defining the structure of the mesh class:
我们存储纹理的 id 及其类型,例如漫反射或镜面反射纹理。
了解顶点和纹理的实际表示后,我们可以开始定义网格类的结构:
class Mesh { public: // mesh data vectorvertices; vector indices; vector textures; Mesh(vector vertices, vector indices, vector textures); void Draw(Shader &shader); private: // render data unsigned int VAO, VBO, EBO; void setupMesh(); };
As you can see, the class isn't too complicated. In the constructor we give the mesh all the necessary data, we initialize the buffers in the setupMesh function, and finally draw the mesh via the Draw function. Note that we give a shader to the Draw function; by passing the shader to the mesh we can set several uniforms before drawing (like linking samplers to texture units).
The function content of the constructor is pretty straightforward. We simply set the class's public variables with the constructor's corresponding argument variables. We also call the setupMesh function in the constructor:
如您所见,该类并不太复杂。在构造函数中,我们为网格提供了所有必要的数据,我们在设置网格 函数,最后通过 画功能。请注意,我们为画功能; 通过将着色器传递给网格,我们可以在绘制之前设置几个统一体(例如将采样器链接到纹理单元)。
构造函数的内容非常简单。我们只需使用构造函数的相应参数变量设置类的公共变量。我们也称设置网格 构造函数中的函数:
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures) { this->vertices = vertices; this->indices = indices; this->textures = textures; setupMesh(); }
Nothing special going on here. Let's delve right into the setupMesh function now.这里没什么特别的。让我们深入研究设置网格 现在发挥作用。
Thanks to the constructor we now have large lists of mesh data that we can use for rendering. We do need to setup the appropriate buffers and specify the vertex shader layout via vertex attribute pointers. By now you should have no trouble with these concepts, but we've spiced it up a bit this time with the introduction of vertex data in structs:
多亏了构造函数,我们现在有大量可用于渲染的网格数据列表。我们确实需要设置适当的缓冲区并通过顶点属性指针指定顶点着色器布局。到目前为止,您应该对这些概念没有任何问题,但是这次我们通过在结构中引入顶点数据来增加它的趣味性:
void setupMesh() { glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); // vertex positions glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); // vertex normals glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal)); // vertex texture coords glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords)); glBindVertexArray(0); }
The code is not much different from what you'd expect, but a few little tricks were used with the help of the Vertex struct.
Structs have a great property in C++ that their memory layout is sequential. That is, if we were to represent a struct as an array of data, it would only contain the struct's variables in sequential order which directly translates to a float (actually byte) array that we want for an array buffer. For example, if we have a filled Vertex struct, its memory layout would be equal to:
代码与您期望的没有太大区别,但是在帮助下使用了一些小技巧 顶点 结构。
结构在 C++ 中有一个很好的特性,即它们的内存布局是顺序的。也就是说,如果我们将一个结构体表示为一个数据数组,它只会按顺序包含结构体的变量,直接转换为我们想要用于数组缓冲区的浮点(实际上是字节)数组。例如,如果我们有一个填充顶点 struct,它的内存布局将等于:
Vertex vertex; vertex.Position = glm::vec3(0.2f, 0.4f, 0.6f); vertex.Normal = glm::vec3(0.0f, 1.0f, 0.0f); vertex.TexCoords = glm::vec2(1.0f, 0.0f); // = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f];
Thanks to this useful property we can directly pass a pointer to a large list of Vertex structs as the buffer's data and they translate perfectly to what glBufferData expects as its argument:
由于这个有用的属性,我们可以直接传递一个指向一个大列表的指针 顶点 结构体作为缓冲区的数据,它们完美地转换为什么 glBufferData 期望的参数:
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices[0], GL_STATIC_DRAW);
Naturally the sizeof operator can also be used on the struct for the appropriate size in bytes. This should be 32 bytes (8 floats * 4 bytes each).
Another great use of structs is a preprocessor directive called offsetof(s,m) that takes as its first argument a struct and as its second argument a variable name of the struct. The macro returns the byte offset of that variable from the start of the struct. This is perfect for defining the offset parameter of the glVertexAttribPointer function:
自然地,sizeof运算符也可以在结构上使用适当的字节大小。这应该是32字节(每个8浮点数 *4字节)。
结构体的另一个重要用途是称为预处理器指令offsetof(s,m),该指令将结构体作为其第一个参数,并将结构体的变量名称作为其第二个参数。宏从结构的开头返回该变量的字节偏移量。这非常适合定义偏移量参数glVertexAttrib 指针 功能:
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
The offset is now defined using the offsetof macro that, in this case, sets the byte offset of the normal vector equal to the byte offset of the normal attribute in the struct which is 3 floats and thus 12 bytes.
Using a struct like this doesn't only get us more readable code, but also allows us to easily extend the structure. If we want another vertex attribute we can simply add it to the struct and due to its flexible nature, the rendering code won't break.
偏移量现在使用 偏移量宏,在这种情况下,将法线向量的字节偏移量设置为等于3浮点数的结构中法线属性的字节偏移量,因此是12字节。
使用这样的结构体不仅能让我们的代码更具可读性,还能让我们轻松地扩展结构体。如果我们想要另一个顶点属性,我们可以简单地将它添加到结构中,并且由于其灵活的性质,渲染代码不会中断。
The last function we need to define for the Mesh class to be complete is its Draw function. Before rendering the mesh, we first want to bind the appropriate textures before calling glDrawElements. However, this is somewhat difficult since we don't know from the start how many (if any) textures the mesh has and what type they may have. So how do we set the texture units and samplers in the shaders?
To solve the issue we're going to assume a certain naming convention: each diffuse texture is named texture_diffuseN, and each specular texture should be named texture_specularN where N is any number ranging from 1 to the maximum number of texture samplers allowed. Let's say we have 3 diffuse textures and 2 specular textures for a particular mesh, their texture samplers should then be called:
我们需要定义的最后一个函数 网 要完成的类是它的 画功能。在渲染网格之前,我们首先要在调用之前绑定适当的纹理绘制元素. 然而,这有点困难,因为我们从一开始就不知道网格有多少(如果有)纹理以及它们可能有什么类型。那么我们如何在着色器中设置纹理单元和采样器呢?
为了解决我们要承担一定的命名规则的问题:每次漫反射纹理而得名texture_diffuseN,每个镜面质感应该被命名为texture_specularN其中N的任何范围内的数1以允许纹理采样的最大数量。假设我们有 3 个漫反射纹理和 2 个特定网格的镜面反射纹理,然后应该调用它们的纹理采样器:
uniform sampler2D texture_diffuse1; uniform sampler2D texture_diffuse2; uniform sampler2D texture_diffuse3; uniform sampler2D texture_specular1; uniform sampler2D texture_specular2;
By this convention we can define as many texture samplers as we want in the shaders (up to OpenGL's maximum) and if a mesh actually does contain (so many) textures, we know what their names are going to be. By this convention we can process any amount of textures on a single mesh and the shader developer is free to use as many of those as he wants by defining the proper samplers.据这个约定,我们可以在着色器中定义任意数量的纹理采样器(最多达到 OpenGL 的最大值),如果网格确实包含(这么多)纹理,我们就知道它们的名称是什么。根据这个约定,我们可以在单个网格上处理任意数量的纹理,并且着色器开发人员可以通过定义适当的采样器自由使用任意数量的纹理。
There are many solutions to problems like this and if you don't like this particular solution it is up to you to get creative and come up with your own approach.此类问题有很多解决方案,如果您不喜欢这个特定的解决方案,则由您来发挥创造力并提出自己的方法。The resulting drawing code then becomes:生成的绘图代码然后变为:
void Draw(Shader &shader) { unsigned int diffuseNr = 1; unsigned int specularNr = 1; for(unsigned int i = 0; i < textures.size(); i++) { glActiveTexture(GL_TEXTURE0 + i); // activate proper texture unit before binding // retrieve texture number (the N in diffuse_textureN) string number; string name = textures[i].type; if(name == "texture_diffuse") number = std::to_string(diffuseNr++); else if(name == "texture_specular") number = std::to_string(specularNr++); shader.setFloat(("material." + name + number).c_str(), i); glBindTexture(GL_TEXTURE_2D, textures[i].id); } glActiveTexture(GL_TEXTURE0); // draw mesh glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); glBindVertexArray(0); }
We first calculate the N-component per texture type and concatenate it to the texture's type string to get the appropriate uniform name. We then locate the appropriate sampler, give it the location value to correspond with the currently active texture unit, and bind the texture. This is also the reason we need the shader in the Draw function.
We also added "material." to the resulting uniform name because we usually store the textures in a material struct (this may differ per implementation).
Note that we increment the diffuse and specular counters the moment we convert them to string. In C++ the increment call: variable++ returns the variable as is and then increments the variable while ++variable first increments the variable and then returns it. In our case the value passed to std::string is the original counter value. After that the value is incremented for the next round.You can find the full source code of the Mesh class here.
The Mesh class we just defined is an abstraction for many of the topics we've discussed in the early chapters. In the next chapter we'll create a model that acts as a container for several mesh objects and implements Assimp's loading interface.
我们首先计算每个纹理类型的 N 分量并将其连接到纹理的类型字符串以获得适当的统一名称。然后我们定位合适的采样器,赋予它与当前活动纹理单元对应的位置值,并绑定纹理。这也是我们需要着色器的原因画 功能。
我们还添加"material."到生成的统一名称里,因为我们通常将纹理存储在材质结构中(这可能因实现而异)。
请注意,当我们将它们转换为string时,我们会增加漫反射和镜面反射计数器。在 C++ 中,增量调用:variable++按原样返回变量,然后增加变量,同时++variable 首先增加变量,然后返回它。在我们的例子中,传递给的值std::string是原始计数器值。之后,下一轮的值会增加。你可以找到完整的源代码 网在这里上课。
这 网我们刚刚定义的类是我们在前面章节中讨论的许多主题的抽象。在接下来的章节中,我们将创建一个充当几个网格对象和工具Assimp的装载面的容器模型。