https://isicad.ru/ru/articles.php?article_num=19029
这篇文章原文是俄文,这里感谢OpenAI的ChatGPT,翻译效果不错,经过微调,发出来大家看看。这篇文章尽管多年之前发表,但是基本的技术路线还是值得参考的。
作者照片

2013年1月,ASKON公司的C3D几何核心被家具CAD开发商Basis-Center公司授权使用。这一事件的后果之一是,2015年8月,Basis-Center公司的开发人员Roman Kolesnikov撰写了一篇名为“CAD中的核技术”的文章。自2005年以来,他一直从事三维建模和可视化工作。

在今天向您介绍的新文章中,Roman提出了一个项目,其目标从标题中可以清楚地看出。与2015年的发表相似,作者展示了自己具有很有价值的倾向和能力来证明自己的决策,这使得他的文章具有额外的有益概述和参考特点。

现在互联网上人们只在谈论云,它们是如何无限和美丽的……还有他们在云中看到的服务器……那么你呢?我决定和读者分享我开发在线三维室内设计服务的经验。在这里,我将尽力介绍整个项目的架构和实现细节。

什么是云端三维设计系统?由于最近“云计算”这个术语非常流行,被滥用到了很多地方,所以我将从定义开始。在我的理解和实现中,云端三维设计是一种软件架构,其中所有三维模型的数据和处理操作都存储在远程服务器(即云端),客户端设备通过互联网请求数据或计算结果。换句话说,这种系统与传统的设计系统不同之处在于,大部分计算操作在服务器上进行,而不是在客户端设备上,并且只传输少量数据以可视化模型和其参数。类似系统的架构被分为紧密互动但远程分布的服务器和客户端部分,需要特殊的方法来确保它们的无缝交互,对于用户来说是无感知的。

接下来的问题是:这种架构有哪些优势?毫无疑问,与用户、数据和处理操作都在一个地方的传统架构相比,云端架构更复杂。但是对于我的项目,云端架构在开发和使用方面都有一些不可否认的优势,使得这种架构复杂化是合理的。我会尝试概述它们:

  1. 客户端应用程序是Web浏览器。现在,这意味着应用程序的跨平台性和能够从任何设备使用服务。
  2. 无需安装即可快速启动应用程序,降低了未来用户的门槛。
  3. 无需保存文档并在设备之间移动,因为所有数据都可以同时从所有设备访问。
  4. 多个用户可以同时编辑或查看单个文档和整个项目,并进行方便的通信,从而在远程客户之间创建统一的工作空间。
  5. 组织连续的开发过程和即时交付更新,使客户可以使用最新版本的软件,并促进在开发过程中使用极限编程技术的积极使用。

当然,这种方法也有其缺点。其中我强调以下几点:

  1. 需要良好的互联网连接才能舒适地使用应用程序。
  2. 复杂的软件和因此增加的开发时间和成本。
  3. 需要部署和随后支持用于软件运行的网络基础设施。

项目架构

在选择架构时,我考虑了未来的可扩展性,并将项目分成了几个部分,以便能够轻松地并行处理最繁重的部分。在计算时,我使用了以下假设,这些假设基于CAD开发的经验并在原型创建时得到了进一步的澄清:

  1. 加载带有家具的平均公寓需要约25 MB未压缩的几何数据和其他属性(5 MB压缩)+ 10 MB纹理。生成数据的时间从0.2秒到5秒(在最复杂的情况下)。我计划将模型限制在3-5百万个三角形的水平上。
  2. 在用户进行平面设计和安装不同产品的过程中,每个操作(插入和编辑产品,重新生成平面)平均需要100-500 KB的传出流量。每个操作在服务器上的执行时间平均为0.1-0.5秒。
  3. 用户活动的水平在每分钟打开一个模型或每分钟执行5-10个编辑操作。

基于此,可以明显地看出,在单个服务器上维护100多个活跃用户将会成为一个问题,因此需要在不同的服务器上处理几何查询,并以某种方式在它们之间分配3D模型。

在选择开发工具方面,我最初受到了几个限制。首先,使用C3D作为几何核心和对模型进行高速几何计算的要求预先决定了在服务器端使用C ++。其次,在浏览器上启动客户端部分也将编程语言的选择缩小到支持编译为JavaScript的语言。

因此,在当前阶段,该项目由四个独立部分组成:两个内部(后端)服务和两个外部Web应用程序。主要的内部服务负责几何建模和计算。它由C ++编写,使用C3D和Qt Core库。辅助服务负责管理用户文件、目录和纹理处理。它由ASP.NET Core编写。Web应用程序也以类似的方式分开。一个直接负责建模,使用TypeScript + WebGL编写,而另一个则为用户管理项目和目录提供用户界面,使用Angular 2 + TypeScript编写。客户端和服务器端之间的交互使用简单的HTTP请求完成。在负责交互式建模的部分中,使用WebSocket连接传输压缩的二进制数据。为了避免在服务器服务之间重复代码,它们也通过HTTP协议交换必要的信息。

服务器端

“早上七点钟——云彩消散,天气变好……”(《那个著名的穆恩豪森》)

就像穆恩豪森一样,为了让云服务能够高效运行,需要尽可能地扩展云。因此,项目的主要亮点是服务器和客户端的组合,负责几何建模、可视化和模型编辑历史记录的保存。这部分项目对性能、内存消耗、并行化和可伸缩性有着高要求,因为几何建模本身就是一项相当耗费计算资源的任务,而对多个活跃用户的模型构建请求的执行更加复杂。作为系统的“心脏”,我们选择了C3D Labs公司的C3D核心来执行几何建模任务,其选择原因在我的上一篇文章“CAD中的核心技术”中已经描述。为了实现对复杂3D项目的管理功能,我们开发了自己的3D模型数据存储系统,其基础是游戏开发者广泛使用的分层ECS(实体组件系统)。它是由不同元素(实体)组成的树状结构模型,每个元素都有不同的数据集合(组件),例如几何参数、BREP壳、三角形网格、用户数据等。

为了使系统满足必要的要求,其实现具有一系列显著特点:

  1. 在加载模型时,只加载其结构,而所有数据(组件)存储在NoSQL数据库中,并在访问组件时自动加载到内存中,并在需要时自动从内存中卸载。这使得在服务器上可以处理成千上万个同时打开的模型,同时内存开销较小。
  2. 组件内部存储与其他实体中的组件的关联,以“实体ID-组件类型”对的形式。在复制模型元素操作中,所有元素都获得新的ID,其中包括旧ID和随机操作代码,使用对称哈希函数,因此在实体中,而不是替换组件内部所有更改的ID,记录将旧ID转换为新ID的代码。这使得可以通过简单快速的按字节复制来复制组件数据,而不会在模型结构中丢失引用完整性。因此,可以在不读取其组件结构的情况下复制大型模型,从而实现在设计房间内瞬间复制大型组装件。
  3. 由于上述机制,实现了特殊的事务组件,其中在各种命令编辑其内容时,自动保存模型结构变更历史记录。将实体分解为相对较小的组件使得可以在每个组件上放置“侦听器”,从而自动跟踪其变化。这使得可以存储模型的所有更改历史记录,并返回到其创建的任何时间点,甚至是几个月前(因为整个历史记录也存储在组件中,不需要加载到内存中)。从开发人员的角度来看,这意味着几何模型具有类似于现有数据库管理系统中的事务的类似物。
  4. 每个元素和组件模型的版本化实现了快速生成特殊补丁文件的功能,其中包含有关客户端模型需要进行哪些实体和组件的调整的信息,以将其与服务器上的版本同步。与使用WebSocket协议的二进制版本结合使用,这实现了在实时同步所有连接的客户端上同步模型数据的有效性。

在实践中,这种系统的工作速度相当快,可以在5-50毫秒内处理大多数对模型的请求。然而,该系统存在一个瓶颈:打开模型需要传输所有用于可视化的数据,对于大型模型,这需要多次访问数据库以提取组件数据,导致显著的延迟,达到数秒钟的模型数万个元素。缓解这个问题的方法是将补丁文件缓存到Redis中。由于补丁文件不会失去其实效性(从版本0到版本100的补丁与从版本100到版本200的补丁等效于从版本0到版本200的单个补丁),这很容易解决缓存失效的问题:可以在后台模式下更新它,而不必担心数据失效。

客户端部分

客户端部分的设计始于选择可视化引擎。几乎尝试了所有流行的WebGL引擎,但由于以下原因,没有选择任何一个引擎:

  1. 对CAD可视化模式的支持较弱,例如删除不可见线条和绘制曲面轮廓。
  2. 在3D模式下,没有针对高质量小尺寸文本输出和任意线条绘制的有效工具组合(这种组合用于在3D模型上绘制平面图)。
  3. 缺乏有效的批处理技术。模型通常由数以万计的小元素组成,具有不同的材料,用户可以随时更改任何对象,因此需要有效的技术来动态地将小对象粘合成大的顶点缓冲区,以达到可接受的性能水平。
  4. 需要编写特定的相机控制、材料叠加和动画组件,因为提供的“盒装”选项不适用于设计系统的需求。

通过分析,我们意识到编写自己的“自行车”将更快、更好、更容易维护。我们选择了出色的库https://twgljs.org 作为基础。

下一个问题是选择编程语言和平台。我已经有了开发约10,000行代码的JavaScript应用程序的经验。基于这个经验,JavaScript的开发想法让我感到惊恐。 TypeScript的最新版本以及Anders Hejlsberg的背景,决定了语言的选择。在Web上,选择的平台是Angular 2(现在是4):我已经需要从大量不同的库中构建项目,而构建自己的Web应用程序组合没有任何兴趣。我希望拥有“一切包括”的框架。先进的系统模块延迟加载功能、高效的代码生成(AOT)和国际化功能都加强了我的选择。唯一仍让我感到困惑的是,当前版本中源文件中缺少消息本地化的功能,但我真诚地希望在第四个版本中实现这个功能。

实现计划

该项目始于使用C++实现未来模型结构的原型和OpenGL实验性可视化。经过几个月的调试后,我开始将应用程序转换为客户端-服务器模型。最初,我是这样做的:用C#编写了一个混合的REST+WebSocket服务器,并将几何服务作为动态库连接,具有用于处理模型的C接口,该接口将用于几何查询。这种混合应用程序的最大不便和不必要的数据复制开销,从C++到C,然后到C#,迫使我寻找替代方案。最终,我将WebSocket服务器包含在C++部分中,并通过代理服务器将所有请求路由到它。在此过程中,为了对客户进行身份验证,几何服务会向主REST服务发送内部请求。

下一步是实现在服务器上更改的模型与客户端上显示的模型的同步算法。最初,监视服务器状态或在同步之前将当前状态发送到服务器的想法被排除,因为它们不太可靠且难以实现。我停留在以下实现上:每个组件都存储一个整数版本号。因此,模型的版本总体上由所有其组件及其子实体组件中的最大版本号确定。在同步时,客户端向服务器发送包含其模型版本的请求,服务器作为响应将发送所有其版本较旧的组件的数据。这确保了客户端和服务器之间树状模型的同步,同时实现了最小可能的流量(同步请求是一个数字,响应仅包含更改的组件)。

在编写客户端和服务器部分的原型之后,我开始寻找在客户端和服务器之间传输几何模型的最佳数据格式。在此格式中,我希望具有以下功能:

  1. 支持从 C++ 和 TypeScript 代码中进行格式的方便记录和读取
  2. 数据模式支持
  3. 尽可能短的数据打包和解包时间
  4. 浮点数传输不会丢失精度
  5. 在数据包大小在 100KB - 10MB 范围内的情况下的工作效率
  6. 为了实现系统不同部分的逐步更新,格式需要版本控制。

在实验中,我们立即排除了使用 JSON 格式的可能性,然后在 MessagePack、Google Protocol Buffers、Apache Thrift、BSON 等类似库之间做出了选择。由于 Google Protocol Buffers 具有更好的性能、良好的压缩和便捷的代码生成器,我选择了它。该库的流行程度也是一个重要的因素,我希望它在长期内不会被抛弃。因此,我在 C++ 中使用原生 protobuf,在客户端使用 protobufjs 进行读写,在 C++ 和 TypeScript 之间使用 proto2typescript 进行统一的数据模式。此外,在通过 WebSockets 传输数据时,数据还会额外被 zlib 压缩。这种方案使得我们可以非常舒适和快速地传输所需的所有数据。PS:最近我发现了同一开发者的 FlatBuffers 库,我认为这个选项可能更好,但是目前完全没有时间尝试它,而且它还没有在主流支持 TypeScript。

在确定了数据格式并对系统的每个部分进行了初步实验后,我们大致了解了整个服务的运作方式。除此之外,我们还评估了瓶颈并草拟了未来扩展的各种方案。然后,我们创建了第一个程序版本,展示了“用户操作 - 模拟服务器请求 - 用户操作可视化”的工作方式。在这个阶段,我们遇到了第一个失望的地方:这种工作方式提供了“拖动标记,松开鼠标,对象重新构造”的数据更新方式。这对于在移动光标时进行交互式用户操作的显示来说太慢了。

这个结果迫使我们重新思考服务器和客户端之间的边界,并使客户端更加广泛,以及通过服务器和客户端之间重复功能,使客户端进行交互式多边形可视化的预计算,而服务器则对 BREP 模型进行最终操作,并在所有客户端之间进行同步。这让我深入思考 WebAssembly 项目,理论上它可以为客户端和服务器端提供统一的代码库,并在需要时快速地管理计算执行,根据需要重新分配负载。但目前这只是梦想……

下一步是实现完整的 WebGL 渲染。目前它的功能还比较有限,但我们必须付出很大的努力。

以下是实现的主要内容:

  1. 目前我使用经典的前向渲染和多个通道进行渲染。在透视图中,我计划基于屏幕空间环境遮蔽或可扩展环境遮蔽实现阴影效果。
  2. 为了达到可接受的性能,我将小的对象组合成大的顶点缓冲区,并在全局坐标系中将其发送到显卡。当对象发生变化时,所有必要的缓冲区都会在处理器上重新计算。这可能看起来有些奇怪,但是将对象矩阵发送到附加属性中会更加昂贵。
  3. 在WebGL中,绘制线条粗度不等于1像素是非常困难的。我通过绘制两个三角形来实现它,这些三角形的所有顶点都位于同一条线上,并且厚度存储在属性-切线向量中。三角形的最终顶点通过将点转换为屏幕坐标系(以考虑屏幕宽高比),添加所需线条厚度并转换为标准化坐标来计算。线条的平滑处理通过片段着色器中的alpha通道实现。

文字渲染使用了Valve公司发布的SDF技术。我使用了libgdx的Hiero实用程序来准备字体。我得到的结果是令人满意的:在字体大小为14-16像素时,文本看起来不错,但如果大小小于此,并且文本与屏幕平面成尖角,则几乎无法阅读。也许是我不会准备SDF,但是花费了很多时间,却没有显著改善结果。在未来,我计划尝试这种技术:http://wdobbie.com/post/gpu-text-rendering-with-vector-textures/。

我还想指出,与Windows下的OpenGL相比,现代浏览器对WebGL的支持非常出色)))这似乎要归功于Angle项目,该项目通过DirectX模拟WebGL调用。即使在IE 11下,没有hack的代码也可以很好地运行。另一方面,在处理具有复杂结构的大量数据的应用程序中,内存泄漏问题非常严重,难以解决。

结语

接下来的开发步骤是实现程序的主题领域-建筑模拟、房间布局和各种构造元素、室内物品摆放和用户目录的创建。当然,还需要关注和花费时间在众多与网络服务相关的事项上,如身份验证和认证、备份、各个部分的扩展和整个开发过程的持续集成等。在这些方面,还有很多工作需要完成,才能将项目开放给公众使用。尽管如此,这项工作让我获得了巨大的经验。希望我的想法能对读者中的某些人有所帮助。如果有人有经验并愿意分享有关实现网络服务的建议,我将非常感兴趣听取。请私信我或在此处留下评论。
分享…