OpenGL学习脚印: 模型变换(model transformation)

写在前面

前面为本节内容准备了向量和矩阵线性变换等内容,本节开始学习OpenGL中的坐标处理。OpenGL中的坐标处理过程包括模型变换、视变换、投影变换、视口变换等内容,这个主题的内容有些多,因此分节学习,主题将分为5节内容来学习。本节主要学习模型变换。本节示例代码均可在我的github处下载。

通过本节可以了解到

  • 模型变换的作用
  • 模型变换的类型和计算方法

坐标处理的全局过程(了解,另文详述)

OpenGL中的坐标处理包括模型变换、视变换、投影变换、视口变换等内容,具体过程如下图1所示:

每一个过程处理都有其原因,这些内容计划将会在不同节里分别介绍,最后再整体把握一遍。

今天我们学习第一个阶段——模型变换。


为什么需要模型变换

我们在OpenGL中通过定义一组顶点来定义一个模型,或者通过其他3D建模软件事先建好模型然后导入到OpenGL中。顶点属性定义了模型。如果我们要在一个场景中不同位置显示同一个模型怎么办? 如果我们要以不同的比例、不同角度显示同一个模型又怎么办 ?

如果继续以类似的顶点属性数据定义同一个模型,调整它满足上述需求的话,不仅浪费显卡内存,而且这个调整的工作量也很大,因此效率很低。更好地解决方法是,我们定义的模型根据需要可以执行放大、缩小等操作来不同比例显示,可以通过平移来放在不同位置,可以通过旋转来按不同角度显示。这种方式就是执行模型变换。

模型变换通过对模型执行平移(translation)、缩放(scale)、旋转(rotation)、镜像(reflection)、错切(shear)等操作,来调整模型的过程。通过模型变换,我们可以按照合理方式指定场景中物体的位置等信息。


平移变换

平移就是将物体从一个位置p=(x,y,z),移动到另一个位置p′=(x′,y′,z′)的过程,记为p′=p+d,其中d=(x′?x,y′?y,z′?z)=(tx,ty,tz)。使用齐次坐标系表示为:

p′=Tp=??????100001000010txtytz1???????????xyz1?????=??????x+txy+tyz+tz1??????

如果对向量和矩阵不熟悉,可以回过去看前面介绍的向量和矩阵;如果对上面使用的齐次坐标系不熟悉,可以回过去看前面介绍的线性变换部分。

本节的模型变换在OpenGL程序中,可以使用GLM数学库实现。例如平移变换实现如下:

glm::mat4 model; // 构造单位矩阵
model = glm::translate(model, glm::vec3(-0.5f, 0.0f, 0.0f));

上述表示平移向量为(-0.5,0.0,0.0),得到一个平移矩阵存储到model中。

在程序中我们绘制了4个矩形,通过平移将其放在不同位置,效果如下图所示:

在上图示例中,我们使用不同的着色器还绘制了坐标轴,坐标轴通过箭头和轴线绘制。在xoy坐标系中,第一个象限为原图,第二个象限为平移(-0.5,0.0,0.0)后的矩形,第三象限为平移(-0.8,-0.8,0.0)后的矩形,第四个象限为平移(0.0,-0.5,0.0)后的矩形。

注意 通过上面坐标处理的全局过程图1可以看到,实际顶点输出还需要经过视变换、投影变换过程等处理,本节主要讨论模型变换,因此我们在代码中,不考虑视变换和投影变换,使用默认的视变换和投影变换,即这两个变换保持为单位矩阵。默认的方式就是我们一直在使用的正交投影方式。变换矩阵在着色器中使用uniform变量传递,在c++程序中使用glm::mat4与之对应。对uniform变量不熟悉的话,可以回过头去看2D纹理部分的使用方法。

设置默认视变换和投影变换矩阵的代码如下:

   glm::mat4 projection;// 投影变换矩阵
   glm::mat4 view; // 视变换矩阵
   glm::mat4 model; // 模型变换矩阵
   glUniformMatrix4fv(
     glGetUniformLocation(shader.programId,"projection"),
     1, GL_FALSE, glm::value_ptr(projection));
  glUniformMatrix4fv(
    glGetUniformLocation(shader.programId,"view"),
    1,GL_FALSE, glm::value_ptr(view));

绘制四个矩形的代码为:

   // 绘制第一个矩形
   glUniformMatrix4fv(
    glGetUniformLocation(shader.programId, "model"),
    1, GL_FALSE, glm::value_ptr(model));
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

  // 绘制第二个矩形
 model = glm::mat4();
 model = glm::translate(model, glm::vec3(-0.5f, 0.0f, 0.0f));
glUniformMatrix4fv(
    glGetUniformLocation(shader.programId, "model"),
     1, GL_FALSE, glm::value_ptr(model));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

// 绘制第三个矩形
model = glm::mat4();
model = glm::translate(model, glm::vec3(-0.8f, -0.8f, 0.0f));
glUniformMatrix4fv(
  glGetUniformLocation(shader.programId, "model"),   1, GL_FALSE, glm::value_ptr(model));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

// 绘制第四个矩形
model = glm::mat4();
model = glm::translate(model, glm::vec3(0.0f, -0.5f, 0.0f));
glUniformMatrix4fv(
    glGetUniformLocation(shader.programId, "model"),1, GL_FALSE, glm::value_ptr(model));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

这里绘制矩形使用的顶点属性数据,以及纹理使用方法,可以回过头去查看上一节2D纹理映射内容。

本节绘制矩形的顶点着色器中都使用代码:

#version 330

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
layout(location = 2) in vec2 textCoord;

out vec3 VertColor;
out vec2 TextCoord;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
     gl_Position =
     projection * view * model * vec4(position, 1.0);
     VertColor = color;
     TextCoord = textCoord;
}

本节绘制矩形的片元着色器中都使用代码:

#version 330

in vec3 VertColor;
in vec2 TextCoord;

uniform sampler2D tex;

out vec4 color;

void main()
{
  color = texture(tex, vec2(TextCoord.s, 1.0 -TextCoord.t) );
}

代码中使用坐标vec2(TextCoord.s, 1.0 -TextCoord.t)表示将纹理的y轴翻转,避免纹理倒立显示。

坐标和变换的数学基础一节中,我们已经提到,对于4x4仿射变换矩阵,可以表示平移(仿射变换)、缩放、旋转等线性变换,其中矩阵的形式为:

记住这一点,对于矩阵形式理解会比较清楚。


缩放变换

缩放可以沿着三个坐标轴的方向独立进行,当缩放参数一致时是均匀缩放,否则是非均匀缩放。对于以原点为中心的缩放来讲,根据坐标和变换的数学基础,一节所得到的结论:线性变换矩阵为变换后基向量组成。缩放因子为(sx,sy,sz),则得到缩放后的+x轴基向量为(sx,0,0),+y轴基向量为(0,sy,0),+z轴缩放后基向量为(0,0,sz),由这些基向量组成的缩放矩阵的前三列,构成4x4矩阵后表示为:

p′=Sp=??????sx0000sy0000sz00001???????????xyz1?????=??????x?sxy?syz?sz1??????

注意 设一个方向的缩放因子为k,当k>0表示物体变长;当k=0时表示正交投影,此时有一些维度的信息变为0了;当k<0时物体将会发射,即发生镜像变换,后面会介绍镜像变换。

执行缩放变换,效果如下图所示:

上图中,第一象限为原图,第二象限为均匀缩放0.5倍,然后平移(-0.25, 0.0,0.0)后的结果;第三象限为均匀缩放2.0倍,然后平移(-1.0, -1.0,0.0)后的结果;第四象限为缩放(2.0,0.5,1.0)后,平移(0.0,-0.25,0.0)后的结果。例如第四象限使用的代码为:

// 绘制第四个矩形 x轴放大两倍 y轴缩小为一半 平移到第四象限
model = glm::mat4();
model = glm::translate(model, glm::vec3(0.0f, -0.25f, 0.0f));
model = glm::scale(model, glm::vec3(2.0f, 0.5f, 1.0f));
glUniformMatrix4fv(
    glGetUniformLocation(shader.programId, "model"),
    1, GL_FALSE, glm::value_ptr(model));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

这里需要注意几点:

(1) 模型变换进行的顺序与结果是相关的。在向量和矩阵一节已经讲过,矩阵乘法不满足交换律,即AB≠BA,因此变换的顺序会影响最终结果。

(2) 上述第四个象限矩形,先进行缩放,然后平移到第四象限,这个过程表示为p′=T?S?p,根据结合了,可以写为p′=(T?S)p=Mp,即只需要在CPU中计算出最终的变换矩阵M即可,而不是在顶点着色器中完成矩阵乘法。

(3)上述代码中,要按照先缩放后平移来完成模型变换,代码书写的顺序和我们变换的顺序是相反的,主要原因是:

model = glm::mat4();
model = glm::translate(model, glm::vec3(0.0f, -0.25f, 0.0f));
model = glm::scale(model, glm::vec3(2.0f, 0.5f, 1.0f));

这个代码等价于:

translate = glm::translate(glm::mat4(), glm::vec3(0.0f, -0.25f, 0.0f));
scale = glm::scale(glm::mat4(), glm::vec3(2.0f, 0.5f, 1.0f));
model = translate * scale

也就是说glm::scale是对单位矩阵进行缩放,然后左乘平移矩阵。这一点尤其要注意。


旋转变换

对于以原点为中心的旋转来讲,旋转矩阵可以这样来推导。以下面的绕+z轴的旋转角度θ为例,从xoy轴角度来看如下图所示:

这样经过旋转以后,原来的+x轴对应的基向量(1,0,0)变为(cosθ,sinθ,0);原来的+y轴对应的基向量(0,1,0)变为(?sinθ,cosθ,0);而+z轴保持不变,为(0,0,1),由三个变换后的基向量构成旋转矩阵的前三列,以4x4矩阵形式书写为:

Rz(θ)=?????cosθsinθ00?sinθcosθ0000100001?????

对于绕y轴,x轴的旋转同理可得到旋转矩阵如下:

Ry(θ)=?????cosθ0?sinθ00100sinθ0cosθ00001?????

Rx(θ)=?????10000cosθsinθ00?sinθcosθ00001?????

不动点与旋转和缩放

对于旋转和缩放来说,我们上面得出的矩阵,都是针对物体以原点为中心进行的旋转和缩放,这种中心点称为不动点pf。对于不动点不再原点的旋转,获取旋转矩阵的思路是:先把物体的中心移到原点,然后应用上面的旋转矩阵,最后再把物体移回到原处,使得它的中心仍旧位于pf。这一过程表示为:

M=T(pf)R(θ)T(?pf)(旋转中心问题)

对于缩放的缩放中心也有类似处理。

执行旋转变换,效果如下图所示:

其中,第一象限为原图,第二象限为绕+z轴旋转90度;第三象限为以中心点(0.25,0.25,0.0)旋转,平移(-0.5,-0.5,0.0)到第三象限的结果;第四象限为绕着右下角(0.5,0.0,0.0)旋转,平移(0.0, -0.5,0.0)到第四象限的结果。例如第三象限的绘制代码为:

// 绘制第三个矩形 绕着矩形中心旋转
model = glm::mat4();
// 平移至第三象限
model = glm::translate(model, glm::vec3(-0.5f, -0.5f, 0.0f));
// 下面为绕着中心旋转的三个矩阵
model = glm::translate(model, glm::vec3(0.25f, 0.25f, 0.0f));
model = glm::rotate(model, (GLfloat)glfwGetTime() * 2.0f,
            glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::translate(model, glm::vec3(-0.25f, -0.25f, 0.0f));
glUniformMatrix4fv(
    glGetUniformLocation(shader.programId, "model"),
    1, GL_FALSE, glm::value_ptr(model));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

注意上面代码中,为了实现绕着矩形中心(0.25,0.25,0.0)旋转,先要平移至原点,然后旋转,最后平移至中心;为了实现平移至第三象限,实行平移变换(-0.5,-0.5,0.0)。


镜像变换

镜像变换,就是反射成像的概念,它是缩放变换的一个特例,当缩放因子k<0时会导致镜像变换。执行镜像变换后的效果如下图所示:

上面图中,第一象限为原图,第二象限为关于y轴的镜像,即点(x,y,z)镜像后点为(?x,y,z),因此所求矩阵为:

Reflecty=??????1000010000100001?????

可以看出这个矩阵,就是上面的缩放矩阵,当缩放因子为(-1.0,1.0,1.0)时的矩阵。因此实现时代码为:

   // 绘制第二个矩形 沿着y轴镜像
model = glm::mat4();
model = glm::scale(model, glm::vec3(-1.0f, 1.0f, 1.0f));
glUniformMatrix4fv(
    glGetUniformLocation(shader.programId, "model"),
    1, GL_FALSE, glm::value_ptr(model));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

上图中,第三象限为以xy轴同时反射形成,缩放因子为(-1.0,-1.0,1.0);第四象限为以x轴进行反射,缩放因子为(1.0, -1.0, 1.0)。


最后说明

上面讨论的模型变换部分涉及到了平移、缩放、旋转和镜像变换,在实际中多半是这些变换的组合,一般地执行变换顺序为缩放–》旋转—》平移。在实行模型变换时,要注意变换的顺序和代码中书写的顺序相反。同时对于缩放和旋转变换,要注意不动点不在原点时的处理方法。对于绕任意方向的旋转,需要推导一个新的旋转矩阵,这个内容会放在数学相关的小节介绍。

时间: 2024-10-12 08:32:53

OpenGL学习脚印: 模型变换(model transformation)的相关文章

OpenGL学习脚印: 视变换(view transformation)

写在前面 OpenGL中的坐标处理过程包括模型变换.视变换.投影变换.视口变换等内容,这个主题的内容有些多,因此分节学习,主题将分为5节内容来学习.上一节模型变换,本节学习模型变换的下一阶段--视变换.到目前位置,主要在2D下编写程序,学习了视变换后,我们可以看到3D应用的效果了.本节示例程序均可在我的github下载. 通过本节可以了解到 视变换的概念 索引绘制立方体 LookAt矩阵的推导(对数学不感兴趣,可以跳过) 相机位置随时间改变的应用程序 坐标处理的全局过程(了解,另文详述) Ope

OpenGL学习脚印: 坐标变换过程(vertex transformation)

写在前面 前面几节分别介绍了模型变换,视变换,以及给出了投影矩阵和视口变换矩阵的推导,本节从全局把握一遍OpenGL坐标转换的过程,从整体上认识坐标变换过程.相关矩阵的数学推导过程请参考前面几节对应的内容. 通过本节可以了解到 坐标变换的各个阶段 利用GLM数学库实现坐标变换 坐标变换的全局图 OpenGL中的坐标处理过程包括模型变换.视变换.投影变换.视口变换等过程,如下图所示: 在上面的图中,注意,OpenGL只定义了裁剪坐标系.规范化设备坐标系和屏幕坐标系,而局部坐标系(模型坐标系).世界

OpenGL学习脚印:模型加载初步-加载obj模型(load obj model)

写在前面 前面介绍了光照基础内容,以及材质和lighting maps,和光源类型,我们对使用光照增强场景真实感有了一定了解.但是到目前为止,我们通过在程序中指定的立方体数据,绘制立方体,看起来还是很乏味.本节开始介绍模型加载,通过加载丰富的模型,能够丰富我们的场景,变得好玩.本节的示例代码均可以在我的github下载. 加载模型可以使用比较好的库,例如obj模型加载的库,Assimp加载库.本节作为入门篇,我们一开始不使用这些库加载很酷的模型,而是熟悉下模型以及模型加载的概念,然后我们封装一个

OpenGL学习脚印:立方体纹理和天空包围盒(Cubemaps And Skybox)

写在前面 之前学习了2D纹理映射,实际上还有其他类型的纹理有待我们进一步学习,本节将要学习的立方体纹理(cubemaps),是一种将多个纹理图片复合到一个立方体表面的技术.在游戏中应用得较多的天空包围盒可以使用cubemap实现.本节示例程序均可以在我的github下载. 本节内容整理自: 1.Tutorial 25:SkyBox 2.www.learnopengl.com Cubemaps 创建Cubemap cubemap是使用6张2D纹理绑定到GL_TEXTURE_CUBE_MAP目标而创

OpenGL学习脚印: 帧缓冲对象(Frame Buffer Object)

写在前面 一直以来,我们在使用OpenGL渲染时,最终的目的地是默认的帧缓冲区,实际上OpenGL也允许我们创建自定义的帧缓冲区.使用自定义的帧缓冲区,可以实现镜面,离屏渲染,以及很酷的后处理效果.本节将学习帧缓存的使用,文中示例代码均可以在我的github下载. 本节内容整理自 1.OpenGL Frame Buffer Object (FBO) 2.www.learnopengl.com Framebuffers FBO概念 在OpenGL中,渲染管线中的顶点.纹理等经过一系列处理后,最终显

OpenGL学习脚印:光源类型和使用多个光源(Light source and multiple lights)

写在前面 上一节光照中使用材质和lighting maps介绍了使用材质属性和lighting maps使物体的光照效果能反映物体的材料特性,看起来更逼真.在前面的章节中使用的实际上都是一个点光源,本节将学习其他几种光源类型,以及在场景中使用多个光源.本节代码均可以在我的github下载. 本节内容整理自: 1.www.learnopengl.com light casters 2.www.learnopengl.com Multiple lights 通过本节可以了解到 方向光源 点光源 聚光

OpenGL学习脚印: 绘制一个三角形

写在前面 接着上一节内容,开发环境搭建好后,我们当然想立即编写3D应用程序了.不过我们还需要些耐心,因为OpenGL是一套底层的API,因而我们要掌握的基本知识稍微多一点,在开始绘制3D图形之前,本节我们将通过绘制一个三角形的程序来熟悉现代OpenGL的概念和流程. 通过本节可以了解到: 缓存对象VAO和VBO GLSL着色器程序的编译.链接和使用方法 OpenGL绘图的基本流程 绘图流水线简要了解 与使用高级绘图API(例如java里swing绘图,MFC里的绘图)不同,使用OpenGL绘制图

OpenGL学习脚印: 向量和矩阵要点(math-vector and matrices)

写在前面 前面几节内容环境搭建,绘制三角形,以及使用索引绘制,让我们对现代OpenGL中绘图做了简单了解.要继续后面的部分,需要熟悉OpenGL中涉及的数学知识.因此本节开始介绍OpenGL中的基本数学. 介绍这部分内容的主旨在于对OpenGL涉及的数学有个整体把握,重点把握一些概念在OpenGL中的应用.内容尽量以例子形式说明,仅在必要时会给出数学证明.一个主题往往涉及过多内容,对于文中省略的部分,请参考相应的教材. 通过本节可以了解到 向量基本概念和操作 矩阵的基本概念和操作 GLM数学库

OpenGl学习进程(7)第五课:点、边和图形(二)边

本节是OpenGL学习的第五个课时,下面介绍OpenGL边的相关知识: (1)边的概念: 数学上的直线没有宽度,但OpenGL的直线则是有宽度的.同时,OpenGL的直线必须是有限长度,而不是像数学概念那样是无限的.可以认为,OpenGL的“直线”概念与数学上的“线段”接近,它可以由两个端点来确定.     (2)如何绘制边: 1)OpenGL支持绘制三种类型的边: GL_LINES :指定两个顶点,在它们之间绘制一条直线.如果为GL_LINES指定了奇数个顶点,那么最后一个顶点会被忽略. GL