阅读 1340
带你一起走进前端图形学

带你一起走进前端图形学

前言

图形学这个领域目前来看是很好玩也很有前景的一个方向,当我们了解它的基础知识,get到它好玩地方的时候,我们可以很轻松延伸到可视化这一领域进行拓展。本文会尽量以很通俗很详细的方式来向大家介绍,希望读者有所收获。

先带大家看几张有趣的照片~

像这个由抛物线,阿基米德螺旋线以及星形线构成的图片,以及这颗随机树都是通过我们一些简单的数学知识来绘制成,下面我也会以详细的代码来实现这些图片。

目录

因为是系列篇之基础入门篇,我会先从图形基础和数学基础这两个部分开始讲起:

图形基础

在 Web 上,图形通常是通过浏览器绘制的。现代浏览器是一个复杂的系统,其中负责绘制图形的部分是渲染引擎。渲染引擎绘制图形的方式,一般大体上有 4 种。

1. 传统的html + css

​ 与传统的 Web 应用相比,可视化项目,尤其是 PC 端的可视化大屏展现,使用HTML 与 CSS 情景相对较少,于是可能有些人会误认为,可视化只能使用 SVG、Canvas 这些方式,不能使用 HTML 与 CSS。当然了,这个想法是不对。

​ 其实现代浏览器的HTML、CSS 表现能力很强大,其实一些简单的可视化图表,完全可以用 CSS 来实现,比如,我们常见的柱状图、饼图和折线图。能简化开发,又不需要引入额外的库,可以节省资源,提高网页打开的速度。

但是使用HTML + CSS也是有一定得弊端:

1)维护麻烦,在 CSS 代码里,我们很难看出数据与图形的对应关系,有很多换算也需要开发人员自己来做。这样一来,一旦图表或数据发生改动,就需要我们重新计算,所以维护起来会很麻烦。

  1. 性能开销是非常大,HTML 和 CSS 作为浏览器渲染引擎的一部分,为了完成页面渲染的工作,除了绘制图形外,还要做很多额外的工作。比如说,浏览器的渲染引擎在工作时,要先解析 HTML、SVG、CSS,构建 DOM 树、RenderObject 树和 RenderLayer 树,然后用 HTML(或 SVG)绘图。当图形发生变化时,我们很可能要重新执行全部的工作。

那有没有更好的实现方式,当我们在重绘图像时,不会发生重新解析文档和构建结构的过程,这个当然是有的,那后面也会介绍到。

2. SVG

SVG 它是浏览器支持的一种基于 XML 语法的图像格式,它的 XML 语言本身和 HTML 非常接近,都是由标签 + 属性构成的,而且浏览器的 CSS、JavaScript 都能够正常作用于 SVG 元素。

3. canvas2D

接下来到了图形基础的重点,canvas2D,后续的数学基础部分也是大多数以它为基础进行绘制。

这里说一下它的声明式绘图系统和指令式绘图系统区别:

1)声明式绘图系统:我们根据数据创建各种不同的图形元素(或者 CSS 规则),然后利用浏览器渲染引擎解析它们并渲染出来。

2)指令式绘图系统:它更多的是浏览器提供的一种可以直接用代码在一块平面的画布上绘制图形的api,使用它来绘图更像是传统的“编写代码”,简单来说就是调用绘图指令,然后引擎直接在页面上绘制图形。

总结来说,如下面图所示,像Canvas 能够直接操作绘图上下文,不需要经过 HTML、CSS 解析、构建渲染树、布局等一系列操作。因此单纯绘图的话,Canvas 比 HTML/CSS 和 SVG 要快得多、在重绘图像时,也不会发生重新解析文档和构建结构的过程,开销要小很多。

4. WebGL

这里webGL我们也不作为重点,这里我们简单说一下其使用场景:

​ 第一种情况,如果我们要绘制的图形数量非常多,比如有多达数万个几何图形需要绘制,而且它们的位置和方向都在不停地变化,如果使用 Canvas2D 绘制,性能是会达到瓶颈的。这个时候,我们就需要使用 GPU 能力,直接用 WebGL 来绘制。

​ 第二种情况,如果我们要对较大图像的细节做像素处理,比如,实现物体的光影、流体效果和一些复杂的像素滤镜。由于这些效果往往要精准地改变一个图像全局或局部区域的所有像素点,要计算的像素点数量非常的多(一般是数十万甚至上百万数量级的),我们也要用 WebGL 来绘制。

​ 第三种情况是绘制 3D 物体。因为 WebGL 内置了对 3D 物体的投影、深度检测等特性,所以用它来渲染 3D 物体就不需要我们自己对坐标做底层的处理了。那在这种情况下,WebGL 无论是在使用上还是性能上都有很大优势。

要使用 WebGL 绘图,我们必须要深入细节里,换句话说就是,我们必须要和内存、GPU 打交道,真正控制图形输出的每一个细节。

数据经过CPU(中央处理单元,负责逻辑计算)处理,成为具有特定结构的几何信息。然后,信息会被送到GPU(图形处理单元,负责图形计算)中进行处理。在GPU中要经过两个步骤生成光栅信息(构成图像的像素矩阵),这些光栅信息会输出到帧缓存(一块内存地址)中,最后渲染到屏幕上。

GPU 是由大量的小型处理单元构成的,它可能远远没有 CPU 那么强大,但胜在数量众多,可以保证每个单元处理一个简单的任务。即使我们要处理一张 800 * 600 大小的图片,GPU 也可以保证这 48 万个像素点分别对应一个小单元,这样我们就可以同时对每个像素点进行计算了。

这里注意一下,图里红框框住的Default levels。里面按照严重的级别排序分别为:Verbose(详细),Info(信息),Warnings(警告),Error(错误)。我们可以通过下拉框的选择搭配Filter的功能来对控制台打印的信息进行筛选。

数学基础

1.1 坐标系与向量之以canvas为例实现坐标系的转换

这里首先我要先从对坐标系进行转换进行讲起,那为什么我要先讲坐标系的转换问题:因为转换坐标系对于图形学绘制而言,实在太重要了,后续所有图形的绘制都要用到这个思想,具体为什么我们先从一个之前前面看到的图形讲起:

首先经过一顿坐标点换算,我们得出每个点具体的坐标(这里我用了一个Rough.js的库,绘制一个手绘风格的图像),最终算出山顶的坐标就是 (-80, 100) 和 (80, 100),山脚的坐标就是 (-180, 0)、(20, 0)、(-20, 0)、(180, 0),太阳的中心点的坐标就是 (0, 150)。

坐标系变化的方案如下:

1:首先,我们通过 translate 变换将 Canvas 画布的坐标原点,从左上角 (0, 0) 点移动至 (256, 256) 位置,即画布的底边上的中点位置。接着,以移动了原点后新的坐标为参照,通过 scale(1, -1) 将 y 轴向下的部分,即 y>0 的部分沿 x 轴翻转 180 度,这样坐标系就变成以画布底边中点为原点,x 轴向右,y 轴向上的坐标系了。

2:山顶的坐标就是 (-80, 100) 和 (80, 100),山脚的坐标就是 (-180, 0)、(20, 0)、(-20, 0)、(180, 0),太阳的中心点的坐标就是 (0, 150)。

3:其实这个思路是非常重要的,因为这个例子要绘制的图形很少,所以还不太能体现使用坐标系变换的好处。不过,可以想一下,在许多应用场景中,我们都要处理成百上千的图形。如果这个时候,我们在原始坐标下通过计算顶点来绘制图形,计算量会非常大,很麻烦。那采用坐标变换的方式就是一个很好的优化思路,它能够简化计算量,这不仅让代码更容易理解,也可以节省 CPU 运算的时间。

1.2 坐标系与向量描述点和线段(基础)

不管我们用什么绘图系统绘制图形,一般的几何图形都是由点、线段和面构成。其中,点和线段是基础的图元信息,因此,如何描述它们是绘图的关键。在讲解完转换坐标系后,那么接下来带大家真正开启数学知识中的向量基础

这里我先用一些代码来表示一些向量的基础知识:

接下来我们进行实战部分,用刚才介绍的向量知识来绘制一个随机的小树,这里的枝干方向是随机的。

1:第一步还是非常重要的一个坐标变换,这里,我们要做的变换是将坐标原点从左上角移动到左下角,并且让 y 轴翻转为向上。

2:我们定义一个画树枝的函数 drawBranch。

3:创建一个单位向量 (1, 0),它是一个朝向 x 轴,长度为 1 的向量。然后我们旋转 dir 弧度,再乘以树枝长度 length。这样,我们就能计算出树枝的终点坐标了。(这里我封装了一个class Vector2D里面定义了一一些方法,包括向量的旋转)

4:我们可以从一个起始角度开始递归地旋转树枝,每次将树枝分叉成左右两个分枝。这样,我们得到的就是一棵形状规律的树。

5:我们修改代码,加入随机因子,让迭代生成的新树枝有一个随机的偏转角度。这样,我们就可以得到一棵随机的树。

1.3 向量和参数方程描述曲线

接下来会从向量过渡到参数方程的阶段:

1:向量绘制折线的方法来绘制正多边形,当多边形的边数非常多的时候,这个图形就会接近圆,将多边形的边数设置得很大,我们就可以绘制出圆形。

2:由于很难精确到图形的位置和大小,并且换算的过程比较繁琐会很容易出错,但是为了画出更多样的以及更多的曲线样式,我们需要选择更好的模型,接下来自然会引出参数方程。

1.3.1 参数方程之基础图形

接下来还是先带大家熟悉一下参数方程的基础概念,首先以圆形与椭圆形进行举例,他们的参数方程比较相似,这里我把公式给大家展示出来。

在代码实现参数方程的过程中呢,这里我通过设置TAU_SEGMENTS的点的数量为60平均分摊到2π(360度)上,它们可以理解成这个圆或者这个椭圆由多少个坐标点来绘制而成,然后将一个个坐标点加入到数组中。然后将包含有60个点的数组坐标返回,传入我接下来封装的draw函数中。

这里再说一下抛物线的参数方程,当x0,y0为0时,经推导 t = x/ y,这里t的含义可以x除以y的值

接下来会通过一个小demo将我们的代码整合起来,这里的图片在分享的开始也和大家介绍过,通过抛物线,阿基米德螺旋线和星形线组成。

如果我们为每一种曲线都分别对应实现一个函数,就会非常笨拙和繁琐。那为了方便,这里我们采用函数式编程思想,封装一个更简单的javascript参数方程绘图模块,以此来绘制出不同的曲线。

那么封装的这个绘图模块的使用过程主要分为三步:

第一步,我们实现一个叫做 parametric 的高阶函数,它的参数分别是 x坐标的参数方程和y坐标的参数方程。

第二步,parametric 会返回一个函数,这个函数会接受几个参数,比如,start、end 这样表示参数方程中关键参数范围的参数,以及 seg 这样表示采样点个数的参数等等。在下面的代码中,当 seg 默认 100 时,就表示在 start、end 范围内采样 101(seg+1)个点,后续其他参数是作为常数传给参数方程的数据。

第三步,我们调用 parametric 返回的函数之后,它会返回一个对象。这个对象有两个属性:一个是 points,也就是它生成的顶点数据;另一个是 draw 方法,我们可以利用这个 draw 方法完成绘图

这里我们就不需要像之前一样,每一个图形线都实现其对应的一个函数,只需要统一调用我们封装好的高阶函数,传入我们的参数方程即可,最后统一调用draw函数进行绘制。

这样的话,通过这个封装好的javascript参数方程绘图模块,就可以绘制出很多有意思的图形。

1.4:仿射变换--拓展了解

接下来呢,仿射变换其实也是图形学需要了解的一个数学基础,但是由于时间关系,我会带大家简单了解一下其基本的概念。

这里先关注一下应用场景,痛点,解决方式和仿射变换。

前面我们学习了用向量表示的顶点,来描述曲线和多边形的方法。但是在实际绘制的时候,我们经常需要在画布上绘制许多轮廓相同的图形,难道这也需要我们重复地去计算每个图形的顶点吗?当然不需要。我们只需要创建一个基本的几何轮廓,然后通过仿射变换来改变几何图形的位置、形状、大小和角度。

仿射变换是拓扑学和图形学中一个非常重要的基础概念。利用它,我们才能在可视化应用中快速绘制出形态、位置、大小各异的众多几何图形。所以接下来我们就来说一说仿射变换的数学基础和基本操作,在以后的后续篇中在所有视觉呈现的案例中,是非常重要的。

仿射变换最基本的概念,大家可以记住就是 线性变换 + 平移 线性变换又分为(旋转和缩放) ,总结起来就是平移,旋转加缩放。

总结下来,合到一起就是左图的表达式,经过公示优化后为右面的矩阵形式,在改写的公式里,我们实际上是给线性空间增加了一个维度。换句话说,我们用高维度的线性变换表示了低维度的仿射变换!

以粒子动画应用场景举例,这个公式是非常重要的,它能在一定时间内生成许多随机运动的小图形,这类动画通常是通过给人以视觉上的震撼,来达到获取用户关注的效果。

结束语

在前端整体迅猛发展的大环境下,因为产品需求的驱动,图形学方向技术发展也非常迅速,前端图形学属于一个比较小众的领域,甚至延伸到可视化方向依然属于一比较小众的领域,那么真正阻挡的技术门槛是什么呢?

其实就是对使用者的数学基础比较高,但当我们真正突破了这个技术门槛,甚至可视化方向上突破了WebGL这种更偏底层的门槛,在行业里我们会是特别非常有竞争力的。

那么本次文章还是先带大家来认识一下图形学,后续系列篇中会再延伸到动画、滤镜等其他视觉基础上。这里分享给大家一句话,学习技术的过程,是从接纳和记忆知识开始的,但绝不仅仅是接纳和记忆知识,而是需要深入思考,并自己总结和沉淀的。

参考资料

www.cnblogs.com/miloyip/arc…

zhuanlan.zhihu.com/p/98007510

zhuanlan.zhihu.com/p/69069042

developer.mozilla.org/zh-CN/docs/…

time.geekbang.org/column/intr…

文章分类
前端
文章标签