开篇
结尾先说说为什么会写这么一篇 webgl 入门的文章,因为最近的工作投入在三维互动相干的开发,基于 webgl 引擎库作一些业务层的封装和调用,再输入 API 给前端应用,算是开始接触 webgl 这个畛域。一开始间接看 shader 的书和引擎库的代码有些不知所云,起初发现是对 webgl 不足一个整体的理解,对其中的一些概念半知不解。于是看了 three.js 的文档教程以及 webgl 编程指南等基础教程,有了全盘的认知之后,再回过头区看代码显著成果更好了。因而,将这个过程中本人认为一些必备的重要入门常识整顿了进去,既是一次温故知新,也心愿能对很多筹备学习 webgl 的同学带来些许帮忙。
webgl 是什么
WebGL(全写 Web Graphics Library)是一种 3D 绘图协定,这种绘图技术标准容许把 JavaScript 和 OpenGL ES 联合在一起,通过减少 OpenGL ES 的一个 JavaScript 绑定(OpenGL 是渲染 2D、3D 矢量图形的一种跨语言、跨平台的应用程序编程接口),WebGL 能够为 HTML5 Canvas 提供硬件 3D 减速渲染,这样 Web 开发人员就能够借助零碎显卡来在浏览器里更流畅地展现 3D 场景和模型了。
webgl 根底
从一个根本场景开始
如上图是一个最简略的 3D 场景,由一些必不可少的元素形成。首先会有一个坐标系的概念(webgl 默认是右上坐标系,y 朝上),作为场景内元素地位的参照。其次是场景中必须有一个摄像机,摄像机就是观察者,用来管制观察者站在场景中的什么地位以什么样的方向去察看场景中实体。实体就是场景中绘制进去的元素,如上图中的立方体,实体有本人的地位、大小、色彩等根本属性。光照是场景中的光源,光照也有色彩和地位等根本属性,影响场景中实体的色彩和亮度。以上这些元素独特形成了一个 3D 场景,有了这个基本概念后,接下来别离对场景中的这些元素进行介绍。
坐标系和转换
坐标系是用来标识实体地位的参照物,三维坐标系由 x、y、z 轴组成。至于 x、y、z 的方向在业内常见的有左手坐标系和右手坐标系两种不同的方向指向,而 webgl 采纳的是右手坐标系。
右手坐标系
顾名思义,右手坐标系能够用右手的手指朝向对照 x、y、z 轴。如下图所示,右手掌心朝向本人,食指朝上作为 y 轴正方向,大拇指和中指天然伸开展的朝向就是 x 轴正方向和 z 轴正方向,三个手指朝向的反方向即为各坐标轴的反方向。
右手坐标系不仅定义了坐标的方向,同时也定义了旋转的方向。如果要实体要绕 x、y、z 的某个轴旋转,只须要用大拇指朝向对应坐标轴的正方向,四个手指指向方向即为旋转的正方向。因而在 webgl 中旋转的正方形是逆时针方向。
有了右手坐标系之后,实体的坐标还跟 webgl 中其余几个坐标系无关,上面再具体来说明一下:
部分坐标系
部分坐标系指的是物体最后开始的坐标系,不同的物体一开始可能不在同一个部分坐标系之下,所以两者的地位是没有关联的,为了可能建设起关联。须要把两者放到一个对立的坐标系上面,这个对立的坐标系就是世界坐标系。
世界坐标系
世界坐标系指的是物体与 WebGL 相机建立联系时的坐标系,是一个 webgl 世界中所有实体搁置的对立坐标系。有了这个坐标系之后,实体之间的坐标的比拟才有意义。然而实体放入世界坐标系空间之后,尽管与 Web 相机建设了分割,然而并没有进一步确定察看物体的状态,摄像机从不同的地位和角度观察实体,看到的成果是不同的,这时候就引出了视图坐标系的概念。
视图坐标系
首先用一个简略的例子来阐明一些视图坐标系的概念,咱们从侧面察看一个物体和从侧面察看一个物体看到的物体的状态是不一样的,因为观察者的地位和角度不同。如果放弃观察者的地位和角度不变,即从侧面看过来,想要达到从侧面察看物体的成果,这时候只能调整物体的地位和姿势来实现。调整后的实体的坐标就是其在视图坐标系下的坐标。所以视图坐标系形容的是模仿相机地位姿势调整下的物体地位。
裁剪坐标系
裁剪坐标系是对视图坐标系的补充和束缚,事实中人的眼睛能看到的区域并不是向周围有限延长的。所以 webgl 中为了模仿人眼的视觉效果,摄像机的投影引入了可视空间的概念,只有在可视空间范畴内的实体能力被绘制进去,可视空间范畴外的会被去掉,这就是裁剪坐标系的由来。
屏幕坐标系
最初还有一个屏幕坐标系的概念,在裁剪坐标系下投影立体上的实体要最终展现在屏幕上被咱们看到同样有一个坐标转换的过程,转换后显示在屏幕视口上的坐标就是屏幕坐标系中的坐标,这一步 WebGL 会帮咱们主动实现。
坐标系的转换
一个物体最终展现在屏幕中的坐标就是通过了上述在各个坐标系之间的转换失去的,坐标系之间转换的过程就是通过矩阵变动来实现(为什么通过矩阵变动能实现前面独自在矩阵中阐明),具体的转换流程如下:
摄像机
摄像机是 3D 场景中观察者的角色,咱们在屏幕上看到的画面实际上是 3D 空间内的物体映射到摄像机内的画面,摄像机具备地位和姿势两个根本属性,地位和姿势影响着看到的场景中的画面。此外还有可视空间和投影两个重要属性:
投影
投影指的是摄像机照耀场景后在屏幕上的成像,在 webgl 中有两种投影模式,别离是正射投影和透视投影,两种投影下成像的特点有所不同。
正射投影
正射投影示意摄像机的投影是平行发射的,因而场景内的实体不管远近最终映射到屏幕上的大小都是雷同的,因而在正射投影的下,场景内实体的大小跟远近无关。正射投影的这一特点罕用来绘制大场景如地图、城市模型等。
透视投影
透视投影示意摄像机的投影是从一个点以射线模式在周围发射的,因而场景内间隔相机近的实体最终投射到屏幕上会大一些,而间隔相机远的实体最终投射到屏幕上会小一些。这跟事实中人眼看到物体近大远小的特点是统一的,因而透视投影下绘制的场景更贴近事实世界的特点。
可视空间
可视空间在后面讲裁剪坐标系的时候有提到,可视空间示意的是场景中可能映射到摄像机内的范畴。顺着摄像机投影的方向,会有近裁剪面和远裁剪面两个重要的垂直立体,这两个屏幕和摄像机的眼帘造成的平面空间就是可视空间。依据投影的不同,正射投影下的可视空间是一个长方体:
而透视投影下的可视空间则是一个四棱锥:
实体
绘制过程
一个实体在场景中被绘制进去须要通过一系列的处理过程,咱们就以场景中的一个立方体为例,介绍一个物体在 3D 场景中被绘制进去的过程:
- 首先会把立方体拆成 6 个面,先获取立体上预设的的顶点坐标和色彩,存入顶点缓冲区,webgl 的顶点着色器(着色器是在 GPU 上运行的程序)会从顶点缓冲区中读取顶点数据,通过逐顶点的解决,作为下一步图形拆卸的输出。
- 而后进行图形拆卸,webgl 中的图形最小单元是三角形,任何简单的图形都是由一个个的小三角形拼接而成的,图形拆卸会以肯定规定连贯各个顶点,造成一个个的三角形的图元。
- 紧接着进行图形光栅化的过程,光栅化就是把图元转化为片元(canvas 画布上图像的每一个像素都对应一个片元,所以片元能够简略了解为像素,但不是像素)。光栅化后造成的一个个片元数据,作为片元着色器的输出。
- 片元着色器会接管每个顶点的色彩数据,逐片元计算出每一个片元的色彩,并存入色彩缓冲区中。
- 色彩缓冲区中的片元,接下来会通过深度测试(解决两个坐标雷同的片元的前后展现)、交融(解决透明度)等解决造成最终的片元数据。
- 最初浏览器会读取色彩缓冲区中的片元数据渲染到屏幕上。
纹理
当实体是外表平滑的简略的物体时,用 webgl 的着色器内插能够绘制出须要的外表成果。然而如果是外表简单的物体,用这种形式就很繁琐了。因而 webgl 提供了纹理来解决这个问题,纹理就是将一张图片贴到几何图形的外表下来,这样图形外表看上去就是这张图片的成果,这张贴图就被成为纹理。
光照
事实中,咱们看到物体的色彩实际上是物体反射光线的色彩,依据光源和光线方向的不同,物体不同外表的明暗水平不统一。三维场景中的实体模拟了事实中的特点,因而物体外表的明暗水平由物体自身的材质(决定对光照的漫反射率)和光照(光照的色彩和方向)两者独特决定。
光照类型
场景中的光照能够分为两类,一类是主光源,是让场景中实体可见的次要光照,大体分为点光源和平行光两类。点光源是从一个点向周围发散出的光源,比方事实中的点灯,照射到物体外表不同地位的入射角度是不同的。而平行光是从很远的光源发射进去的与昂,因而光线能够看作是互相平行的,比方太阳光,照射到物体外表不同地位的入射角度是雷同的。
另一类是环境光,环境光指的是经主光源收回后被墙壁等其余物体屡次反射后照到指标实体上的光,是用来模仿真实世界的辅助光源。为什么要有环境光呢,因为如果只有一个主光源,那么场景中和光照方向平行的立体是齐全不能反射光照的,因而体现是乌黑的,这显然于理论不同,所以哪怕是与主光源齐全平行的立体也会有暗色,而不是全黑。环境光的存在就是为了补充这一成果,使三维场景更加贴近现实情况。
光照漫反射
光源照射到实体外表的最终被反射的成果与入射角度和实体的立体法向量无关。光照漫反射就是在三维场景中用来模仿因物体外表的毛糙水平不同,反射光的方向也不同的现实模型。
抛开实体自身材质因素的影响,能够认为实体漫反射的色彩是由入射光色彩、实体外表基底色和入射角决定的,能够由公式示意为:
漫反射光色彩 = 入射光色彩 实体外表基底色 cosθ
当入射角为 0 度,即垂直实体外表入射时,cosθ=1,反射光色彩不会受到减弱;当入射角为 90 度,即平行实体外表入射时,cosθ=0,反射光度色彩为 0,体现为彩色。这与理论状况是统一的。
动画
场景中的简略动画(如旋转)实现的原理就是每隔肯定工夫批改实体的坐标并从新在场景中绘制,webgl 中利用了浏览器提供的 requestAnimationFrame 这个 api 来实现,和 setInterval 相比的劣势在于:
- requestAnimationFrame 采纳零碎工夫距离,间隔时间绝对准确,不会引起丢帧和卡断,绘制的工夫紧跟浏览器的刷新频率, 个别是大概每 16.7ms(浏览器的刷新频率 fps 个别在每秒 60 次左右)执行一次,因而绘制效率更佳。
- requestAnimationFrame 是浏览器原生的 api,因而如果以后页面没有激活时,渲染办法会主动进行执行。而 setInterval 的执行与浏览器是否激活无关,是始终运行的。(举个例子:如果关上 10 个雷同的浏览器页面,requestAnimationFrame 只会在以后正在浏览的页面中执行,而 setInterval 在十个页面中都在执行)因而应用 requestAnimationFrame 的性能更佳。
requestAnimationFrame() 通知浏览器——你心愿执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该办法须要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
/**
* 应用形式的伪代码
*/
let then = 0; // 上一帧
let now = 0; // 以后帧
const render = function(timestamp) {
now = timestamp;
const deltaTime = now - then; // 与上一帧的工夫差值
then = now;
currentAngle = animate(currentAngle,datalTime); // 更新旋转角度
draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix); // 从新绘制实体
requestAnimationFrame(render); // 在浏览器下一帧反复执行 render 函数
};
render();
requestAnimationFrame 应用中有 2 个须要留神的点:
- requestAnimationFrame 要放在要执行的回调函数中能力达到反复调用的目标
- requestAnimationFrame 会给回调函数传入一个参数(timestamp),示意每次调用之间的工夫距离,通过这个距离算出与上一帧的工夫差值,进一步就能够调整绘制的逻辑保障渲染的平滑。(如:每次旋转的角度依据 datalTime 进行修改,这样能够保障旋转的速度是不变的,否则在高帧率浏览器中旋转会越来越快)
空间几何根底
向量
向量,指具备大小和方向的量。它能够形象化地示意为带箭头的线段。箭头代表向量的方向,线段长度代表向量的大小。上面介绍一下在 webgl 中最罕用到的点乘和叉乘以及利用场景
点乘
几何意义
向量 a(x1,y1,z1),向量 b(x2,y2,z2),则向量 a 点乘向量 b 为:a·b = |a||b|·cosθ
咱们思考当向量 a 和 b 都是单位向量的时候,a·b = cosθ
而 cosθ 又能够示意为单位向量 a 在单位向量 b 方向上的投影:cosθ = 投影长度 / 1 = 投影长度
即 a.b = a 向量在 b 向量方向上的投影长度,θ 值越小,a 向量就越贴合 b 向量。所以点乘在 webgl 中的意义是判断两个向量的靠近水平。
利用场景
如下图所示:已知一条行进路线,路线由一个个点连接起来,要在这条行进路线上的两侧选取对应的点,这时候就能够利用向量点乘,判断后退路线方向的向量和与与两侧点形成的向量之间的点乘大小进行筛选。
叉乘
几何意义
叉乘,是一种在向量空间中向量的二元运算,它的运算后果是一个向量,并且这个向量与原来两个向量所在的立体垂直。因为叉乘后失去的是一个与原来两向量垂直的向量,方向遵循右手法令,所以叉乘在 webgl 中的几何意义就是求所在立体的法向量。
利用场景
最常见的利用就是求已知立体的法向量,用一个具体的利用例子来阐明一下:
为了求 p3 点的坐标,能够先求出向量 p2p1 和向量 vertVec3 所在立体的法向量 croVec3,再通过向量 croVec3 和向量 p2p1 之间的运算关系得出最终 p3 点的坐标。伪代码如下所示:
import {vec3} from 'gl-matrix';
private p1: vec3 = vec3.create(); // 已知坐标点_p1
private p2: vec3 = vec3.create(); // 已知坐标点_p2
private p3: vec3 = vec3.create(); // 待求坐标点_p3
const normVec3 = vec3.create();
vec3.subtract(normVec3, this.p1, this.p2); // 失去向量 p2p1
vec3.normalize(normVec3, normVec3) // 归一化
const vertVec3 = [0, 1, 0] as vec3; // 垂直向量 p2p1 所在立体的向量
const croVec3 = vec3.create();
vec3.cross(croVec3, vertVec3, normVec3); // 叉乘失去垂直向量 方向遵循右手法令
vec3.normalize(croVec3, croVec3); // 归一化
vec3.scale(normVec3, normVec3, 0.5); // 缩放成 0.5 单位长度的向量(0.5m)vec3.scale(croVec3, croVec3, 1) // 缩放成单位长度的向量(1m)const tempVec3 = vec3.create();
vec3.add(tempVec3, normVec3, croVec3); // 相加失去向量 p2p3
vec3.add(this.p3, tempVec3, this.p2); // 换算失去 p3 点坐标
矩阵
矩阵是一个依照长方阵列排列的复数或实数汇合,由 m × n 个数 aij 排成的 m 行 n 列的数表称为 m 行 n 列的矩阵,简称 m × n 矩阵。记作:
矩阵在 webgl 中有十分宽泛的使用,因为坐标的变动都能够通过变幻矩阵来示意,在后面介绍坐标系转换的时候,各个视图之间的转换都是通过模型矩阵来实现的。实体从 A 地位变幻到 B 地位,是通过一系列的旋转平移失去的,坐标的变幻都能够用矩阵的乘法示意为:
等式左侧的这个矩阵就是变幻矩阵,通过推导(具体的推导过程能够自行查阅)能够得出平移矩阵为:
旋转矩阵为:
缩放矩阵为:
插值
插值是离散函数迫近的重要办法,利用它可通过函数在无限个点处的取值情况,估算出函数在其余点处的近似值。插值在图像渲染中也被用来填充图像变换时像素之间的空隙。
插值的求法能够有多种插值函数来求,比方最邻近元法、线性插值法等,这些具体等算法在工程中不强制要把握,会有罕用的数学库封装好插值函数。在 webgl 中遇到求间断变动过程中的实时值时,插值就有了用武之地了,上面通过一个具体的例子来具体阐明下插值的利用场景:
利用场景
假如一个君子以匀速从 A 点走到 B 点,已知 A 点和 B 点的坐标,要计算行走过程中的实时坐标。
要求 A 到 B 过程中到实时坐标变动,只须要对终点 A 和起点 B 的坐标以总工夫为插值系数进行插值即可得出君子每一帧的坐标,具体的伪代码如下所示:(其中插值的两行要害代码在 update 函数中)
import {vec3} from 'gl-matrix';
private p_start: vec3 | undefined; // 终点坐标
private p_end: vec3 | undefined; // 起点坐标
private p_move: vec3 = vec3.create(); // 挪动过程中的实时坐标
private speed: number // 挪动的速度
private time: number // 挪动须要的工夫
private currentTime = 0 // 以后工夫
private ratio = 0 // 挪动的间隔和总间隔的比例
// 只会在进入时执行一次的钩子函数
onEnter() {const distance = vec3.distance(this.p_start, this.p_end);
this.time = distance / this.speed;
const dir = vec3.create();
vec3.subtract(dir, this.p_end, this.p_end);
dir[1] = 0; // 高空行走,解决 y 坐标为 0
vec3.normalize(dir, dir); // 归一化
this.srcInfo.playAnimation('walk'); // 行走动画
this.faceToDir(dir, [0, 1, 0]); // 自定义函数,朝向行走方向
}
// 在浏览器每一帧都会执行的钩子函数
update(deltaTime: number) {if (!this.p_start || !this.p_end) {return;}
this.currentTime += deltaTime;
this.ratio = this.currentTime / this.time;
if (this.currentTime >= this.time) {this.ratio = 1;}
vec3.lerp(this.p_move, this.p_start, this.p_end, this.ratio); // 插值求以后挪动的坐标
this.setPosition(this.p_move); // 自定义函数,扭转以后人物坐标
if (this.ratio === 1) {this.reset();
// 达到起点,能够做自定义的业务逻辑
......
}
}
// 只会在退出时执行一次的钩子函数
onExit () {this.reset();
}
reset(){
this.currentTime = 0;
this.ratio = 0;
}
序幕
通过上面对 webgl 中的一些基础知识和相干的空间几何中的罕用基础知识的简略介绍,对 webgl 的入门有了初步的窥探。前面有工夫会逐渐对 webgl 中的一些外围模块作进一步的介绍和剖析,在 webgl 方向上的不断深入。