第四章 opengl之变换

齐次坐标(Homogeneous Coordinates)

向量的w分量也叫齐次坐标。想要从齐次向量得到3D向量,我们可以把x、y和z坐标分别除以w坐标。我们通常不会注意这个问题,因为w分量通常是1.0。
使用齐次坐标有几点好处:
它允许我们在3D向量上进行位移(如果没有w分量我们是不能位移向量的),而且可以用w值创建3D视觉效果。
如果一个向量的齐次坐标是0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能位移(这也就是我们说的不能位移一个方向)

GLM

GLM是一个只有头文件的库。也就是说我们只需包含对应的头文件就行了,不用链接和编译。GLM可以在它们的网站上下载(https://glm.g-truc.net/0.9.8/index.html)。把头文件的根目录复制到你的includes文件夹,然后你就可以使用这个库了。

注:
GLM库从0.9.9版本起,默认会将矩阵类型初始化为一个零矩阵(所有元素均为0),而不是单位矩阵(对角元素为1,其它元素为0)。如果你使用的是0.9.9或0.9.9以上的版本,你需要将所有的矩阵初始化改为 glm::mat4 mat = glm::mat4(1.0f)

加上着三个头文件,就可以满足GLM的大多数功能:

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

举例:把向量(1,0,0)位移(1,1,0)个单位:

glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
// 译注:下面就是矩阵初始化的一个例子,如果使用的是0.9.9及以上版本
// 下面这行代码就需要改为:
// glm::mat4 trans = glm::mat4(1.0f)
// 之后将不再进行提示
glm::mat4 trans;//单位矩阵
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内建的向量类定义一个叫做vec的向量。接下来定义一个mat4类型的trans,默认是一个4×4单位矩阵。下一步是创建一个变换矩阵,我们是把单位矩阵和一个位移向量传递给glm::translate函数来完成这个工作的(然后用给定的矩阵乘以位移矩阵就能获得最后需要的矩阵)。 之后我们把向量乘以位移矩阵并且输出最后的结果。如果你仍记得位移矩阵是如何工作的话,得到的向量应该是(1 + 1, 0 + 1, 0 + 0),也就是(2, 1, 0)。这个代码片段将会输出210,所以这个位移矩阵是正确的。

再次举例:把图片逆时针旋转90度。然后缩放0.5倍,使它变成原来的一半大。我们先来创建变换矩阵:

glm::mat4 trans;
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
//沿z轴旋转90度。GLM希望它的角度是弧度制的(Radian),
//所以我们使用glm::radians将角度转化为弧度。
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
//每个轴都缩放到0.5倍

因为有纹理的那面矩形是在XY平面上的,所以我们需要把它绕着z轴旋转。因为我们把这个矩阵传递给了GLM的每个函数,GLM会自动将矩阵相乘,返回的结果是一个包括了多个变换的变换矩阵。

接下来将修改顶点着色器让其接收一个mat4的uniform变量,然后再用矩阵uniform乘以位置向量:

#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);
    //vec4是需要旋转的向量
    TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}

在把位置向量传给gl_Position之前,我们先添加一个uniform,并且将其与变换矩阵相乘。我们的箱子现在应该是原来的二分之一大小并(向左)旋转了90度。当然,我们仍需要把变换矩阵传递给着色器:

unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));

首先查询uniform变量的地址,然后用有Matrix4fv后缀的glUniform函数矩阵数据发送给着色器
第一个参数你现在应该很熟悉了,它是uniform的位置值。
第二个参数告诉OpenGL我们将要发送多少个矩阵,这里是1。
第三个参数询问我们是否希望对我们的矩阵进行转置(Transpose),也就是说交换我们矩阵的行和列。
OpenGL开发者通常使用一种内部矩阵布局,叫做列主序(Column-major Ordering)布局。GLM的默认布局就是列主序,所以并不需要转置矩阵,我们填GL_FALSE。
最后一个参数是真正的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望接受的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据。

下面举例让图片随着时间旋转,把图片放到窗口的右下角。
必须在游戏循环中更新变换矩阵,因为它在每一次渲染迭代中都要更新。我们使用GLFW的时间函数来获取不同时间的角度:

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

可以在任何地方声明变换矩阵,但是必须在每一次迭代中创建它,来保证能够不断更新旋转角度。我们不得不在每次游戏循环的迭代中重新创建变换矩阵。
则先把图片围绕原点(0,0,0)旋转,再把旋转过的图片位移到右下角。
实际变换顺序与阅读顺序相反。则代码中先位移再旋转,实际得到的是先旋转再位移

坐标系统

将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:
局部空间(Local Space,或者称为物体空间(Object Space))
世界空间(World Space)
观察空间(View Space,或者称为视觉空间(Eye Space))
裁剪空间(Clip Space)
屏幕空间(Screen Space)

将坐标从一个坐标系变换到另一个坐标系,需要用到几个变换矩阵:模型(Model)、观察(View)、投影(Projection)三个矩阵
顶点坐标起始于局部空间,在这被称为局部坐标;之后编程世界坐标,观察坐标,裁剪坐标,最后以屏幕坐标的形式结束
在这里插入图片描述

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

裁剪空间

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

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

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

一旦所有顶点被变换到裁剪空间,最终的操作——透视除法(Perspective Division)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。
在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用glViewport中的设定),并被变换成片段。
将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。

正射投影

正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。
由宽、高、近(Near)平面和远(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。正射平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标,因为每个向量的w分量都没有进行改变;如果w分量等于1.0,透视除法则不会改变这个坐标。

要创建一个正射投影矩阵,我们可以使用GLM的内置函数glm::ortho:

glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);

前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。

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

透视投影

离你越远的东西看起来更小。这个奇怪的效果称之为透视。
这个投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被变换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小。
在GLM中可以这样创建一个透视投影矩阵:

glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);

glm::perspective所做的其实就是创建了一个定义了可视空间的大平截头体,任何在这个平截头体以外的东西最后都不会出现在裁剪空间体积内,并且将会受到裁剪。一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。

它的第一个参数定义了视野(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,但想要一个末日风格的结果你可以将其设置一个更大的值。
第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的近和远平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。

组合上述矩阵

模型矩阵 * 观察矩阵 * 投影矩阵
在这里插入图片描述
注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

由2D->3D

先创建一个模型矩阵。包含位移,缩放和旋转操作。
应用到所有物体的顶点上,用变换的方式到全局的世界空间中。
举例:如果将图片绕x轴旋转,使它看起来像放到地上一样:

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

因为,OpenGL是右手坐标系,则z轴正半轴朝向自己。想要摄像头往前移动,则可以让场景往后移动。观察矩阵如下:

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

再把这个传入着色器。
首先,在顶点着色器中声明一个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

则要从一个图片渲染成一个立方体,需要36个顶点。(6个面,每个面是2个三角形,一个三角形是3个顶点)
举例:实现立方体随着时间旋转:

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

再使用glDrawArrays来绘制立方体:

glDrawArrays(GL_TRIANGLES, 0, 36);

补充:Z缓冲

OpenGL存储深度信息在一个叫做Z缓冲(Z-buffer)的缓冲中,它允许OpenGL决定何时覆盖一个像素而何时不覆盖。通过使用Z缓冲,我们可以配置OpenGL来进行深度测试。

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

可以通过glEnable函数来开启深度测试。glEnable和glDisable函数允许我们启用或禁用某个OpenGL功能。这个功能会一直保持启用/禁用状态,直到另一个调用来禁用/启用它。现在我们想启用深度测试,需要开启GL_DEPTH_TEST:

glEnable(GL_DEPTH_TEST);

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

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

很多立方体3D

因为一个立方体的图片布局已经定义好了,当渲染更多物体时候,不需要改变缓冲数组和属性数组。只需要改变每个对象的模型矩阵,来将立方体变换到世界坐标系中。
先在数组中定义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)  
};

其次,在循环中调用glDrawArrays10次,但是渲染之前每次传入一个不同的模型矩阵到顶点着色器中。

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);
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《OpenGL编程指南》第四版是由Dave Shreiner、Graham Sellers、John Kessenich和Bill Licea-Kane合著的一本经典OpenGL编程教材。这本书简洁明了地介绍了OpenGL图形编程的基本原理和技术,并提供了大量的示例代码和实践经验。 首先,该书对OpenGL的基础知识进行了详细的介绍,包括OpenGL的架构、渲染管线、图元绘制和变换等方面。它深入浅出地解释了OpenGL的核心概念和工作原理,帮助读者建立起对OpenGL编程的整体框架的理解。 其次,该书详细介绍了OpenGL的各种高级功能和技术,如纹理映射、光照、阴影、几何细节和透视效果等。它通过逐步讲解示例代码的方式,让读者能够深入了解OpenGL的特色功能和高级技巧的实现方法。 此外,该书还介绍了OpenGL的最新发展和扩展,包括OpenGL的核心剖析、OpenGL ES和WebGL等。它对不同平台上的OpenGL应用开发做了介绍,使得读者能够掌握在不同环境下进行OpenGL图形程序开发的技巧。 总的来说,这本书是一本权威的OpenGL编程教材,它以通俗易懂的方式介绍了OpenGL的基本原理和技术,并提供了大量的实例代码和实践经验,使得读者能够迅速掌握OpenGL编程的核心知识和技巧。无论是初学者还是有一定OpenGL编程基础的开发者,都可以从中获得收益。 ### 回答2: 《OpenGL编程指南 第四版》是一本关于OpenGL图形编程的权威教程。该书全面介绍了OpenGL的基础知识、图形渲染流程、3D图形编程、着色器编程、光照和纹理等方面的内容。 首先,该书详细介绍了OpenGL的基本概念和工作原理。读者可以了解到OpenGL的渲染管线以及各个阶段的功能和作用,对于理解OpenGL的内部机制非常有帮助。 其次,书中提供了大量的代码示例和实践案例,帮助读者更好地掌握OpenGL的使用方法和技巧。通过实践,读者可以学习到如何创建窗口、绘制基本几何图形、管理顶点数据、应用矩阵变换、完成光照计算等常见的图形编程任务。 此外,书中还介绍了现代OpenGL的特性,如着色器编程、顶点缓冲对象和纹理映射等。对于想要深入了解OpenGL高级特性的读者来说,这些内容非常有价值。 总之,《OpenGL编程指南 第四版》以其深入浅出的风格和丰富的示例代码,成为了学习和掌握OpenGL编程的必备参考。无论是初学者还是有一定基础的开发者,都可以通过该书系统地学习和掌握OpenGL的相关知识和技巧,从而在图形编程领域有所建树。 ### 回答3: 《OpenGL编程指南》第四版是一本非常经典的OpenGL编程入门书籍,下面我来简单介绍一下。 这本书由几位OpenGL专家编写,旨在向读者介绍OpenGL的基础知识、常见技术和高级特性。通过理论和实例的结合,读者可以掌握OpenGL的基本原理和应用技巧。 《OpenGL编程指南》第四版的内容包括OpenGL的基础知识,如图形管线、坐标系、顶点和片元着色器等。同时还介绍了纹理映射、深度缓冲、帧缓冲等高级技术。此外,还有关于光照、阴影、反射、抗锯齿等图形渲染的进阶内容。 在每一章的结尾,书中都提供了大量的示例代码供读者参考和实践。通过实际动手编写代码,读者可以更好地理解并应用所学的知识。 不仅如此,这本书还包含了对OpenGL ES和WebGL的介绍,使得读者能够将OpenGL应用于移动设备和Web开发。 总的来说,《OpenGL编程指南》第四版内容深入浅出,适合初学者入门,同时也能满足有一定OpenGL基础的读者的需要。无论是想从事图形编程的工程师还是对计算机图形学感兴趣的爱好者,这本书都是一本不可多得的学习资料。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值