opengl编程指南第8版源码怎么下载、编译,请参考《opengl编程指南第8版源码编译详细说明》
1 程序启动
C++的程序都是从main函数启动的,但是这工程咋一看死活找不到main,给人的感觉是:不知道怎么被操作系统调用起来的。通过阅读vapp.h,在99~103行发现了如下代码块:
即这里定义了一个宏MAIN_DECL,在Windows下该宏表示就是如下字符串:
int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE
hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
即表示WinMain函数,搞过MFC的人都知道,这就是Windows下的图形界面的main函数入口,该函数内部调用了main,是对main的包装,再加了一些额外的东西。
如果当前的操作系统不是Windows,如:linux则该宏表示一般的main。
再往下看,第117~126行发现如下代码:
上面的代码说白了,就是通过字符串的拼接,凑齐WinMain(在Windows下)或main(在非Windows 下)的函数定义,根据上面提到的MAIN_DECL宏的定义,在Windows下WinMain函数的定义为:
int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
VermilionApplication * app = appclass::Create();
app->Initialize(title);
app->MainLoop();
app->Finalize();
return 0;
}
void APIENTRY VermilionApplication::DebugOutputCallback(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
GLvoid* userParam)
{
OutputDebugStringA(message);
OutputDebugStringA("\n");
}
而在非Windows下main函数的定义为:
int main(int argc, char ** argv)
{
VermilionApplication * app = appclass::Create();
app->Initialize(title);
app->MainLoop();
app->Finalize();
return 0;
}
void APIENTRY VermilionApplication::DebugOutputCallback(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
GLvoid* userParam)
{
OutputDebugStringA(message);
OutputDebugStringA("\n");
}
这就是main函数即程序被操作系统调起来的入口。在03-drawcommands.cpp的第63行如下:
而DEFINE_APP宏在vapp.h的定义如下:
#define DEFINE_APP(appclass,title) \
VermilionApplication * VermilionApplication::s_app; \
\
void VermilionApplication::MainLoop(void) \
{ \
do \
{ \
Display(); \
glfwPollEvents(); \
} while (!glfwWindowShouldClose(m_pWindow)); \
} \
\
MAIN_DECL \
{ \
VermilionApplication * app = appclass::Create(); \
\
app->Initialize(title); \
app->MainLoop(); \
app->Finalize(); \
\
return 0; \
} \
\
DEBUG_OUTPUT_CALLBACK
综合上面对main的分析,则DEFINE_APP宏在vapp.h展开后的代码如下(以Windows平台为例子讲解,下同, 非Windows平台原理一样):
VermilionApplication * VermilionApplication::s_app;
void VermilionApplication::MainLoop(void)
{
do
{
Display();
glfwPollEvents();
} while (!glfwWindowShouldClose(m_pWindow));
}
int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
VermilionApplication * app = DrawCommandExample::Create();
app->Initialize(title);
app->MainLoop();
app->Finalize();
return 0;
}
void APIENTRY VermilionApplication::DebugOutputCallback(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
GLvoid* userParam)
{
OutputDebugStringA(message);
OutputDebugStringA("\n");
}
同样地:对03-drawcommands.cpp的如下代码:
进行宏展开及结合前文对DEFINE_APP宏展开的分析,得出03-drawcommands.cpp中第45~63经过宏展开后代码为:
class DrawCommandExample: public VermilionApplication
{
public:
typedef class VermilionApplication base;
static VermilionApplication * Create(void)
{
return (s_app = new DrawCommandExample);
}
};
VermilionApplication * VermilionApplication::s_app;
void VermilionApplication::MainLoop(void)
{
do
{
Display();
glfwPollEvents();
} while (!glfwWindowShouldClose(m_pWindow));
}
int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
VermilionApplication * app = DrawCommandExample::Create();
app->Initialize(title);
app->MainLoop();
app->Finalize();
return 0;
}
void APIENTRY VermilionApplication::DebugOutputCallback(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
GLvoid* userParam)
{
OutputDebugStringA(message);
OutputDebugStringA("\n");
}
到此类的结构就很清晰了,余下就是C++继承、多态的知识了
2 程序难点
程序中用到glBufferData、glVertexAttribPointer、glEnableVertexAttribArray、glBindVertexArray,它们的用法在《OPenGL编程指南第八版》的第2、3章节有详细的描述。它们之间的关系,请参考:《理解glVertexAttribPointer、glEnableVertexAttribArray、VAO、VBO的关系》、《glVertexAttribPointer第一个参数理解》。
如下:
glDrawElements最后一个参数为何传NULL,而不是存放顶点索引的数组,请参考:《glDrawElements参数在新旧版本传最后一个参数的不同》。glDrawArraysInstanced和glDrawArrays的区别,请参考《OPenGL实例化绘制、普通绘制说明》。
下述代码:
vmath::mat4 projection_matrix(vmath::frustum(-1.0f, 1.0f, -aspect, aspect, 1.0f, 500.0f));
构建了如下的一个平头截体:
其中:
Left Pannel = -1.0f
Right Pannel = 1.0f
Bottom Pannel = -aspect
Top Pannel = aspect
zNear Pannel = 1.0
zFar Pannel = 500
vmath::frustum对应OPenGL中的透视投影函数glFrustum, 至于glFrustum是怎么推导出来的,请参考:《OPengGL投影矩阵的推导(OpenGLD3D)》
如下代码:
// Draw Arrays...
model_matrix = vmath::translate(-3.0f, 0.0f, -5.0f);
glUniformMatrix4fv(render_model_matrix_loc, 1, GL_FALSE, model_matrix);
glDrawArrays(GL_TRIANGLES, 0, 3);
我们来分析,是怎么怎么画出来三角形的:
model_matrix = vmath::translate(-3.0f, 0.0f, -5.0f);
跟进vmath::translate代码处,发现其构建了一个如下以列为主序的矩阵,如下:
即model_matrix为上面的矩阵,如下代码:
glUniformMatrix4fv(render_model_matrix_loc, 1, GL_FALSE, model_matrix);
将model_matrix矩阵设置到顶点着色器中,顶点着色器如下:
#version 330
uniform mat4 model_matrix;
uniform mat4 projection_matrix;
layout (location = 0) in vec4 position;
layout (location = 1) in vec4 color;
out vec4 vs_fs_color;
void main(void)
{
const vec4 pos[3] = vec4[3](vec4(-0.3, -0.3, 0.0, 1.0), vec4(0.3, -0.3, 0.0, 1.0), vec4(-0.3, 0.3, 0.0, 1.0) );
vs_fs_color = color;
gl_Position = projection_matrix * (model_matrix * position);
}
可以发现着色器中的uniform mat4 model_matrix就是外层即前文所述的model_matrix矩阵,着色器中的position就是外层即主程序中即如下所示的顶点坐标数组:
请注意:OPenGL中的矩阵是以列主序的(详细讨论请参考《OpenGL列向量和OSG行向量的理解》),所以,上面的、以行为主序表示的顶点数组传入到OPenGL后,存储为以列为主序的矩阵了,即变为如下矩阵:
上述着色器中的:
model_matrix * position
则将position矩阵做了一个model_matrix 的变换,这里其实model_matrix可以看成是视图矩阵,position则可以看成是模型矩阵,通过这两个矩阵相乘后,就是把模型矩阵变换到视图即相机矩阵下了,其相乘的结果如下:
可以看到相乘后,四个点都做了一个平移,即原来的p1(-1,-1, 0, 1)变为了现在的p1(-4, -1, -5, 1), 原来的p2(1,-1, 0, 1)变为了现在的p2(-2, -1, -5, 1), 原来的p3(-1,1, 0, 1)变为了现在的p3(-4, 1, -5, 1), 原来的p4(-1,-1, 0, 1)变为了现在的p4(-4, -1, -5, 1),即上面等号后面的矩阵就是model_matrix * position的结果,这就是OPenGL中提到的模型视图矩阵,然后 projection_matrix再与其相乘,就得到了模型视图投影矩阵,也就是被平头截体裁剪后的模型。需要说明的是:因为画的是三角形,所以本例中的点P4在画图中未用到,同样的,对剩余的三个三角形绘制代码进行分析,得出最终的输出结果如下:
至此:《opengl编程指南第8版》第3章中的第1个例子分析完成。