网格

文章推薦指數: 80 %
投票人數:10人

我们最终仍要将这些数据转换为OpenGL能够理解的格式,这样才能渲染这个物体。

我们从上一节中学到,网格(Mesh)代表的是单个的可绘制实体,我们现在先来定义一个我们自己 ... Togglenavigation LearnOpenGLCN 主页 目录 简介 入门 OpenGL 创建窗口 你好,窗口 你好,三角形 着色器 纹理 变换 坐标系统 摄像机 复习 光照 颜色 基础光照 材质 光照贴图 投光物 多光源 复习 模型加载 Assimp 网格 模型 高级OpenGL 深度测试 模板测试 混合 面剔除 帧缓冲 立方体贴图 高级数据 高级GLSL 几何着色器 实例化 抗锯齿 高级光照 高级光照 Gamma校正 阴影 阴影映射 点阴影 CSM 法线贴图 视差贴图 HDR 泛光 延迟着色法 SSAO PBR 理论 光照 IBL 漫反射辐照 镜面IBL 实战 调试 文本渲染 2D游戏 Breakout 准备工作 渲染精灵 关卡 碰撞 球 碰撞检测 碰撞处理 粒子 后期处理 道具 音效 渲染文本 结语 历史存档 代码仓库 搜索 上一节 下一节 GitHub 支持原作者 网格 初始化 渲染 网格 原文 Mesh 作者 JoeyDeVries 翻译 Krasjet 校对 暂未校对 通过使用Assimp,我们可以加载不同的模型到程序中,但是载入后它们都被储存为Assimp的数据结构。

我们最终仍要将这些数据转换为OpenGL能够理解的格式,这样才能渲染这个物体。

我们从上一节中学到,网格(Mesh)代表的是单个的可绘制实体,我们现在先来定义一个我们自己的网格类。

首先我们来回顾一下我们目前学到的知识,想想一个网格最少需要什么数据。

一个网格应该至少需要一系列的顶点,每个顶点包含一个位置向量、一个法向量和一个纹理坐标向量。

一个网格还应该包含用于索引绘制的索引以及纹理形式的材质数据(漫反射/镜面光贴图)。

既然我们有了一个网格类的最低需求,我们可以在OpenGL中定义一个顶点了: structVertex{ glm::vec3Position; glm::vec3Normal; glm::vec2TexCoords; }; 我们将所有需要的向量储存到一个叫做Vertex的结构体中,我们可以用它来索引每个顶点属性。

除了Vertex结构体之外,我们还需要将纹理数据整理到一个Texture结构体中。

structTexture{ unsignedintid; stringtype; }; 我们储存了纹理的id以及它的类型,比如是漫反射贴图或者是镜面光贴图。

知道了顶点和纹理的实现,我们可以开始定义网格类的结构了: classMesh{ public: /*网格数据*/ vectorvertices; vectorindices; vectortextures; /*函数*/ Mesh(vectorvertices,vectorindices,vectortextures); voidDraw(Shadershader); private: /*渲染数据*/ unsignedintVAO,VBO,EBO; /*函数*/ voidsetupMesh(); }; 你可以看到这个类并不复杂。

在构造器中,我们将所有必须的数据赋予了网格,我们在setupMesh函数中初始化缓冲,并最终使用Draw函数来绘制网格。

注意我们将一个着色器传入了Draw函数中,将着色器传入网格类中可以让我们在绘制之前设置一些uniform(像是链接采样器到纹理单元)。

构造器的内容非常易于理解。

我们只需要使用构造器的参数设置类的公有变量就可以了。

我们在构造器中还调用了setupMesh函数: Mesh(vectorvertices,vectorindices,vectortextures) { this->vertices=vertices; this->indices=indices; this->textures=textures; setupMesh(); } 这里没什么可说的。

我们接下来讨论setupMesh函数。

初始化 由于有了构造器,我们现在有一大列的网格数据用于渲染。

在此之前我们还必须配置正确的缓冲,并通过顶点属性指针定义顶点着色器的布局。

现在你应该对这些概念都很熟悉了,但我们这次会稍微有一点变动,使用结构体中的顶点数据: voidsetupMesh() { 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(unsignedint), &indices[0],GL_STATIC_DRAW); //顶点位置 glEnableVertexAttribArray(0); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)0); //顶点法线 glEnableVertexAttribArray(1); glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)offsetof(Vertex,Normal)); //顶点纹理坐标 glEnableVertexAttribArray(2); glVertexAttribPointer(2,2,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)offsetof(Vertex,TexCoords)); glBindVertexArray(0); } 代码应该和你所想得没什么不同,但有了Vertex结构体的帮助,我们使用了一些小技巧。

C++结构体有一个很棒的特性,它们的内存布局是连续的(Sequential)。

也就是说,如果我们将结构体作为一个数据数组使用,那么它将会以顺序排列结构体的变量,这将会直接转换为我们在数组缓冲中所需要的float(实际上是字节)数组。

比如说,如果我们有一个填充后的Vertex结构体,那么它的内存布局将会等于: Vertexvertex; 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]; 由于有了这个有用的特性,我们能够直接传入一大列的Vertex结构体的指针作为缓冲的数据,它们将会完美地转换为glBufferData所能用的参数: glBufferData(GL_ARRAY_BUFFER,vertices.size()*sizeof(Vertex),&vertices[0],GL_STATIC_DRAW); 自然sizeof运算也可以用在结构体上来计算它的字节大小。

这个应该是32字节的(8个float*每个4字节)。

结构体的另外一个很好的用途是它的预处理指令offsetof(s,m),它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。

这个宏会返回那个变量距结构体头部的字节偏移量(ByteOffset)。

这正好可以用在定义glVertexAttribPointer函数中的偏移参数: glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)offsetof(Vertex,Normal)); 偏移量现在是使用offsetof来定义了,在这里它会将法向量的字节偏移量设置为结构体中法向量的偏移量,也就是3个float,即12字节。

注意,我们同样将步长参数设置为了Vertex结构体的大小。

使用这样的一个结构体不仅能够提供可读性更高的代码,也允许我们很容易地拓展这个结构。

如果我们希望添加另一个顶点属性,我们只需要将它添加到结构体中就可以了。

由于它的灵活性,渲染的代码不会被破坏。

渲染 我们需要为Mesh类定义最后一个函数,它的Draw函数。

在真正渲染这个网格之前,我们需要在调用glDrawElements函数之前先绑定相应的纹理。

然而,这实际上有些困难,我们一开始并不知道这个网格(如果有的话)有多少纹理、纹理是什么类型的。

所以我们该如何在着色器中设置纹理单元和采样器呢? 为了解决这个问题,我们需要设定一个命名标准:每个漫反射纹理被命名为texture_diffuseN,每个镜面光纹理应该被命名为texture_specularN,其中N的范围是1到纹理采样器最大允许的数字。

比如说我们对某一个网格有3个漫反射纹理,2个镜面光纹理,它们的纹理采样器应该之后会被调用: uniformsampler2Dtexture_diffuse1; uniformsampler2Dtexture_diffuse2; uniformsampler2Dtexture_diffuse3; uniformsampler2Dtexture_specular1; uniformsampler2Dtexture_specular2; 根据这个标准,我们可以在着色器中定义任意需要数量的纹理采样器,如果一个网格真的包含了(这么多)纹理,我们也能知道它们的名字是什么。

根据这个标准,我们也能在一个网格中处理任意数量的纹理,开发者也可以自由选择需要使用的数量,他只需要定义正确的采样器就可以了(虽然定义少的话会有点浪费绑定和uniform调用)。

Important 像这样的问题有很多种不同的解决方案。

如果你不喜欢这个解决方案,你可以自己想一个你自己的解决办法。

最终的渲染代码是这样的: voidDraw(Shadershader) { unsignedintdiffuseNr=1; unsignedintspecularNr=1; for(unsignedinti=0;i



請為這篇文章評分?