有一点3D基础应该是知道这么个事情,我们会在*.obj文件,即3D对象中放至少两个东西——顶点坐标和顶点索引,索引表明了坐标之间的顺序,这样可以省略一些重复的坐标。什么意思呢?比如我们有四个顶点:
四个点分别对应到0,1,2,3(因为索引是0开始的,当然也有1开始的,这里我们用0开始),在实际绘图的时候,一般会用很多的小三角形去近似一个图形,比如我们会使用B -> A -> C,A -> C -> D的方式去创造两个小三角形来拼出一个四边形:
注意我们的连线顺序,A和C是相交的位置,它们所关联的两个小三角形的边,方向是相同的。在之后会创造一些彩色的图形,这个顺序会保证两个小三角形相交的部分颜色相同。
好处很明显,我们使用了很少的坐标,外加一个索引,表达出了一个四边形。所以,OpenGL中自然也有这个玩意,它被叫做索引缓冲对象IBO(Index Buffer Object)。
创建IBO的方法和VBO类似,使用glGenBuffers(IBO_ID, &hIBO)创建。接着,我们需要使用glBindBuffer和glBufferData对它进行绑定,这一切都是在初始化内容里面完成的。在绘制的时候,我们使用glBindBuffer来表明所使用的索引缓冲,然后OpenGL就会按照这个缓冲绘制。索引缓冲的下标是从0开始的。
需要注意的是,VAO会储存IBO绑定,因此我们的绑定依然可以在初始化过程完成。这样一来,绘制流程就变成了:
// 绑定VAO
glBindVertexArray
// 载入VBO
glBindBuffer
glBufferData
// 载入IBO
glBindBuffer
glBufferData
// 设置顶点属性
glVertexAttribPointer
glEnableVertexAttribArray
// ......
// 渲染
glUseProgram
glBindVertexArray(Handle of VAO)
glDrawElements
glBindVertexArray
具体而言,参照我的实现。里面的一些函数被移入了utils.h中——它们没有修改,也大概不会再修改,它们的实现和上一篇文章是一样的。同样的,我只修改了渲染循环和初始化渲染代码部分。可以看到新增的内容。
#include "pch.h"
#include "utils.h"
#include "resource.h"
#define VBO_ID 1
#define IBO_ID 1
#define VAO_ID 1
void framebufferResize(GLFWwindow *, int, int);
void keyInput(GLFWwindow *win, int key, int scancode, int action, int mods);
bool initRender();
void renderLoop();
const float vetPos[] =
{/* X Y Z */
0.5f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
-0.5f, 0.5f, 0.0f
};
const GLuint index[] =
{
0, 1, 3,
1, 2, 3,
};
const char vetShader[] =
{
"#version 400 core \n\
layout (location = 0) in vec3 pos; \n\
void main() \n\
{ \n\
gl_Position = vec4(pos.x, pos.y, pos.z, 1.0); \n\
}"
};
const char pixelShader[] =
{
"#version 400 core \n\
out vec4 outColor; \n\
void main() \n\
{ \n\
outColor = vec4(0.5f, 0.2f, 0.5f, 1.0f); \n\
}"
};
GLuint hVBO, hVAO, hIBO;
GLuint hShaderProg, hVetShader, hPixelShader;
int APIENTRY wWinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow
)
{
GLFWwindow *win;
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 4);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#if _DEBUG
FILE *ous;
AllocConsole();
setlocale(LC_CTYPE, "chs");
freopen_s(&ous, "CONOUT$", "w", stdout);
#endif
if ((win = glfwCreateWindow(420, 320, "Graph3D", NULL, NULL)) == NULL)
{
popMessageBox(_T("无法创建窗口"));
goto err;
}
glfwMakeContextCurrent(win);
glfwSetKeyCallback(win, keyInput);
glfwSetFramebufferSizeCallback(win, framebufferResize);
if (gladLoadGLLoader((GLADloadproc)glfwGetProcAddress) == 0)
{
popMessageBox(_T("无法初始化GLad, 请检查您的设备是否支持OpenGL 4.0"));
goto err;
}
if (initRender() == false)
{
popMessageBox(_T("无法初始化渲染"));
goto err;
}
while (glfwWindowShouldClose(win) == 0)
{
renderLoop();
glfwPollEvents(); // 检查, 触发事件
glfwSwapBuffers(win);
}
goto succ;
err:
#if _DEBUG
system("pause");
#endif
succ:
glfwTerminate();
#if _DEBUG
FreeConsole();
#endif
return 0;
}
// 窗口尺寸被改变
void framebufferResize(GLFWwindow* win, int w, int h)
{
glViewport(0, 0, w, h);
}
// 键盘输入
void keyInput(GLFWwindow *win, int key, int scancode, int action, int mods)
{
if (action == GLFW_RELEASE) // 松开按键
if (key == GLFW_KEY_ESCAPE)
{
glfwSetWindowShouldClose(win, true);
}
}
// 初始化
bool initRender()
{
glGenBuffers(VBO_ID, &hVBO); // 创建并绑定顶点缓冲
glGenBuffers(IBO_ID, &hIBO); // 索引缓冲
glGenVertexArrays(VAO_ID, &hVAO);
hVetShader = glCreateShader(GL_VERTEX_SHADER);
if (compileShader(vetShader, hVetShader) == false)
{
return false;
}
hPixelShader = glCreateShader(GL_FRAGMENT_SHADER);
if (compileShader(pixelShader, hPixelShader) == false)
{
return false;
}
hShaderProg = glCreateProgram();
if (linkShaderProg(hShaderProg, hVetShader, hPixelShader) == false)
{
return false;
}
glDeleteShader(hVetShader);
glDeleteShader(hPixelShader);
glBindVertexArray(hVAO); // 绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, hVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vetPos), vetPos, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, hIBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index), index, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
return true;
}
// 渲染循环
void renderLoop()
{
glClearColor(0.2f, 0.3f, 0.5f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(hShaderProg);
glBindVertexArray(hVAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}
如果你运行成功了,你会看到一个暗紫红色的矩形。它是由两个三角形组成的。这是因为我们一般把三角形作为基本图元,OpenGL绝大部分时候都是处理三角形的。glDrawElements的第二个参数表示了我们使用6个索引。
如果你没有看到矩形,那么说明你有某一步做错了,请仔细检查。下面我们切换绘图模式——使用glPolygonMode切换,在初始化代码段末尾(即initRender()函数的最后)添加下面的代码:
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
编译运行,你会看到三角形线框:
我们可以用GL_LINE表示线框模式,稍后可以改成GL_FILL,切换成我们原本的绘图模式。
自此,可以长叹一口气了————OpenGL绘制出这些图案需要了解非常多的知识,目前来说,我们成功的踏出了第一步。