1. 具体实例

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

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

//包围盒范围
var minX = 399589.072;
var maxX = 400469.072;
var minY = 3995118.062;
var maxY = 3997558.062;
var minZ = 732;
var maxZ = 1268;

2. 解决方案

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

1) Cube.html

<!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>

2) Cube.js

// 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;
}

3) 运行结果

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

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

3. 详细讲解

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

1) 模型变换

在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就是包围盒的中心:

//包围盒中心
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中,从而实现了三维模型随鼠标旋转。

// 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;
};

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

2) 视图变换

通过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;
}
};

3) 投影变换

通过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;
};

4) 模型视图投影矩阵

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

//模型视图投影矩阵
var mvpMatrix = new Matrix4();
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. 基于async/non-blocking高性能redis组件库BeetleX.Redis

    BeetleX.Redis是基于async/non-blocking模式实现的高性能redis组件库,组件支持redis基础指令集,并封装更简便的List,Hashset和Subscribe操作.除了 ...

  2. .net之设计模式

    在上一篇文章里我通过具体场景总结了“.net面向对象的设计原则”,其中也多次提到一些设计模式方面的技术,可想而知,设计模式在我们的开发过程中也是必不可少的.今天我们就来简单交流下设计模式.对于设计模式 ...

  3. 如何设置织梦cms自定义表单字段为必填项

    1.编辑器打开\plus\diy.php2.在40行左右找到此行代码:$dede_fields = empty($dede_fields) ? '' : trim($dede_fields);3.在这 ...

  4. AI应用开发实战 - 从零开始搭建macOS开发环境

    AI应用开发实战 - 从零开始搭建macOS开发环境 本视频配套的视频教程请访问:https://www.bilibili.com/video/av24368929/ 建议和反馈,请发送到 https ...

  5. ReactiveSwift源码解析(二) Bag容器的代码实现

    今天博客我接着上篇博客的内容来,上篇博客我们详细的看了ReactiveSwift中的Observer已经Event的代码实现.接下来我们来看一下ReactiveSwift中的结构体Bag的实现.Bag ...

  6. octotree-chrome插件,Github代码阅读神器

    1.下载octotree-chrome插件 下载地址 2.安装问题 由于新版chrome为了安全,已经不支持像以前一样拖拽插件进行安装,只能从其 Chrome Web Store 下载安装扩展程序. ...

  7. 引入外部 CDN失效时--怎么加载本地资源文件(本文以jquery为例)

    相信大家都使用过CDN静态资源库,比如下面 CDN官方静态资源库:https://cdnjs.com/ 七牛前端公开库:http://staticfile.org   (vue,react,nl都有) ...

  8. Android开发:Android Studio开发环境配置

    一.android studio下载: 1.Windows版: 下载地址:https://pan.baidu.com/s/1-sg4dN_2B5nn2YJf-C7XLQ 提取码:yedc 2.Mac版 ...

  9. JavaScript第一回-来龙去脉

    简写:JavaScript-JS   ECMAScript-ES 写在前面的话:啃文字大多时间不是件愉快的事情,但是我们必须过这个坎,让自己习惯,让自己不讨厌,至于喜欢不喜欢,我们等时间给出答案. J ...

  10. pip安装python库时使用国内镜像资源加速下载过程

    pip默认安装包是从网站https://pypi.org/simple下载,我们可以将其改成国内的镜像网站,加速下载过程,下面以安装numpy库为例: pip install -i https://p ...