目录html
看了很多的关于WebGL/OpenGL的资料,笔者发现这些资料在讲解图形变换的时候都讲了不少的原理,而后举出一个特别简单的实例(坐标是1.0,0.5的那种)来说解。确实一看就懂,但用到实际的场景之中就一脸懵逼了(好比地形的三维坐标都是很大的数字)。因此笔者这里结合一个具体的实例,总结下WebGL/OpenGL中,关于模型变换、视图变换、投影变换的设置技巧。web
绘制任何复杂的场景以前,均可以先绘制出其包围盒,能应用于包围盒的图形变换,基本上就能用于该场景了,所以,笔者这里绘制一幅地形的包围盒。它的最大最小范围为:chrome
//包围盒范围 var minX = 399589.072; var maxX = 400469.072; var minY = 3995118.062; var maxY = 3997558.062; var minZ = 732; var maxZ = 1268;
WebGL是OpenGL的子集,所以我这里直接用WebGL的例子,可是各类接口函数跟OpenGL是很是相似的,尤为是图形变换的函数。编程
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="utf-8" /> <title>Hello cube</title> </head> <body onload="main()"> <canvas id="webgl" width="600" height="600"> Please use a browser that supports "canvas" </canvas> <script src="lib/webgl-utils.js"></script> <script src="lib/webgl-debug.js"></script> <script src="lib/cuon-utils.js"></script> <script src="lib/cuon-matrix.js"></script> <script src="Cube.js"></script> </body> </html>
// Vertex shader program var VSHADER_SOURCE = 'attribute vec4 a_Position;\n' + 'attribute vec4 a_Color;\n' + 'uniform mat4 u_MvpMatrix;\n' + 'varying vec4 v_Color;\n' + 'void main() {\n' + ' gl_Position = u_MvpMatrix * a_Position;\n' + ' v_Color = a_Color;\n' + '}\n'; // Fragment shader program var FSHADER_SOURCE = '#ifdef GL_ES\n' + 'precision mediump float;\n' + '#endif\n' + 'varying vec4 v_Color;\n' + 'void main() {\n' + ' gl_FragColor = v_Color;\n' + '}\n'; //包围盒范围 var minX = 399589.072; var maxX = 400469.072; var minY = 3995118.062; var maxY = 3997558.062; var minZ = 732; var maxZ = 1268; //包围盒中心 var cx = (minX + maxX) / 2.0; var cy = (minY + maxY) / 2.0; var cz = (minZ + maxZ) / 2.0; //当前lookAt()函数初始视点的高度 var eyeHight = 2000.0; //根据视点高度算出setPerspective()函数的合理角度 var fovy = (maxY - minY) / 2.0 / eyeHight; fovy = 180.0 / Math.PI * Math.atan(fovy) * 2; //setPerspective()远截面 var far = 3000; // function main() { // Retrieve <canvas> element var canvas = document.getElementById('webgl'); // Get the rendering context for WebGL var gl = getWebGLContext(canvas); if (!gl) { console.log('Failed to get the rendering context for WebGL'); return; } // Initialize shaders if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) { console.log('Failed to intialize shaders.'); return; } // Set the vertex coordinates and color var n = initVertexBuffers(gl); if (n < 0) { console.log('Failed to set the vertex information'); return; } // Get the storage location of u_MvpMatrix var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix'); if (!u_MvpMatrix) { console.log('Failed to get the storage location of u_MvpMatrix'); return; } // Register the event handler var currentAngle = [0.0, 0.0]; // Current rotation angle ([x-axis, y-axis] degrees) initEventHandlers(canvas, currentAngle); // Set clear color and enable hidden surface removal gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); // Start drawing var tick = function () { //setPerspective()宽高比 var aspect = canvas.width / canvas.height; // draw(gl, n, aspect, u_MvpMatrix, currentAngle); requestAnimationFrame(tick, canvas); }; tick(); } function initEventHandlers(canvas, currentAngle) { var dragging = false; // Dragging or not var lastX = -1, lastY = -1; // Last position of the mouse // Mouse is pressed canvas.onmousedown = function (ev) { var x = ev.clientX; var y = ev.clientY; // Start dragging if a moue is in <canvas> var rect = ev.target.getBoundingClientRect(); if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) { lastX = x; lastY = y; dragging = true; } }; //鼠标离开时 canvas.onmouseleave = function (ev) { dragging = false; }; // Mouse is released canvas.onmouseup = function (ev) { dragging = false; }; // Mouse is moved canvas.onmousemove = function (ev) { var x = ev.clientX; var y = ev.clientY; if (dragging) { var factor = 100 / canvas.height; // The rotation ratio var dx = factor * (x - lastX); var dy = factor * (y - lastY); // Limit x-axis rotation angle to -90 to 90 degrees //currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0); currentAngle[0] = currentAngle[0] + dy; currentAngle[1] = currentAngle[1] + dx; } lastX = x, lastY = y; }; //鼠标缩放 canvas.onmousewheel = function (event) { var lastHeight = eyeHight; if (event.wheelDelta > 0) { eyeHight = Math.max(1, eyeHight - 80); } else { eyeHight = eyeHight + 80; } far = far + eyeHight - lastHeight; }; } function draw(gl, n, aspect, u_MvpMatrix, currentAngle) { //模型矩阵 var modelMatrix = new Matrix4(); modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis modelMatrix.translate(-cx, -cy, -cz); //视图矩阵 var viewMatrix = new Matrix4(); viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0); //投影矩阵 var projMatrix = new Matrix4(); projMatrix.setPerspective(fovy, aspect, 10, far); //模型视图投影矩阵 var mvpMatrix = new Matrix4(); mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix); // Pass the model view projection matrix to u_MvpMatrix gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements); // Clear color and depth buffer gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Draw the cube gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0); } function initVertexBuffers(gl) { // Create a cube // v6----- v5 // /| /| // v1------v0| // | | | | // | |v7---|-|v4 // |/ |/ // v2------v3 var verticesColors = new Float32Array([ // Vertex coordinates and color maxX, maxY, maxZ, 1.0, 1.0, 1.0, // v0 White minX, maxY, maxZ, 1.0, 0.0, 1.0, // v1 Magenta minX, minY, maxZ, 1.0, 0.0, 0.0, // v2 Red maxX, minY, maxZ, 1.0, 1.0, 0.0, // v3 Yellow maxX, minY, minZ, 0.0, 1.0, 0.0, // v4 Green maxX, maxY, minZ, 0.0, 1.0, 1.0, // v5 Cyan minX, maxY, minZ, 0.0, 0.0, 1.0, // v6 Blue minX, minY, minZ, 1.0, 0.0, 1.0 // v7 Black ]); // Indices of the vertices var indices = new Uint8Array([ 0, 1, 2, 0, 2, 3, // front 0, 3, 4, 0, 4, 5, // right 0, 5, 6, 0, 6, 1, // up 1, 6, 7, 1, 7, 2, // left 7, 4, 3, 7, 3, 2, // down 4, 7, 6, 4, 6, 5 // back ]); // Create a buffer object var vertexColorBuffer = gl.createBuffer(); var indexBuffer = gl.createBuffer(); if (!vertexColorBuffer || !indexBuffer) { return -1; } // Write the vertex coordinates and color to the buffer object gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer); gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW); var FSIZE = verticesColors.BYTES_PER_ELEMENT; // Assign the buffer object to a_Position and enable the assignment var a_Position = gl.getAttribLocation(gl.program, 'a_Position'); if (a_Position < 0) { console.log('Failed to get the storage location of a_Position'); return -1; } gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0); gl.enableVertexAttribArray(a_Position); // Assign the buffer object to a_Color and enable the assignment var a_Color = gl.getAttribLocation(gl.program, 'a_Color'); if (a_Color < 0) { console.log('Failed to get the storage location of a_Color'); return -1; } gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3); gl.enableVertexAttribArray(a_Color); // Write the indices to the buffer object gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); return indices.length; }
这份代码改进《WebGL编程指南》一书里面绘制一个简单立方体的例子,引用的几个JS-lib也是该书提供。本例所有源代码地址连接为:https://share.weiyun.com/52XmsFv ,密码:h1lbay。
用chrome打开Cube.html,会出现一个长方体的包围盒,还能够用鼠标左键旋转,鼠标滚轮缩放:
canvas
本例的思路是经过JS的requestAnimationFrame()函数不停的调用绘制函数draw(),同时将一些变量关联到鼠标操做事件和draw(),达到页面图形变换的效果。这里笔者就不讲原理,重点讲一讲设置三个图形变换的具体过程,网上已经有很是多的原理介绍了。数组
在draw()函数中设置模型矩阵:函数
//模型矩阵 var modelMatrix = new Matrix4(); modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis modelMatrix.translate(-cx, -cy, -cz);
因为这个包围盒(长方体)的坐标值都很是大,因此第一步须要对其作平移变换translate(-cx, -cy, -cz),cx,cy,cz就是包围盒的中心:webgl
//包围盒中心 var cx = (minX + maxX) / 2.0; var cy = (minY + maxY) / 2.0; var cz = (minZ + maxZ) / 2.0;
接下来是旋转变换,数组currentAngle记录了绕X轴和Y轴旋转的角度,初始值为0。配合onmousedown,onmouseup,onmousemove三个鼠标事件,将页面鼠标X、Y方向的移动,转换成绕X轴,Y轴的角度值,累计到currentAngle中,从而实现了三维模型随鼠标旋转。.net
// Mouse is moved canvas.onmousemove = function (ev) { var x = ev.clientX; var y = ev.clientY; if (dragging) { var factor = 100 / canvas.height; // The rotation ratio var dx = factor * (x - lastX); var dy = factor * (y - lastY); // Limit x-axis rotation angle to -90 to 90 degrees //currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0); currentAngle[0] = currentAngle[0] + dy; currentAngle[1] = currentAngle[1] + dx; } lastX = x, lastY = y; };
注意模型矩阵的平移变换要放后面,须要把坐标轴换到包围盒中心,才能绕三维模型自转。debug
经过lookAt()函数设置视图矩阵:
//当前lookAt()函数初始视点的高度 var eyeHight = 2000.0; // … //视图矩阵 var viewMatrix = new Matrix4(); viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0);
视图变换调整的是观察者的状态,lookAt()函数分别设置了视点、目标观察点以及上方向。虽然能够在任何位置去观察三维场景的点,从而获得渲染结果。但在实际的应用当中,这个函数设置的结果很不可思议,因此笔者设置成,观察者站在包围盒中心上方的位置,对准坐标系原点(注意这个时候通过模型变换,包围盒的中心点已是坐标系原点了),常见的Y轴做为上方向。这样,视图内不管如何都是可见的。
这里将视点的高度设置成变量eyeHight,初始值为2000,是一个大于0的经验值。同时经过鼠标的滚轮事件onmousewheel()调整该值,从而实现三维模型的缩放的:
//鼠标缩放 canvas.onmousewheel = function (event) { var lastHeight = eyeHight; if (event.wheelDelta > 0) { eyeHight = Math.max(1, eyeHight - 80); } else { eyeHight = eyeHight + 80; } };
经过setPerspective()来设置投影变换:
//根据视点高度算出setPerspective()函数的合理角度 var fovy = (maxY - minY) / 2.0 / eyeHight; fovy = 180.0 / Math.PI * Math.atan(fovy) * 2; //setPerspective()远截面 var far = 3000; //setPerspective()宽高比 var aspect = canvas.width / canvas.height; //... //投影矩阵 var projMatrix = new Matrix4(); projMatrix.setPerspective(fovy, aspect, 10, far);
前面的视图变换已经论述了,这个模型是在中心点上方去观察中心点,至关于视线垂直到前界面near的表面,那么setPerspective()就能够肯定其角度fovy了,示意图以下:
很明显的看出,当光线射到包围盒的中心,包围盒Y方向长度的一半,除以视点高,就是fovy通常的正切值。
宽高比aspect便是页面canvas元素的宽高比。
近界面near通常设置成较近的值,可是不能太近(好比小于1),不然会影响深度判断的精度形成页面闪烁。《OpenGL绘制纹理,缩放相机致使纹理闪烁的解决方法gluPerspective ()》论述了这个问题。
而远界面far也是须要跟着鼠标滚轮一块儿变换的,不然当eyeHight变大,三维物体会逐渐离开透视变换的视锥体:
//鼠标缩放 canvas.onmousewheel = function (event) { var lastHeight = eyeHight; if (event.wheelDelta > 0) { eyeHight = Math.max(1, eyeHight - 80); } else { eyeHight = eyeHight + 80; } far = far + eyeHight - lastHeight; };
将三个矩阵都应用起来,就获得最终的模型视图投影矩阵。注意计算式是:投影矩阵 * 视图矩阵 * 模型矩阵:
//模型视图投影矩阵 var mvpMatrix = new Matrix4(); mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
本例中的三维物体随着鼠标旋转,是把鼠标X、Y方向的移动距离转换成绕X轴,Y轴方向的角度来实现的。可是如何用鼠标实现绕Z轴(第三轴)旋转呢?例如像OSG这样的渲染引擎,是能够用鼠标绕第三个轴旋转的(固然操做有点费力)。这里但愿你们能批评指正下。