可视化学习:如何用WebGL绘制3D物体
在之前的文章中,我们使用WebGL绘制了很多二维的图形和图像,在学习2D绘图的时候,我们提过很多次关于GPU的高效渲染,但是2D图形的绘制只展示了WebGL部分的能力,WebGL更强大的地方在于,它可以绘制各种3D图形,而3D图形能够极大地增强可视化的表现能力。
相信很多小伙伴都对此有所耳闻,也有不少人学习WebGL,就是冲着它的3D绘图能力。
接下来,我就用一个简单的正立方体的例子来演示在WebGL中如何绘制3D物体。
从二维到三维
首先,我们先来绘制一个熟悉的2D图形,正方形。
// vertex
attribute vec2 a_vertexPosition;
attribute vec4 color;
varying vec4 vColor;
void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = vec4(a_vertexPosition, 1, 1);
}
// fragment
#ifdef GL_ES
precision highp float;
#endif
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}
// ...
renderer.setMeshData([{
positions: [
[-0.5, -0.5],
[-0.5, 0.5],
[0.5, 0.5],
[0.5, -0.5]
],
attributes: {
color: [
[1, 0, 0, 1],
[1, 0, 0, 1],
[1, 0, 0, 1],
[1, 0, 0, 1],
]
},
cells: [[0, 1, 2], [2, 0, 3]]
}]);
// ...
上述这些代码比较简单,我就不过多解释了。
在画布上我们看到,绘制了一个红色的正方形,它是一个平面图形。

接下来,我们就在这个图形的基础上,将它拓展为3D的正立方体。
要想把2维图形拓展为3维几何体,第一步就是要把顶点扩展到3维。也就是把vec2扩展为vec3。
// vertex
attribute vec3 a_vertexPosition;
attribute vec4 color;
varying vec4 vColor;
void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = vec4(a_vertexPosition, 1);
}
当然仅仅修改Shader是不够的,因为数据是从JavaScript传递过来的,所以我们需要在JavaScript中计算立方体的顶点数据,然后再传递给Shader。
一个立方体有8个顶点,能组成6个面。在WebGL中需要用12个三角形来绘制它。

如果6个面的属性相同的话,我们可以复用8个顶点来绘制;
但如果属性不完全相同,比如每个面要绘制成不同的颜色,或者添加不同的纹理图片,就得把每个面的顶点分开。这样的话,就需要24个顶点来分别处理6个面。
为了方便使用,我们可以定义一个JavaScript函数,用来生成立方体6个面的24个顶点,以及12个三角形的索引,并且定义每个面的颜色。
/**
* 生成立方体6个面的24个顶点,12个三角形的索引,定义每个面的颜色信息
* @param size
* @param colors
* @returns {{cells: *[], color: *[], positions: *[]}}
*/
export function cube(size = 1.0, colors = [[1, 0, 0, 1]]) {
const h = 0.5 * size;
const vertices = [
[-h, -h, -h],
[-h, h, -h],
[h, h, -h],
[h, -h, -h],
[-h, -h, h],
[-h, h, h],
[h, h, h],
[h, -h, h]
];
const positions = [];
const color = [];
const cells = [];
let colorIdx = 0;
let cellsIdx = 0;
const colorLen = colors.length;
function quad(a, b, c, d) {
[a, b, c, d].forEach(item => {
positions.push(vertices[item]);
color.push(colors[colorIdx % colorLen]);
});
cells.push(
[0, 1, 2].map(i => i + cellsIdx),
[0, 2, 3].map(i => i + cellsIdx)
);
colorIdx ++;
cellsIdx += 4;
}
quad(1, 0, 3, 2); // 内
quad(4, 5, 6, 7); // 外
quad(2, 3, 7, 6); // 右
quad(5, 4, 0, 1); // 左
quad(3, 0, 4, 7); // 下
quad(6, 5, 1, 2); // 上
return {positions, color, cells};
}
现在我们就可以通过调用cube这个函数,构建出立方体的顶点信息。
const geometry = cube(1.0, [
[1, 0, 0, 1], // 红
[0, 0.5, 0, 1], // 绿
[0, 0, 1, 1] // 蓝
]);
通过这段代码,我们就能创建出一个棱长为1的立方体,并且六个面的颜色分别是“红、绿、蓝、红、绿、蓝”。
接下来我们就要把这个立方体的顶点信息传递给Shader。
在传递数据之前,我们需要先了解一个知识点,是关于绘制3D图形与2D图形存在的一点不同,那就是绘制3D图形时,必须要开启深度检测和启用深度缓冲区。
在WebGL中,我们可以通过gl.enable(gl.DEPTH_TEST);这段代码来开启深度检测;在清空画布的时候,也要用gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);这段代码来同时清空颜色缓冲区和深度缓冲区。
启动和清空深度检测和深度缓冲区这两个步骤,非常重要。但是一般情况下,我们几乎不会用原生的方式来编写代码,所以了解一下即可。为了方便使用,在本文演示的例子中,我们还是直接使用gl-renderer这个库,它封装了深度检测,我们在使用时,在创建renderer的时候配置一个参数depth: true就可以了。
现在我们就把这个三维立方体用gl-renderer渲染出来。
// ...
renderer = new GlRenderer(glRef.value, {
depth: true // 开启深度检测
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);
renderer.setMeshData([{
positions: geometry.positions,
attributes: {
color: geometry.color
},
cells: geometry.cells
}]);
renderer.render();
现在我们在画布上看到的是一个红色正方形,这是因为其他面被遮挡住了。

投影矩阵:变换WebGL坐标系
但是,等等,为什么我们看到的是红色的一面呢?按照我们所编写的代码,预期看到的应该是绿色的一面,也就是说我们预期Z轴是向外的,因为规范的直角坐标系是右手坐标系。所以按照现在的绘制结果,我们发现WebGL的坐标系其实是左手系的?
但一般来说,不管什么图形库或者图形框架,在绘图的时候,都会默认将坐标系从左手系转换为右手系,因为这更符合我们的使用习惯。所以这里,我们也去把WebGL的坐标系从左手系转换为右手系,简单来说,就是将Z轴坐标方向反转。关于坐标转换,可以通过齐次矩阵来完成。对坐标转换不熟悉的小伙伴,可以参考我之前的一篇关于仿射变换的文章。
将Z轴坐标方向反转,对应的齐次矩阵是这样的:
[
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1
]
这种转换坐标的齐次矩阵,也被称为投影矩阵,ProjectionMatrix。
现在我们修改一下顶点着色器,把这个投影矩阵添加进去。
// vertex
attribute vec3 a_vertexPosition; // 1:把顶点从vec2扩展到vec3
attribute vec4 color; // 四维向量
varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩阵-变换坐标系
void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = projectionMatrix * vec4(a_vertexPosition, 1.0);
}
现在我们就能看到画布上显示的是绿色的正方形了。

模型矩阵:让立方体旋转起来
现在我们只能看到立方体的一个面,因为Z轴是垂直于屏幕的,这样子从视觉上看好像和2维图形没什么区别,没法让人很直观地联想、感受到这是一个三维的几何体,为了将其他的面露出来,我们可以去旋转立方体。
要想旋转立方体,我们同样可以通过矩阵运算来实现。这个矩阵叫做模型矩阵,ModelMatrix,它定义了被绘制的物体变换。
把模型矩阵加入到顶点着色器中,将它与投影矩阵相乘,再乘上齐次坐标,就得到最终的顶点坐标了。
attribute vec3 a_vertexPosition; // 1:把顶点从vec2扩展到vec3
attribute vec4 color; // 四维向量
varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩阵-变换坐标系
uniform mat4 modelMatrix; // 3:模型矩阵-使几何体旋转
void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = projectionMatrix * modelMatrix * vec4(a_vertexPosition, 1.0);
}
现在我们定义一个JavaScript函数,用立方体沿x、y、z轴的旋转来生成模型矩阵。
以x、y、z三个方向的旋转得到三个齐次矩阵,然后将它们相乘,就能得到最终的模型矩阵。
import { multiply } from 'ogl/src/math/functions/Mat4Func.js';
// ...
export function fromRotation(rotationX, rotationY, rotationZ) {
let c = Math.cos(rotationX);
let s = Math.sin(rotationX);
const rx = [
1, 0, 0, 0, // 绕X轴旋转
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
c = Math.cos(rotationY);
s = Math.sin(rotationY);
const ry = [
c, 0, s, 0,
0, 1, 0, 0, // 绕Y轴旋转
-s, 0, c, 0,
0, 0, 0, 1
];
c = Math.cos(rotationZ);
s = Math.sin(rotationZ);
const rz = [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0, // 绕Z轴旋转
0, 0, 0, 1
];
const ret = [];
multiply(ret, rx, ry);
multiply(ret, ret, rz);
return ret;
}
我们把模型矩阵传给顶点着色器,不断更新三个旋转角度,就能实现立方体旋转的效果。
// ...
let rotationX = 0;
let rotationY = 0;
let rotationZ = 0;
function update() {
rotationX += 0.003;
rotationY += 0.005;
rotationZ += 0.007;
renderer.uniforms.modelMatrix = fromRotation(rotationX, rotationY, rotationZ);
requestAnimationFrame(update);
}
update();
// ...
现在我们就能在旋转中看到立方体的其他几个面了,能更直观地感受到这是一个三维物体。

总结
至此,我们就实现了正立方体的绘制。在3D物体的绘制中,正立方体属于是比较简单的一类,屏幕前的小伙伴们都可以来动手尝试下,感兴趣的小伙伴,还可以尝试去实现圆柱体、正四面体等等这些几何体的绘制。
可视化学习:如何用WebGL绘制3D物体的更多相关文章
- 使用DX绘制3D物体时新手常犯错误,看不见物体时可以一一排查
1.镜头不对: 物体不在镜头范围内,检查视图矩阵,世界矩阵,投影矩阵. 2.颜色全黑: 打开光照情况下,MATERIAL全为0, 或,在没有打开光照情况下,颜色值为0,造成全黑.检查当前Materia ...
- 如何用webgl(three.js)搭建一个3D库房,3D仓库,3D码头,3D集装箱可视化孪生系统——第十五课
序 又是快两个月没写随笔了,长时间不总结项目,不锻炼文笔,一开篇,多少都会有些生疏,不知道如何开篇,如何写下去.有点江郎才尽,黔驴技穷的感觉. 写随笔,通常三步走,第一步,搭建框架,先把你要写的内容框 ...
- 如何用webgl(three.js)搭建一个3D库房,3D仓库3D码头,3D集装箱,车辆定位,叉车定位可视化孪生系统——第十五课
序 又是快两个月没写随笔了,长时间不总结项目,不锻炼文笔,一开篇,多少都会有些生疏,不知道如何开篇,如何写下去.有点江郎才尽,黔驴技穷的感觉. 写随笔,通常三步走,第一步,搭建框架,先把你要写的内容框 ...
- 如何用webgl(three.js)搭建处理3D园区、3D楼层、3D机房管线问题(机房升级版)-第九课(一)
写在前面的话: 说点啥好呢?就讲讲前两天的小故事吧,让我确实好好反省了一下. 前两天跟朋友一次技术对话,对方问了一下Geometry与BufferGeometry的具体不同,我一下子脑袋短路,没点到重 ...
- 如何用webgl(three.js)搭建一个3D库房,3D密集架,3D档案室(升级版)
很长一段时间没有写3D库房,3D密集架相关的效果文章了,刚好最近有相关项目落地,索性总结一下 与之前我写的3D库房密集架文章<如何用webgl(three.js)搭建一个3D库房,3D密集架,3 ...
- 如何用webgl(three.js)搭建处理3D隧道、3D桥梁、3D物联网设备、3D高速公路、三维隧道桥梁设备监控-第十一课
开篇废话: 跟之前的文章一样,开篇之前,总要写几句废话,大抵也是没啥人看仔细文字,索性我也想到啥就聊啥吧. 这次聊聊疫情,这次全国多地的疫情挺严重的,本人身处深圳,深圳这几日报导都是几十几十的新增病例 ...
- 如何用three.js(webgl)搭建3D粮仓、3D仓库、3D物联网设备监控-第十二课
序: 最近因为疫情,居家办公,索性翻翻之前的项目案例,总结总结. 这次疫情,深圳停摆,群众也挺恐慌的,封闭前一天,超市被抢购一空,虽然官方媒体一再强调,材米油盐蔬菜肉类管够,但是任然挡不住群众们的抢购 ...
- 如何用webgl(three.js)搭建一个3D库房-第二课
闲话少叙,我们接着第一课继续讲(http://www.cnblogs.com/yeyunfei/p/7899613.html),很久没有做技术分享了.很多人问第二课有没有,我也是抽空写一下第二课. 第 ...
- 如何用webgl(three.js)搭建一个3D库房,3D密集架,3D档案室,-第二课
闲话少叙,我们接着第一课继续讲(http://www.cnblogs.com/yeyunfei/p/7899613.html),很久没有做技术分享了.很多人问第二课有没有,我也是抽空写一下第二课. 第 ...
- 绘制3D的js库
有哪些值得推荐的绘制3D的js库? 4 个回答 默认排序 草皮子 HTML5/GAME 4 人赞同了该回答 只用过three.js,所以推荐这个.不清楚你打算用来做什么,总的来说,得看你的运 ...
随机推荐
- WPF 稳定的全屏化窗口方法
本文来告诉大家在 WPF 中,设置窗口全屏化的一个稳定的设置方法.在设置窗口全屏的时候,经常遇到的问题就是应用程序虽然设置最大化加无边框,但是此方式经常会有任务栏冒出来,或者说窗口没有贴屏幕的边.本文 ...
- 支持4k的远程桌面软件有哪些
在当今的数字时代,在全球向灵活工作安排和分布式团队转变的推动下,对远程工作解决方案的需求猛增.远程桌面软件已成为寻求让员工能够在任何地方工作的企业的重要工具.在用户在此类软件中寻求的众多功能中,对 4 ...
- C语言:删除顺序表中重复的信息—(删除顺序表中重复的单词)
如何删除顺序表中的重复单词: (开始看内容之前容朕说一句:如果你最后怎么都运行不了你想要的结果,①我敢保证大概率是你的下标越界你的下标越界了你的下标越界了.②在我这程序里面你肯定打少了p--,少了p- ...
- 如何使用 JS 判断用户是否处于活跃状态
有时候,我们需要在网页判断用户是否处与非活跃状态,如果用户长时间没有在页面上进行任何操作,我们则判定该用户是非活跃的. 在 javascript 中我们可以通过监听某些鼠标或键盘相关的事件来判定用户是 ...
- openstack以后需要研究一下的知识
1. openvt是一个用于在虚拟终端上启动程序的命令行工具.它允许用户在一个新的虚拟终端(VT)上启动一个程序,并将标准输入.输出和错误输出定向到该终端. openvt的用法如下: 打开一个虚拟终端 ...
- spring boot整合maybatis plus 的 文件生成代码
/** * 代码生成 */public class AutoGenerator_ { public static void main(String[] args) { AutoGenerator ge ...
- Android 13 - Media框架(9)- NuPlayer::Decoder
关注公众号免费阅读全文,进入音视频开发技术分享群! 这一节我们将了解 NuPlayer::Decoder,学习如何将 MediaCodec wrap 成一个强大的 Decoder.这一节会提前讲到 M ...
- cmd shutdown命令:关机,重启,休眠
一段时间后关机:shutdown -s -t 秒数效果是倒计时到该秒数后关机,例如shutdown -s -t 3600就是3600秒后关机,也就是一小时后关机 立即关机命令:shutdown -p关 ...
- .NET桌面程序混合开发之四:键盘事件的响应
1. 问题 在生产环境中,有一些场景需要窗体来响应键盘事件(注意,是窗体响应,而不是窗体上的控件响应),如解析扫码枪的扫描结果.但在嵌入WebView2的Form程序,Host Form无法对键盘事件 ...
- .NetCore 3.1 教程之 EFCore连接Mysql DBFirst模式 从数据库生成实体
一:创建EF的类库,同时将此项目设置为启动项(为Scaffold-DbContext -tables指令使用),同时安装2个包 ①Microsoft.EntityFrameworkCore.Too ...