有一次,设计师打电话给前端,并要求在雾玻璃后面做一个“蜘蛛网”。 但是后来发现,这不是一个“蜘蛛网”,而是一个六角形的网格,不在玻璃后面,但它伸向了远方,前端用户对WebGL并不熟悉,因此我不得不学习绘画过程中的所有动画。 那个前端是
Yuri Artyukh (
akella )。

尤里(Yuri)从事排版很久了,在星期天,记录流与对真实项目的分析。 他不是WebGL的专家,没有在WebGL上制作地图,没有在Web汇编器中编写程序,但是他喜欢学习新知识。 在
FrontendConf RIT ++上,Yuri讲了如何从布局到交付给客户的过程中进行一种动画制作,以便每个人都很高兴并在此过程中学习WebGL。 这个故事来自第一人称视角,包括:Three.js,GLSL,Canvas 2D,图形和一些数学运算。
雾玻璃后面的蜘蛛网
我不知何故坐在一个重要的项目上工作。 然后工作室的设计师打电话给他们,他们特别喜欢特殊效果,并问:“你能像在雾玻璃后面一样制作一个蜘蛛网吗?”
当然,这立即描述了整个问题。 后来发现,雾玻璃后面的“蜘蛛网”就是这个。
这是一个六边形网格,但出于某种原因,对于设计者来说,这是一个“蜘蛛网”。 雾状的玻璃-该栅格进入远处。 沟通困难。 想象一下要成为一个内向的人并制作动画有多困难? 但是我就是这样,这就是我正在做的。
这个“网络”看起来不像是一个动画,之后您可以撰写成功案例的报告,开设一家初创公司,获得十亿美元的投资,拥有一堆粉丝并向太空发射火箭。 这是怎么回事? 白色灰色背景上的棕色线,好像是由鼠标绘制的。 后来发现她应该沿着边缘走,但后来更多。 通常,代号为“雾玻璃后面的网”。
在带有该动画的网站上,“蜘蛛网”还有多个其他选择:顶部为灰色背景,底部为白色。 必须使其具有交互性,以使其响应用户鼠标的移动。
设计师首先问我的是它有多困难以及要花多少钱。 我想到了一些想法:如何绘制一条线和这样的网格,如何使其不减速,应如何工作。 我以前从未遇到过。 但是,作为一个从事开发工作的人,他回答:“
那很容易,我们会做的……”我喜欢参与奇怪的冒险,因为当我这样做时,我通常会受苦。
苦难来自成长。 它不可避免地与苦难相关联-您无法对所有事情感到满意,快乐地生活并不能同时发展专业。
Three.js
我立即开始思考如何解决问题。 由于所有内容均为3D,因此我记得
Three.js 。 这是所有会议上讨论最多的图书馆。 与本地WebGL相比,该库使WebGL更简单,更方便,更有趣。
Three.js有许多现成的对象。
PlaneGeometry是第一个对我来说似乎很完美的对象。 这是原始平面。 该库具有各种六边形,十二面体,二十面体,圆柱体,但是有许多三角形组成的简单平面。
有很多三角形,因为我需要详细的波浪-表面一定要担心。如果您查看Three.js内,则实际上该平面是一个简单的JS对象,其中包含所有点的坐标。
就我而言,我有一个50×50的正方形平面,因此我需要2601个顶点。 为什么50×50 = 2601? 这是学校数学。 坐标z = 0,因为平面y = 1,因为这是50个顶点的第一行,并且x发生变化。
但是为什么我需要飞机,我需要以某种方式弯曲飞机? 数组可以做的第一件事是对数组执行数学运算。 例如,遍历
for each
循环并将z坐标分配给x坐标的正弦值。
事实证明,它类似于正弦波,因为每个顶点的高度值将等于该值沿x轴的正弦值。 为了使这种情况复杂化(现在将是一个困难的数学时刻,请准备好)-我将为正弦添加时间,并且画布会移动,仅仅是因为数学就是这样工作的。 如果将时间添加到x坐标,则图形将水平移动。 如果要协调y-将垂直移动。 我对水平运动很感兴趣-我想让我的海洋担心。
设计师没有想到我会有从左向右蠕动的正弦曲线。 他希望它美丽如蜘蛛网,海洋或脑海中的一切。 因此,带有正弦曲线的选项不适合。 这需要某种随机性。 该平面不应作为正弦波预测。 但是,如果您对这些峰中的每个峰进行随机调用,那么我们将获得非常不可预测的结果。
随机性的本质是随机性和独立性。 每个随机顶点都不以任何方式依赖于其邻居。 随机地,不传递任何参数,它不在乎邻居。
结果是一条折断的曲线,仅在远处类似于海洋或网络。 更适合作为有关“黑客”和网络盗窃的电影的插图。
如果从屏幕上每个点的角度看随机,它看起来就像白噪声-许多小的白点和黑点。 每个点都是黑白的-0或1。
但是,我需要创建一波波的海洋应该看起来像这样。
它类似于雾,云和山脉。有随机函数返回此类图片。 它们被称为噪声或
噪声 :单形噪声,珀林噪声。 Perlin以噪声的名义是梯度噪声算法创建者的名字,该算法返回一个漂亮的随机数。 他通过处理电影《特隆》第一部分的特殊效果来创建它。 这种数学算法以前就存在过,但现在在电影和游戏中得到了积极的应用。
当在“魔法门之英雄无敌III”(针对30岁以上的英雄)或策略中生成随机地图时,通常会看到类似的内容。 返回这些噪声的函数总是相同的。
整个运动都是“生成艺术”。 参与者使用噪音功能生成艺术品,风景。 例如,在下面的图片中,其中一位艺术家的伪自然风景。 目前尚不清楚它是数学还是山脉的地形。 生成艺术的任务恰恰是从数学上生成与当前没有区别的景观。
事实证明,与随机性不同,此函数采用参数,因为这些点必须取决于最接近它们的邻居。 如果使用噪声函数而不是通常的随机性,则会得到类似的结果。
黑白就是高度:0是黑谷,1是白峰。 它变成波浪形的表面。该功能适用于所有PL,因为它只是一个算法-正弦,余弦,乘法。
通过遍历我的PlaneGeometry对象的所有顶点,并将每个值分配给噪波函数,可以以相同的方式进行变形:
geometry.vertices.forEach(v => { vz = noise(vx, vy, time); });
该函数仅占用30-40行,但数学上很复杂。
存在所有维度的噪声函数:一维,二维,三维。 在我的情况下,这是三维噪声,因为传递了三个参数。 除了x和y平面的空间坐标外,我还传递时间-表面会不断扭曲,改变其位置。
Three.js! == GPU
当我启动算法时,波浪开始移动。 当我为Web做某事时,我总是查看分析器,现在我也查看了它,这就是海浪在那儿的样子。
屏幕上是浏览器绘制的一帧。 框架由垂直的灰色虚线表示。 在帧内部,噪声函数的执行占用了时间的2/3。 在Web上制作动画时,可以使用请求动画帧,该帧最多每16毫秒运行一次。 每16 ms的帧计数2600个顶点的噪波功能。 对于每个顶点,都要考虑上下运动和高度。 在每个后续帧上,将重新计算值,因为曲面必须及时存在。
原来,噪音功能已翻了2600次,已经占用了我计算机上帧的2/3。 这还不是整个框架。 开发动画时,这已经是一个危险信号。
动画不应占据帧的一半以上。
如果更多,那么在任何交互,任何按钮,任何鼠标悬停期间,丢失帧的风险很高。
因此,这是一个严峻的危险信号。 我意识到Three.js不一定是WebGL。 尽管事实上我似乎使用Three.js,但我以3D绘制了所有内容,并以WebGL进行了渲染,但WebGL并没有获得出色的性能。 我只有2600个顶点-对于WebGL来说这还不够。 例如,在每个地图上都有成千上万个对象,每个对象都包含数十个三角形。 估计规模:成千上万是正常的,但只有2600个峰。
顶点着色器
框架出现问题后,我发现有着色器。 它们只有两种:
我对顶点着色器-Vertex Shader感兴趣。 如果我们在上面重写动画,则它看起来像这样:
position.z = noise( vec3(position.x, position.y, time) );
Position.z
每个点具有其自己的数据类型的分量z坐标。
vec3
表示将有三个参数。
着色器中没有循环。
在此之前,在脚本中,我
for each
循环放置了
for each
,对于每个顶点,计算都在一个循环中进行。 着色器和非着色器之间的区别是没有循环。
着色器-这是循环。
一次对所有顶点并行执行。 这是它的主要含义,任务,芯片和任务。
与主处理器CPU不同,视频卡上的GPU具有更大的内核。 在处理器上,它们的数量要少得多,但是它能够更快地执行通用计算。 视频卡上提供了非常简单的计算,但是有许多核,因此您可以并行执行许多计算。 这就是着色器中通常发生的情况。 顶点着色器的含义是,将针对视频卡上的着色器中的2600个顶点并行计算噪声。
如果您查看探查器,动画的外观不会改变,但是看起来会像这样。
CPU完全不执行任何操作 。 当然,下面添加了GPU上的另一个线程。 GPU,CPU,Web工作者上也有线程,但是这些计算将已经在视频卡上的单独线程中进行。
当然,这不是免费的。 工作中的视频卡比主处理器加热得更多。 因此,当您访问具有相似动画的网站时,粉丝经常会为您服务。 这是因为与其他时间不同,打开视频卡时需要冷却。 在移动设备上,这比在台式计算机上更重要-手机的运行速度更快。 但是与此同时,您会获得相当不错的性能提升。
结果是这样的表面-这是通常的佩林噪声。 如果启动它并仅更改时间,则会获得凉爽的波浪。但这还不是全部。 我仍然需要“蜘蛛网”-表面上的六角形网格。 拥有布局经验,最简单,最明显的方法是选择可以重复的片段。 有趣的是,对于六角形网格,它不是正方形,而是矩形。 如果将图案重复为矩形,则会得到一个网格。 Three.js库允许您覆盖png,而不是在此之前学习所有WebGL。 我切出png并将其放在表面上,结果像这样。
乍一看,您需要什么! 但仅在开始时。 这不适合我,因为加密货币网站需要动画-一切都应该“昂贵,丰富”。
当使用png纹理并且它们靠近相机时,您可以看到最近的元素的边缘模糊。 没有感觉图片清晰。 png似乎已经在浏览器中扩展了。 问题在于,在WebGL中无法完全使用矢量纹理来使用矢量纹理。 所以我哭了,然后在互联网上阅读
GLSL解决了这个问题。
GLSL是一种类似于C的语言,其中编写了着色器。 每个人都害怕使用它,因为这些是着色器,WebGL-尚不清楚! 但是我发现可以在上面制作清晰的图像,然后转向第二种着色器。
片段着色器
此着色器与顶点着色器具有相同的功能。 但是,如果顶点构造了折线表面,并为每个顶点执行计算,则“片段着色器”将为该表面的每个像素计算颜色。
最基本的
fragment shader – step(a,b)
功能
fragment shader – step(a,b)
。 它仅返回0和1:
我在JS中做了一个伪实现,以明确说明此函数有多简单。
function step(a, b) { if (a < b) return 0 else return 1 }
在WebGL中工作时,通常在任何对象上都有一个坐标系。 如果它是一个正方形对象,则坐标系是原始的:点(0,0),(0,1),(1,0),(1,1)。
为每个像素执行一个片段着色器。 如果“顶点着色器”每帧2600次,则“片段着色器”将执行与像素数一样多的次数。 如果表面为1000×1000像素,则每帧可能进行一百万次。 这听起来很可怕,但这仅仅是因为在我们这个时代很少有人熟悉视频卡的资源。
如果将step(a,b)函数与这些像素的坐标一起使用,则可以使用参数0.4执行step函数,并将每个像素的x坐标传递到每个点。
事实证明,小于0.4的所有内容均为0,大于1的所有内容。在WebGL中,数字和颜色相同且相同。 每种颜色是一个数字。 白色-1,黑色-0。在RGB中有三个,但仍为0.0.0和1.1.1。

如果我们更复杂地执行此功能步骤,则左侧将显示白色。 此功能将在屏幕上的每个点执行,并会认为它是0或1。这是正常的,不必担心。
如果将这两个表达式相乘,则会得到一条垂直的白色条纹。 如果在不同的轴上执行相同的操作,则可以绘制一个白色正方形:
应该是高潮了-我们画了一个白色的正方形!使用一个功能的组合,您就可以绘制任何想要的东西。
如果您还记得的话,图案的元素是成角度的。 如果制作仅由黑白组成的倾斜表面,则该表面将是肋状的,而不是平滑的。 肋排吸引您的目光-难看。 为了使表面看起来更光滑,不仅需要黑白像素,还需要灰色半色调。
平稳步伐
着色器具有平滑步长功能。 它与step的作用相同,但是在0到1之间进行插值,以便有一个渐变。
从左到右,经过最大压缩后。如果您尽可能地压缩此功能,则会得到一条最小的渐变线。 这就是在Fragment着色器中以任意角度生成完美平滑线所需要的。
因此,我能够制作出具有平滑边缘的白色正方形。 如果有一个白色正方形,则可以制作3个白色正方形。
正方形可以旋转,使用正弦和余弦功能。然后,我不得不用纸和树叶打破我的模式。
生产截图。那里的所有事物都与23种不同的多重度有关,因此计算所有这些点的坐标并不是很困难。 然后您可以得到这样的模式。
我画了一个片段,重复了很多遍。 您可以清楚地看到调试模式,在该模式下重复该模式。 使用参数函数
step
和
smoothstep
执行所有片段。 这意味着通过制作一个用于倾斜平面的图案,您可以生成无限数量的这些图案。 如果在片段内部更改线宽或六边形的大小,则会得到许多其他图案。
我扭曲了参数,发现了无数个模式。 就像“生成艺术”一样-不清楚做过什么,但是很漂亮。
自卫队
然后我发现还有一个带
符号的距离场,可以生成带有距离图的图像 。 因为它是最佳的,所以SDF在卡片或计算机游戏中用于绘制文本和对象。 在WebGL中,很难以其他方式绘制文本,尤其是平滑且轮廓分明的文本。
这是一种数学格式,在WebGL之外很难使用。 这个想法很简单,但是优雅,效果很好。
如果我们想画一个清晰的星星,那么我们需要将图片保存在右侧-这是图片的生成图像。 现有的算法可以将任何清晰的图片变成模糊的图片。 之后,它可以用于生成清晰的版本,但与此同时,我们得到的不是一张图片,而是许多图片。 从相同大小的清晰图片中,您可以生成相同但更大的图片。 会有错误,但是这种方法在数学上很有趣。
例如,如果您拍摄尺寸为128×128 px的图片,则从较小尺寸的图片中可以获得清晰的图像,其大小是源图像的几倍。 这就是为什么他们使用SDF的原因之一-模糊字体的重量通常比优化矢量格式的重量小。
当然,有一个限制。 将字母增加到1000像素是不可能的,即使100像素也很难看。 但是这种大小的字体多久需要一次?
片段着色器,绘制矩形,展开-在这些扰动的帮助下,我终于设法找到了所需的表面。
新条件
她应有的状态:蠕动,所有要素都很清晰。 一切都是我想要的,但事实证明它还有更多:
-让他用鼠标移动,并铺设一条新路径。 并且蜂窝突出显示!假定当用户移动鼠标时,他使用该服务隐喻地沿着破碎的“蜘蛛网”铺砌了棘手的路径。
用语言描述任务并不难,但是如何实现呢? 我想到的第一件事-由于我有一个六角形的网格,可能已经研究过了。 然后我碰到了一篇有趣的文章“
六角网格参考和实施指南 ”。 在其中,作者收集了20年的资料。 他毫无疑问很酷,对于那些喜欢算法和数学的人来说,这篇文章是神圣的。 它包含许多有关六角形网格的有趣数据。
这篇文章很长,但是包含了有趣的数学方法:如何在六边形网格上构建坐标系,如何对这些六边形编号,然后在使用它们的地方引用它们。 事实证明,这总是在我眼前,因为在旧的计算机游戏中,到处都有六角形的网。
如果您已经调到六角形琴格了,那就看看城堡吧。 在其他纹理上,还会猜出六角形网格。
在“文明”中,一切通常都是显而易见的。有趣的是,如果您沿着三维立方体的对角线进行截面,该立方体由许多小立方体组成,则一方面是立方体,另一方面是正六边形。
三维立方体的横截面为二维六边形网格。 得知三维立方体以某种方式与二维六边形相连很有趣。
在包括的文章中,有一种算法可以找到沿六边形网格的路径。 我必须寻找一种通过鼠标调高高度的方法。
路径搜索算法既复杂又简单。 最原始的方法是在两点之间画一条线,看看该线落在哪些六角形上。 事实证明,过去单位从A点到B点。
我需要这样的东西。但这不是我所需要的。 在这里,路径沿着六边形区域放置,而我需要沿着边缘放置。 我不得不以不同的方式解决问题。
画布2d
也许有更好的方法,但是我的方法更有趣。 刚开始,我只是画
Canvas2D进行调试-步骤1。

在此之前,有WebGL,Three.js,着色器,而这仅仅是Canvas2D! 我在上面画了六角网格的所有点。 如果仔细观察,它们是相同的六边形。 然后,我想起了图形,这些图形存储了有关点如何相互连接的信息,并将每个点与三个相邻点相连,并得到了图的步骤编号2。为此,我使用了Open Source
Beautiful Graphs 。
图形仅是点和有关其连接方式的信息的集合。 他们进行了充分的研究,对于每种口味,都有许多不错的算法可以找到图中图形中从A点到B点的路径。 所有卡都使用这种算法。
看起来像这样。
graph = createGraph( ); graph.addNode(..);
我们构建一个图形,添加1000个点以及它们之间的所有连接,接下来我们传递每个点的ID-第一个连接到第三个,第三个连接到第五个。 无需发明任何东西,有一种具有现成功能的优化算法,可让您找到此路径。
该算法运行的时间少于一帧。 当然,它需要某种资源,但是您不需要每16毫秒执行一次,而仅在路径更改时执行。
因此,我能够在第3步中构建此路线。在Canvas2D中,它看起来像这样:从点A到点B的最短路径-一切,就像生活中一样。 乍看之下,这似乎不是最短的路径,但事实证明,沿着六角形网格从A点到B点有很多最短的路径。
在WebGL中,所有图片都是数字。 在那里,您可以在着色器中传输纹理,例如,我尝试传输png。 浏览器没有区别-通过png或Canvas2D传递。 对于Canvas2D浏览器,它与完成的图片位图相同。 因此,我首先以蛇的形式绘制了这张照片。 № 4.
Canvas2D . Canvas2D , — . , . , Canvas2D 3D, , .
, , WebGL — , «, », .
, , Canvas2D, , , . Canvas2D , WebGL, .
: , , ., --. , , «» .
?
: « ? ?» , : «, !». , . .
, WebGL. , . , WebGL. 2 — , .
, , . , .

, . , , , 3D — .
, . , , .
FrontendConf . , Canvas , RxJS JSX React .
30 . , , .