1. 具体实例

看了不少的关于WebGL/OpenGL的资料,笔者发现这些资料在讲解图形变换的时候都讲了很多的原理,然后举出一个特别简单的实例(坐标是1.0,0.5的那种)来讲解。确实一看就懂,但用到实际的场景之中就一脸懵逼了(比如地形的三维坐标都是很大的数字)。所以笔者这里结合一个具体的实例,总结下WebGL/OpenGL中,关于模型变换、视图变换、投影变换的设置技巧。

绘制任何复杂的场景之前,都可以先绘制出其包围盒,能应用于包围盒的图形变换,基本上就能用于该场景了,因此,笔者这里绘制一幅地形的包围盒。它的最大最小范围为:

  1. //包围盒范围
  2. var minX = 399589.072;
  3. var maxX = 400469.072;
  4. var minY = 3995118.062;
  5. var maxY = 3997558.062;
  6. var minZ = 732;
  7. var maxZ = 1268;

2. 解决方案

WebGL是OpenGL的子集,因此我这里直接用WebGL的例子,但是各种接口函数跟OpenGL是非常类似的,尤其是图形变换的函数。

1) Cube.html

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>Hello cube</title>
  6. </head>
  7. <body onload="main()">
  8. <canvas id="webgl" width="600" height="600">
  9. Please use a browser that supports "canvas"
  10. </canvas>
  11. <script src="lib/webgl-utils.js"></script>
  12. <script src="lib/webgl-debug.js"></script>
  13. <script src="lib/cuon-utils.js"></script>
  14. <script src="lib/cuon-matrix.js"></script>
  15. <script src="Cube.js"></script>
  16. </body>
  17. </html>

2) Cube.js

  1. // Vertex shader program
  2. var VSHADER_SOURCE =
  3. 'attribute vec4 a_Position;\n' +
  4. 'attribute vec4 a_Color;\n' +
  5. 'uniform mat4 u_MvpMatrix;\n' +
  6. 'varying vec4 v_Color;\n' +
  7. 'void main() {\n' +
  8. ' gl_Position = u_MvpMatrix * a_Position;\n' +
  9. ' v_Color = a_Color;\n' +
  10. '}\n';
  11. // Fragment shader program
  12. var FSHADER_SOURCE =
  13. '#ifdef GL_ES\n' +
  14. 'precision mediump float;\n' +
  15. '#endif\n' +
  16. 'varying vec4 v_Color;\n' +
  17. 'void main() {\n' +
  18. ' gl_FragColor = v_Color;\n' +
  19. '}\n';
  20. //包围盒范围
  21. var minX = 399589.072;
  22. var maxX = 400469.072;
  23. var minY = 3995118.062;
  24. var maxY = 3997558.062;
  25. var minZ = 732;
  26. var maxZ = 1268;
  27. //包围盒中心
  28. var cx = (minX + maxX) / 2.0;
  29. var cy = (minY + maxY) / 2.0;
  30. var cz = (minZ + maxZ) / 2.0;
  31. //当前lookAt()函数初始视点的高度
  32. var eyeHight = 2000.0;
  33. //根据视点高度算出setPerspective()函数的合理角度
  34. var fovy = (maxY - minY) / 2.0 / eyeHight;
  35. fovy = 180.0 / Math.PI * Math.atan(fovy) * 2;
  36. //setPerspective()远截面
  37. var far = 3000;
  38. //
  39. function main() {
  40. // Retrieve <canvas> element
  41. var canvas = document.getElementById('webgl');
  42. // Get the rendering context for WebGL
  43. var gl = getWebGLContext(canvas);
  44. if (!gl) {
  45. console.log('Failed to get the rendering context for WebGL');
  46. return;
  47. }
  48. // Initialize shaders
  49. if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
  50. console.log('Failed to intialize shaders.');
  51. return;
  52. }
  53. // Set the vertex coordinates and color
  54. var n = initVertexBuffers(gl);
  55. if (n < 0) {
  56. console.log('Failed to set the vertex information');
  57. return;
  58. }
  59. // Get the storage location of u_MvpMatrix
  60. var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
  61. if (!u_MvpMatrix) {
  62. console.log('Failed to get the storage location of u_MvpMatrix');
  63. return;
  64. }
  65. // Register the event handler
  66. var currentAngle = [0.0, 0.0]; // Current rotation angle ([x-axis, y-axis] degrees)
  67. initEventHandlers(canvas, currentAngle);
  68. // Set clear color and enable hidden surface removal
  69. gl.clearColor(0.0, 0.0, 0.0, 1.0);
  70. gl.enable(gl.DEPTH_TEST);
  71. // Start drawing
  72. var tick = function () {
  73. //setPerspective()宽高比
  74. var aspect = canvas.width / canvas.height;
  75. //
  76. draw(gl, n, aspect, u_MvpMatrix, currentAngle);
  77. requestAnimationFrame(tick, canvas);
  78. };
  79. tick();
  80. }
  81. function initEventHandlers(canvas, currentAngle) {
  82. var dragging = false; // Dragging or not
  83. var lastX = -1, lastY = -1; // Last position of the mouse
  84. // Mouse is pressed
  85. canvas.onmousedown = function (ev) {
  86. var x = ev.clientX;
  87. var y = ev.clientY;
  88. // Start dragging if a moue is in <canvas>
  89. var rect = ev.target.getBoundingClientRect();
  90. if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) {
  91. lastX = x;
  92. lastY = y;
  93. dragging = true;
  94. }
  95. };
  96. //鼠标离开时
  97. canvas.onmouseleave = function (ev) {
  98. dragging = false;
  99. };
  100. // Mouse is released
  101. canvas.onmouseup = function (ev) {
  102. dragging = false;
  103. };
  104. // Mouse is moved
  105. canvas.onmousemove = function (ev) {
  106. var x = ev.clientX;
  107. var y = ev.clientY;
  108. if (dragging) {
  109. var factor = 100 / canvas.height; // The rotation ratio
  110. var dx = factor * (x - lastX);
  111. var dy = factor * (y - lastY);
  112. // Limit x-axis rotation angle to -90 to 90 degrees
  113. //currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0);
  114. currentAngle[0] = currentAngle[0] + dy;
  115. currentAngle[1] = currentAngle[1] + dx;
  116. }
  117. lastX = x, lastY = y;
  118. };
  119. //鼠标缩放
  120. canvas.onmousewheel = function (event) {
  121. var lastHeight = eyeHight;
  122. if (event.wheelDelta > 0) {
  123. eyeHight = Math.max(1, eyeHight - 80);
  124. } else {
  125. eyeHight = eyeHight + 80;
  126. }
  127. far = far + eyeHight - lastHeight;
  128. };
  129. }
  130. function draw(gl, n, aspect, u_MvpMatrix, currentAngle) {
  131. //模型矩阵
  132. var modelMatrix = new Matrix4();
  133. modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis
  134. modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis
  135. modelMatrix.translate(-cx, -cy, -cz);
  136. //视图矩阵
  137. var viewMatrix = new Matrix4();
  138. viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0);
  139. //投影矩阵
  140. var projMatrix = new Matrix4();
  141. projMatrix.setPerspective(fovy, aspect, 10, far);
  142. //模型视图投影矩阵
  143. var mvpMatrix = new Matrix4();
  144. mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
  145. // Pass the model view projection matrix to u_MvpMatrix
  146. gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
  147. // Clear color and depth buffer
  148. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  149. // Draw the cube
  150. gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
  151. }
  152. function initVertexBuffers(gl) {
  153. // Create a cube
  154. // v6----- v5
  155. // /| /|
  156. // v1------v0|
  157. // | | | |
  158. // | |v7---|-|v4
  159. // |/ |/
  160. // v2------v3
  161. var verticesColors = new Float32Array([
  162. // Vertex coordinates and color
  163. maxX, maxY, maxZ, 1.0, 1.0, 1.0, // v0 White
  164. minX, maxY, maxZ, 1.0, 0.0, 1.0, // v1 Magenta
  165. minX, minY, maxZ, 1.0, 0.0, 0.0, // v2 Red
  166. maxX, minY, maxZ, 1.0, 1.0, 0.0, // v3 Yellow
  167. maxX, minY, minZ, 0.0, 1.0, 0.0, // v4 Green
  168. maxX, maxY, minZ, 0.0, 1.0, 1.0, // v5 Cyan
  169. minX, maxY, minZ, 0.0, 0.0, 1.0, // v6 Blue
  170. minX, minY, minZ, 1.0, 0.0, 1.0 // v7 Black
  171. ]);
  172. // Indices of the vertices
  173. var indices = new Uint8Array([
  174. 0, 1, 2, 0, 2, 3, // front
  175. 0, 3, 4, 0, 4, 5, // right
  176. 0, 5, 6, 0, 6, 1, // up
  177. 1, 6, 7, 1, 7, 2, // left
  178. 7, 4, 3, 7, 3, 2, // down
  179. 4, 7, 6, 4, 6, 5 // back
  180. ]);
  181. // Create a buffer object
  182. var vertexColorBuffer = gl.createBuffer();
  183. var indexBuffer = gl.createBuffer();
  184. if (!vertexColorBuffer || !indexBuffer) {
  185. return -1;
  186. }
  187. // Write the vertex coordinates and color to the buffer object
  188. gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
  189. gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
  190. var FSIZE = verticesColors.BYTES_PER_ELEMENT;
  191. // Assign the buffer object to a_Position and enable the assignment
  192. var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  193. if (a_Position < 0) {
  194. console.log('Failed to get the storage location of a_Position');
  195. return -1;
  196. }
  197. gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  198. gl.enableVertexAttribArray(a_Position);
  199. // Assign the buffer object to a_Color and enable the assignment
  200. var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
  201. if (a_Color < 0) {
  202. console.log('Failed to get the storage location of a_Color');
  203. return -1;
  204. }
  205. gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  206. gl.enableVertexAttribArray(a_Color);
  207. // Write the indices to the buffer object
  208. gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  209. gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
  210. return indices.length;
  211. }

3) 运行结果

这份代码改进《WebGL编程指南》一书里面绘制一个简单立方体的例子,引用的几个JS-lib也是该书提供。本例全部源代码地址链接为:https://share.weiyun.com/52XmsFv ,密码:h1lbay。

用chrome打开Cube.html,会出现一个长方体的包围盒,还可以用鼠标左键旋转,鼠标滚轮缩放:

3. 详细讲解

本例的思路是通过JS的requestAnimationFrame()函数不停的调用绘制函数draw(),同时将一些变量关联到鼠标操作事件和draw(),达到页面图形变换的效果。这里笔者就不讲原理,重点讲一讲设置三个图形变换的具体过程,网上已经有非常多的原理介绍了。

1) 模型变换

在draw()函数中设置模型矩阵:

  1. //模型矩阵
  2. var modelMatrix = new Matrix4();
  3. modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis
  4. modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis
  5. modelMatrix.translate(-cx, -cy, -cz);

由于这个包围盒(长方体)的坐标值都非常大,所以第一步需要对其做平移变换translate(-cx, -cy, -cz),cx,cy,cz就是包围盒的中心:

  1. //包围盒中心
  2. var cx = (minX + maxX) / 2.0;
  3. var cy = (minY + maxY) / 2.0;
  4. var cz = (minZ + maxZ) / 2.0;

接下来是旋转变换,数组currentAngle记录了绕X轴和Y轴旋转的角度,初始值为0。配合onmousedown,onmouseup,onmousemove三个鼠标事件,将页面鼠标X、Y方向的移动,转换成绕X轴,Y轴的角度值,累计到currentAngle中,从而实现了三维模型随鼠标旋转。

  1. // Mouse is moved
  2. canvas.onmousemove = function (ev) {
  3. var x = ev.clientX;
  4. var y = ev.clientY;
  5. if (dragging) {
  6. var factor = 100 / canvas.height; // The rotation ratio
  7. var dx = factor * (x - lastX);
  8. var dy = factor * (y - lastY);
  9. // Limit x-axis rotation angle to -90 to 90 degrees
  10. //currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0);
  11. currentAngle[0] = currentAngle[0] + dy;
  12. currentAngle[1] = currentAngle[1] + dx;
  13. }
  14. lastX = x, lastY = y;
  15. };

注意模型矩阵的平移变换要放后面,需要把坐标轴换到包围盒中心,才能绕三维模型自转。

2) 视图变换

通过lookAt()函数设置视图矩阵:

  1. //当前lookAt()函数初始视点的高度
  2. var eyeHight = 2000.0;
  3. // …
  4. //视图矩阵
  5. var viewMatrix = new Matrix4();
  6. viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0);

视图变换调整的是观察者的状态,lookAt()函数分别设置了视点、目标观察点以及上方向。虽然可以在任何位置去观察三维场景的点,从而得到渲染结果。但在实际的应用当中,这个函数设置的结果很难以想象,所以笔者设置成,观察者站在包围盒中心上方的位置,对准坐标系原点(注意这个时候经过模型变换,包围盒的中心点已经是坐标系原点了),常见的Y轴作为上方向。这样,视图内无论如何都是可见的。

这里将视点的高度设置成变量eyeHight,初始值为2000,是一个大于0的经验值。同时通过鼠标的滚轮事件onmousewheel()调整该值,从而实现三维模型的缩放的:

  1. //鼠标缩放
  2. canvas.onmousewheel = function (event) {
  3. var lastHeight = eyeHight;
  4. if (event.wheelDelta > 0) {
  5. eyeHight = Math.max(1, eyeHight - 80);
  6. } else {
  7. eyeHight = eyeHight + 80;
  8. }
  9. };

3) 投影变换

通过setPerspective()来设置投影变换:

  1. //根据视点高度算出setPerspective()函数的合理角度
  2. var fovy = (maxY - minY) / 2.0 / eyeHight;
  3. fovy = 180.0 / Math.PI * Math.atan(fovy) * 2;
  4. //setPerspective()远截面
  5. var far = 3000;
  6. //setPerspective()宽高比
  7. var aspect = canvas.width / canvas.height;
  8. //...
  9. //投影矩阵
  10. var projMatrix = new Matrix4();
  11. projMatrix.setPerspective(fovy, aspect, 10, far);

前面的视图变换已经论述了,这个模型是在中心点上方去观察中心点,相当于视线垂直到前界面near的表面,那么setPerspective()就可以确定其角度fovy了,示意图如下:

很明显的看出,当光线射到包围盒的中心,包围盒Y方向长度的一半,除以视点高,就是fovy一般的正切值。

宽高比aspect即是页面canvas元素的宽高比。

近界面near一般设置成较近的值,但是不能太近(比如小于1),否则会影响深度判断的精度造成页面闪烁。《OpenGL绘制纹理,缩放相机导致纹理闪烁的解决方法gluPerspective ()》论述了这个问题。

而远界面far也是需要跟着鼠标滚轮一起变换的,否则当eyeHight变大,三维物体会逐渐离开透视变换的视锥体:

  1. //鼠标缩放
  2. canvas.onmousewheel = function (event) {
  3. var lastHeight = eyeHight;
  4. if (event.wheelDelta > 0) {
  5. eyeHight = Math.max(1, eyeHight - 80);
  6. } else {
  7. eyeHight = eyeHight + 80;
  8. }
  9. far = far + eyeHight - lastHeight;
  10. };

4) 模型视图投影矩阵

将三个矩阵都应用起来,就得到最终的模型视图投影矩阵。注意计算式是:投影矩阵 * 视图矩阵 * 模型矩阵:

  1. //模型视图投影矩阵
  2. var mvpMatrix = new Matrix4();
  3. mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);

4. 存在问题

本例中的三维物体随着鼠标旋转,是把鼠标X、Y方向的移动距离转换成绕X轴,Y轴方向的角度来实现的。但是如何用鼠标实现绕Z轴(第三轴)旋转呢?例如像OSG这样的渲染引擎,是可以用鼠标绕第三个轴旋转的(当然操作有点费力)。这里希望大家能批评指正下。

WebGL或OpenGL关于模型视图投影变换的设置技巧的更多相关文章

  1. WebGL简易教程(六):第一个三维示例(使用模型视图投影变换)

    目录 1. 概述 2. 示例:绘制多个三角形 2.1. Triangle_MVPMatrix.html 2.2. Triangle_MVPMatrix.js 2.2.1. 数据加入Z值 2.2.2. ...

  2. three.js中的矩阵变换(模型视图投影变换)

    目录 1. 概述 2. 基本变换 2.1. 矩阵运算 2.2. 模型变换矩阵 2.2.1. 平移矩阵 2.2.2. 旋转矩阵 2.2.2.1. 绕X轴旋转矩阵 2.2.2.2. 绕Y轴旋转矩阵 2.2 ...

  3. Opengl使用模型视图变换移动光源

    光源绕一个物体旋转,按下鼠标左键时,光源位置旋转. #include <GL/glut.h> static int spin = 0;static GLdouble x_1 = 0.0;s ...

  4. OpenGL模型视图变换、投影变换、视口变换的理解

    OpenGL中不设置模型,投影,视口,所绘制的几何图形的坐标只能是-1到1(X轴向右,Y轴向上,Z轴垂直屏幕向外). 产生目标场景的过程类似于用照相机进行拍照: (1)把照相机固定在三角架上,并让他对 ...

  5. WEBGL学习【四】模型视图矩阵

    <html lang="zh-CN"> <!--服务器运行地址:http://127.0.0.1:8080/webgl/LearnNeHeWebGL/NeHeWe ...

  6. 【GISER&&Painter】Chapter02:WebGL中的模型视图变换

    上一节我们提到了如何在一张画布上画一个简单几何图形,通过创建画布,获取WebGLRendering上下文,创建一个简单的着色器,然后将一些顶点数据绑定到gl的Buffer中,最后通过绑定buffer数 ...

  7. 简单理解OpenGL模型视图变换

    前几天学习了OpenGL的绘图原理(其实就是坐标的不停变换变换),看到网上有个比较好的例程,于是学习了下,并在自己感兴趣的部分做了注释. 首先通过glMatrixMode(GL_MODELVIEW)设 ...

  8. WEBGL学习【八】模型视图投影矩阵

    <!--探讨WEBGL中不同图形的绘制方法:[待测试2017.11.6]--> <!DOCTYPE HTML> <html lang="en"> ...

  9. OpenGL学习笔记4——模型视图变换

    以日月地为例的一个模型视图变换.绕了比较多的弯路,下面是几个注意点总结. 注意点: 1.GL函数对模型的操作是基于当前局部坐标系,即模型坐标系而非世界坐标系,二者只在第一次初始化完毕之后才重合: 2. ...

随机推荐

  1. “崩溃了?不可能,我全 Catch 住了” | Java 异常处理

    前言 今天我们来讨论一下,程序中的错误处理. 在任何一个稳定的程序中,都会有大量的代码在处理错误,有一些业务错误,我们可以通过主动检查判断来规避,可对于一些不能主动判断的错误,例如 RuntimeEx ...

  2. css节点选择器

    基础选择器 基础选择器是选择器的所有选择器的基本组成元素,也最简单,包含如下5个类别: ID选择器 标签选择器 类选择器 属性选择器:类选择器算是一个特殊的属性选择器,通用的属性选择器举例如下: #c ...

  3. js 对象拷贝

    在JavaScript中,数据类型分为两大类:基本数据类型和复杂数据类型.基本数据类型包括Number.Boolean.String.Null.String),而复杂数据类型包括Object.Func ...

  4. Android组件化探索与实践

    什么是组件化 不用去纠结组件和模块语义上的区别,如果模块间不存在强依赖且模块间可以任意组合,我们就说这些模块是组件化的. 组件化的好处 实现组件化本身就是一个解耦的过程,同时也在不断对你的项目代码进行 ...

  5. 阿里如何实现海量数据实时分析技术-AnalyticDB

    导读:随着数据量的快速增长,越来越多的企业迎来业务数据化时代,数据成为了最重要的生产资料和业务升级依据.本文由阿里AnalyticDB团队出品,近万字长文,首次深度解读阿里在海量数据实时分析领域的多项 ...

  6. 自学python的日记分享

    2019.4.22登记 课堂笔记 2019.4.8 在windows环境下,用python写出第一个程序“hello world” print("Hello World!!!") ...

  7. Linux 虚拟网络设备 veth-pair 详解,看这一篇就够了

    本文首发于我的公众号 Linux云计算网络(id: cloud_dev),专注于干货分享,号内有 10T 书籍和视频资源,后台回复「1024」即可领取,欢迎大家关注,二维码文末可以扫. 前面这篇文章介 ...

  8. string类的几种方法

    string str="123abc456";int i=3;1 取字符串的前i个字符   str=str.Substring(0,i); // or  str=str.Remov ...

  9. Spring入门(二):自动化装配bean

    Spring从两个角度来实现自动化装配: 组件扫描(component scanning):Spring会自动发现应用上下文中需要创建的bean. 自动装配(autowiring):Spring会自动 ...

  10. 前端笔记之服务器&Ajax(下)数据请求&解决跨域&三级联动&session&堆栈

    一.请求后端的JSON数据 JSON是前后端通信的交互格式,JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式. JSON是互联网各个后台与 ...