准备知识

在前面的几节教程中,我们已经提到过几种变换,为物体在3D世界中的移动提供的极大的灵活性。但是我们还有很多东西需要学习(如摄像机控制和透视投影),你可以已经猜到,我们需要將这些变换组合起来。在很多情况下,你可能需要缩放一个物体使它适合3D场景,然后还要旋转使它面向正确的方向,接着把它移动到合适的位置。到现在为止我们都在练习一次只做一个变换,为了执行一系列的变换,我们要用顶点位置乘上第一个变换矩阵,然后用结果乘上第二个变换矩阵,一直乘下去直到所有的变换都应用于该顶点上。有一种简单的做法,我们可以把变换矩阵提供给shader,然后由shader来完成相乘操作。然而这种做法效率比较低,因为所有顶点都要乘上所有的变换矩阵,而这些顶点仅仅是位置不同。幸运的是,线性代数中有一个规则可以使它变得更简单。

给定一组矩阵M0…Mn 和一个向量V,则有:

Mn * Mn-1 * ... * M0 * V = (Mn* Mn-1 * ... * M0) * V

所以你可以先把矩阵的乘积算出来:

N = Mn * Mn-1 * ... * M0

则有:

Mn * Mn-1 * ... * M0 * V = N * V

这意味着我们可以先把矩阵N算出来,然后作为一个uniform类型的变量传递到shader中,在shader中把它和每一个顶点相乘,这些操作將由GPU来完成。

在把变换矩阵相乘获得N矩阵时,这些矩阵的顺序是怎样的呢?你需要记住的第一件事就是最初相乘的矩阵必须在最右边(本例中为M0),然后每个变换矩阵从右倒左依次相乘。在3D图形学中,通常先缩放对象,然后是旋转,最后才是移动。

我们来看看如果先旋转后平移会是什么情况:



接下来看看先平移后旋转的情况:

正如你所看到的,当我们先平移后旋转时,物体在世界坐标系中的位置很难把握,因为你把物体从原始点移动以后,再旋转时物体的位置也会跟着不断改变,这是我们要避免的现象。这就是为什么我们通常先做旋转变换后做平移变换。

在这个案例中我们开始处理同事存在多种变换的情况,我们必须放弃直接在渲染函数中更新变换矩阵的做法。这种方法无法扩展,容易出差,这里使用Pipeline类代替。这个类隐藏了矩阵操作的细节,使用简单的API进行平移、旋转、缩放等操作。设置了所有的参数后,你只需要简单的获取所有变换后的最终矩阵,这个矩阵可以直接用于shader中。

程序代码

清单1.主程序代码tutorial11.cpp


  1. #include "stdafx.h"
  2. /*
  3. Copyright 2010 Etay Meiri
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. Tutorial 11 - Concatenating transformation2
  15. */
  16. #include <stdio.h>
  17. #include <string.h>
  18. #include <assert.h>
  19. #include <math.h>
  20. #include <GL/glew.h>
  21. #include <GL/freeglut.h>
  22. #include "ogldev_util.h"
  23. #include "ogldev_pipeline.h"
  24. GLuint VBO;
  25. GLuint IBO;
  26. GLuint gWorldLocation;
  27. const char* pVSFileName = "shader.vs";
  28. const char* pFSFileName = "shader.fs";
  29. static void RenderSceneCB()
  30. {
  31. glClear(GL_COLOR_BUFFER_BIT);
  32. static float Scale = 0.0f;
  33. Scale += 0.001f;
  34. Pipeline p;
  35. p.Scale(sinf(Scale * 0.1f), sinf(Scale * 0.1f), sinf(Scale * 0.1f));
  36. p.WorldPos(sinf(Scale), 0.0f, 0.0f);
  37. p.Rotate(sinf(Scale) * 90.0f, sinf(Scale) * 90.0f, sinf(Scale) * 90.0f);
  38. glUniformMatrix4fv(gWorldLocation, 1, GL_TRUE, (const GLfloat*)p.GetWorldTrans());
  39. glEnableVertexAttribArray(0);
  40. glBindBuffer(GL_ARRAY_BUFFER, VBO);
  41. glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
  42. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
  43. glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_INT, 0);
  44. glDisableVertexAttribArray(0);
  45. glutSwapBuffers();
  46. }
  47. static void InitializeGlutCallbacks()
  48. {
  49. glutDisplayFunc(RenderSceneCB);
  50. glutIdleFunc(RenderSceneCB);
  51. }
  52. static void CreateVertexBuffer()
  53. {
  54. Vector3f Vertices[4];
  55. Vertices[0] = Vector3f(-1.0f, -1.0f, 0.0f);
  56. Vertices[1] = Vector3f(0.0f, -1.0f, 1.0f);
  57. Vertices[2] = Vector3f(1.0f, -1.0f, 0.0f);
  58. Vertices[3] = Vector3f(0.0f, 1.0f, 0.0f);
  59. glGenBuffers(1, &VBO);
  60. glBindBuffer(GL_ARRAY_BUFFER, VBO);
  61. glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
  62. }
  63. static void CreateIndexBuffer()
  64. {
  65. unsigned int Indices[] = { 0, 3, 1,
  66. 1, 3, 2,
  67. 2, 3, 0,
  68. 0, 1, 2 };
  69. glGenBuffers(1, &IBO);
  70. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
  71. glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
  72. }
  73. static void AddShader(GLuint ShaderProgram, const char* pShaderText, GLenum ShaderType)
  74. {
  75. GLuint ShaderObj = glCreateShader(ShaderType);
  76. if (ShaderObj == 0) {
  77. fprintf(stderr, "Error creating shader type %d\n", ShaderType);
  78. exit(1);
  79. }
  80. const GLchar* p[1];
  81. p[0] = pShaderText;
  82. GLint Lengths[1];
  83. Lengths[0]= strlen(pShaderText);
  84. glShaderSource(ShaderObj, 1, p, Lengths);
  85. glCompileShader(ShaderObj);
  86. GLint success;
  87. glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
  88. if (!success) {
  89. GLchar InfoLog[1024];
  90. glGetShaderInfoLog(ShaderObj, 1024, NULL, InfoLog);
  91. fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
  92. exit(1);
  93. }
  94. glAttachShader(ShaderProgram, ShaderObj);
  95. }
  96. static void CompileShaders()
  97. {
  98. GLuint ShaderProgram = glCreateProgram();
  99. if (ShaderProgram == 0) {
  100. fprintf(stderr, "Error creating shader program\n");
  101. exit(1);
  102. }
  103. string vs, fs;
  104. if (!ReadFile(pVSFileName, vs)) {
  105. exit(1);
  106. };
  107. if (!ReadFile(pFSFileName, fs)) {
  108. exit(1);
  109. };
  110. AddShader(ShaderProgram, vs.c_str(), GL_VERTEX_SHADER);
  111. AddShader(ShaderProgram, fs.c_str(), GL_FRAGMENT_SHADER);
  112. GLint Success = 0;
  113. GLchar ErrorLog[1024] = { 0 };
  114. glLinkProgram(ShaderProgram);
  115. glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &Success);
  116. if (Success == 0) {
  117. glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
  118. fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
  119. exit(1);
  120. }
  121. glValidateProgram(ShaderProgram);
  122. glGetProgramiv(ShaderProgram, GL_VALIDATE_STATUS, &Success);
  123. if (!Success) {
  124. glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
  125. fprintf(stderr, "Invalid shader program: '%s'\n", ErrorLog);
  126. exit(1);
  127. }
  128. glUseProgram(ShaderProgram);
  129. gWorldLocation = glGetUniformLocation(ShaderProgram, "gWorld");
  130. assert(gWorldLocation != 0xFFFFFFFF);
  131. }
  132. int _tmain(int argc, _TCHAR* argv[])
  133. {
  134. glutInit(&argc, argv);
  135. glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA);
  136. glutInitWindowSize(1024, 768);
  137. glutInitWindowPosition(100, 100);
  138. glutCreateWindow("Tutorial 11");
  139. InitializeGlutCallbacks();
  140. // Must be done after glut is initialized!
  141. GLenum res = glewInit();
  142. if (res != GLEW_OK) {
  143. fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
  144. return 1;
  145. }
  146. printf("GL version: %s\n", glGetString(GL_VERSION));
  147. glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
  148. CreateVertexBuffer();
  149. CreateIndexBuffer();
  150. CompileShaders();
  151. glutMainLoop();
  152. return 0;
  153. }

代码解读

代码中我们用到一个矩阵类Matrix4f,它提供了矩阵常用的操作 ,该类重载了乘法运算符,实现如下:

  1. inline Matrix4f operator*(const Matrix4f& Right) const
  2. {
  3. Matrix4f Ret;
  4. for (unsigned int i = 0 ; i < 4 ; i++) {
  5. for (unsigned int j = 0 ; j < 4 ; j++) {
  6. Ret.m[i][j] = m[i][0] * Right.m[0][j] +
  7. m[i][1] * Right.m[1][j] +
  8. m[i][2] * Right.m[2][j] +
  9. m[i][3] * Right.m[3][j];
  10. }
  11. }
  12. return Ret;
  13. }

我们还用到了管道类Pipeline,声明如下:

  1. class Pipeline
  2. {
  3. public:
  4. Pipeline() { ... }
  5. void Scale(float ScaleX, float ScaleY, float ScaleZ) { ... }
  6. void WorldPos(float x, float y, float z) { ... }
  7. void Rotate(float RotateX, float RotateY, float RotateZ) { ... }
  8. const Matrix4f* GetTrans();
  9. private:
  10. Vector3f m_scale;
  11. Vector3f m_worldPos;
  12. Vector3f m_rotateInfo;
  13. Matrix4f m_transformation;
  14. };

Pipeline::GetTrans函数实现代码如下:

  1. const Matrix4f* Pipeline::GetTrans()
  2. {
  3. Matrix4f ScaleTrans, RotateTrans, TranslationTrans;
  4. InitScaleTransform(ScaleTrans);
  5. InitRotateTransform(RotateTrans);
  6. InitTranslationTransform(TranslationTrans);
  7. m_transformation = TranslationTrans * RotateTrans * ScaleTrans;
  8. return &m_transformation;
  9. }

这个函数中初始化三个独立的变换矩阵,然后返回三个矩阵的乘积作为最终的变换矩阵。相乘的顺序是通过硬编码写死的。需要注意的是最终变换矩阵是该类的成员属性,你可以通过检查一个脏数据标志位来优化该方法,返回一个存储的矩阵以防止函数最后一次调用时配置上没发生任何变化。

主程序中代码:

  1. Pipeline p;
  2. p.Scale(sinf(Scale * 0.1f), sinf(Scale * 0.1f), sinf(Scale * 0.1f));
  3. p.WorldPos(sinf(Scale), 0.0f, 0.0f);
  4. p.Rotate(sinf(Scale) * 90.0f, sinf(Scale) * 90.0f, sinf(Scale) * 90.0f);
  5. glUniformMatrix4fv(gWorldLocation, 1, GL_TRUE, (const GLfloat*)p.GetTrans());

这里是主程序中和上一节不同的地方,我们定义一个Pipeline 对象,通过该对象的成员函数来完成缩放、平移、旋转变换。

运行效果

可以看到四面体在屏幕上同时移动、旋转、变大变小。

OpenGL编程逐步深入(十一)组合变换的更多相关文章

  1. 用MFC实现OpenGL编程

    一.OpenGL简介 众所周知,OpenGL原先是Silicon Graphics Incorporated(SGI公司)在他们的图形工作站上开发高质量图像的接口.但最近几年它成为一个非常优秀的开放式 ...

  2. OpenGL编程指南(第七版)

    OpenGL编程指南(第七版) 转自:http://blog.csdn.net/w540982016044/article/details/21287645 在接触OpenGL中,配置显得相当麻烦,特 ...

  3. 编译opengl编程指南第八版示例代码通过

    最近在编译opengl编程指南第八版的示例代码,如下 #include <iostream> #include "vgl.h" #include "LoadS ...

  4. C#编程总结(十一)数字证书

    C#编程总结(十一)数字证书 之前已经通过文章介绍了数字证书的基础知识,包括加密和数字签名. 具体可见: 1.C#编程总结(七)数据加密——附源码 2.C#编程总结(八)数字签名 这里来讲述数字证书的 ...

  5. 在 Mac OS X Yosemite 10.10.5 上配置 OpenGL 编程环境

    这个教程主要参考了youtube上的视频 Getting Started in OpenGL with GLFW/GLEW in Xcode 6 ,这个视频有点问题,不能照搬.本人通过自己摸(瞎)索( ...

  6. [转]VS 2012环境下使用MFC进行OpenGL编程

    我就不黏贴复制了,直接给出原文链接:VS 2012环境下使用MFC进行OpenGL编程 其它好文链接: 1.OpenGL系列教程之十二:OpenGL Windows图形界面应用程序

  7. VS15 openGL 编程指南 配置库 triangle例子

    最近去图书馆借了一本书<OpenGL编程指南(原书第八版)>,今天倒腾了一天才把第一个例子运行出来. 所以,给大家分享一下,希望能快速解决配置问题. 一.下载需要的库文件 首先,我们需要去 ...

  8. Win32 OpenGL 编程( 1 ) Win32 下的 OpenGL 编程必须步骤

    http://blog.csdn.net/vagrxie/article/details/4602961 Win32 OpenGL 编程( 1 ) Win32 下的 OpenGL 编程必须步骤 wri ...

  9. OpenGL编程(一)渲染一个指定颜色的背景窗口

    上次已经搭好了OpenGL编程的环境.已经成功运行了第一个程序.可只是照搬书上的代码,并没弄懂其中的原理.这次通过一个小程序来解释使用GLUT库编写OpenGL程序的过程. 程序的入口 与其他程序一样 ...

  10. [转]OpenGL编程指南(第9版)环境搭建--使用VS2017

    1.使用CMake Configure中选择VS2017 Win64 , Finish: 点击Generate. 2.进入build目录 打开GLFW.sln , 生成解决方案. 打开vermilio ...

随机推荐

  1. 关于Android的.so文件所须要知道的

    早期的Android系统差点儿仅仅支持ARMv5的CPU架构,你知道如今它支持多少种吗?7种. Android系统眼下支持以下七种不同的CPU架构:ARMv5.ARMv7 (从2010年起),x86 ...

  2. Cocos2d-x 常见宏

    1)NS_CC_BEGIN cocos2d命名空间開始 2) NS_CC_END  cocos2d命名空间结束 3)USING_NS_CC 声明cocos2d命名空间 4)CC_SYNTHESIZE_ ...

  3. ubuntu16.04安装破解pycharm

    分两步,首先安装jdk,然后安装并破解pycharm 一.安装jdk 参考:http://blog.csdn.net/yebhweb/article/details/55098189 下载jdk1.8 ...

  4. nyoj--90--整数划分(母函数)

    整数划分 时间限制:3000 ms  |  内存限制:65535 KB 难度:3 描述 将正整数n表示成一系列正整数之和:n=n1+n2+-+nk,  其中n1≥n2≥-≥nk≥1,k≥1.  正整数 ...

  5. VC6.0 设置动态链接库工程生成dll以及lib文件的位置

    在"Projet"->"Settings..."的"Link"选项卡中 "Output file name"中设置 ...

  6. Batch脚本获取日期SET YEAR=%date:~10,4%

    在batch脚本中我们可以通过下面的语句来对日期进行操作: SET YEAR=%date:~10,4% SET MONTH=%date:~4,2% SET DAY=%date:~7,2% SET HO ...

  7. 从SQL注入谈数据访问层

    什么是SQL注入? SQL注入就是应用程序的开发人员未预期的吧SQL语句传入到应用程序的过程,如果直接使用用户输入的值来构建SQL语句的应用程序是很可能会受到SQL注入攻击的.特别是基于浏览器的网络应 ...

  8. 使用js获取url中的get参数并转成json格式

    写在前面的 没啥说的 上代码 思路就是先获取到?后面的参数区,然后 利用字符串转数组方法获取到各个参数 var json = {}; var url = 'https://www.baidu.com/ ...

  9. 影像服务——加载CESIUM自带的影像服务

    1.加载arcgis数据——ArcGisMapServerImageryProvider var viewer = new Cesium.Viewer("cesiumDiv",{ ...

  10. HDU-1052 Tian Ji -- The Horse Racing 贪心 考虑特殊位置(首尾元素)的讨论

    题目链接:https://cn.vjudge.net/problem/HDU-1052 题意 田忌赛马问题扩展版 给n匹马,马的能力可以相同 问得分最大多少 思路 贪心做得还是太少,一开始一点思虑都没 ...