本文主要解决的问题是:
如何在OpenGL中显示一个3D盒子?
欢迎来到3D世界!学了这么多东西,还只能画一些三角形和矩形,是不是感觉特别憋屈?“我是来学3D的,为啥到现在还都是2D的图片???”想必你已经在心里抱怨过很多次了。不过不用担心,从这一章开始,我们就正式进入3D世界啦,因为在本章我们会显示一个3D盒子(没准还不止一个哦!)!
坐标系统
想想在现实世界中,我们怎样能看到一样东西,比如说就是我们常用的电脑?首先,电脑会在工厂中叮叮当当地装配起来,然后,通过飞机火车汽车之类的东西,将它送到我们手中。我们拿到之后,要把包装拆掉,然后放到一个合适的地方,这样我们才能看到。什么,你说你没看到?废话,你闭着眼睛能看到个球啊!
让我们一步一步拆解这个过程,看看我们要显示一个3D的物体是有多么复杂!
局部空间(物体空间)
电脑在工厂中制造的时候,工人需要去考虑这颗螺丝需要装到东经多少度,北纬多少度的位置上吗?当然不可能,他只需要考虑需要装到电脑的什么位置!这个就是局部空间。在局部空间中,物体位于空间的原点,所有的调整都是基于物体的相对位置去调整的。
世界空间
电脑装配好之后,自然是要卖出去才有价值。这时候,就需要把它运到专卖店里,或者直接快递到买家手里。于是,它被装到火车上,从生产地(A)送到了销售点(B),卫星定位起始点位于东经xx度,北纬xx度。这就是世界空间。物体首先要有一个初始位置,然后才能从一个位置移动到另一个位置。
在OpenGL中,我们使用模型矩阵将物体放到世界空间中的某个位置上。
这时,我的手机响了,快递员告诉我电脑已经到了,让我签收。但是,我没看到啊?于是我就起床去拿快递了……到了楼下,环顾四周,发现快递员正好在我左前方40米的距离,于是我转个弯就直直地朝他走过去。我终于看到我的电脑了!
看上去非常流畅的过程,在OpenGL中就需要分成两个操作:1、人眼的视线转换到正对-z轴的方向,并将物体转换到以人眼为原点的位置上。2、物体必须在人眼的视野之内。这就是观察空间和裁剪空间的概念。可能第一步很难察觉,我们从OpenGL的角度来解释一下。
观察空间
观察空间是以摄像机为位置为原点,观察方向为-z轴方向的一个空间。我们通常会用一系列的平移和旋转的变换来把世界空间中的物体转换到观察空间中。用来执行这种变换的东西,我们成为观察矩阵。
裁剪空间
摄像机有朝向,也有拍摄的视野范围,所有在视野范围之外的东西都看不到,都被剔除了。
在每个顶点着色器运行结束的时候,OpenGL希望所有的坐标都在一个指定的范围内,所有超出范围的坐标都会被裁剪,被抛弃,剩下的坐标才会进入片元着色阶段然后显示到你的屏幕上。这,就是裁剪空间名字的由来。
将物体从观察空间转换到裁剪空间的操作叫做投影变换(perspective projection),用到的也就是一个投影变换的矩阵而已。
还是从现实世界中看到东西的角度去分析。
人眼在看东西时,左右宽度上有一定的范围,上下高度上也有一定的范围。放到OpenGL中,就是摄像机的左右和上下方向上都有一定的视野角度(FOV),只有在这个角度范围内的东西,才可能被看到。
裁剪空间从上面的图中可以看出,h和w就确定了摄像机上下左右可以看到的范围大小。通常我们会设置上下左右的视野都是90度。为了方便计算(这点非常重要!想把所有的物体都渲染出来,世界上所有的计算机的运算能力加起来都不够,所以,不要显示的就坚决剔除。),我们也会设置一个近裁剪面和一个远裁剪面。比近裁剪面更近的物体被剔除,比远裁剪面更远的物体被剔除。我们需要把两个裁剪面之间的所有物体都映射到投影平面上(投影平面可以在裁剪面和远裁剪面之间,图上只是一种情况),其他的物体都被剔除!
视口空间
视口空间,可以简单地理解成应用窗口。投影平面上的东西和窗口上的像素通过一一对应的方式映射到窗口,在窗口上显示!
这一步由OpenGL完成,我们不管。
欢迎来到3D世界!
为了看到真正的3D效果,我们先给之前的图片加几个面,让它成为一个3D盒子。然后,让这个盒子不停地旋转。
定义所有顶点的工作非常复杂繁琐,我这里直接把定义好的36个顶点都列出来(6个面 * 每个面2个三角形 * 每个三角形3个顶点),每个面顶点都包含了纹理坐标。
float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};
修改顶点属性的设置
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
修改顶点着色器中纹理坐标的位置
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
再显示的方式就是我们之前讲过的glDrawArrays(GL_TRIANGLES, 0, 36);
接下来就是走一个显示流水线,局部空间-->世界空间-->观察空间-->裁剪空间-->视口空间。
定义模型变换矩阵:
glm::mat4 model;
model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
模型会根据运行时间旋转一定的角度,看起来有动画的效果。
定义观察变换矩阵:
glm::mat4 view;
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -4.0f));
这里涉及到OpenGL的坐标系类型,OpenGL是右手坐标系,而DX是左手坐标系,简单讲讲右手坐标系:
右手坐标系在图上画右手坐标系,一条水平的直线表示X轴,从左往右为正方向;一条垂直的直线表示Y轴,从下到上位正方向;一条垂直纸面的轴为Z轴,从纸内到纸外为正方向。
被称为右手坐标系是因为当你伸出右手,四指的方向指向X轴正方向,弯曲四指指向Y轴正方向,大拇指的方向为Z轴正方向。
摄像机初始位置在原点,朝向是-z轴方向。想要看到我们的物体,可以让摄像机往+z轴方向移动,也可以让整个场景往-z轴方向移动。我们选择让整个场景往-z轴方向移动。关于摄像机的更多东西我们会在下一节讨论。
定义投影变换矩阵:
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::radians定义了FOV,角度为45度。第二个参数定义了屏幕宽高比(aspect ratio),这个值会影响显示到窗口中的物体是原样显示还是被拉伸。0.1f是近裁剪面,100.0f是远裁剪面。
我们试着将这些矩阵应用到顶点着色器中,顶点着色器需要3个变量来接收这些矩阵然后使用:
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4 (aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
主循环中也要每次给这三个变量赋值:
shader.setMat4("model", glm::value_ptr(model));
shader.setMat4("view", glm::value_ptr(view));
shader.setMat4("projection", glm::value_ptr(projection));
调整完成,编译运行。效果如下图所示(文章里只能放图片,将就一下)
显示效果嗯?不对,不是我们想要的,居然能“看”到里面的东西。这是因为在绘制三角形的时候,是逐个绘制的。OpenGL没有检测它们在位置上的前后关系,只是用后来的像素覆盖掉之前的像素而已。
我们需要开启深度检测!
OpenGL内部本就保存了一份顶点的深度信息,这个信息的名字叫z缓存(z-buffer),也叫深度缓存。默认情况下,深度检测是关闭的,我们需要在某个位置把它打开。打开的方法是调用glEnable(GL_DEPTH_TEST);
。
然后,在清除屏幕的时候,也需要把深度缓存的数据清除掉!
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
好了,再次编译运行!
启用深度缓存后的效果效果完美!如果显示不对,下载源码对比一下。
这就够了吗?
不!!!我要10个!我们不用傻傻地再去复制36个顶点9次,只需要把这个盒子移动某个位置,然后绘制一遍就行了。
定义10个位置:
glm::vec3 cubePositions[] = {
glm::vec3( 0.0f, 0.0f, 0.0f),
glm::vec3( 2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3( 2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3( 1.3f, -2.0f, -2.5f),
glm::vec3( 1.5f, 2.0f, -2.5f),
glm::vec3( 1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
然后在主循环中绘制10次
glBindVertexArray(VAO);
for (int i = 0; i < 10; ++i) {
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
shader.setMat4("model", glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 36);
}
编译运行。
运行效果非常完美!有问题的童鞋可以下载源码对比。
总结
变换流水线这一章我们学习了3D显示背后的空间变换,以及变换空间的方法。学完基础知识后,立刻上手进行运用,结果十分振奋人心,我们终于可以在3D世界中绘制了!
如果对文章中的内容有不同的观点,欢迎留言给笔者。
参考资料:
www.learningopengl.com(非常好的网站,推荐学习)
网友评论