Vortex背后的技术:基于浏览器的实时无缝纹理生成器

Vortex是一个基于Web的程序,用于为3D建模和游戏生成无缝平铺的纹理。 它完全基于浏览器,使用TypeScript编写,并使用WebGL实时交互生成纹理,这意味着实际图像由GPU生成。

界面如下所示:

想自己尝试吗? 只需转到https://vortex.run/

想要破解源代码吗? 查看https://github.com/viridia/vortex

保存后的Vortex文档可以通过URL共享。 目的是创建一个概念上与jsFiddle或CodePen非常相似的在线“ scratchpad”。

可以在GitHub页面上找到有关如何使用Vortex的说明,请参见上面的链接。

整体应用架构

大多数代码在客户端中,该客户端使用Preact(一种轻巧的Rea​​ct替代品)。 状态管理通过MobX处理。

还有一个服务器组件,用于将文档保存到持久性存储中。 请注意,您可以在没有服务器组件的情况下运行Vortex。 您将无法保存任何内容(尽管您正在处理的文档将保留在本地存储中)。

资料模型

主要数据模型是一个Graph ,由GraphNodeConnection对象组成。 每个GraphNode都有许多表示输入和输出的终端 。 所有这些都是MobX可观察的,因此视图组件可以在节点和连接更改时自动重新呈现它们。

在以前的项目中,我使用过Redux,但在这种情况下,我选择了MobX,因为它看起来很自然-我将不得不管理大量的互连对象,并且尝试使用不可变数据对此建模非常麻烦。 MobX的好处是,它对我的​​数据结构的设计几乎没有限制,并且所涉及的重复样板要比使用Redux少得多。

除了该图以外,还有代表各种节点类型的运算符 -噪声,混合,掩码等。 每种运算符类型都有一个Operator实例,并且多个图节点可以引用同一运算符。

图的大部分智能都在运算符中。 图形节点是相当“笨拙”的对象,除了其坐标,与其他节点的连接列表以及可编辑属性的映射外,几乎没有其他信息。 另一方面,运算符定义那些可编辑属性的数据类型和名称,描述节点可能具有的输入和输出,并包含用于实际渲染节点的逻辑(通过调用用OpenGL着色器语言编写的着色器程序) 。

用户界面

主界面由许多面板组成,这些面板是Preact组件。 我决定选择Preact,因为它简单且体积小。 由于该应用程序的UI相当适中,并且不需要任何复杂的第三方UI库,因此我决定不使用preact-compat包,而只使用“基本” Preact。

我应该提到Preact和TypeScript可以一起很好地工作。 “基本” Preact不包括对组件的属性验证,但是所有这些都由TypeScript在编译时进行处理,因此,运行时属性验证没有太大的价值。

除标准HTML5的按钮外,页面上的所有控件均为“自定义”控件。 这样,就不需要诸如React-bootstrap之类的小部件库。

工作区主面板是最复杂的视图组件,实际上包含几层:

  • 节点层是一个绝对定位的元素,其中包含节点的表示形式。
  • 连接层是SVG文档元素,用于绘制连接线的贝塞尔曲线。
  • 由微妙的CSS棋盘格图案组成的background元素,还用于捕获背景点击。

工作区面板支持HTML5拖放,用于添加新组件和拖动组件之间的连接。 (不幸的是, 编辑连接不使用拖放操作,而是使用普通的鼠标事件,其原因是SVG元素不可拖动。哦。)

(如果您想知道为什么不将整个内容制作为SVG,答案是我想利用HTML的动态布局算法和节点内容的文本呈现功能。)

物业小组

屏幕右侧的属性面板是根据当前选定的节点动态构建的。 它将内省该节点的各种属性并呈现适当的UI元素-标量属性的滑块,颜色属性的颜色选择器,等等。

滑块不是传统的GUI滑块,而是根据Blender3D中的滑块建模的-它们允许单击,拖动,单击两端的箭头,但是您也可以双击它们直接编辑数字值。

渲染图

每个运算符构造OpenGL着色器程序,以为其附着的每个节点渲染纹理。 通常,这种“构造”只是将包含OpenGL函数的预先编写的片段缝合在一起,但是,每个着色器的“ main”函数都是从表达式树生成的。 通过将图从当前节点移动到其所有传递输入,并使用此树生成函数调用的层次结构,将一个节点的返回结果作为参数传递给另一节点,来创建此树。 如果连接加入两个具有不同数据类型(例如float与RGBA)的节点,则根据需要将类型强制转换插入生成的代码中。

请注意,这意味着接受其他节点输入的节点将包含这些其他节点的源代码副本。 Vortex不会将每个节点渲染为一个纹理,然后将该纹理馈送到下一个节点。 相反,每个节点的着色器代码是完全独立的,并且包含其自身及其输入的所有代码。 这意味着您可以复制着色器代码并直接在OpenGL应用程序中使用它!

该规则的一个例外是“模糊”节点,它实际上确实将连接到其输入端子的节点的输出缓存为纹理。 原因是为了正确进行模糊处理,需要对一堆像素进行采样,然后计算加权平均值。 如果每个样本都需要重做该像素的整个计算,这将非常慢!

每个节点还维护一个“修改的”位,每当其控制参数之一更改时,该位就会置位。 这告诉渲染器需要再次运行节点的着色器(尽管已使用requestAnimationFrame对其进行了反跳,以便快速拖动滑块不会导致大量浪费的重绘)。 还有另一个信号告诉节点重建其着色器。 输入连接更改时,这是必需的。

ShaderAssembly类包含许多用于动态生成着色器代码的有用方法。 这包括导入通用代码函数(并确保即使多个节点需要它们也不会被导入两次),声明统一变量以及生成main()函数。

GLResources类用于跟踪已为节点分配的着色器程序和纹理,以便从图中删除该节点时可以释放这些资源。

最后, 渲染器包含一个屏幕外的WebGL画布。 我决定不使用多个WebGL绘图上下文(这是困难且得不到很好的支持),而是决定使用一个共享的WebGL上下文,然后将生成的渲染图像复制到单独的屏幕画布元素,每个RenderView组件一个。