当前位置: 首页 > news >正文

OpenGL学习笔记(六):Transformations 变换(变换矩阵、坐标系统、GLM库应用)

文章目录

  • 向量变换
  • 使用GLM变换(缩放、旋转、位移)
  • 将变换矩阵传递给着色器
  • 坐标系统与MVP矩阵
  • 三维变换
  • 绘制3D立方体 & 深度测试(Z-buffer)
  • 练习1——更多立方体


现在我们已经知道了如何创建一个物体、着色、加入纹理。但它们都还是静态的物体,我们可以尝试着在每一帧改变物体的顶点并且重配置缓冲区从而使它们移动,但这太繁琐了,而且会消耗很多的处理时间。通常我们使用矩阵(Matrix)对象来变换一个物体。

相关图形学原理参考:GAMES101学习笔记(二):Transformation 变换 (仿射变换、模型、视图、投影)
线性代数相关基础知识不在此介绍。本节重点关注如何使用矩阵对向量进行变换,以及GLM数学库的使用。

GLM是OpenGL Mathematics的缩写,它是一个只有头文件的库,也就是说我们只需包含对应的头文件就行了,不用链接和编译。在官网下载

向量变换

在Shader部分,我们了解了GLSL(OpenGL Shading Language)中会用到两种容器类型:向量(Vector)矩阵(Matrix)。我们用向量来表示位置,表示颜色,甚至是纹理坐标。

深入了解一下向量,它其实就是一个N×1矩阵,N表示向量分量的个数(也叫N维(N-dimensional)向量)。如果我们有一个M×N矩阵,我们可以用这个矩阵乘以我们的N×1向量。

用变换矩阵和向量相乘,即完成了向量的坐标变换。单位矩阵与向量相乘向量不变,但单位矩阵通常是生成其他变换矩阵的起点。

在OpenGL中,我们通常使用4×4的变换矩阵,而其中最重要的原因就是大部分的向量都是4分量的(x, y, z, w)

齐次坐标(Homogeneous Coordinates)

  • 向量的w分量也叫齐次坐标。想要从齐次向量得到3D向量,我们可以把x、y和z坐标分别除以w坐标。w分量通常是1.0。
  • 如果一个向量的齐次坐标是0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能位移。
  • 使用齐次坐标有几点好处:它允许我们在3D向量上进行位移(如果没有w分量我们是不能位移向量的),可以用w值创建3D视觉效果。
  • 缩放变换(Scaling)
    缩放是对向量的长度进行缩放,而保持它的方向不变。
    在这里插入图片描述
  • 位移变换(Translation)
    位移是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上移动了原始向量。
    在这里插入图片描述
  • 旋转变换(Rotation)
    在3D空间中旋转需要定义一个角度和一个旋转轴。物体会沿着给定的旋转轴旋转特定角度。旋转矩阵在3D空间中每个单位轴都有不同定义,旋转角度用θθ表示:在这里插入图片描述

可以将多个旋转矩阵复合,比如先沿着x轴旋转再沿着y轴旋转。但是这会导致一个问题——万向节死锁(Gimbal Lock)。避免万向节死锁的真正解决方案是使用四元数(Quaternion),它不仅更安全,而且计算会更有效率。这些内容我们暂时不在这里讨论。

变换矩阵的组合
根据矩阵之间的乘法,我们可以把多个变换组合到一个矩阵中。

需要注意的是,矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。
在使用GLM创建变换矩阵时,通常的执行顺序是:缩放 -> 旋转 -> 位移。这是因为变换矩阵是按照从右到左的顺序相乘的,而每个变换都是相对于物体自身的坐标系进行的。下面解释为什么这个顺序很重要以及它是如何工作的:

  • 缩放:首先应用缩放变换是因为你通常希望物体在它自己的局部坐标系中均匀或非均匀地伸缩。如果先进行了旋转或位移再缩放,那么缩放因子会同时影响物体的位置和方向,这通常不是预期的行为。
  • 旋转:然后应用旋转变换,因为它是在缩放之后,物体已经处于正确的大小下进行的。旋转也是基于物体的局部坐标系,因此它可以改变物体的方向而不改变它的位置。
  • 位移:最后应用位移(平移)。此时物体已经有了正确的大小和方向,所以现在可以将其移动到世界坐标系中的正确位置。

在代码中,我们的阅读顺序和实际变换顺序是相反的。

mat4 transform = mat4(1.0f);
transform = glm::translate(transform, translation); // 最后应用 (从右往左读)
transform = glm::rotate(transform, angle, axis); // 中间应用
transform = glm::scale(transform, scale); // 最先应用

使用GLM变换(缩放、旋转、位移)

我们已经了解了变换背后的所有理论,实践中将使用GLM库进行变换。这是一个抽象所有的数学细节,专门为OpenGL量身定做的数学库。我们需要的GLM的大多数功能都可以从下面这3个头文件中找到:

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

使用glm::translate进行位移变换

//应用示例:把一个向量(1, 0, 0)位移(1, 1, 0)个单位
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);	//目标向量
glm::mat4 trans = glm::mat4(1.0f);	//单位矩阵
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));	//变换矩阵
vec = trans * vec;	//位移矩阵 * 目标向量
std::cout << vec.x << vec.y << vec.z << std::endl;

使用glm::rotate进行旋转变换。
使用glm::scale进行缩放变换。

//应用示例:创建变换矩阵实现先缩放0.5倍,然后逆时针旋转90度。
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0)); 
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));

我们的2维图形是在XY面上的,所以把它绕Z轴glm::vec3(0.0, 0.0, 1.0)旋转。应用旋转时,使用glm::radians将角度转化为弧度。我们trans矩阵传递给了GLM的每个函数,GLM会自动将矩阵相乘,返回的结果是一个包括了多个变换的变换矩阵。

将变换矩阵传递给着色器

修改顶点着色器 让其接收一个mat4uniform变量。在把位置向量传给gl_Position之前,将其与变换矩阵相乘:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;out vec2 TexCoord;uniform mat4 transform;void main()
{gl_Position = transform * vec4(aPos, 1.0f);TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}

在渲染循环中把变换矩阵传递给着色器

// get matrix's uniform location and set matrix
ourShader.use();
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));

首先用glGetUniformLocation查询uniform变量的地址,
然后用glUniformMatrix4fv函数把矩阵数据发送给着色器。

  • 第一个参数指定uniform的位置值。

  • 第二个参数指定将要发送多少个矩阵,这里是1。

  • 第三个参数指定是否对矩阵进行转置(Transpose),也就是交换矩阵的行和列。

    OpenGL开发通常使用一种内部矩阵布局,叫做列主序(Column-major Ordering)布局。GLM的默认布局就是列主序,所以并不需要转置矩阵,我们填GL_FALSE。

  • 第四个参数是真正的矩阵数据,但是GLM的矩阵储存与OpenGL所接受的形式不同,因此要先用GLM的自带的函数value_ptr来变换这些数据。

我们创建了一个变换矩阵,在顶点着色器中声明了一个uniform,并把矩阵发送给了着色器,着色器会变换我们的顶点坐标。此时重新编译生成就可以看到我们的图形向左侧旋转,并是原来的一半大小。

修改变换矩阵 尝试让图形 随着时间推移旋转,并移动到窗口右下角:

glm::mat4 transform = glm::mat4(1.0f); 
transform = glm::translate(transform, glm::vec3(0.5f, -0.5f, 0.0f));
transform = glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));

要让箱子随着时间推移旋转,我们必须在渲染循环中更新变换矩阵,因为它在每一次渲染迭代中都要更新。我们通过glfwGetTime来获取不同时间的角度。

前面的例子中我们可以在任何地方声明变换矩阵,但是现在我们必须在每一次迭代中创建它,从而保证我们能够不断更新旋转角度。这也就意味着我们不得不在每次渲染循环的迭代中重新创建变换矩阵。

通常在渲染场景的时候,我们也会有多个需要在每次渲染迭代中都用新值重新创建的变换矩阵。

在这里我们先把箱子围绕Z轴glm::vec3(0.0f, 0.0f, 1.0f)旋转之后,我们把旋转过后的箱子位移到屏幕的右下角。

如果我们调换位移和旋转的顺序会发生什么?

int main()
{[...]while(!glfwWindowShouldClose(window)){[...]        // Create transformationsglm::mat4 transform;transform = glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f)); // Switched the ordertransform = glm::translate(transform, glm::vec3(0.5f, -0.5f, 0.0f)); // Switched the order               [...]}
}

结果是箱子先位移到了右下角,然后绕着屏幕中心旋转,而不是绕自身中心旋转。

/* Why does our container now spin around our screen?:
== ===================================================
Remember that matrix multiplication is applied in reverse. This time a translation is thus
applied first to the container positioning it in the bottom-right corner of the screen.
After the translation the rotation is applied to the translated container.A rotation transformation is also known as a change-of-basis transformation
for when we dig a bit deeper into linear algebra. Since we're changing the
basis of the container, the next resulting translations will translate the container
based on the new basis vectors. Once the vector is slightly rotated, the vertical
translations would also be slightly translated for example.If we would first apply rotations then they'd resolve around the rotation origin (0,0,0), but 
since the container is first translated, its rotation origin is no longer (0,0,0) making it
looks as if its circling around the origin of the scene.If you had trouble visualizing this or figuring it out, don't worry. If you
experiment with transformations you'll soon get the grasp of it; all it takes
is practice and experience.
*/

需要牢记的是,实际的变换顺序应该与阅读顺序相反:尽管在代码中我们先位移再旋转,实际的变换却是先应用旋转再是位移的。明白所有这些变换的组合,并且知道它们是如何应用到物体上是一件非常困难的事情。只有不断地尝试和实验这些变换你才能快速地掌握它们。

现在我们可以明白为什么矩阵在图形领域是一个如此重要的工具了。我们可以定义无限数量的变换,而把它们组合为仅仅一个矩阵,如果愿意的话我们还可以重复使用它。在着色器中使用矩阵可以省去重新定义顶点数据的功夫,它也能够节省处理时间,因为我们没有一直重新发送我们的数据(这是个非常慢的过程)。

坐标系统与MVP矩阵

现在,我们已经了解了如何有效地利用矩阵的变换来对所有顶点进行变换。接下来回到渲染流程中:

回顾渲染管线流程,OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。

这个转化过程通常是分部进行的。顶点坐标->标准化设备坐标->屏幕坐标过程中会被转换到多个过渡坐标系统。在这些特定的坐标系统中,一些操作或运算更加方便和容易:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)观察(View)投影(Projection) 三个矩阵,即MVP矩阵。变换过程如图所示:
在这里插入图片描述

  1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
  2. 世界坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放
  3. 观察坐标,通过view矩阵变换使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
  4. 裁剪坐标,通过projection矩阵变换将坐标处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
  5. 最后,我们使用视口变换(Viewport Transform)将裁剪坐标变换为屏幕坐标。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。

局部空间

局部空间是指物体所在的坐标空间,一般也是建模软件中创建模型时的坐标空间。
局部空间坐标是对象相对于局部原点的坐标,也是物体起始的坐标。


世界空间

当我们将多个物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,模型会重叠在一起,这并不是我们想要的结果。我们想为每一个物体定义一个位置,从而能在更大的世界当中分别放置它们。世界空间坐标相对于世界的全局原点。物体的坐标将会由模型矩阵(Model Matrix) 从局部变换到世界空间。

模型矩阵是一种变换矩阵,它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。

你可以将它想像为变换一个房子,你需要先将它 缩小(它在局部空间中太大了),并将其 位移 至郊区的一个小镇,然后在y轴上往左 旋转 一点以搭配附近的房子。


观察空间

观察空间经常被称为OpenGL的摄像机(Camera)(有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。

因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个 观察矩阵(View Matrix) 里,它被用来将世界坐标变换到观察空间。

Camera详解:


裁剪空间

在经过Model矩阵变换和View矩阵变换之后,我们还需要定义一个 投影矩阵(Projection Matrix) 将定点坐标从观察空间变换到裁剪空间。OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。

投影矩阵(Projection Matrix)指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)。所有在范围外的坐标不会被映射到在-1.0到1.0的范围之间,所以会被裁剪掉。在上面这个投影矩阵所指定的范围内,坐标(1250, 500, 750)将是不可见的,这是由于它的x坐标超出了范围,它被转化为一个大于1.0的标准化设备坐标,所以被裁剪掉了。

如果只是图元(Primitive),例如三角形,的一部分超出了裁剪体积(Clipping Volume),则OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。

将特定范围内的坐标转化到标准化设备坐标系的过程 被称之为投影(Projection),使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。

一旦所有顶点被变换到裁剪空间,最终的操作——透视除法(Perspective Division) 将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。

在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用glViewport中的设定),并被变换成片段。

将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。

透视原理可以参考:GAMES101学习笔记(二):Transformation 变换 (仿射变换、模型、视图、投影)

正射/正交投影

正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。它由宽、高、近(Near)平面和远(Far)平面所指定。
在这里插入图片描述
使用GLM的内置函数glm::ortho创建一个正射投影矩阵:

glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
  • 前两个参数指定了平截头体的左边缘和右边缘。
  • 第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小
  • 第五和第六个参数定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。

正射投影矩阵直接将坐标映射到2D平面中,即你的屏幕,但实际上一个直接的投影矩阵会产生不真实的结果,因为这个投影没有将透视(Perspective)考虑进去。所以我们需要一个透视投影矩阵来解决这个问题。

透视投影

透视(Perspective):在实际生活中,我们会发现离你越远的东西看起来更小,这就是透视效果。
在这里插入图片描述
如上图,由于透视,这两条线在很远的地方看起来会相交。这正是透视投影想要模仿的效果,它是使用透视投影矩阵来完成的。

这个投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。 之后作为顶点着色器的输出,透视除法会被应用到裁剪空间坐标上,即顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小。这是我们在向量变换时提到的w的另一个重要作用。

最后的结果坐标就是处于标准化设备空间中的。可以在这篇文章中了解正射投影和透视投影的详细计算。
在这里插入图片描述
相比于正射投影,投射投影创建了一个定义了可视空间的大平截头体,一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。

使用GLM库中glm::perspective方法创建一个透视投影矩阵:

glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
  • 第一个参数定义了FOV的值,即视野(Field of View),它设置了观察空间的大小。想要一个真实的观察效果,它的值通常设置为45.0f
  • 第二个参数设置了宽高比,由视口的宽除以高所得。
  • 第三和第四个参数设置了平截头体的近和远平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。

当使用正射投影时,每一个顶点坐标都会直接映射到裁剪空间中而不经过任何精细的透视除法(它仍然会进行透视除法,只是w分量没有被改变(它保持为1),因此没有起作用)。因为正射投影没有使用透视,远处的物体不会显得更小,所以产生奇怪的视觉效果。由于这个原因,正射投影主要用于二维渲染以及一些建筑或工程的程序,在这些场景中我们更希望顶点不会被透视所干扰。


屏幕坐标

我们通过Model矩阵、View矩阵、Projection矩阵将局部坐标变换到了裁剪坐标:
V c l i p = M p r o j e c t i o n ⋅ M v i e w ⋅ M m o d e l ⋅ V l o c a l V_{clip} = M_{projection} · M_{view} · M_{model} · V_{local} Vclip=MprojectionMviewMmodelVlocal 矩阵运算顺序是相反的(我们需要从右往左阅读矩阵的乘法)。最后顶点坐标被赋值到顶点着色器中的gl_Positon中,OpenGL然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。

视口变换:OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点。

光栅化原理可以参考:GAMES101学习笔记(三):Rasterization 光栅化(三角形的离散化、抗锯齿、深度测试)

三维变换

上文中我们了解了如何将3D坐标转换为2D坐标。接下来我们就可以真正引入3D模型,进行3D绘图(前面我们一直使用的是2D的三角形或矩形)

首先创建一个模型矩阵。这个模型矩阵包含了位移、缩放与旋转操作,它们会被应用到所有物体的顶点上,以变换它们到全局的世界空间。让我们变换一下我们的平面,将其绕着x轴旋转,使它看起来像放在地上一样。这个模型矩阵看起来是这样的:

glm::mat4 model;
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));

通过将顶点坐标乘以这个模型矩阵,我们将该顶点坐标变换到世界坐标。我们的平面看起来就是在地板上,代表全局世界里的平面。

接下来创建一个观察矩阵。我们想要在场景里面稍微往后移动,以使得物体变成可见的(当在世界空间时,我们位于原点(0,0,0))。
想象在场景中移动:将摄像机向后移动,和将整个场景向前移动是一样的。

这正是观察矩阵所做的,我们以相反于摄像机移动的方向移动整个场景。因为我们想要往后移动,并且OpenGL是一个右手坐标系(Right-handed System),所以我们需要沿着z轴的正方向移动。我们会通过将场景沿着z轴负方向平移来实现。它会给我们一种我们在往后移动的感觉。

在这里插入图片描述
就目前来说,观察矩阵是这样的:(在摄像机Camera章节中我们将会详细讨论如何在场景中移动)

glm::mat4 view;
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));	// 注意,我们将矩阵向我们要进行移动场景的反方向移动。

最后定义一个投影矩阵。我们希望在场景中使用透视投影,所以像这样声明一个投影矩阵:

glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);

将创建的MVP变换矩阵传入着色器
首先,让我们在顶点着色器中声明一个uniform变换矩阵然后将它乘以顶点坐标:

#version 330 core
layout (location = 0) in vec3 aPos;
...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;void main()
{// 注意乘法要从右向左读(左乘)gl_Position = projection * view * model * vec4(aPos, 1.0);...
}

在渲染循环中将矩阵传入着色器(这通常在每次的渲染迭代中进行,因为变换矩阵会经常变动):

int modelLoc = glGetUniformLocation(ourShader.ID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
... // 观察矩阵和投影矩阵与之类似

我们的顶点坐标已经使用模型、观察和投影矩阵进行变换了,最终的物体应该会:稍微向后倾斜至地板方向、离我们有一些距离、有透视效果(顶点越远,变得越小)。它看起来就像是一个3D的平面,静止在一个虚构的地板上: 源码参考

在这里插入图片描述

绘制3D立方体 & 深度测试(Z-buffer)

接下来拓展我们的2D平面为一个3D立方体。要想渲染一个立方体,我们一共需要36个顶点:

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
};

为了直观一点,我们将让立方体随着时间旋转:

model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));

然后我们使用glDrawArrays来绘制立方体,但这一次总共有36个顶点。

glDrawArrays(GL_TRIANGLES, 0, 36);

使用glDrawArrays而不是glDrawElements,所以这个例子中没有使用EBO

在这里插入图片描述
成功绘制了一个立方体,但他的面看起来相互遮挡的不正确。立方体的某些本应被遮挡住的面被绘制在了这个立方体其他面之上。之所以这样是因为OpenGL是逐个三角形绘制立方体的,所以即便之前那里有东西它也会覆盖之前的像素。因为这个原因,有些三角形会被绘制在其它三角形上面,虽然它们本不应该是被覆盖的。

这里我们就要引入Z-buffer
OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中,也被称为深度缓冲(Depth Buffer)。它允许OpenGL决定何时覆盖一个像素而何时不覆盖。GLFW会自动为你生成这样一个缓冲(就像它也有一个颜色缓冲来存储输出图像的颜色)。深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的

我们只需要告诉OpenGL启用深度测试(它默认是关闭的)

glEnable(GL_DEPTH_TEST);
//glDisable(GL_DEPTH_TEST);

因为我们使用了深度测试,我们也想要在每次渲染迭代之前清除深度缓冲(否则前一帧的深度信息仍然保存在缓冲中)。就像清除颜色缓冲一样,我们可以通过在glClear函数中指定DEPTH_BUFFER_BIT位来清除深度缓冲:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

现在看起来正常了: 源码参考

在这里插入图片描述

练习1——更多立方体

现在我们想在屏幕上显示10个立方体。每个立方体看起来都是一样的,区别在于它们在世界的位置及旋转角度不同。参考源码

立方体的图形布局已经定义好了,所以当渲染更多物体的时候我们不需要改变我们的缓冲数组和属性数组,我们唯一需要做的只是改变每个对象的模型矩阵来将立方体变换到世界坐标系中。

首先,让我们为每个立方体定义一个位移向量来指定它在世界空间的位置。我们将在一个glm::vec3数组中定义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)  
};

在渲染循环中,我们调用glDrawArrays 10次,但这次在我们渲染之前每次传入一个不同的模型矩阵到顶点着色器中。我们将会在渲染循环中创建一个小的循环用不同的模型矩阵渲染我们的物体10次。注意我们也对每个箱子加了一点旋转:

glBindVertexArray(VAO);
for(unsigned 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));ourShader.setMat4("model", model);glDrawArrays(GL_TRIANGLES, 0, 36);
}

在这里插入图片描述

相关文章:

OpenGL学习笔记(六):Transformations 变换(变换矩阵、坐标系统、GLM库应用)

文章目录 向量变换使用GLM变换&#xff08;缩放、旋转、位移&#xff09;将变换矩阵传递给着色器坐标系统与MVP矩阵三维变换绘制3D立方体 & 深度测试&#xff08;Z-buffer&#xff09;练习1——更多立方体 现在我们已经知道了如何创建一个物体、着色、加入纹理。但它们都还…...

PHP 常用函数2025.02

PHP implode() 函数 语法 implode(separator,array) 参数描述separator可选。规定数组元素之间放置的内容。默认是 ""&#xff08;空字符串&#xff09;。array必需。要组合为字符串的数组。 技术细节 返回值&#xff1a;返回一个由数组元素组合成的字符串。PHP 版…...

17.3.4 颜色矩阵

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 17.3.4.1 矩阵基本概念 矩阵&#xff08;Matrix&#xff09;是一个按照长方阵列排列的复数或实数集合&#xff0c;类似于数组。 由…...

1. 【.NET 8 实战--孢子记账--从单体到微服务--转向微服务】--前言

在我们的专栏《单体开发》中&#xff0c;我们实现了一个简单的记账软件的服务端&#xff0c;并且成功上线。随着用户数量的不断增长&#xff0c;问题逐渐开始显现。访问量逐渐增加&#xff0c;服务端的压力也随之加大。随着访问量的攀升&#xff0c;服务端的响应时间变得越来越…...

02.04 数据类型

请写出以下几个数据的类型&#xff1a; 整数 a ----->int a的地址 ----->int* 存放a的数组b ----->int[] 存放a的地址的数组c ----->int*[] b的地址 ----->int* c的地址 ----->int** 指向printf函数的指针d ----->int (*)(const char*, ...) …...

WPS动画:使图形平移、围绕某个顶点旋转一定角度

1、平移 案例三角形如下图&#xff0c;需求&#xff1a;该三角形的A点平移至原点 &#xff08;1&#xff09;在预想动画结束的位置绘制出图形 &#xff08;2&#xff09;点击选中原始图像&#xff0c;插入/动画/绘制自定义路径/直线 &#xff08;3&#xff09;十字星绘制的直线…...

想表示消息返回值为Customer集合

道奈特(240***10) 14:34:55 EA中序列图。我想表示消息返回值为 Customer 集合。目前只有一个Customer实体类&#xff0c;我需要另外新建一个CustomerList 类吗&#xff1f; 潘加宇(35***47) 17:01:26 不需要。如果是分析&#xff0c;在类的操作中&#xff0c;定义一个参数&…...

93,【1】buuctf web [网鼎杯 2020 朱雀组]phpweb

进入靶场 页面一直在刷新 在 PHP 中&#xff0c;date() 函数是一个非常常用的处理日期和时间的函数&#xff0c;所以应该用到了 再看看警告的那句话 Warning: date(): It is not safe to rely on the systems timezone settings. You are *required* to use the date.timez…...

五、定时器实现呼吸灯

5.1 定时器与计数器简介 定时器是一种通过对内部时钟脉冲计数来测量时间间隔的模块。它的核心是一个递增或递减的寄存器&#xff08;计数器值&#xff09;。如果系统时钟为 1 MHz&#xff0c;定时器每 1 μs 计数一次。 计数器是一种对外部事件&#xff08;如脉冲信号&#xff…...

顺序打印数字的进一步理解

之前博客写了篇博文&#xff0c;面试时要求使用多线程顺序打印ABC循环20次&#xff0c;这是我当时使用join函数实现代码&#xff1a; public class TestABCJoin {public static void main(String[] args) {// 创建任务Runnable taskA () -> {for (int i 0; i < 5; i) …...

使用React和Material-UI构建TODO应用的前端UI

使用React和Material-UI构建TODO应用的前端UI 引言环境准备代码解析1. 导入必要的模块2. 创建React组件3. 定义函数3.1 获取TODO列表3.2 创建TODO项3.3 更新TODO项3.4 删除TODO项3.5 处理编辑点击事件3.6 关闭编辑对话框3.7 保存编辑内容 4. 使用Effect钩子5. 渲染组件 功能实现…...

开屏广告-跳过神器

给大家介绍一款超实用的软件——SKIP&#xff0c;它堪称李跳跳的最佳平替&#xff01;这款软件已经在Github开源免费&#xff0c;完全无需担心内置源问题&#xff0c;也无需导入任何规则。安装完成后&#xff0c;即可直接使用&#xff0c;非常便捷&#xff01; 首次打开软件时…...

bat脚本实现自动化漏洞挖掘

bat脚本 BAT脚本是一种批处理文件&#xff0c;可以在Windows操作系统中自动执行一系列命令。它们可以简化许多日常任务&#xff0c;如文件操作、系统配置等。 bat脚本执行命令 echo off#下面写要执行的命令 httpx 自动存活探测 echo off httpx.exe -l url.txt -o 0.txt nuc…...

如何运行Composer安装PHP包 安装JWT库

1. 使用Composer Composer是PHP的依赖管理工具&#xff0c;它允许你轻松地安装和管理PHP包。对于JWT&#xff0c;你可以使用firebase/php-jwt这个库&#xff0c;这是由Firebase提供的官方库。 安装Composer&#xff08;如果你还没有安装的话&#xff09;&#xff1a; 访问Co…...

(回溯递归dfs 电话号码的字母组合 remake)leetcode 17

只找边界条件和非边界条件&#xff0c;剩下的交给数学归纳法就行&#xff0c;考虑子问题的重复性 [class Solution {vector<string>str { "","","abc","def","ghi","jkl","mno","pqrs"…...

it基础使用--5---git远程仓库

文章目录 it基础使用--5---git远程仓库1. 按顺序看2. 什么是远程仓库3. Gitee操作3.1 新建远程仓库3.2 远程操作基础命令3.3 查看当前所有远程地址别名 git remote -v3.4 创建远程仓库别名 git remote add 别名 远程地址3.4 推送本地分支到远程仓库 git push 别名 分支3.5 拉取…...

Git 的起源与发展

序章&#xff1a;版本控制的前世今生 在软件开发的漫长旅程中&#xff0c;版本控制犹如一位忠诚的伙伴&#xff0c;始终陪伴着开发者们。它的存在&#xff0c;解决了软件开发过程中代码管理的诸多难题&#xff0c;让团队协作更加高效&#xff0c;代码的演进更加有序。 简单来…...

c++提取矩形区域图像的梯度并拟合直线

c提取旋转矩形区域的边缘最强梯度点&#xff0c;并拟合直线 #include <opencv2/opencv.hpp> #include <iostream> #include <vector>using namespace cv; using namespace std;int main() {// 加载图像Mat img imread("image.jpg", IMREAD_GRAYS…...

【PyQt】超级超级笨的pyqt计算器案例

计算器 1.QT Designer设计外观 1.pushButton2.textEdit3.groupBox4.布局设计 2.加载ui文件 导入模块&#xff1a; sys&#xff1a;用于处理命令行参数。 QApplication&#xff1a;PyQt5 应用程序类。 QWidget&#xff1a;窗口基类。 uic&#xff1a;用于加载 .ui 文件。…...

每日一题——小根堆实现堆排序算法

小根堆实现堆排序算法 堆排序的基本思想堆排序的步骤 实现步骤1. 构建小根堆2. 删除最小元素并调整堆 C语言实现输出示例代码解释1. percolateDown 函数2. buildMinHeap 函数3. heapSort 函数4. printArray函数 排序过程详解步骤1&#xff1a;构建小根堆步骤2&#xff1a;删除堆…...

k8s集群

文章目录 项目描述项目环境系统与软件版本概览项目步骤 环境准备IP地址规划关闭selinux和firewall配置静态ip地址修改主机名添加hosts解析 项目步骤一、使用kubeadm安装k8s单master的集群环境&#xff08;1个master2个node节点&#xff09;1、互相之间建立免密通道2.关闭交换分…...

数据分析系列--[11] RapidMiner,K-Means聚类分析(含数据集)

一、数据集 二、导入数据 三、K-Means聚类 数据说明:提供一组数据,含体重、胆固醇、性别。 分析目标:找到这组数据中需要治疗的群体供后续使用。 一、数据集 点击下载数据集 二、导入数据 三、K-Means聚类 Ending, congratulations, youre done....

技术架构师成长路线(2025版)

目录 通用知识 计算机原理&#xff08;1 - 2 个月&#xff09; 数据结构&#xff08;2 - 3 个月&#xff09; 网络编程&#xff08;1 - 2 个月&#xff09; 软件工程&#xff08;1 个月&#xff09; 基础知识 Java 编程语言基础&#xff08;2 - 3 个月&#xff09; JVM&…...

NetLify账号无法登录解决办法

本文收录在【建站】专栏内&#xff0c;专栏目录&#xff1a;【建站】专栏目录 用github账号登录时&#xff0c;说校验失败 Authentication Error Authenticating failed due to the following error: Your account requires additional verification. Please check your email…...

linux本地部署deepseek-R1模型

国产开源大模型追平甚至超越了CloseAI的o1模型&#xff0c;大国崛起时刻&#xff01;&#xff01;&#xff01; DeepSeek R1 本地部署指南   在人工智能技术飞速发展的今天&#xff0c;本地部署AI模型成为越来越多开发者和企业关注的焦点。本文将详细介绍如何在本地部署DeepS…...

集合通讯概览

集合通信概览 &#xff08;1&#xff09;通信的算法 是根据通讯的链路组成的 &#xff08;2&#xff09;因为通信链路 跟硬件强相关&#xff0c;所以每个CCL的库都不一样 芯片与芯片、不同U之间是怎么通信的 多卡训练&#xff1a;多维并行&#xff08;xxx并行在上一期已经讲述…...

如何解决云台重力补偿?

如何解决云台重力补偿? 最近在调试步兵云台的时候,由于枪管、图传、摄像头等重力的原因,pitch轴的参数尤其难以调整,又不想抬升和降低使用两套不同的参数,所以使用了重力补偿,效果也是比较理想的,于是整理为一篇文章记录一下 一、问题根源:枪管重力在“搞事情” 想象…...

【LeetCode 刷题】回溯算法(4)-排列问题

此博客为《代码随想录》二叉树章节的学习笔记&#xff0c;主要内容为回溯算法排列问题相关的题目解析。 文章目录 46.全排列47.全排列 II 46.全排列 题目链接 class Solution:def permute(self, nums: List[int]) -> List[List[int]]:res, path [], []used [0] * len(n…...

FPGA|IP核PLL调用测试:建立工程

闲来无事&#xff0c;测试下FPGA的IP核调用 一、生成工程 1、打开软件新建工程 2、命名为PLL_test 3、选择芯片型号 4、生成工程如图...

数据的表示和运算

一-定点数的表示 1-定点数和浮点数 定点数&#xff1a;小数点的位置固定&#xff08;常规计数&#xff09; 浮点数&#xff1a;小数点的位置不固定&#xff08;科学计数法&#xff09; 2-定点数的表示 ①无符号数 整个机器字长的全部二进制位均为数值位&#xff0c;没有符号…...

文字显示省略号

多行文本溢出显示省略号...

22.Word:小张-经费联审核结算单❗【16】

目录 NO1.2 NO3.4​ NO5.6.7 NO8邮件合并 MS搜狗输入法 NO1.2 用ms打开文件&#xff0c;而不是wps❗不然后面都没分布局→页面设置→页面大小→页面方向→上下左右&#xff1a;页边距→页码范围&#xff1a;多页&#xff1a;拼页光标处于→布局→分隔符&#xff1a;分节符…...

鲸鱼算法 matlab pso

算法原理 鲸鱼优化算法的核心思想是通过模拟座头鲸的捕食过程来进行搜索和优化。座头鲸在捕猎时会围绕猎物游动并产生气泡网&#xff0c;迫使猎物聚集。这一行为被用来设计搜索策略&#xff0c;使算法能够有效地找到全局最优解。 算法步骤 ‌初始化‌&#xff1a;随机生成一…...

c++之模板进阶

在前面的文章中&#xff0c;我们已经简单的了解了模板的使用&#xff0c;在这篇文章中&#xff0c;我们将继续深入探讨模板 1.模板的特化 1.1 概念 通常情况下&#xff0c;使用模板可以实现一些与类型无关的代码&#xff0c;但对于一些特殊类型的可能会得到一些错误的结果&a…...

C语言中的信号量

信号是操作系统中用来传递特定消息的机制。操作系统可以使用这种方式将程序运行过程中发生的各类特殊情况发送给程序&#xff0c;并按照其指定的逻辑进行处理。信号名称都以 “SIG” 作为前缀&#xff0c;如程序访问非法内存时&#xff0c;会产生名为 SIGSEGV 的信号。 程序需…...

AI主流大模型介绍和API价格比较

主流的大模型系列 ###1. OpenAI: GPT-4&#xff0c;GPT-4 Turbo&#xff0c; GPT-4o OpenAI 的介绍&#xff1a; 全称&#xff1a;Open Artificial Intelligence&#xff0c;简称OpenAI。性质&#xff1a;起初为非营利性组织&#xff0c;后转变为由营利性公司OpenAI LP及非营…...

自主Shell命令行解释器

什么是命令行 我们一直使用的"ls","cd","pwd","mkdir"等命令&#xff0c;都是在命令行上输入的&#xff0c;我们之前对于命令行的理解&#xff1a; 命令行是干啥的&#xff1f;是为我们做命令行解释的。 命令行这个东西实际上是我们…...

git笔记-简单入门

git笔记 git是一个分布式版本控制系统&#xff0c;它的优点有哪些呢&#xff1f;分为以下几个部分 与集中式的版本控制系统比起来&#xff0c;不用担心单点故障问题&#xff0c;只需要互相同步一下进度即可。支持离线编辑&#xff0c;每一个人都有一个完整的版本库。跨平台支持…...

重新刷题求职2-DAY1

DAY1 1.704. 二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 最普通的二分查找&#xff0c;查用的习惯左闭右…...

浅析DDOS攻击及防御策略

DDoS&#xff08;分布式拒绝服务&#xff09;攻击是一种通过大量计算机或网络僵尸主机对目标服务器发起大量无效或高流量请求&#xff0c;耗尽其资源&#xff0c;从而导致服务中断的网络攻击方式。这种攻击方式利用了分布式系统的特性&#xff0c;使攻击规模更大、影响范围更广…...

路径规划之启发式算法之二十九:鸽群算法(Pigeon-inspired Optimization, PIO)

鸽群算法(Pigeon-inspired Optimization, PIO)是一种基于自然界中鸽子群体行为的智能优化算法,由Duan等人于2014年提出。该算法模拟了鸽子在飞行过程中利用地标、太阳和磁场等导航机制的行为,具有简单、高效和易于实现的特点,适用于解决连续优化问题。 更多的仿生群体算法…...

SwiftUI 在 Xcode 预览修改视图 FetchedResults 对象的属性时为什么会崩溃?

概览 从 SwiftUI 诞生那天起&#xff0c;让秃头码农们又爱又恨的 Xcode 预览就在界面调试中扮演了及其重要的角色。不过&#xff0c;就是这位撸码中的绝对主角却尝尝在关键时刻“掉链子”。 比如当修改 SwiftUI 视图中 FetchedResults 对象的属性时&#xff0c;Xcode 预览可能…...

pytorch实现基于Word2Vec的词嵌入

PyTorch 实现 Word2Vec&#xff08;Skip-gram 模型&#xff09; 的完整代码&#xff0c;使用 中文语料 进行训练&#xff0c;包括数据预处理、模型定义、训练和测试。 1. 主要特点 支持中文数据&#xff0c;基于 jieba 进行分词 使用 Skip-gram 进行训练&#xff0c;适用于小数…...

MVC 文件夹:架构之美与实际应用

MVC 文件夹:架构之美与实际应用 引言 MVC(Model-View-Controller)是一种设计模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种架构模式不仅提高了代码的可维护性和可扩展性,而且使得开发流程更加清晰。本文将深入探讨MVC文…...

Python在线编辑器

from flask import Flask, render_template, request, jsonify import sys from io import StringIO import contextlib import subprocess import importlib import threading import time import ast import reapp Flask(__name__)RESTRICTED_PACKAGES {tkinter: 抱歉&…...

The Simulation技术浅析(四):随机数生成

随机数生成技术 是 The Simulation 中的核心组成部分,广泛应用于蒙特卡洛模拟、密码学、统计建模等领域。随机数生成技术主要分为 伪随机数生成器(PRNG,Pseudo-Random Number Generator) 和 真随机数生成器(TRNG,True Random Number Generator)。 1. 伪随机数生成器(PR…...

centos stream 9 安装 libstdc++-static静态库

yum仓库中相应的镜像源没有打开&#xff0c;libstdc-static在CRB这个仓库下&#xff0c;但是查看/etc/yum.repos.d/centos.repo&#xff0c;发现CRB镜像没有开启。 解决办法 如下图开启CRB镜像&#xff0c; 然后执行 yum makecache yum install glibc-static libstdc-static…...

Java自定义IO密集型和CPU密集型线程池

文章目录 前言线程池各类场景描述常见场景案例设计思路公共类自定义工厂类-MyThreadFactory自定义拒绝策略-RejectedExecutionHandlerFactory自定义阻塞队列-TaskQueue&#xff08;实现 核心线程->最大线程数->队列&#xff09; 场景1&#xff1a;CPU密集型场景思路&…...

Shell $0

个人博客地址&#xff1a;Shell $0 | 一张假钞的真实世界 我们已经知道在Shell中$0表示Shell脚本的文件名&#xff0c;但在有脚本调用的情形中&#xff0c;子脚本中的$0会是什么值呢&#xff1f;我们通过下面的实例来看。 已测试系统列表&#xff1a; Mac OS X EI Capitan 1…...

C++ Primer 标准库类型string

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…...