1. 概述

在上一篇教程《WebGL简易教程(七):绘制一个矩形体》中,通过一个绘制矩形包围盒的实例,进一步理解了模型视图投影变换。其实,三维场景的UI交互工作正是基于模型视图投影变换的基础之上的。这里就通过之前的知识实现一个三维场景的浏览实例:通过鼠标实现场景的旋转和缩放。

2. 实例

改进上一篇教程的JS代码,得到新的代码如下:

// 顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' + // attribute variable
'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' + // Set the vertex coordinates of the point
' v_Color = a_Color;\n' +
'}\n'; // 片元着色器程序
var FSHADER_SOURCE =
'precision mediump float;\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n'; //定义一个矩形体:混合构造函数原型模式
function Cuboid(minX, maxX, minY, maxY, minZ, maxZ) {
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
this.minZ = minZ;
this.maxZ = maxZ;
} Cuboid.prototype = {
constructor: Cuboid,
CenterX: function () {
return (this.minX + this.maxX) / 2.0;
},
CenterY: function () {
return (this.minY + this.maxY) / 2.0;
},
CenterZ: function () {
return (this.minZ + this.maxZ) / 2.0;
},
LengthX: function () {
return (this.maxX - this.minX);
},
LengthY: function () {
return (this.maxY - this.minY);
}
} var currentAngle = [0.0, 0.0]; // 绕X轴Y轴的旋转角度 ([x-axis, y-axis])
var curScale = 1.0; //当前的缩放比例 function main() {
// 获取 <canvas> 元素
var canvas = document.getElementById('webgl'); // 获取WebGL渲染上下文
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
} // 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
} // 设置顶点位置
var cuboid = new Cuboid(399589.072, 400469.072, 3995118.062, 3997558.062, 732, 1268);
var n = initVertexBuffers(gl, cuboid);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
} //注册鼠标事件
initEventHandlers(canvas); // 指定清空<canvas>的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 开启深度测试
gl.enable(gl.DEPTH_TEST); //绘制函数
var tick = function () {
//设置MVP矩阵
setMVPMatrix(gl, canvas, cuboid); //清空颜色和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //绘制矩形体
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0); //请求浏览器调用tick
requestAnimationFrame(tick);
}; //开始绘制
tick(); // 绘制矩形体
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
} //注册鼠标事件
function initEventHandlers(canvas) {
var dragging = false; // Dragging or not
var lastX = -1, lastY = -1; // Last position of the mouse //鼠标按下
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;
}; //鼠标释放
canvas.onmouseup = function (ev) {
dragging = false;
}; //鼠标移动
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);
currentAngle[0] = currentAngle[0] + dy;
currentAngle[1] = currentAngle[1] + dx;
}
lastX = x, lastY = y;
}; //鼠标缩放
canvas.onmousewheel = function (event) {
if (event.wheelDelta > 0) {
curScale = curScale * 1.1;
} else {
curScale = curScale * 0.9;
}
};
} //设置MVP矩阵
function setMVPMatrix(gl, canvas, cuboid) {
// 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;
} //模型矩阵
var modelMatrix = new Matrix4();
modelMatrix.scale(curScale, curScale, curScale);
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(-cuboid.CenterX(), -cuboid.CenterY(), -cuboid.CenterZ()); //投影矩阵
var fovy = 60;
var near = 1;
var projMatrix = new Matrix4();
projMatrix.setPerspective(fovy, canvas.width / canvas.height, 1, 10000); //计算lookAt()函数初始视点的高度
var angle = fovy / 2 * Math.PI / 180.0;
var eyeHight = (cuboid.LengthY() * 1.2) / 2.0 / angle; //视图矩阵
var viewMatrix = new Matrix4(); // View matrix
viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0); //MVP矩阵
var mvpMatrix = new Matrix4();
mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix); //将MVP矩阵传输到着色器的uniform变量u_MvpMatrix
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
} //
function initVertexBuffers(gl, cuboid) {
// Create a cube
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
// 顶点坐标和颜色
var verticesColors = new Float32Array([
cuboid.maxX, cuboid.maxY, cuboid.maxZ, 1.0, 1.0, 1.0, // v0 White
cuboid.minX, cuboid.maxY, cuboid.maxZ, 1.0, 0.0, 1.0, // v1 Magenta
cuboid.minX, cuboid.minY, cuboid.maxZ, 1.0, 0.0, 0.0, // v2 Red
cuboid.maxX, cuboid.minY, cuboid.maxZ, 1.0, 1.0, 0.0, // v3 Yellow
cuboid.maxX, cuboid.minY, cuboid.minZ, 0.0, 1.0, 0.0, // v4 Green
cuboid.maxX, cuboid.maxY, cuboid.minZ, 0.0, 1.0, 1.0, // v5 Cyan
cuboid.minX, cuboid.maxY, cuboid.minZ, 0.0, 0.0, 1.0, // v6 Blue
cuboid.minX, cuboid.minY, cuboid.minZ, 1.0, 0.0, 1.0 // v7 Black
]); //顶点索引
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // 前
0, 3, 4, 0, 4, 5, // 右
0, 5, 6, 0, 6, 1, // 上
1, 6, 7, 1, 7, 2, // 左
7, 4, 3, 7, 3, 2, // 下
4, 7, 6, 4, 6, 5 // 后
]); //
var FSIZE = verticesColors.BYTES_PER_ELEMENT; //数组中每个元素的字节数 // 创建缓冲区对象
var vertexColorBuffer = gl.createBuffer();
var indexBuffer = gl.createBuffer();
if (!vertexColorBuffer || !indexBuffer) {
console.log('Failed to create the buffer object');
return -1;
} // 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
// 向缓冲区对象写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW); //获取着色器中attribute变量a_Position的地址
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;
}
// 将缓冲区对象分配给a_Position变量
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0); // 连接a_Position变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position); //获取着色器中attribute变量a_Color的地址
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;
}
// 将缓冲区对象分配给a_Color变量
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
// 连接a_Color变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Color); // 将顶点索引写入到缓冲区对象
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); return indices.length;
}

与之前的代码相比,这里主要改进了两个方面的内容:重绘刷新和鼠标事件调整参数。

2.1. 重绘刷新

与之前只绘制一次场景不同,为了满足浏览交互工作,页面就必须实时刷新,来满足不同的鼠标、键盘事件对场景的影响。可以使用JS的requestAnimationFrame()函数进行定时重绘刷新操作。其函数定义如下:

在代码中的实现如下:

//绘制函数
var tick = function () {
//设置MVP矩阵
setMVPMatrix(gl, canvas, cuboid); //清空颜色和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //绘制矩形体
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0); //请求浏览器调用tick
requestAnimationFrame(tick);
}; //开始绘制
tick();

在这段代码中,定义了一个绘制函数tick(),而在该函数的结束处,调用了requestAnimationFrame()函数来向浏览器请求调用其回调函数,也就是tick()。以此循环往复,页面会不停的请求调用绘制tick(),从而带到了重绘刷新的效果。

前面提到过,重绘刷新每一帧之前,都要清空颜色缓冲区和深度缓冲区,不让上一帧的效果影响到下一帧。同理,MVP矩阵也是每绘制一帧之前就需要重新设置的。

2.2. 鼠标事件调整参数

在设置MVP矩阵函数setMVPMatrix()中,可以发现视图矩阵和投影矩阵都是初次计算好就固定的,只有模型矩阵随着变量currentAngle和curScale变化而变化,相关代码如下:

var currentAngle = [0.0, 0.0]; // 绕X轴Y轴的旋转角度 ([x-axis, y-axis])
var curScale = 1.0; //当前的缩放比例 //设置MVP矩阵
function setMVPMatrix(gl, canvas, cuboid) {
//... //模型矩阵
var modelMatrix = new Matrix4();
modelMatrix.scale(curScale, curScale, curScale);
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(-cuboid.CenterX(), -cuboid.CenterY(), -cuboid.CenterZ()); //...
}

currentAngle和curScale是预先定义的全局变量,它们在函数initEventHandlers中被设置。在initEventHandlers函数中,注册了画布元素canvas的鼠标事件。当鼠标在画布视图中拖动的时候,currentAngle根据鼠标在X、Y方向上位移变化而变化:

//鼠标按下
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.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);
currentAngle[0] = currentAngle[0] + dy;
currentAngle[1] = currentAngle[1] + dx;
}
lastX = x, lastY = y;
};

当鼠标在画布上滑动滚轮的时候,curScale根据滚动的幅度变化而变化:

//鼠标缩放
canvas.onmousewheel = function (event) {
if (event.wheelDelta > 0) {
curScale = curScale * 1.1;
} else {
curScale = curScale * 0.9;
}
};

currentAngle和curScale的变化使得模型矩阵发生改变,而每绘制一帧就会重新设置MVP矩阵,这就使得三维场景随着鼠标操作而变化,从而完成交互操作。

3. 结果

在浏览器中打开对应的HTML文件,运行结果如下:

4. 参考

本来部分代码和插图来自《WebGL编程指南》,源代码链接:地址 。会在此共享目录中持续更新后续的内容。

WebGL简易教程(八):三维场景交互的更多相关文章

  1. WebGL简易教程(九):综合实例:地形的绘制

    目录 1. 概述 2. 实例 2.1. TerrainViewer.html 2.2. TerrainViewer.js 3. 结果 4. 参考 1. 概述 在上一篇教程<WebGL简易教程(八 ...

  2. WebGL简易教程——目录

    目录 1. 绪论 2. 目录 3. 资源 1. 绪论 最近研究WebGL,看了<WebGL编程指南>这本书,结合自己的专业知识写的一系列教程.之前在看OpenGL/WebGL的时候总是感觉 ...

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

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

  4. WebGL简易教程(七):绘制一个矩形体

    目录 1. 概述 2. 示例 2.1. 顶点索引绘制 2.2. MVP矩阵设置 2.2.1. 模型矩阵 2.2.2. 投影矩阵 2.2.3. 视图矩阵 2.2.4. MVP矩阵 3. 结果 4. 参考 ...

  5. WebGL简易教程(十三):帧缓存对象(离屏渲染)

    目录 1. 概述 2. 示例 2.1. 着色器部分 2.2. 初始化/准备工作 2.2.1. 着色器切换 2.2.2. 帧缓冲区 2.3. 绘制函数 2.3.1. 初始化顶点数组 2.3.2. 传递非 ...

  6. WebGL简易教程(十五):加载gltf模型

    目录 1. 概述 2. 实例 2.1. 数据 2.2. 程序 2.2.1. 文件读取 2.2.2. glTF格式解析 2.2.3. 初始化顶点缓冲区 2.2.4. 其他 3. 结果 4. 参考 5. ...

  7. WebGL简易教程(二):向着色器传输数据

    目录 1. 概述 2. 示例:绘制一个点(改进版) 1) attribute变量 2) uniform变量 3) varying变量 3. 结果 4. 参考 1. 概述 在上一篇教程<WebGL ...

  8. WebGL简易教程(四):颜色

    目录 1. 概述 2. 示例:绘制三角形 1) 数据的组织 2) varying变量 3. 结果 4. 理解 1) 图形装配和光栅化 2) 内插过程 5. 参考 1. 概述 在上一篇教程<Web ...

  9. WebGL简易教程(十):光照

    目录 1. 概述 2. 原理 2.1. 光源类型 2.2. 反射类型 2.2.1. 环境反射(enviroment/ambient reflection) 2.2.2. 漫反射(diffuse ref ...

随机推荐

  1. Badboy录制模式

    参考: http://leafwf.blog.51cto.com/872759/1109940 http://www.51testing.com/html/00/130600-1367743.html ...

  2. POJ-3169 Layout (差分约束+SPFA)

    POJ-3169 Layout:http://poj.org/problem?id=3169 参考:https://blog.csdn.net/islittlehappy/article/detail ...

  3. 牛客小白月赛 G 异或 找规律

    链接:https://www.nowcoder.com/acm/contest/135/G来源:牛客网 题目描述 从前,Apojacsleam家的水族箱里,养了一群热带鱼. 在这几条热带鱼里,Apoj ...

  4. zoj 3261 Connections in Galaxy War(并查集逆向加边)

    题目链接:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=3261 题意:有很多颗星球,各自有武力值,星球间有一些联系通道,现 ...

  5. Disruptor框架中生产者、消费者的各种复杂依赖场景下的使用总结

    版权声明:原创作品,谢绝转载!否则将追究法律责任. Disruptor是一个优秀的并发框架,可以实现单个或多个生产者生产消息,单个或多个消费者消息,且消费者之间可以存在消费消息的依赖关系.网上其他博客 ...

  6. SpringBoot集成Zipkin实现分布式全链路监控

    目录 Zipkin 简介 Springboot 集成 Zipkin 安装启动 zipkin 版本说明 项目结构 工程端口分配 引入 Maven 依赖 配置文件.收集器的设置 编写 Controller ...

  7. tarjan缩点(洛谷P387)

    此题解部分借鉴于九野的博客 题目分析 给定一个 \(n\) 个点 \(m\) 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大.你只需要求出这个权值和. 允许多次经过一条边或者一个 ...

  8. React Hooks 你不来了解下?

    前言 最近在看 React 的新语法-- React Hooks,只能一句话概括:React 语法真的是越来越强大,越写代码越少. 强烈推荐还没看 React Hooks 的同学去学习下,这会让你写r ...

  9. spring aop 之链式调用

    关关雎鸠,在河之洲.窈窕淑女,君子好逑. 概述 AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横 ...

  10. Java 教程(开发环境配置+基础语法)

    Java 开发环境配置 在本章节中我们将为大家介绍如何搭建Java开发环境. window系统安装java 下载JDK 首先我们需要下载java开发工具包JDK,下载地址:http://www.ora ...