Normal view

There are new articles available, click to refresh the page.
Before yesterday治部少辅

在 VPN 场景下的跨子网通信防火墙配置方法

24 October 2024 at 10:38

这篇文章介绍手搓 Site-to-Site 组网的方法。

1 场景简述

假设我们有两个机器 A 与 B,其中 A 和 B 在各自的局域网中的网段分别是 192.168.2.0/24 和 192.168.3.0/24。A 和 B 都接入了一个 VPN 网络,其内网地址是 10.0.0.0/16。那么,如何让 A 访问 B 所在局域网内的另一台机器 C 呢?注意 C 是没有接入到 VPN 私有网络的。一个办法是直接让 C 也接入到 VPN 网络中,但是如果 B 的局域网中有众多设备需要被 A 访问时,将这些节点都加入 VPN 网络配置会非常麻烦。在一些场景下,VPN 的连接数量有限制(例如 Zerotier)。因此我们需要新的方案。这篇文章我们介绍通过巧妙地配置路由表和防火墙来实现。

我们假定 A 的地址是 192.168.2.2/24 和 10.0.0.2/16,B的地址是 192.168.3.2/24 和 10.0.0.3/16,C 的地址是 192.168.3.3/24。我们这里介绍的是让 A 访问 B 所在的子网。反过来让 B访问 A的子网的设置方法是类似的。进行双向设置以后就可以实现完整的 Site-to-Site 组网。这里我们假设 A 和 B 都安装了 Ubuntu 操作系统。

2 路由配置

首先我们需要进行路由表的配置,让 A 知道应该以 B 为网关来来访问 192.168.3.0/24 子网。在 A 上运行

1
sudo ip route add 192.168.3.0/24 via 10.0.0.3

然后,我们需要在 B 上启用 IP 转发。在Linux系统中开启IP转发功能,可以通过以下步骤:

  1. 临时开启(系统重启后失效)
    • 对于基于Debian或Ubuntu的系统,可以使用以下命令:
      • 查看当前IP转发状态:cat /proc/sys/net/ipv4/ip_forward,如果输出为0,则表示IP转发功能是关闭的;如果输出为1,则表示IP转发功能是开启的。
      • 开启IP转发:sudo sysctl -w net.ipv4.ip_forward=1。这条命令会在运行时修改内核参数,使系统能够进行IP转发。
    • 对于基于Red Hat或CentOS的系统,操作如下:
      • 查看IP转发状态:cat /proc/sys/net/ipv4/ip_forward
      • 开启IP转发:sudo sysctl -w net.ipv4.ip_forward=1。这会立即生效,但是在系统重启后会恢复到原来的设置。
  2. 永久开启(系统重启后依然有效)
    • 在Debian或Ubuntu系统中:
      • 编辑/etc/sysctl.conf文件:sudo nano /etc/sysctl.conf
      • 在文件中找到#net.ipv4.ip_forward=1这一行(如果没有这一行,可以添加),将前面的#去掉,使这一行变为net.ipv4.ip_forward = 1。保存并退出文件。
      • 使配置生效:sudo sysctl -p。这条命令会重新加载sysctl.conf文件中的配置,使IP转发功能在系统重启后依然有效。
    • 在Red Hat或CentOS系统中:
      • 编辑/etc/sysctl.conf文件:sudo vi /etc/sysctl.conf
      • 找到net.ipv4.ip_forward = 0这一行,将0改为1。保存并退出文件。
      • 执行sudo sysctl -p命令,让配置生效,这样在系统重启后IP转发功能也会开启。

3 防火墙配置

完成路由配置之后,A 机器能够知道到达 192.168.3.0/24 子网的路由路径,但是此时让 A 直接访问 C 的地址会失败,因为他们之间的通信会被 B 的防火墙拦截。因此我们需要对防火墙进行进一步配置。防火墙的配置分为两个主要的步骤:首先我们需要允许经过 B进行路由的数据包经过防火墙。在 B 上运行下面的代码:

1
2
sudo iptables -A FORWARD -i tun0 -o enp2s0 -s 10.0.0.0/16 -d 192.168.3.0/24 -j ACCEPT
sudo iptables -A FORWARD -i enp2s0 -o tun0 -s 192.168.3.0/24 -d 10.0.0.0/16 -j ACCEPT

注意其中的 -i -o 指定的接口名称需要针对你的服务器的实际情况调整。这里我们进行的往返路由路径的分别配置,将路由策略都设置为 ACCEPT 使得数据包能够通过 B 的防火墙。

对防火墙进行配置的第二个步骤是在 B 上开启地址伪装,即 MASQUERADE 机制。注意到我们在 A 上配置了路由表,但是没有作为访问目标的 C 则并不知道通向 A 的路由路径(A 和 B 之间在的 VPN 私有网络中是直通的,不需要额外路由)。因此我们需要配置进行一个额外的配置,即在 B 中开启地址伪装,此时 B 会将来自 A 的,以 C 为目标的数据包的源地址修改为自身的地址,那么 C 会认为这个包来自 B,响应也会被发送给 B,B 再将包转发回到 A。要开启地址伪装,需要在 B 上开启 iptables 配置:

1
sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/16 -o enp2s0 -j MASQUERADE

注意上面的命令结果会在重启后失效。要持久化 iptables 命令的配置,可以采用下面的方法:

  1. 使用iptables - save和iptables - restore命令(适用于大多数Linux发行版)
    • 保存配置
      • 首先,使用iptables - save命令可以将当前的iptables规则保存到一个文件中。例如,可以将规则保存到/etc/iptables.rules文件中,命令如下:sudo iptables - save > /etc/iptables.rules
      • 这个命令会将当前活动的iptables规则以文本格式输出并保存到指定的文件。输出的内容包含了表(如filternatmangle)以及每个表中的链(如INPUTOUTPUTFORWARD)的规则。
    • 恢复配置
      • 要在系统启动时自动恢复这些规则,可以在合适的启动脚本中添加iptables - restore命令。例如,在基于Debian或Ubuntu的系统中,可以创建一个脚本文件(如/etc/network/if - post - up.d/iptables - restore),并在其中添加以下内容:
        1
        2
        #!/bin/sh
        iptables - restore < /etc/iptables.rules
        然后给这个脚本文件添加可执行权限:sudo chmod +x /etc/network/if - post - up.d/iptables - restore
      • 在基于Red Hat或CentOS的系统中,可以将iptables - restore命令添加到/etc/rc.d/rc.local文件中(不过要确保rc.local文件有可执行权限),如:iptables - restore < /etc/iptables.rules。这样,在系统启动后,iptables规则就会根据保存的文件进行恢复。
  2. 使用iptables - persistent工具(适用于Debian和Ubuntu系统)
    • 安装iptables - persistent
      • 在Debian或Ubuntu系统中,可以使用以下命令安装iptables - persistent工具:sudo apt - get install iptables - persistent
      • 这个工具会自动在安装过程中保存当前的iptables规则,并且会创建一些系统服务相关的配置来确保规则在系统启动时自动加载。
    • 保存和恢复规则
      • 安装完成后,iptables规则会被自动保存到/etc/iptables目录下的文件中(/etc/iptables/rules.v4用于IPv4规则,/etc/iptables/rules.v6用于IPv6规则)。
      • 在系统启动时,iptables - persistent服务会自动读取这些文件中的规则并恢复iptables配置。如果后续修改了iptables规则,想保存新规则,可以使用sudo iptables - save > /etc/iptables/rules.v4(对于IPv4)和sudo iptables - save > /etc/iptables/rules.v6(对于IPv6)命令手动保存,或者使用dpkg - reconfigure iptables - persistent命令重新配置并保存规则。
  3. 使用systemd服务(适用于有systemd的Linux发行版)
    • 创建systemd服务单元文件
      • 可以创建一个自定义的systemd服务单元文件来保存和恢复iptables规则。例如,创建一个名为iptables - save - service.service的文件,内容如下:
        1
        2
        3
        4
        5
        6
        7
        8
        [Unit]
        Description=Save iptables rules
        After=network.target
        [Service]
        Type=oneshot
        ExecStart=/usr/bin/iptables - save > /etc/iptables.rules
        [Install]
        WantedBy=multi - user.target
      • 将这个文件保存到/etc/systemd/system/目录下,然后使用systemd命令来管理这个服务。
    • 启用和启动服务
      • 执行sudo systemd - enable iptables - save - service.service命令来设置服务在系统启动时自动运行。
      • 可以使用sudo systemd - start iptables - save - service.service命令来手动启动服务,保存当前的iptables规则。
      • 同时,还需要创建一个用于恢复规则的服务单元文件,例如iptables - restore - service.service,内容如下:
        1
        2
        3
        4
        5
        6
        7
        8
        [Unit]
        Description=Restore iptables rules
        After=network.target
        [Service]
        Type=oneshot
        ExecStart=/usr/bin/iptables - restore < /etc/iptables.rules
        [Install]
        WantedBy=multi - user.target
      • 同样将这个文件保存到/etc/systemd/system/目录下,然后使用systemd - enable iptables - restore - service.service命令来设置在系统启动时自动恢复规则,并且可以使用systemd - start iptables - restore - service.service命令手动启动恢复规则的服务。

Windows 环境下 Maven 的安装以及代理设置

25 August 2024 at 17:52

访问 Maven 的下载页面:https://maven.apache.org/download.cgi (可能需要梯子)

点击 Link 下载 Binary 版本即可。此处我们下载的文件中包含了可执行文件。完成下载之后我们需要将这些文件放到一个较为固定的位置,然后设置环境变量以便于我们在命令行环境能够正常访问这些脚本。

Maven 具有包管理器的作用,这类应用在国内都面 GFW 的问题,因此需要需要将 Maven 接入代理以便其能够正常下载以来资源。设置方式是在当前用户目录下 .m2/settings.xml 文件(如果没有此文件需要手动创建),并在此文件中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<settings>
<proxies>
<proxy>
<id>local-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>localhost</host>
<port>port</port>
<!-- 如果你的代理需要身份验证,可以取消下面的注释并填写 -->
<!-- <username>your-username</username> -->
<!-- <password>your-password</password> -->
<nonProxyHosts>localhost</nonProxyHosts> <!-- 可选:不使用代理的主机 -->
</proxy>
</proxies>
</settings>

注意将其中的地址和端口设置替换成你自己的 HTTP 代理的地址和端口。

引发 LTO 优化后续错误的一种可能及解决方法

31 May 2024 at 14:02

最近开发 C++ 的程序在链接阶段发生了一个 LTO 相关的错误。链接时优化(Link Time Optimization, LTO) 是一种编译优化技术,旨在通过在链接阶段执行全程序优化来提升代码性能和减少二进制文件大小。其主要原理是在链接阶段而不是在编译阶段进行更高级的优化,允许跨文件的全局优化。LTO 的基本原理和过程是:

  • 中间表示:在编译阶段,编译器将每个源文件编译为一种中间表示(如 LLVM bitcode),而不是直接生成目标代码。
  • 全局优化:在链接阶段,链接器将所有中间表示文件合并,生成一个完整的程序表示。此时,优化器可以进行跨文件的全局优化,如函数内联、去除冗余代码和跨模块优化。
  • 生成目标文件:优化完成后,链接器生成最终的可执行文件或库。

我遇到的错误信息中的管件一句是:

1
attempt to add bitcode file after LTO (htons)

可以看到错误信息提示我们链接系统试图在 LTO 优化之后添加 bitcode。并且在后面的括号中链接器提到了一个符号 htons,这是来自 libc 库的一个符号。基于的 https://github.com/emscripten-core/emscripten/issues/16836 这个链接中的启发,我发现了一个 LTO 优化可能面临的普遍性问题。这个问题是 LTO 会尝试移除一些它认为不需要的符号从而减少编译出来的程序的体积,但是这个判断可能会不准确。它可能将一些实际会被使用的符号删除。为了解决这个问题,我们可以为链接器指定链接输入,强制保留指定的符号以避免错误的移除。针对我遇到的情况,为链接器加上 -Wl,-u,htons 的配置即可解决问题。

  • -Wl: 这个标志告诉编译器将接下来的选项传递给链接器(ld)。-Wl,option 是传递给链接器的标准方法。
  • -u,symbol: 这个选项告诉链接器强制包含指定的符号 symbol。在这个情况下,-u,htons 和 -u,htonl 告诉链接器强制包含 htons 和 htonl 函数

OCCT Visualization 文档通读与理解

19 February 2024 at 18:30

链接: https://dev.opencascade.org/doc/overview/html/occt_user_guides__visualization.html


1 Introduction

OCCT 中的可视化模块建立在下面的概念分离上:

  • 一方面是: 用来存储你想要渲染以及交互的几何和拓扑数据;
  • 另一方面是:上述对象的表示对象(presentation)和选择对象(selection

表示对象由 Presentation 模块来管理,而选择对象则由 Selection 模块来管理;

Applicaiton Interactive Service (AIS) 正在提供应用 GUI 和这些组建之间的交互,使得这个过程能够更加直观和透明。AIS 模块使用 Interactive Object,即可交互对象来表示一个可显示,可选择实体。除非你需要进行深度定制,否则你一般不需要触及底层的表示与选择组件的对应对象;

如果你真的要进行深度定制, 你需要知道可表示对象和可选择对象的实现原理,具体而言,你需要知道应该实现那些虚函数。首先你需要了解一些更为基础的概念,包括 Sensitive Primitive 以及 Presentable Object。

以下这些包都参与了三维物体的渲染:

  • AIS
  • StdPrs
  • Prs3d
  • PrsMgr
  • V3d
  • Graphic3d

以上这些模块也适用于二维物体的渲染。

下面这张图显示了 Visualization 模块的一些重要的概念和组建之间的关系。“几何与拓扑”数据只是 AIS 可以处理的应用数据的一个例子,你可以根据实际应用的需要基于其他形式的数据来支持可视化。

2 基础概念

2.1 表示 · Presentation

表示模块的一个基础概念是,表示数据与他背后的几何拓扑数据是解耦的,这使得你可以在不改变一个图形的呈现形式的前提下修改图形数据。

2.1.1 表示的结构

要讲一个对象展示到屏幕上需要三类不同的对象的参与:

  • 可表示对象,一般是 AIS_InteractiveObject
  • Viewer
  • 可交互上下文,即 AIS_InteractiveContext

接下来我们一一阐述。

2.1.2 可表示对象

可表示对象的主要目标是一 Graphic3d_Structure 的形式来提供一个对象的图形化表示数据。在初次展示请求时,可表示对象会调用合适的算法来创建这些结构数据。

StdPrsPrs3d 报提供了标注的可视化算法。你也可以创建自己的算法实现,来创建对应的表示结构数据(来自 Graphic3d 包)。你也可以为单个可表示对象创建多个表示结构数据,每个对应一种显示模式。

每个需要被展示的对象都要是 presenable 的,或者是与一个可展示对象关联。

2.1.3 The Viewer

Viewer 允许以交互式的方式来操作对象。当你缩放、平移或者旋转视角时 Viewer 会操作可表示对象创建的 Graphic 结构数据,而不是直接操作应用的数据层对象。创建 Graphic3d 包中的定义的结构数据对象可以使得你能够直接应用 Viewer 提供的变换机制。

2.1.4 可交互上下文

可交互上下文为整个渲染过程提供了一些通用的高层 API。 当应用需要显示某个对象时,上下文会请求这一对象的表示数据结构,并发往 Viewer 进行渲染。

2.1.5 Presentation 相关包

Presentation 至少涉及到 AIS, PrsMgr, StdPrs 以及 V3d 等子包。如果你需要实现自己的渲染算法,你可能还需要涉及到 Prs3d 和 Graphic3d 这两个包;

  • 标准可交互对象
    • AIS 包提供了一些具体的自烈性来实现 Interactive 对象( presentable and selectable entities);
    • PrsDim 提供了绘制标注和关系的可表示对象;
    • MeshVS 包提供了表示曲面的数据;
  • 标准表示构造器(Presentation Builders)
    • Prs3d 包提供了即开即用的标准表示算法,可以用来处理一些简单的几何体,包括箭头,圆柱,球体等。它还丁你要了 Prs3d_Drawer 来来控制表示对象的属性,包括颜色,线型,厚度等;
    • StdPrs 包提供了对 B-rep 对象的即开即用的表示算法试下。这些表示算法包括 Shading、Wireframe、isolines 以及 hidden line removal 等;
    • DsgPrs 提供了用来展示标注、关系以及 XYZ 坐标盒(trihendrons)的工具;
  • 选择服务(Selection Service)
    • Select3D,SelectBasics 以及 SelectMgr 实现了选择服务;
    • StdSelect 包提供了 B-rep 形状的选择构造器;
  • Viewoer 管理由 V3d 包来负责;
  • 底层接口
    • PrsMgr 包定义了一些可展示对象的基础接口和工具,它包含了实现展示过程所需要的所有类:抽象基类 PrsMgr_PresentationPrsMgr_PresentableObject 以及 concrete 类 PrsMgr_PresentationManager
    • Graphic3d 包提供了底层的图形结构数据定义。这个包还提供了 Graphic3d_GraphicDriver,用于同底层图形 API(如 OpenGL)的沟通;

2.1.6 基本示例:如何显示一个 3D 对象

1
2
3
4
5
6
7
Handle(V3d_Viewer) theViewer;
Handle(AIS_InteractiveContext) aContext = new AIS_InteractiveContext (theViewer);

BRepPrimAPI_MakeWedge aWedgeMaker (theWedgeDX, theWedgeDY, theWedgeDZ, theWedgeLtx);
TopoDS_Solid aShape = aWedgeMaker.Solid();
Handle(AIS_Shape) aShapePrs = new AIS_Shape (aShape); // creation of the presentable object
aContext->Display (aShapePrs, AIS_Shaded, 0, true); // display the presentable object and redraw 3d viewer

我们通过 BRepPrimAPI_MakeWedge 工具类来创建了一个几何形状,然后调用 Display 进行展示,在 SDK 的内部,交互上下文会调用可表示对象(即 AIS_Shape) 的 Compute 方法来计算出渲染数据,并将数据转交给 Viewer。这个流程如下:

2.2 选择 · Selection

标准的 OCCT 选择算法由两个部分组成:静态和动态。动态选择会导致数据在经过对应的对象上方时被高亮,而静态的选择会让对象可以被选中,从而参与后续的过程。

选择分为三种类型:

  • 点选择 - 在鼠标的位置下进行选择;
  • 矩形选择 - 在一个起点和一个终点表示一个矩形的区域内进行选择;
  • 多段线选择 - 允许在用户定义的一段非自交的多段线区域内进行选择。

在 OCCT 的选择算法中,所有的可选择对象都由一些列的敏感区域(Sensitive Zones)表示,也被称为 Sensitive entities。当鼠标移动时,这些敏感区域会被分析用作碰撞检测。

2.2.1 术语与标记

这个部分介绍一些在后续的算法描述中涉及的基本的术语和标记。

2.2.2 Sensitive Entitiy

敏感实体与实体所有者一样,是对象与选择机制之间的链接。

实体的目的是特别定义对象的哪些部分将可被选择。因此,任何意图可被选择的对象都必须被拆分成敏感实体(一个或多个)。例如,要对一个对象应用面选择,就必须将其拆分成面,并使用这些面来创建一个敏感实体集。

根据用户的不同需要,敏感实体可以是原子化的(点或者线条),也可以是复杂的。敏感实体被用作选择算法的内部单元,并且其不包含任何拓扑信息,他们连接到上层接口来获取这些拓扑信息。

2.2.3 实体所有者

每个敏感实体 Select3D_SensitiveEntity 存储了一个指向其所有者 SelectMgr_EntityOwner 的引用,这个类将一个实体和对应的可选择对象(SelectMgr_SelectableObject)关联起来。另外,拥有者可以存储一些额外的信息。例如,敏感实体的图谱形状,高亮颜色,选中状态等。

2.2.4 选择集合

为了简化不同的选择模式的处理,敏感实体被组成集合,即 SelectMgr_Selection,每个选择集合包含了某个特定的选择模式下的敏感实体内容以及对应的设置与状态。

2.2.5 可选择对象

可选择对象(SelectMgr_SelectableObject, AIS_InteractiveObject)存储所有创建出来的选择集合和敏感实体的信息。

所有可选择对象的子类都需要实现预先定义的接口以将表示数据根据选择模式拆解成敏感实体。这些计算出来的敏感实体被组成成一个选择集合,并加入到此可选择对象的选择集合列表中。仅有可选择对象被永久销毁时,这些选择集合相关的数据才会被删除。

对于标准的 OCCT 可交互对象,选择模式 0 表示选中整个对象(这些行为可以在子类实现中被修改)。例如 AIS_Shape 对象定义下面的选择模式(见 AIS_Shape::SelectionMode()

  • 0 - 选择整个对象 (AIS_Shape)
  • 1 - 选择顶点 (TopAbs_VERTEX)
  • 2 - 选择边 (TopAbs_EDGE)
  • 3 - 选择环 (TopAbs_WIRE)
  • 4 - 选择面 (TopAbs_FACE)
  • 5 - 选择壳 (TopAbs_SHELL)
  • 6 - 选择实心体 (TopAbs_SOLID)

2.2.6 Viewer 选择器

对于每个 OCCT Viewer 都有一个 Viewer 选择器类 SelectMgr_ViewerSelector3d,它提供了整个选择算法模块的一些高阶 AI,并为每次鼠标点击封装了对象以及敏感实体的处理过程。选择器维护了选择模式的激活状态,启动算法,并且探测将要被拾取的对象,存储结果,并实现了接口来保证选择结构的及时更新。

2.2.7 选择管理器

选择管理器 SelectMgr_SelectionManager 是一个操作所有展示对象的选择集合的高阶 API 提供者。它负责处理所有 Viewer 的选择器,负责对象的选择模式的激活,负责维护每个对象的选择集合的计算。最终奥的任务是保证选择数据的及时更新。

2.2.8 算法

OCCT 的所有三种选择类型都作为一个单一概念实现,基于通过三级 BVH 树遍历搜索视锥与敏感实体之间的重叠。

2.2.9 选择视锥

选择算法每次运行的第一步都是根据当前的选择类型来构建选择视锥。

对于点选或矩形选取,视锥的底面是一个矩形,根据像素容差或用户定义区域的尺寸分别构建。对于多段线选择,由构建线条定义的多边形被三角剖分,每个三角形作为其自己的视锥的底面。因此,这种类型的选择使用一组三角形视锥来进行重叠检测。

视锥的长度由近视和远视视觉体积平面限制,并且每个平面都平行于相应的视觉体积平面建造。

2.2.10 BVH 树

为了在 Viewer 的层级维持选择机制,我们构建了一个 3 层 BVH 树。

第一级树由每个可选择对象的轴对齐边界框构成。因此,这棵树的根包含了所有可选择边界的组合,即使它们当前没有激活的选择。对象在显示AIS_InteractiveObject时被添加,并且只有当对象被销毁时才会从这棵树中移除。第 1 级 BVH 树是在选择算法首次运行时按需构建的。

第二级 BVH 树包含一个可选择对象的所有敏感实体。当默认模式被激活时,第 2 级树会自动构建,并且每当首次为新的选择模式计算时,就会重新构建。

第三级 BVH 树用于包含许多元素的复杂敏感实体,例如:三角剖分、带有许多段的线、点集等。它是按需为拥有超过 800K 子元素的敏感实体构建的(由StdSelect_BRepSelectionTool::PreBuildBVH()定义)。

2.2.11 算法的各个阶段

选择算法包含预处理阶段和三个主要阶段

2.2.11.1 预处理阶段

包含计算选择视锥及其主要特征数据;

2.2.11.2 第一阶段 - 遍历第一层 BVH 树

在成功构建选择视锥之后,算法开始遍历对象级 BVH 树。包含轴对齐边界框的节点按照分离轴定理(SAT)的条件测试与选择视锥的重叠。当遍历进行到叶节点时,意味着找到了一个可能包含重叠敏感实体的候选对象。如果没有检测到这样的对象,算法停止,假设没有对象需要被选择。否则,它进入下一个阶段处理找到的可选择对象的实体。

关于轴分离定理:

SAT 代表“分离轴定理”(Separating Axis Theorem),这是计算机图形学和物理引擎中用于检测两个凸多边形之间是否存在重叠(即碰撞检测)的一个重要方法。根据这个定理,如果能找到一个轴(分离轴),使得将两个多边形在这个轴上的投影完全分开,那么这两个多边形就不相交;反之,如果对于所有可能的分离轴,两个多边形的投影都重叠,则两个多边形相交。这个定理简化了复杂形状之间碰撞检测的计算,使得它成为实现高效碰撞检测算法的关键。

2.2.11.3 第二阶段 - 遍历第二层 BVH 树

在这个阶段,需要确定在一个对象的所有敏感实体中是否有候选者。

首先,在这个阶段,算法会检查当前对象是否应用了任何变换。如果它有自己的位置,那么相应变换后的视锥将被用于进一步的计算。在下一步,访问给定对象的第二级 BVH 树的节点,以搜索重叠的叶子。如果没有找到这样的叶子,算法返回到上一个阶段。否则,它开始通过执行以下检查来处理找到的实体:

激活检查 - 实体此刻可能是不活跃的,因为它属于被停用的选择; 容差检查 - 当前的选择视锥对于进一步的检查可能太大,因为它总是以所有激活实体中的最大容差构建的;因此,在这一步,视锥可能会被缩放。

经过这些检查后,算法进入最后阶段。

2.2.11.4 第三阶段 - 特定敏感实体的重叠和包含检测

如果实体是原子的,将执行一个简单的 SAT 测试。在复杂实体的情况下,将遍历第三级 BVH 树。分析匹配的敏感实体的定量特征(如深度、到几何中心的距离),并应用裁剪平面(如果已设置)。检测结果被存储,算法返回到第一主要阶段。

2.2.12 Packages and Classes

选择机制的实现涉及到多个包 - SelectBasics, Select3D, SelectMgr, StdSelect

2.2.12.1 SelectBasics

这个包定义了一些非常基础的类型和接口,其中值得一提的是

  • SelectBasics_PickResult:存储探测过程的量化结果,例如深度以及距离几何中心的距离;
  • SelectBasics_SelectingVolumeManager: 与当前选择视锥的接口和交互方式。

每个自定义选择实体都必须至少继承 SelectBasics_SensitiveEntity

2.2.12.2 Select3D

Select3D 包包括了一系列标准的选择实体的定义,例如

  • box
  • circle
  • curve
  • face
  • group
  • point
  • segment
  • triangle
  • triangulation
  • wire

这些基础的选择实体都继承了 Select3D_SensitiveEntity。这包还引入了两个辅助类 Select3D_SensitivePoly 以及Select3D_SensitiveSet,其中后者是需要第三级 BVH 树的复杂选择实体的基类,前者则描述了任意点集并实现了基础的选择函数(注意,Select3D_SensitivePoly 类的内部并不会进行数据检查,因此,如果你需要自行派生加入自定义实现,你需要满足 SAT 理论以便使用标准的 OCCT 重叠检测方法)。

2.2.12.3 SelectMgr

这个包含用来维护整个选择过程,基于这个目的,此包提供了以下服务:

  • 激活或者取消激活所有可选择对象的选择模式;
  • 为对象计算选择数据的接口;
  • 定义了选择过滤器的类;
  • 确保 BVH 树的数据为最新;

一下是对主要类的简要描述:

  • SelectMgr_BaseFrustum, SelectMgr_Frustum, SelectMgr_RectangularFrustum, SelectMgr_TriangularFrustum and SelectMgr_TriangularFrustumSet - 这些类和皆苦与选择视锥有关,他们定义了不同的 SAT 测试方法,同时也包含了计算定量特质数值的方法(例如计算深度和距离)
  • SelectMgr_SensitiveEntity, SelectMgr_Selection and SelectMgr_SensitiveEntitySet - 存储和处理敏感实体的数据,SelectMgr_SensitiveEntitySet 实现了第二级 BVH 树的元素集合;
  • SelectMgr_SelectableObject and SelectMgr_SelectableObjectSet - 描述了可选择对象。它们也负责存储,计算和移除选择数据。SelectMgr_SelectableObjectSet 提供了第一级 BVH 树的实现;
  • SelectMgr_SelectionManager - 管理所有可选择对虾干的激活状态,选择数据计算以及 BVH 的更新。

2.2.12.4 StdSelect

这个包包含了一些 SelectMgr 的类和工具来创建选择数据结构,例如

  • StdSelect_BRepOwner - 定义了一个敏感实体的拥有者,它连接到拓扑形状以及高亮方法;
  • StdSelect_BRepSelectionTool - 包含了一些算法实现来将标准 AIS 形状分割成敏感组元;
  • StdSelect_FaceFilter, StdSelect_EdgeFilter - 实现选择过滤器;

2.2.13 样例

第一个代码片段展示了在自定义交互对象中实现 SelectMgr_SelectableObject::ComputeSelection() 方法的例子。该方法用于计算用户定义的选择模式。假设需要使一个盒子在两种模式下可选择——整个形状(模式 0)和每个边缘(模式 1)。为了选择整个盒子,应用程序可以为交互对象的每个面创建一个敏感组元。在这种情况下,所有组元共享同一个所有者——盒子本身。为了选择盒子的边缘,应用程序必须为每个边缘创建一个敏感组元。这里所有敏感实体不能共享所有者,因为不同的几何组元必须作为选择程序的结果被高亮显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void InteractiveBox::ComputeSelection (const Handle(SelectMgr_Selection)& theSel,
const Standard_Integer theMode)
{
switch (theMode)
{
case 0: // creation of face sensitives for selection of the whole box
{
Handle(SelectMgr_EntityOwner) anOwner = new SelectMgr_EntityOwner (this, 5);
for (Standard_Integer aFaceIter = 1; aFaceIter <= myNbFaces; ++aFaceIter)
{
Select3D_TypeOfSensitivity aSensType = myIsInterior;
theSel->Add (new Select3D_SensitiveFace (anOwner, myFaces[aFaceIter]->PointArray(), aSensType));
}
break;
}
case 1: // creation of edge sensitives for selection of box edges only
{
for (Standard_Integer anEdgeIter = 1; anEdgeIter <= 12; ++anEdgeIter)
{
// 1 owner per edge, where 6 is a priority of the sensitive
Handle(MySelection_EdgeOwner) anOwner = new MySelection_EdgeOwner (this, anEdgeIter, 6);
theSel->Add (new Select3D_SensitiveSegment (anOwner, myFirstPnt[anEdgeIter]), myLastPnt[anEdgeIter]));
}
break;
}
}
}

创建选择结构的算法将敏感组元存储在SelectMgr_Selection实例中。对象的选择列表中的每个SelectMgr_Selection序列必须对应于特定的选择模式。为了描述对象分解为可选择组元的过程,Select3D包中提供了一套现成的敏感实体。可以通过从Select3D_SensitiveEntity继承来定义自定义敏感组元。为了使自定义交互对象可选择或自定义现有对象的选择模式,必须定义实体所有者。它们必须继承SelectMgr_EntityOwner接口。

任何交互对象的选择结构都在SelectMgr_SelectableObject::ComputeSelection()方法中创建。下面的示例展示了如何使用标准 OCCT 机制,实现在StdSelect_BRepSelectionTool中,来计算拓扑形状的不同选择模式。

1
2
3
4
5
6
7
8
9
10
11
12
void MyInteractiveObject::ComputeSelection (const Handle(SelectMgr_Selection)& theSelection,
const Standard_Integer theMode)
{
switch (theMode)
{
case 0: StdSelect_BRepSelectionTool::Load (theSelection, this, myShape, TopAbs_SHAPE); break;
case 1: StdSelect_BRepSelectionTool::Load (theSelection, this, myShape, TopAbs_VERTEX); break;
case 2: StdSelect_BRepSelectionTool::Load (theSelection, this, myShape, TopAbs_EDGE); break;
case 3: StdSelect_BRepSelectionTool::Load (theSelection, this, myShape, TopAbs_WIRE); break;
case 4: StdSelect_BRepSelectionTool::Load (theSelection, this, myShape, TopAbs_FACE); break;
}
}

StdSelect_BRepSelectionTool类为计算给定类型(例如,面、顶点、边、线等)的敏感实体提供了一个高级 API,使用来自给定TopoDS_Shape的拓扑数据。

Open CASCADE Technology 采用的传统方式是每个实体所有者自行高亮显示自己。这种方法有两个缺点:

  • 每个实体所有者必须维护自己的Graphic3d_Structure对象,这会导致相当大的内存开销;
  • 逐个绘制选定的所有者在视觉效果上并不高效。

因此,为了克服这些限制,OCCT 提供了一种替代方式来实现选定表示的高亮显示。采用这种方法,交互对象本身将负责高亮显示,而不是实体所有者。

基于SelectMgr_EntityOwner::IsAutoHilight()返回值,AIS_InteractiveContext对象要么使用传统的高亮显示方式(如果IsAutoHilight() 返回 TRUE),要么根据它们的可选择对象将这些所有者分组,最终调用SelectMgr_SelectableObject::HilightSelected()SelectMgr_SelectableObject::ClearSelected(),并传递所有者组作为参数。

因此,应用程序可以派生自己的交互对象并重新定义来自SelectMgr_SelectableObject的虚拟方法HilightSelected()ClearSelected()HilightOwnerWithColor()SelectMgr_SelectableObject::GetHilightPresentationSelectMgr_SelectableObject::GetSelectPresentation方法可以用来根据用户的需求优化填充选择和高亮表示。

在通过重定义SelectMgr_SelectableObject::ComputeSelection()方法计算并打包了所有必要的敏感实体以及相应的所有者到SelectMgr_Selection实例后,需要通过以下步骤在SelectMgr_SelectionManager中注册准备好的选择数据:

  • 如果没有打开AIS_InteractiveContext,创建一个交互上下文并在其中显示可选择对象;
  • 使用AIS_InteractiveContext::Load()方法将可选择对象加载到交互上下文的选择管理器中。如果传递给此方法的选择模式不等于-1,则将调用此选择模式的ComputeSelection()
  • 使用AIS_InteractiveContext::Activate()AIS_InteractiveContext::Deactivate()方法激活或停用定义的选择模式。

完成这些步骤后,创建的交互上下文的选择管理器将包含给定的对象及其选择实体,它们将参与检测程序。

下面的代码片段展示了上面的步骤,其中还包含了开启选择过程并解析选择结果的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Suppose there is an instance of class InteractiveBox from the previous sample.
// It contains an implementation of method InteractiveBox::ComputeSelection() for selection
// modes 0 (whole box must be selected) and 1 (edge of the box must be selectable)
Handle(InteractiveBox) theBox;
Handle(AIS_InteractiveContext) theContext;
// To prevent automatic activation of the default selection mode
theContext->SetAutoActivateSelection (false);
theContext->Display (theBox, false);

// Load a box to the selection manager without computation of any selection mode
theContext->Load (theBox, -1, true);
// Activate edge selection
theContext->Activate (theBox, 1);

// Run the detection mechanism for activated entities in the current mouse coordinates and in the current view.
// Detected owners will be highlighted with context highlight color
theContext->MoveTo (aXMousePos, aYMousePos, myView, false);
// Select the detected owners
theContext->Select();
// Iterate through the selected owners
for (theContext->InitSelected(); theContext->MoreSelected() && !aHasSelected; theContext->NextSelected())
{
Handle(AIS_InteractiveObject) anIO = theContext->SelectedInteractive();
}

// deactivate all selection modes for aBox1
theContext->Deactivate (aBox1);

另外需要提及的一个重点是,OCCT 的矩形选择支持两种探测模式:

  • 包含(Inclusive):只有目标完全包含在选择视锥内部时才会被认为是被选中的;
  • 覆盖(Overlap):只要目标与选择视锥存在交集就认为选中。

默认情况下 OCCT 会使用第一种,要修改这个默认行为,可以使用下面的方法:

1
2
3
4
5
6
// Assume there is a created interactive context
const Handle(AIS_InteractiveContext) theContext;
// Retrieve the current viewer selector
const Handle(StdSelect_ViewerSelector3d)& aMainSelector = theContext->MainSelector();
// Set the flag to allow overlap detection
aMainSelector->AllowOverlapDetection (true);

3 Applicaiton Interactive Service

3.1 介绍

应用交互服务允许以简单和透明的方式管理查看器中的表示和动态选择。管理可视化和选择的核心实体是交互上下文(AIS_InteractiveContext)。它连接到主查看器(V3d_Viewer)。

交互上下文默认在中立点启动,每个可选择对象作为一个整体被选中,但用户可以为特定对象激活局部选择,以使对象的部分可选择。局部/全局选择由为每个显示对象激活的选择模式列表管理,其中 0(默认选择模式)通常意味着全局(整个对象)选择。

交互对象(AIS_InteractiveObject)是被可视化和选择的实体。你可以使用标准交互对象的类,所有必要的功能已经被编程,或者你可以通过遵循下面描述的一定数量的规则和约定,实现你自己的交互对象类。

交互对象是一个“虚拟”实体,可以被呈现和选择。交互对象可以具有一定数量的特定图形属性,如可视化模式、颜色和材料。当交互对象被可视化时,所需的图形属性从它自己的绘制器(Prs3d_Drawer)中获取,如果它具有所需的自定义属性,否则从上下文绘制器中获取。

可能需要过滤要选择的实体。因此,有过滤器实体(SelectMgr_Filter),它们允许精细化动态检测上下文。这些过滤器中的一些只能在中立点使用,其他的只能在局部选择中使用。可以编程自定义过滤器并将它们加载到交互上下文中。

3.2 可交互对象

在 AIS 查看器中被可视化和选择的实体是对象(AIS_InteractiveObject)。它们将模型的底层参考几何连接到其在 AIS 中的图形表示。你可以使用预定义的 OCCT 标准交互对象类,所有必要的功能已经被编程,或者,如果你是高级用户,你可以实现你自己的交互对象类。

3.2.1 表示 · Presentation

一个交互对象可以有任意多个表示。三维表示数据由表示管理器(PrsMgr_PresentationManager)来管理;

表示数据由一个索引(显示模式)和对表示管理器的引用来表示。默认情况下,交互对象的默认表示索引是 0.

交互对象的表示数据计算由继承自 PrsMgr_PresentableObject::Compute 函数来进行。这个函数由表示管理器在显示或者更新的时候调用。

如果你要创建自己的交互对象,你需要以下面的方式来实现 Compute 函数。

3.2.1.1 For 3D

1
2
3
void PackageName_ClassName::Compute (const Handle(PrsMgr_PresentationManager)& thePresentationManager,
const Handle(Prs3d_Presentation)& thePresentation,
const Standard_Integer theMode);

3.2.1.2 For HLR Mode in 3D

1
2
void PackageName_ClassName::Compute (const Handle(Prs3d_Projector)& theProjector,
const Handle(Prs3d_Presentation)& thePresentation);

视图可以有两种状态:正常模式或计算模式(隐藏线删除模式)。当后者被激活时,视图会查找在正常模式下显示的所有表示,这些表示已被标记为接受 HLR 模式。一个内部机制允许调用交互对象自己的 Compute,即投影器(projector)函数。

按照惯例,交互对象接受或拒绝 HLR 模式的表示。可以通过以下两种方式之一进行此声明:

  • 最初通过使用枚举PrsMgr_TypeOfPresentation3d的值之一:
    • PrsMgr_TOP_AllView
    • PrsMgr_TOP_ProjectorDependent
  • 随后通过使用函数PrsMgr_PresentableObject::SetTypeOfPresentation

AIS_Shape类是支持 HLR 表示的交互对象的一个例子。HLR 算法的类型存储在形状的Prs3d_Drawer中。它是Prs3d_TypeOfHLR枚举的一个值,可以设置为:

  • Prs3d_TOH_PolyAlgo,用于基于形状三角化的多边形算法;
  • Prs3d_TOH_Algo,用于与形状的真实几何工作的精确算法;
  • Prs3d_TOH_NotSet,如果没有为给定的交互对象实例设置算法类型。

用于AIS_Shape的 HLR 算法类型可以通过调用AIS_Shape::SetTypeOfHLR()方法来更改。当前HLR算法类型可以使用AIS_Shape::TypeOfHLR()方法获得。

这些方法从AIS_Shape的绘制器中获取值。如果在Prs3d_Drawer中设置的HLR算法类型为Prs3d_TOH_NotSet,则Prs3d_Drawer会从AIS_InteractiveContext的默认绘制器中获取值。因此,可以更改所有新显示的交互对象所使用的默认HLR算法。存储在上下文绘制器中的HLR算法类型的值可以是Prs3d_TOH_AlgoPrs3d_TOH_PolyAlgo。多边形算法是默认算法。

3.2.2 表示模式

AIS 中的交互对象有四种类型:

  • the "construction element" or Datum,
  • the Relation (dimensions and constraints)
  • the Object
  • the None type (when the object is of an unknown type).

在每个类别内部还可以使用一个签名(索引形式)来提供更多的特征信息。默认情况下,交互对象具有 NONE类型,和 0 值签名,如果你需要更改这些属性,你需要重新盯一下下面两个虚函数:

  • AIS_InteractiveObject::Type
  • AIS_InteractiveObject::Signature

注意:某些签名值已经被 AIS 内部的标准交互类使用。

交互上下文可以为一组交互对象设定一个默认的表示模式。这个模式可能不被某个给定的对象类接受。因此,要获取关于这个类的信息,需要使用虚拟函数AIS_InteractiveObject::AcceptDisplayMode

3.2.3 显示模式

函数AIS_InteractiveContext::SetDisplayModeAIS_InteractiveContext::UnsetDisplayMode允许为对象设置一个自定义的显示模式,这可以与交互上下文提议的模式不同。

3.2.4 高亮模式

在动态检测中,交互上下文回显的表示默认是已经在屏幕上的表示。

函数AIS_InteractiveObject::SetHilightModeAIS_InteractiveObject::UnsetHilightMode允许指定用于高亮显示的显示模式(所谓的高亮模式),这个模式与对象的活动表示独立有效。无论这个选择是临时的还是最终的都没有关系。

注意,用于高亮检测到的对象和用于高亮选中的对象的是相同的表示(因此是相同的高亮模式),后者用特殊的选择颜色绘制(参考与交互上下文服务相关的部分)。

例如,你想要系统地高亮显示一个形状的线框表示——不管它是在线框表示中可视化还是以阴影方式。因此,你在交互对象的构造器中将高亮模式设置为 0。不要忘记在 Compute 函数中实现这种表示模式。

3.2.5 无限状态(Infinite Status)

如果你不希望一个对象受到全视图(FitAll)的影响,你必须声明它为无限的;你可以使用AIS_InteractiveObject::SetInfiniteStateAIS_InteractiveObject::IsInfinite函数取消其“无限”的状态。

以一个代表交互对象的类 IShape 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
myPk_IShape::myPk_IShape (const TopoDS_Shape& theShape, PrsMgr_TypeOfPresentation theType)
: AIS_InteractiveObject (theType), myShape (theShape) { SetHilightMode (0); }

Standard_Boolean myPk_IShape::AcceptDisplayMode (const Standard_Integer theMode) const
{
return theMode == 0 || theMode == 1;
}

void myPk_IShape::Compute (const Handle(PrsMgr_PresentationManager)& thePrsMgr,
const Handle(Prs3d_Presentation)& thePrs,
const Standard_Integer theMode)
{
switch (theMode)
{
// algo for calculation of wireframe presentation
case 0: StdPrs_WFDeflectionShape::Add (thePrs, myShape, myDrawer); return;
// algo for calculation of shading presentation
case 1: StdPrs_ShadedShape::Add (thePrs, myShape, myDrawer); return;
}
}

void myPk_IShape::Compute (const Handle(Prs3d_Projector)& theProjector,
const Handle(Prs3d_Presentation)& thePrs)
{
// Hidden line mode calculation algorithm
StdPrs_HLRPolyShape::Add (thePrs, myShape, myDrawer, theProjector);
}

3.2.6 选择

一个交互对象可以有无限数量的选择模式,每个模式代表一种敏感原语的“分解”。每个原语都有一个所有者(SelectMgr_EntityOwner),它允许识别已检测到的确切交互对象或形状(见选择章节)。

对应于给定模式的敏感原语集合存储在一个选择(SelectMgr_Selection)中。

每种选择模式由一个索引标识。按照惯例,允许我们完整把握交互对象的默认选择模式是模式0。然而,可以在自定义交互对象中使用方法SelectMgr_SelectableObject::setGlobalSelMode()进行修改。

选择原语(或敏感实体)的计算在一个虚拟函数ComputeSelection中完成。对于假定具有不同选择模式的每种类型的交互对象,都应实现该函数AIS_InteractiveObject::ComputeSelection。在选择章节中已给出了关于此机制和实现此函数方式的详细说明。

有一些最广泛使用的交互对象在 OCCT 中的选择模式计算示例——AIS_Shape(按顶点、按边缘等选择)。要创建具有与AIS_Shape相同选择行为的新交互对象类——如顶点和边缘——你必须重新定义虚拟函数AIS_InteractiveObject::AcceptShapeDecomposition

3.2.7 图形属性

图形属性管理器,或者说 Prs3d_Drawer,存储特定交互对象的图形属性以及由交互上下文控制的交互对象的图形属性。

在初始条件下,所有 Drawer 属性都填充了预定义的值,这些值将定义默认的 3D 对象外观。当交互对象被可视化时,所需的图形属性首先从其自有的 Drawer 中取得(如果存在的话),或者如果没有该类型对象的特定 Drawer,则从上下文 Drawer 中取得。

关于图形属性,需记住以下几点:

  • 每个交互对象可以拥有自己的可视化属性。
  • 默认情况下,交互对象取其所在上下文的图形属性(可视化模式、用于计算表示的偏差值、等参数线数量、颜色、线型、材料等)。
  • AIS_InteractiveObject 抽象类中,包括颜色、线条粗细、材质和透明度在内的标准属性被特别优先考虑。因此,有一定数量的虚拟函数,允许对这些属性进行操作。每个新的交互对象类可以重新定义这些函数并改变类的行为。
Redefinition of virtual functions for changes in AIS_Shape and AIS_TextLabel.

以下虚拟函数提供了颜色、宽度、材料和透明度的设置:

  • AIS_InteractiveObject::UnsetColor
  • AIS_InteractiveObject::SetWidth
  • AIS_InteractiveObject::UnsetWidth
  • AIS_InteractiveObject::SetMaterial
  • AIS_InteractiveObject::UnsetMaterial
  • AIS_InteractiveObject::SetTransparency
  • AIS_InteractiveObject::UnsetTransparency

这些方法可以作为一种通用方式分配属性的快捷方式,但结果可能不可用。一些交互对象可能根本不实现这些方法,或者只实现它们的一个子集。直接修改由AIS_InteractiveObject::Attributes返回的Prs3d_Drawer属性,可以用于更精确和可预测的配置。

重要的是要知道哪些函数可能意味着对象的表示需要重新计算。如果要更新交互对象的表示模式,PrsMgr_PresentableObject指示这一点的一个标志。可以使用AIS_InteractiveContext中的DisplayRedisplay函数更新模式。

3.2.8 Complementary Services

注意以下问题

3.2.8.1 改变交互对象的位置

下面的这些函数允许移动一个表示对象的位置而不需要重新计算表示数据或者修原始的形状数据:

  • AIS_InteractiveContext::SetLocation
  • AIS_InteractiveContext::ResetLocation
  • AIS_InteractiveContext::HasLocation
  • AIS_InteractiveContext::Location

3.2.8.2 关联到应用对象

每个交互对象都允许通过 GetOwner 方法来获取 Transient 形式的数据;

  • AIS_InteractiveObject::SetOwner
  • AIS_InteractiveObject::HasOwner
  • AIS_InteractiveObject::GetOwner

这种数据关联不会影响交互对象本身的行为。

3.2.8.3 解决拓扑精度问题(coincident topology)

由于三维图形坐标的精确度具有有限的分辨率,拓扑对象的元素可以重合,产生“弹出”某些元素覆盖另一些元素的效果。

对于两个或多个交互对象的元素重合的问题,你可以应用多边形偏移。这是一种图形计算偏移,或深度缓冲区偏移,允许你通过修改它们的深度值来排列元素,而不改变它们的坐标。接受这种偏移的图形元素是实心多边形或作为边界线和点显示的多边形。通过设置适当的内部样式,可以将多边形显示为线条或点。

方法AIS_InteractiveObject::SetPolygonOffsetsAIS_InteractiveContext::SetPolygonOffsets允许设置多边形偏移。

3.2.8.4 对象树

每个PrsMgr_PresentableObject都有一个称为 myChildren 的对象列表。对PrsMgr_PresentableObject的任何变换也会应用到其子对象上。这种层次结构不会传播到Graphic3d级别及以下。

PrsMgr_PresentableObject将其结合的(根据层次结构)变换下传到Graphic3d_Structure。结构的材质不受层次结构的影响。

对象层次结构可以通过以下 API 调用来控制:

  • PrsMgr_PresentableObject::AddChild;
  • PrsMgr_PresentableObject::RemoveChild

3.2.8.5 实例化

实例化的概念按如下方式操作对象层次结构:

  • 实例由分离的AIS对象表示。
  • 实例不计算任何表示。
  • AIS_ConnectedInteractiveAIS_MultipleConnectedInteractive 用于实现这一概念。

AIS_ConnectedInteractive 是一个对象实例,它重用连接对象的几何体,但拥有自己的变换和可见性标志。这种连接传递到 OpenGl 级别,即到 OpenGl_StructureOpenGl_Structure 只能连接到单个其他结构。

AIS_ConnectedInteractive 通常可以引用任何 AIS_InteractiveObject。当它引用另一个 AIS_ConnectedInteractive 时,它只是复制引用。

AIS_MultipleConnectedInteractive 表示一个不具有自己的表示的装配体。装配体能够参与对象层次结构,并旨在处理一组分组的实例化对象。就选择而言,它表现为单一对象。因为它位于层次结构中的较高位置,它对所有子元素应用高级变换。

所有 AIS_MultipleConnectedInteractive都能拥有子装配体。如果一个装配体附加到另一个装配体上,则执行对象实例树的深度复制。

注意,AIS_ConnectedInteractive不能引用 AIS_MultipleConnectedInteractiveAIS_ConnectedInteractive复制原始对象的敏感实体以进行选择,不同于AIS_MultipleConnectedInteractive 重用原始对象的实体。

实例可以通过以下DRAW命令控制:

  • vconnect:从输入对象和位置创建并显示AIS_MultipleConnectedInteractive对象。
  • vconnectto:使用给定位置制作对象的一个实例。
  • vdisconnect:断开装配体中的所有对象的连接,或通过名称或编号断开对象的连接。
  • vaddconnected:将对象添加到装配体中。
  • vlistconnected:列出装配体中的对象。

我们来看下面的例子:

1
2
3
4
5
6
pload MODELING VISUALIZATION
vinit
psphere s 1
vdisplay s
vconnectto s2 3 0 0 s # make instance
vfit

下面是 OpenGL_Structure 表示这些数据结构的方式:

被引用的对象即便没有显示的情况下,实例对象也可以正常工作。实例对象同时还会处理好选择数据的变换。

3.3 交互上下文

3.3.1 规则

交互上下文以透明的方式管理一个或多个查看器中交互对象的图形和可选择行为。在前一章中介绍的允许修改交互对象属性的大多数功能,在这里将再次审视。

有一个基本规则需要遵循:对已经被上下文认识的交互对象的修改,必须使用上下文函数来完成。只有当交互对象尚未加载到交互上下文中时,才可以直接调用可用于交互对象的函数。

1
2
3
Handle(AIS_Shape) aShapePrs = new AIS_Shape (theShape);
myIntContext->Display (aShapePrs, AIS_Shaded, 0, false, aShapePrs->AcceptShapeDecomposition());
myIntContext->SetColor(aShapePrs, Quantity_NOC_RED)

也可以这么写

1
2
3
4
Handle(AIS_Shape) aShapePrs = new AIS_Shape (theShape);
aShapePrs->SetColor (Quantity_NOC_RED);
aShapePrs->SetDisplayMode (AIS_Shaded);
myIntContext->Display (aShapePrs);

3.3.2 函数组

中立点和局部选择构成了交互上下文的两种操作模式或状态,交互上下文是控制可视化和选择的中心实体。中立点,作为默认模式,允许轻松地可视化和选择已加载到上下文中的交互对象。为特定对象激活局部选择允许选择它们的子部分。

3.3.3 上下文的管理

交互对象可以具有一定数量的特定图形属性,如可视化模式、颜色和材料。相应地,交互上下文具有一组图形属性,即Drawer,这是默认对其控制的对象有效的。当交互对象被可视化时,所需的图形属性首先从对象自己的Drawer中获取(如果存在),否则从上下文Drawer中获取。

以下可调整设置允许个性化呈现和选择的行为:

  • 默认Drawer,包含所有可以被没有自己属性的交互对象使用的颜色和线条属性。
  • 交互对象的默认可视化模式。默认为:模式0;
  • 通过鼠标移动检测到的实体的高亮颜色。默认为:Quantity_NOC_CYAN1
  • 预选择颜色。默认为:Quantity_NOC_GREEN
  • 选择颜色(当你点击检测到的对象时)。默认为:Quantity_NOC_GRAY80

所有这些设置都可以通过AIS_InteractiveContext的适当函数修改。当你改变与上下文有关的图形属性(例如,可视化模式)时,所有没有相应适当属性的交互对象都会被更新。

看下面的例子:

1
2
3
4
5
6
theCtx->Display (theObj1, false);
theCtx->Display (theObj2, true); // TRUE for viewer update
theCtx->SetDisplayMode (theObj1, 3, false);
theCtx->SetDisplayMode (2, true);
// theObj2 is visualized in mode 2 (if it accepts this mode)
// theObj1 stays visualized in its mode 3

PrsMgr_PresentationManagerSelectMgr_ViewerSelector3d,管理当前交互对象的展示和选择,与主查看器相关联。

警告!不要在真实代码中使用整数值(如上例所示)- 而应使用相应的枚举代替!每个可展示对象都有支持的显示和选择模式的独立列表;例如,AIS_DisplayMode枚举只适用于AIS_Shape展示。

3.4 局部选择

3.4.1 选择模式

局部选择由索引(选择模式)定义。特定交互对象实现的选择模式及其含义应在该类的文档中检查。例如,参见MeshVS_SelectionModeFlags,用于MeshVS_Mesh对象。

AIS_Shape是最常用的交互对象。它提供了管理对形状构成元素的选择操作的API(选择顶点、边缘、面等)。特定形状类型(TopAbs_ShapeEnum)的选择模式由方法AIS_Shape::SelectionMode()返回。

方法AIS_InteractiveContext::Display()没有选择模式参数激活对象的默认选择模式。方法AIS_InteractiveContext::Activate()AIS_InteractiveContext::Deactivate()激活和停用特定的选择模式。

可以同时激活多个选择模式(但默认的 0 模式用于选择整个对象是排他的 - 它不能与其他模式组合)。可以使用函数AIS_InteractiveContext::ActivatedModes检索活动模式的列表。

3.4.2 过滤器

为了定义动态检测的环境,你可以使用标准过滤器类或创建你自己的过滤器类。过滤器查询敏感原语的所有者,以确定它是否具有所需的特质。如果答案是肯定的,它就被保留;如果不是,则被拒绝。

对象的根类是SelectMgr_Filter。其背后的原理很简单:过滤器测试鼠标位置由选择器检测到的所有者(SelectMgr_EntityOwner)是否回答OK。如果是,它就被保留,否则就被拒绝。你可以通过实现延迟函数SelectMgr_Filter::IsOk()来创建一个自定义的过滤器对象类。

SelectMgr中,还有组合过滤器(AND过滤器,OR过滤器),允许组合多个过滤器。在交互上下文中,你添加的所有过滤器都存储在一个OR过滤器中(如果至少有一个过滤器回答OK,则回答OK)。

有一些标准过滤器,已经在几个包中实现:

  • StdSelect_EdgeFilter – 用于边缘,如线条和圆;
  • StdSelect_FaceFilter – 用于面,如平面、圆柱和球体;
  • StdSelect_ShapeTypeFilter – 用于形状类型,如复合体、实体、壳体和线条;
  • AIS_TypeFilter – 用于交互对象的类型;
  • AIS_SignatureFilter – 用于交互对象的类型和签名;
  • AIS_AttributeFilter – 用于交互对象的属性,如颜色和宽度。

有几个函数用于操作过滤器:

  • AIS_InteractiveContext::AddFilter 添加一个作为参数传递的过滤器。
  • AIS_InteractiveContext::RemoveFilter 移除一个作为参数传递的过滤器。
  • AIS_InteractiveContext::RemoveFilters 移除所有现有的过滤器。
  • AIS_InteractiveContext::Filters 获取上下文中活动的过滤器列表。

3.4.3 例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// shading visualization mode, no specific mode, authorization for decomposition into sub-shapes
const TopoDS_Shape theShape;
Handle(AIS_Shape) aShapePrs = new AIS_Shape (theShape);
myContext->Display (aShapePrs, AIS_Shaded, -1, true, true);

// activates decomposition of shapes into faces
const int aSubShapeSelMode = AIS_Shape::SelectionMode (TopAbs_Face);
myContext->Activate (aShapePrs, aSubShapeSelMode);

Handle(StdSelect_FaceFilter) aFil1 = new StdSelect_FaceFilter (StdSelect_Revol);
Handle(StdSelect_FaceFilter) aFil2 = new StdSelect_FaceFilter (StdSelect_Plane);
myContext->AddFilter (aFil1);
myContext->AddFilter (aFil2);

// only faces of revolution or planar faces will be selected
myContext->MoveTo (thePixelX, thePixelY, myView, true);

3.4.4 选择数据

动态检测和选择以一种直接的方式实施。只有一些约定和函数需要熟悉:

  • AIS_InteractiveContext::MoveTo – 将鼠标位置传递给交互上下文选择器。
  • AIS_InteractiveContext::SelectDetected – 存储在最后一次MoveTo时检测到的内容。改变之前选中的对象。根据选择方案,选择被丰富、替换或其他。
  • AIS_InteractiveContext::SelectPoint/SelectRectangle/SelectPolygon – 对点、矩形或周围区域应用选择。改变之前选中的对象。根据选择方案,选择被丰富、替换或其他。

检测到的和选中的实体的高亮显示由交互上下文自动管理。高亮颜色是上面处理过的颜色。如果你想自己管理这一部分,你可以断开这种自动模式:

1
2
AIS_InteractiveContext::SetAutomaticHilight
AIS_InteractiveContext::AutomaticHilight

你可以通过移动鼠标来查询交互上下文。以下函数可以使用:

  • AIS_InteractiveContext::HasDetected – 检查是否有检测到的实体;
  • AIS_InteractiveContext::DetectedOwner – 返回(当前高亮显示的)检测到的实体。

使用 Select 函数后,你可以探索选择列表。以下函数可以使用:

  • AIS_InteractiveContext::InitSelected – 初始化迭代器;
  • AIS_InteractiveContext::MoreSelected – 检查迭代器是否有效;
  • AIS_InteractiveContext::NextSelected – 将迭代器移动到下一个位置;
  • AIS_InteractiveContext::SelectedOwner – 返回当前迭代器位置的实体。

所有者对象SelectMgr_EntityOwner是一个关键对象,用于识别查看器中的可选择实体(由方法AIS_InteractiveContext::DetectedOwnerAIS_InteractiveContext::SelectedOwner返回)。交互对象本身可以通过方法SelectMgr_EntityOwner::Selectable检索,而识别子部分则取决于交互对象的类型。在AIS_Shape的情况下,(子)形状通过方法StdSelect_BRepOwner::Shape返回。

3.4.5 例子

1
2
3
4
5
6
7
8
9
10
for (myAISCtx->InitSelected(); myAISCtx->MoreSelected(); myAISCtx->NextSelected())
{
Handle(SelectMgr_EntityOwner) anOwner = myAISCtx->SelectedOwner();
Handle(AIS_InteractiveObject) anObj = Handle(AIS_InteractiveObject)::DownCast (anOwner->Selectable());
if (Handle(StdSelect_BRepOwner) aBRepOwner = Handle(StdSelect_BRepOwner)::DownCast (anOwner))
{
// to be able to use the picked shape
TopoDS_Shape aShape = aBRepOwner->Shape();
}
}

3.4.6 选择机制

AIS_InteractiveContextSelect* 方法接受一个额外参数指定选择机制,下面的表格阐述了这些机制的含义:

3.5 标准交互对象

交互对象是可选择和可视的对象,连接图形表示和底层参考几何体。

它们被分为四种类型:

  • Datum – 一个构造几何元素;
  • Relation – 对交互形状和相应参考几何体的约束;
  • Object – 一个拓扑形状或形状之间的连接;
  • None – 一个标记,它不是消除对象,而是告诉应用程序继续查找,直到它找到其生成中一个可接受的对象定义。

在这些类别内部,还可以通过签名进一步进行特征化。签名提供了进一步特征化的索引。默认情况下,交互对象具有None类型和0的签名(等同于None)。如果你想给你的交互对象指定特定的类型和签名,你必须重新定义两个虚拟方法:TypeSignature

3.5.1 Datum

基准元素将构造元素如线、圆、点、三面体、平面三面体、平面和轴线等聚集在一起。

AIS_PointAIS_AxisAIS_LineAIS_CircleAIS_PlaneAIS_Trihedron具有四种选择模式:

  • 模式AIS_TrihedronSelectionMode_EntireObject:选择一个三面体;
  • 模式AIS_TrihedronSelectionMode_Origin:选择三面体的原点;
  • 模式AIS_TrihedronSelectionMode_Axes:选择轴线;
  • 模式AIS_TrihedronSelectionMode_MainPlanes:选择XOY、YOZ、XOZ平面。

当你激活其中一种模式时,你可以选择的AIS对象类型包括:

  • AIS_Point
  • AIS_Axis(及轴线的类型信息);
  • AIS_Plane(及平面的类型信息)。

AIS_PlaneTrihedron提供三种选择模式:

  • 模式0:选择整个三面体;
  • 模式1:选择三面体的原点;
  • 模式2:选择轴线——与三面体的备注相同。

对于平面和三面体的表示,默认的长度单位是毫米,轴线表示的默认值是10。要修改这些尺寸,你必须暂时恢复对象Drawer。从中取出DatumAspect()并改变值FirstAxisLength。最后,重新计算表示。

3.5.2 Object

对象类型包括拓扑形状及形状之间的连接。

AIS_Shape有两种可视化模式:

  • 模式AIS_WireFrame:线框(默认模式)
  • 模式AIS_Shaded:着色(取决于形状的类型)

AIS_ConnectedInteractive是一个连接到另一个交互对象引用的交互对象,并且位于查看器的其他地方,使得不需要计算表示和选择,而是可以从你的对象引用中推断它们。AIS_MultipleConnectedInteractive是一个连接到一系列交互对象(也可以是连接的对象;它不需要占用大量内存的表示计算)的对象。

MeshVS_Mesh是一个表示网格的交互对象,它有一个提供几何信息(节点,元素)的数据源,并且可以使用自定义的表示构建器从源数据构建。

AIS_ColoredShape允许对TopoDS_Shape对象及其子形状使用自定义颜色和线宽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AIS_ColoredShape aColoredShape = new AIS_ColoredShape (theShape);

// setup color of entire shape
aColoredShape->SetColor (Quantity_NOC_RED);

// setup line width of entire shape
aColoredShape->SetWidth (1.0);

// set transparency value
aColoredShape->SetTransparency (0.5);

// customize color of specified sub-shape
aColoredShape->SetCustomColor (theSubShape, Quantity_NOC_BLUE1);

// customize line width of specified sub-shape
aColoredShape->SetCustomWidth (theSubShape, 0.25);

表示类AIS_PointCloud可用于高效绘制大量任意集合的彩色点。它使用Graphic3d_ArrayOfPoints将点数据传递到OpenGl图形驱动,以将一组点作为"点精灵"的数组绘制。点数据被打包进顶点缓冲对象以提高性能。

用于绘制点的点标记类型可以作为表示方面被指定。该表示提供了通过可视化点集的边界框进行选择的支持。它支持两种显示/高亮模式:点或边界框。

下面是一个例子:

1
2
3
4
5
6
Handle(Graphic3d_ArrayOfPoints) aPoints = new Graphic3d_ArrayOfPoints (2000, Standard_True);
aPoints->AddVertex (gp_Pnt(-40.0, -40.0, -40.0), Quantity_Color (Quantity_NOC_BLUE1));
aPoints->AddVertex (gp_Pnt (40.0, 40.0, 40.0), Quantity_Color (Quantity_NOC_BLUE2));

Handle(AIS_PointCloud) aPntCloud = new AIS_PointCloud();
aPntCloud->SetPoints (aPoints);
A random colored cloud of points

3.5.3 Relations

关系由一个或多个交互形状和相应的参考几何体上的约束组成。例如,你可能想要将两条边约束在平行关系中。这个约束被视为一个独立的对象,并显示为一个敏感原语。它采取的图形形式是一个带有||符号的垂直箭头,位于两条边之间。

PrsDim提供以下关系:

  • PrsDim_ConcentricRelation
  • PrsDim_FixRelation
  • PrsDim_IdenticRelation
  • PrsDim_ParallelRelation
  • PrsDim_PerpendicularRelation
  • PrsDim_Relation
  • PrsDim_SymmetricRelation
  • PrsDim_TangentRelation

关系列表不是详尽无遗的。

PrsDim 提供了这个包,这表明 Relation 的作用主要是在标注领域。

3.5.4 Dimension

  • PrsDim_AngleDimension
  • PrsDim_Chamf3dDimension
  • PrsDim_DiameterDimension
  • PrsDim_DimensionOwner
  • PrsDim_LengthDimension
  • PrsDim_OffsetDimension
  • PrsDim_RadiusDimension

3.5.5 MeshVS_Mesh

MeshVS_Mesh是一个表示网格的交互对象。这个对象与AIS_Shape不同,因为它的几何数据由描述对象节点和元素的数据源MeshVS_DataSource支持。结果是,你可以提供你自己的数据源。

然而,DataSource不提供任何关于属性的信息,例如节点颜色,但你可以以一种特殊的方式应用它们——通过选择适当的表示构建器。

MeshVS_Mesh的表示是通过表示构建器MeshVS_PrsBuilder构建的。你可以在构建器之间选择,以不同的方式表示对象。此外,你可以重新定义基础构建器类并提供你自己的表示构建器。

你可以使用以下方法添加/移除构建器:

1
2
3
MeshVS_Mesh::AddBuilder (const Handle(MeshVS_PrsBuilder)& theBuilder, Standard_Boolean theToTreatAsHilighter);
MeshVS_Mesh::RemoveBuilder (const Standard_Integer theIndex);
MeshVS_Mesh::RemoveBuilderById (const Standard_Integer theId);

MeshVS_Mesh有一组保留的显示和高亮模式标志。模式值是一个位数,允许选择额外的显示参数并组合以下模式标志,这些模式标志允许以线框、着色和收缩模式显示网格:

1
2
3
MeshVS_DMF_WireFrame
MeshVS_DMF_Shading
MeshVS_DMF_Shrink

也可以以下方式来以线框、着色和收缩模式展示拆解后的 Mesh:

1
2
3
MeshVS_DMF_DeformedPrsWireFrame
MeshVS_DMF_DeformedPrsShading
MeshVS_DMF_DeformedPrsShrink

下面这些方法代表了不同类型的数据:

1
2
3
4
5
MeshVS_DMF_VectorDataPrs
MeshVS_DMF_NodalColorDataPrs
MeshVS_DMF_ElementalColorDataPrs
MeshVS_DMF_TextDataPrs
MeshVS_DMF_EntitiesWithData

下面这些方法提供选择和高亮

1
2
MeshVS_DMF_SelectionPrs
MeshVS_DMF_HilightPr

MeshVS_DMF_User 则是用户自定义的模式;

下面这些值为表示构造器使用,它们也是选择模式的控制符:

1
2
3
4
5
6
7
8
9
MeshVS_SMF_0D
MeshVS_SMF_Link
MeshVS_SMF_Face
MeshVS_SMF_Volume
MeshVS_SMF_Element // groups 0D, Link, Face and Volume as a bit mask;
MeshVS_SMF_Node
MeshVS_SMF_All // groups Element and Node as a bit mask;
MeshVS_SMF_Mesh
MeshVS_SMF_Group

例如下面的对象,可以并用来显示 STL 文件格式的对象:

1
2
3
4
5
6
7
8
9
10
11
// read the data and create a data source
Handle(Poly_Triangulation) aSTLMesh = RWStl::ReadFile (aFileName);
Handle(XSDRAWSTLVRML_DataSource) aDataSource = new XSDRAWSTLVRML_DataSource (aSTLMesh);

// create mesh
Handle(MeshVS_Mesh) aMeshPrs = new MeshVS();
aMeshPrs->SetDataSource (aDataSource);

// use default presentation builder
Handle(MeshVS_MeshPrsBuilder) aBuilder = new MeshVS_MeshPrsBuilder (aMeshPrs);
aMeshPrs->AddBuilder (aBuilder, true);

MeshVS_NodalColorPrsBuilder允许表示一个网格,并在其上映射一个颜色缩放纹理。为此,你应该为颜色刻度定义一个颜色映射,将这个映射传递给表示构建器,并为每个节点定义一个范围在0.0到1.0之间的适当值。以下示例展示了你如何做到这一点(检查视图是否已设置为显示纹理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// assign nodal builder to the mesh
Handle(MeshVS_NodalColorPrsBuilder) aBuilder = new MeshVS_NodalColorPrsBuilder (theMeshPrs, MeshVS_DMF_NodalColorDataPrs | MeshVS_DMF_OCCMask);
aBuilder->UseTexture (true);

// prepare color map
Aspect_SequenceOfColor aColorMap;
aColorMap.Append (Quantity_NOC_RED);
aColorMap.Append (Quantity_NOC_BLUE1);

// assign color scale map values (0..1) to nodes
TColStd_DataMapOfIntegerReal aScaleMap;
...
// iterate through the nodes and add an node id and an appropriate value to the map
aScaleMap.Bind (anId, aValue);

// pass color map and color scale values to the builder
aBuilder->SetColorMap (aColorMap);
aBuilder->SetInvalidColor (Quantity_NOC_BLACK);
aBuilder->SetTextureCoords (aScaleMap);
aMesh->AddBuilder (aBuilder, true);

3.6 动态选择

动态选择通过敏感原语的分解来表示你想要选择的拓扑形状——将被检测和高亮显示的形状的子部分。这些原语的集合由强大的三级 BVH 树选择算法处理。

有关算法的更多细节和使用示例,请参考选择章节。

4 3D 表示

4.1 三维渲染术语

  • 群组(Group) - 一组原语及其属性的集合。原语和属性可以被添加到群组中,但除非进行全局擦除,否则不能从中移除。群组可以拥有一个拾取标识。
  • 光源(Light) - 有五种类型的光源 - 环境光、头灯、定向光、位置光和聚光灯。
  • 原语(Primitve) - 一个可绘制元素。它在 3D 空间中有定义。原语可以是线条、面、文本或标记。一旦显示,标记和文本保持相同大小。线条和面可以被修改,例如缩放。属性在群组内设置。原语必须存储在一个群组中。
  • 结构(Structure) - 管理一组群组。这些群组是互斥的。结构可以被编辑,添加或移除群组。结构可以引用其他结构形成层次结构。它有一个默认(身份)变换,也可以对其应用其他变换(旋转、平移、缩放等)。每个结构都有一个与之关联的显示优先级,这决定了它在 3D 查看器中重绘的顺序。
  • 视图(View) - 由视图定向、视图映射和上下文视图定义。
  • 查看器(Viewer) - 管理一组视图。
  • 视图定向(View Orientation) - 定义观察者以视图参考坐标查看场景的方式。
  • 视图映射(View mapping) - 定义从视图参考坐标到归一化投影坐标的转换。这遵循 Phigs 方案。
  • Z缓冲(Z-Buffering) - 仅在着色模式下的一种隐藏表面去除形式。这对于着色模式下的视图总是激活的,并且不能被抑制。

PHIGS代表“程序化图形标准”(Programmer's Hierarchical Interactive Graphics System)。它是一个三维计算机图形标准,用于描述和控制三维对象和场景的构造和渲染。PHIGS被设计为支持复杂的图形应用程序,提供了一种结构化的、层次化的方法来创建和管理图形数据。

PHIGS提供了一套丰富的图形编程接口,允许开发者定义图形对象(如线、面、体)、组织这些对象成结构,并通过变换(如旋转、缩放和平移)操作这些结构。这些图形结构可以被组织成一个场景图,支持复杂的三维场景表示。

视图映射(View mapping)在PHIGS中指的是将这些三维图形数据从模型空间转换到视口(即屏幕空间或投影空间)的过程,这包括投影变换(如透视投影或正交投影)、视图变换(定义观察者的位置和方向)等步骤。

总的来说,PHIGS方案提供了一个高度结构化和层次化的方法来处理复杂的三维图形数据和场景,虽然它已经被更现代的图形API(如OpenGL和DirectX)所超越,但它在历史上对三维图形标准的发展起到了重要作用。

4.2 图形原语

Graphic3d包用于在3D查看器中创建3D图形对象。这些被称为结构的对象由原语(如线段、三角形、文本和标记)的组合以及属性(如颜色、透明度、反射、线型、线宽和文本字体)组成。群组是结构中最小的可编辑元素。可以对结构应用变换。结构可以连接起来形成由变换组成的结构树。结构由查看器全局操作。

图形结构可以被:

  • 显示,
  • 高亮,
  • 擦除,
  • 变换,
  • 连接形成由变换创建的结构的树状层次结构。

有类用于:

  • 线条、面、标记、文本、材料的视觉属性,
  • 向量和顶点,
  • 图形对象、群组和结构。

4.2.1 Structure 层级结构

根是结构层次或结构网络的顶部。父结构的属性会传递给其后代。后代结构的属性不会影响父结构。不支持递归结构网络。

4.2.2 图形原语

  • Marker
    • 包含一个或者多个顶点;
    • 具有一个类型,一个缩放系数以及一个颜色;
    • 具有尺寸、形状、以及和变换无关的朝向;
  • Triangulation
    • 至少具有三个顶点;
    • 具有法线;
    • 具有一些内部属性,例如样式,颜色,前后材料,纹理以及反射比例等;
  • Polylines or Segments
    • 具有两个或者更多的顶点
    • 具有如下属性:类型,宽度缩放系数以及颜色
  • Text
    • 具有几何和非几何属性;
    • 几何属性:字符高度、字符向上方向,文本路径,水平和垂直对齐方式,三维位置,缩放系数等;
    • 非几何属性:字体,文本间距、字符扩张系数以及颜色等;

4.2.3 原语数组

不同类型的原语可以通过以下原语数组来表示:

  • Graphic3d_ArrayOfPoints,
  • Graphic3d_ArrayOfPolylines,
  • Graphic3d_ArrayOfSegments,
  • Graphic3d_ArrayOfTriangleFans,
  • Graphic3d_ArrayOfTriangles,
  • Graphic3d_ArrayOfTriangleStrips.

Graphic3d_ArrayOfPrimitives是这些原语数组的基类。方法Graphic3d_ArrayOfPrimitives::AddVertex允许向原语数组添加带有其属性(颜色、法线、纹理坐标)的顶点。你也可以通过顶点索引修改分配给顶点的值或查询这些值。

以下示例展示了如何定义一个点的数组:

1
2
3
4
5
6
7
8
9
10
11
// create an array
Handle(Graphic3d_ArrayOfPoints) anArray = new Graphic3d_ArrayOfPoints (theVerticiesMaxCount);

// add vertices to the array
anArray->AddVertex (10.0, 10.0, 10.0);
anArray->AddVertex (0.0, 10.0, 10.0);

// add the array to the structure
Handle(Graphic3d_Group) aGroup = thePrs->NewGroup();
aGroup->AddPrimitiveArray (anArray);
aGroup->SetGroupPrimitivesAspect (myDrawer->PointAspect()->Aspect());

如果原语共享相同的顶点(多边形、三角形等),那么你可以将它们定义为顶点数组的索引。方法Graphic3d_ArrayOfPrimitives::AddEdge允许通过索引定义原语。这个方法在数组的范围[1, VertexNumber()]中添加一个“边”。也可以使用方法Graphic3d_ArrayOfPrimitives::Edge查询由边定义的顶点。

以下示例展示了如何定义一个三角形数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// create an array
Handle(Graphic3d_ArrayOfTriangles) anArray = new Graphic3d_ArrayOfTriangles (theVerticesMaxCount, theEdgesMaxCount, Graphic3d_ArrayFlags_None);
// add vertices to the array
anArray->AddVertex (-1.0, 0.0, 0.0); // vertex 1
anArray->AddVertex ( 1.0, 0.0, 0.0); // vertex 2
anArray->AddVertex ( 0.0, 1.0, 0.0); // vertex 3
anArray->AddVertex ( 0.0,-1.0, 0.0); // vertex 4

// add edges to the array
anArray->AddEdges (1, 2, 3); // first triangle
anArray->AddEdges (1, 2, 4); // second triangle

// add the array to the structure
Handle(Graphic3d_Group) aGroup = thePrs->NewGroup();
aGroup->AddPrimitiveArray (anArray);
aGroup->SetGroupPrimitivesAspect (myDrawer->ShadingAspect()->Aspect());

4.2.4 文本原语

TKOpenGl工具包使用纹理字体渲染文本标签。Graphic3d文本原语具有以下特性:

  • 固定大小(不可缩放)或可缩放,
  • 可以在视图平面内旋转到任意角度,
  • 支持 Unicode 字符集。

群组的文本属性可以通过Graphic3d_AspectText3d属性群组来定义。要将任何文本添加到图形结构中,你可以使用以下方法:

1
2
void Graphic3d_Group::AddText (const Handle(Graphic3d_Text)& theTextParams,
const Standard_Boolean theToEvalMinMax);

如果你不希望文本位置影响Graphic3d结构的边界,可以传递FALSE作为theToEvalMinMax参数。

注意:文本的朝向角度可以通过 Graphic3d_AspectText3d 的属性来设置;

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// get the group
Handle(Graphic3d_Group) aGroup = thePrs->NewGroup();

// change the text aspect
Handle(Graphic3d_AspectText3d) aTextAspect = new Graphic3d_AspectText3d();
aTextAspect->SetTextZoomable (true);
aTextAspect->SetTextAngle (45.0);
aGroup->SetPrimitivesAspect (aTextAspect);

// add a text primitive to the structure
Handle(Graphic3d_Text) aText = new Graphic3d_Text (16.0f);
aText->SetText ("Text");
aText->SetPosition (gp_Pnt (1, 1, 1));
aGroup->AddText (aText);

4.2.5 材质

Graphic3d_MaterialAspect定义了以下常见的材料属性:

  • 透明度;
  • 漫反射——对象颜色的一个组成部分;
  • 环境反射;
  • 镜面反射——光源颜色的一个组成部分。

确定三种反射颜色需要以下项目:

  • 颜色;
  • 漫反射系数;
  • 环境反射系数;
  • 镜面反射系数。

常见材料属性在 Phong 着色模型中使用(Graphic3d_TypeOfShadingModel_PhongGraphic3d_TypeOfShadingModel_PhongFacetGraphic3d_TypeOfShadingModel_Gouraud)。在PBR 着色模型(Graphic3d_TypeOfShadingModel_PbrGraphic3d_TypeOfShadingModel_PbrFacet)中,材料属性由以下Graphic3d_PBRMaterial属性定义(Graphic3d_MaterialAspect::PBRMaterial()):

  • Albedo(主颜色);
  • 金属度因子;
  • 粗糙度因子;
  • 透明度;
  • 折射率指数。

4.2.6 纹理

纹理由一个名字来定义。以下三类纹理受到支持:

  • 1D;
  • 2D;
  • Environment Mapping;

4.2.7 自定义 Shader

OCCT可视化核心支持GLSL着色器。自定义着色器可以通过其绘制器属性(Graphic3d方面)分配给通用表示。要在你的应用程序中为特定的AIS_Shape启用自定义着色器,可以使用以下 API 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Create shader program
Handle(Graphic3d_ShaderProgram) aProgram = new Graphic3d_ShaderProgram();

// Attach vertex shader
aProgram->AttachShader (Graphic3d_ShaderObject::CreateFromFile (Graphic3d_TOS_VERTEX, "<Path to VS>"));

// Attach fragment shader
aProgram->AttachShader (Graphic3d_ShaderObject::CreateFromFile (Graphic3d_TOS_FRAGMENT, "<Path to FS>"));

// Set values for custom uniform variables (if they are)
aProgram->PushVariable ("MyColor", Graphic3d_Vec3 (0.0f, 1.0f, 0.0f));

// Set aspect property for specific AIS_Shape
theAISShape->Attributes()->ShadingAspect()->Aspect()->SetShaderProgram (aProgram);

4.3 图形属性

4.3.1 Aspect 包预览

Aspect包提供了查看器中图形元素的类:

  • 图形属性的组;
  • 边缘、线条、背景;
  • 窗口;
  • 驱动程序;
  • 上述许多内容的枚举。

4.4 3D View 设施

4.4.1 概览

V3d包提供了定义 3D 查看器及其附属视图(正交视图、透视视图)的资源。这个包提供了操作屏幕上视图中可视化的任何3D对象的图形场景的命令。

一组高级命令允许分别操作参数和投影结果(旋转、缩放、平移等)以及任何特定视图中的可视化属性(模式、照明、裁剪等)。

V3d包基本上是一套由查看器前端指令驱动的工具集。这套工具包含了创建和编辑查看器类的方法,例如:

  • 查看器的默认参数,
  • 视图(正交视图、透视视图),
  • 照明(位置光、定向光、环境光、聚光灯、头灯),
  • 裁剪平面,
  • 实例化的视图、平面、光源、图形结构和拾取序列,
  • 各种包方法。

4.4.2 编程指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// create a default display connection
Handle(Aspect_DisplayConnection) aDispConnection = new Aspect_DisplayConnection();
// create a Graphic Driver
Handle(OpenGl_GraphicDriver) aGraphicDriver = new OpenGl_GraphicDriver (aDispConnection);
// create a Viewer to this Driver
Handle(V3d_Viewer) aViewer = new V3d_Viewer (aGraphicDriver);
aViewer->SetDefaultBackgroundColor (Quantity_NOC_DARKVIOLET);
// Create a structure in this Viewer
Handle(Graphic3d_Structure) aStruct = new Graphic3d_Structure (aViewer->StructureManager());
aStruct->SetVisual (Graphic3d_TOS_SHADING); // Type of structure

// Create a group of primitives in this structure
Handle(Graphic3d_Group) aPrsGroup = aStruct->NewGroup();

// Fill this group with one quad of size 100
Handle(Graphic3d_ArrayOfTriangleStrips) aTriangles = new Graphic3d_ArrayOfTriangleStrips (4);
aTriangles->AddVertex (-100./2., -100./2., 0.0);
aTriangles->AddVertex (-100./2., 100./2., 0.0);
aTriangles->AddVertex ( 100./2., -100./2., 0.0);
aTriangles->AddVertex ( 100./2., 100./2., 0.0);

Handle(Graphic3d_AspectFillArea3d) anAspects = new Graphic3d_AspectFillArea3d (Aspect_IS_SOLID, Quantity_NOC_RED,
Quantity_NOC_RED, Aspect_TOL_SOLID, 1.0f,
Graphic3d_NameOfMaterial_Gold, Graphic3d_NameOfMaterial_Gold);
aPrsGroup->SetGroupPrimitivesAspect (anAspects);
aPrsGroup->AddPrimitiveArray (aTriangles);

// Create Ambient and Infinite Lights in this Viewer
Handle(V3d_AmbientLight) aLight1 = new V3d_AmbientLight (Quantity_NOC_GRAY50);
Handle(V3d_DirectionalLight) aLight2 = new V3d_DirectionalLight (V3d_Zneg, Quantity_NOC_WHITE, true);
aViewer->AddLight (aLight1);
aViewer->AddLight (aLight2);
aViewer->SetLightOn();

// Create a 3D quality Window with the same DisplayConnection
Handle(Xw_Window) aWindow = new Xw_Window (aDispConnection, "Test V3d", 100, 100, 500, 500);
aWindow->Map(); // Map this Window to this screen

// Create a Perspective View in this Viewer
Handle(V3d_View) aView = new V3d_View (aViewer);
aView->Camera()->SetProjectionType (Graphic3d_Camera::Projection_Perspective);
// Associate this View with the Window
aView->SetWindow (aWindow);
// Display presentation in this View
aStruct->Display();
// Finally update the Visualization in this View
aView->Update();
// Fit view to object size
aView->FitAll();

4.4.3 定义查看参数

OCCT的V3d_View视图投影和方向由摄像机控制。摄像机计算并提供投影和视图方向矩阵供OpenGL渲染。这允许用户控制所有的投影参数。摄像机由以下属性定义:

  • 眼睛(Eye)- 定义观察者(摄像机)位置。确保眼睛点永远不会处于前后裁剪平面之间。
  • 中心(Center)- 定义视图参考坐标的原点(摄像机指向的地方)。
  • 方向(Direction)- 定义摄像机视图的方向(从眼睛到中心)。
  • 距离(Distance)- 定义眼睛和中心之间的距离。
  • 前平面(Front Plane)- 在视图参考坐标系统中定义前裁剪平面的位置。
  • 后平面(Back Plane)- 在视图参考坐标系统中定义后裁剪平面的位置。
  • Z近(ZNear)- 定义眼睛和前平面之间的距离。
  • Z远(ZFar)- 定义眼睛和后平面之间的距离。

最常见的视图操作(平移、缩放、旋转)作为V3d_View类的便利方法或通过AIS_ViewController工具实现。然而,Graphic3d_Camera类也可以直接由应用程序开发者使用。

通过硬链接方式来衔接 PT 下载文件和 Jellyfin 库

15 November 2023 at 10:23

请注意,硬链接有一些限制:

  • 不能对目录创建硬链接,只能对文件创建。
  • 硬链接必须在同一文件系统中创建。
  • 当您删除源文件时,硬链接仍然保持对文件内容的访问,因为硬链接和源文件本质上是文件系统中相同文件内容的不同入口点。

我们可以通过 find + ln 来批量创建硬链接。要结合使用 findln 命令来为给定目录中的每个文件在另一个目录中创建硬链接,可以通过管道(pipe)将 find 命令的输出传递给 xargs 命令,然后使用 ln 创建硬链接。这种方法可以高效地处理大量文件和复杂的文件名。

以下是具体的步骤和命令示例:

  1. 使用 find 查找文件:首先,使用 find 命令来查找您想要链接的所有文件。确保只选取文件,不包括目录。

  2. find 的输出传递给 ln 命令:通过 xargsfind 命令的输出作为 ln 命令的输入。

这是一个具体的命令示例:

1
find /path/to/source-directory -type f -exec ln {} /path/to/destination-directory/ \;

这个命令解释如下:

  • find /path/to/source-directory:在 /path/to/source-directory 目录下查找文件。
  • -type f:确保只选取文件(file),不包括目录。
  • -exec ln {} /path/to/destination-directory/ \;:对于 find 命令找到的每个文件,执行 ln 命令创建硬链接。其中 {} 是一个占位符,代表 find 命令找到的每个文件的路径。

这个命令会在 /path/to/destination-directory/ 目录下为源目录中的每个文件创建一个硬链接。请确保目标目录已经存在,因为 ln 命令不会自动创建目录。同时,由于硬链接的限制,源文件和目标目录必须位于同一文件系统内。

Unraid 上 Nextcloud 的部署问题

3 September 2023 at 14:56

之前我写过一篇文章来吐槽 Nextcloud 的性能问题。当时我是尝试在群晖上试用 Nextcloud。考虑到我使用的群晖服务器的 CPU 性能比价差,因此我尝试在 Unraid 上部署 Nextcloud。但是性能差的问题仍然没有改观。不过近期在 Unraid 发布了 6.12.0 版本以后这个问题得到了很大的改观。

1 问题分析

问题出在 shfs 这个进程上。在 Unraid 中,这个进程负责将分散在各个 Disk 上的文件夹内容聚合成一个统一的目录库,这意味着如果我们要访问任何文件都需要通过 shfs 来查找文件,这也使得 shfs 成文高吞吐率场景下的一个性能瓶颈。之前我经常可以看到 shfs 的 CPU 占用飙升到 100% 甚至 200%。在新版的 Unraid 中提供了 exclusive access 的特性。 Exclusive 的是排他性,独占的意思。Exclusive Access 是指固定将一个目录放在一个 Disk 中,这样我们在访问这个目录下的文件时,就不需要通过 shfs 来检索目录,而是可以直接访问磁盘上的目录系统,从而降低开销。

另一方面,Exclusive 也意味着这个目录无法有效的利用磁盘阵列提供的存储池,而只能利用单磁盘空间。因此,Nextcloud 的数据文件夹是不能使用 Exclusive Access 的。不过好在数据文件的访问其实并不是主要瓶颈,造成 Nextcluod 卡顿的主要原因在对容器挂载的配置文件(也就是 appdata)里面的众多小文件的高频读写。

另一个需要指出的是,在 Unraid 中,尽管缓存有多个 SSD,但是这些 SSD 会预先通过 Raid 机制聚合成一个统一的缓存磁盘,对于 Shfs 来说这个缓存池就是一个单一的磁盘,因此对缓存池的访问可以启用 Exclusive Access

2 如何使用

要是用 Exclusive Access,首先需要再 Settings --> Global Share Settings 下打开(如下图,将 Permit exclusive shares 设置为 yes)。

但是在共享目录使用时,Exclusive Access 的含义是比较模糊的。因为 Share 的管理界面并不会提供一个可以由用户可以直接控制的选项来为具体的 Share 开启 Exclusive Access。事实上,Exclusive Access 是否会起作用是系统根据目录的状态来自动确定的。如前文所述,Exclusive Access 作用的前提是数据只位于一个磁盘中。因此,在 Permit exclusive shares 启用的前提下,如果某个 Share 的文件只可能位于一个磁盘中,那么 Exclusive Access 将自动启用。

要实现 Share 的文件只属于一个磁盘中,需要满足一下条件:

  1. Share 只启用了一个 Storage,没有 Secondary Storage
  2. 如果 Share 是存储在 Array 中的,在设置 Include disks 时只能选择一个磁盘;
  3. 如果是事先已经存在的目录,那么在满足上两条的情况下,还需要通过适当的 Mover 动作,将数据迁移到单一磁盘中。(注意如果之前 Share 是存在多个磁盘中时,做前两条修改,并不会自动将数据迁移到目标磁盘)

在本文的场景中,我们需要在 appdata 这个共享目录上启用 Exclusive Access,就可能需要进行必要的 Mover 操作。如果你看到 appdata 的 Share settings 页面中显示了如下 Exclusive access: no 的文字,进行如下操作:

  1. 出现这个状态,说明你的 appdata 并不完全位于缓存中,有一部分是位于阵列中的,此时我们需要让 Mover 将数据从阵列中移动到缓存中。注意:
    1. 如果在现在的状态下,你的 appdata 是 Cache only 的,但是你仍然看到 Exclusive access: no,这是因为曾经某个时刻你为 appdata 设置了阵列存储的选项(以老版本的术语来说,就是 use cache 不是 only,而是 yes 和prefer 的选项),那么在那个时间段内,shfs 可能会将数据文件调度到阵列上。此时即便你讲 appdata 设置为 cache only,Mover 也不会自动将文件从阵列迁移到缓存。修改共享目录的这些相关设置只会影响新文件,而不会直接影响已有文件。
    2. 在上面这种情况下,你需要首先恢复缓存-阵列两级缓存架构,然后将 Mover 行为设置为 prefer cache(在 6.12,0 的属于下,是将 Mover action 设置为 Array -> Cache。然后运行 Mover 完毕。这时 Mover 会将还停留在阵列上文件移动到缓存(当然前提是的缓存的空间要够)。
    3. 完成上面的操作之后,我们再将 appdata 设置为 cache only 的形式(Primiary Storage 为缓存,无 Secondary Storage),这时我们应该能够看到 Exclusive access 变成了 yes。

3 结语

上面的这套操作,能够极大改善 Nextcloud 在 Unraid 上的表现。其他的应用也会有很大的改观(例如 Jellyfin 的打开和搜索速度会有质的提升)。

Unraid: 解决 12 版本中网页界面无法打开的问题

17 August 2023 at 10:36

最近在 Unraid 升级 6.12.x 版本以后会偶发性的产生一个 Bug,其表现为 Unraid 网页无法打开(这里是指访问 Unraid IP 地址无法访问 Web 管理界面)。这个问题实际上是由于 Unraid 上的 Nginx 服务失去响应导致的。这种情况下我们只需要干掉 Nginx 进程并重启就可以了。

首先通过 ps -aux | grep nginx 找到 Nginx 主进程的 PID,然后通过 kill -9 <PID> 干掉进程。要再次启动 Nginx,可以运行

1
2
cd /etc/rc.d/
./rc.nginx start

注意通过 ./rc.nginx restart 在正常情况下可以重启 Nginx 服务,但是在 Nginx 处于无法响应的状态时,这个脚本无法杀死 Nginx 进程,必须要手动终止。

Ubuntu 中如何彻底删除一个用户

16 August 2023 at 18:51

在 Ubuntu 中彻底删除一个用户涉及到几个步骤,不仅仅是删除用户,还可能包括删除与用户相关的数据。以下是步骤指南:

  1. 确保用户未登录: 在删除用户之前,最好确保该用户未登录。你可以使用 who 命令来检查哪些用户当前已登录。

    1
    who
  2. 删除用户: 使用 userdel 命令删除用户。如果你还希望删除用户的主目录和邮件池,可以使用 -r 选项。

    1
    sudo userdel -r username

    其中,username 是你想删除的用户的用户名。

    注意:-r 选项会删除用户的主目录(通常是 /home/username)。确保你已经备份了所有重要的数据!

  3. 检查文件系统: 即使删除了用户和其主目录,可能仍然在文件系统上遗留一些属于该用户的文件。你可以使用 find 命令来搜索这些文件:

    1
    sudo find / -user username

    这将列出所有属于 username 的文件。根据你的需要,你可以手动删除这些文件或更改它们的所有权。

  4. 删除用户的 cron 作业: 如果用户配置了任何 cron 作业,你还需要手动删除它们。检查 /var/spool/cron/crontabs/username 是否存在,如果存在,删除它。

    1
    sudo rm /var/spool/cron/crontabs/username
  5. 其他服务或配置: 如果该用户有其他特定的配置,例如在 /etc/sudoers 中的条目或在其他服务中的特殊访问权限,你需要手动检查并删除它们。

请在进行任何删除操作之前确保备份所有重要数据。确保你明确知道正在执行的操作,避免意外删除重要文件或配置。

读论文: Segment Anything

11 May 2023 at 16:32

这是 Meta 最近发布的一篇挺火的论文,标题就是 Segment Anything,也可以称之为 Segment Anything Model,简称 SAM。这个模型的野心很大,试图建立起一个“大一统”的分割模型,可以对任意类别的元素完成像素级分割;

为了达成这个任务,作者建立了一个极为庞大的数据集,包含了 1100 万个样本上的 10 亿个 Mask。这个数据集的名字叫做 SA-1B。作者还引入了一个 promptable segmentation 的概念,即我们可以通过输入文本来控制被 Segment 标记出来的像素集合的类别,如上图 (a) 所示。

参考大语言模型中在近期取得突飞猛进的进展,如果在图像领域也构造出足够强大的通用模型成为一个新的热点。本文的目标就是为图像分割任务训练一个基础的通用模型。另一个重点是将图像分割筒 Prompt 工程集合起来。

Weekly-86

23 April 2023 at 16:10

1 头条

1.1 政经

  • 中国2023年一季度GDP284997亿元,同比增长4.5%,超出市场预期的4%;一季度GDP比上年四季度环比增长2.2%。1

  • 中国驻法国大使卢沙野4月21日在访谈中有关“前苏联国家不具独立国家的主权地位”等言论引起欧美国家广泛挞伐。中国外交部发言人4月24日被问及是否认可其言论时称,“中方尊重苏联解体后各加盟共和国主权国家地位”。2

    • 不就是口嗨么?谁不会啊~

  • 孟加拉国与俄罗斯同意以人民币结算核电站项目3亿美元款项。3
路布尔核电站项目 图自《华盛顿邮报》

1.2 科技

  • 星舰发射失败。美国当地时间20日8时许星舰发射,但在发射3分钟后,“超重型推进器”部分似乎未能分离,在高空发生爆炸。这是 SpaceX 星舰的首次发射。4

  • Dota2 发布 7.33 “大展宏图”更新。本次更新重做了地图。5

2 其他

  • 苏丹冲突持续超过一周,各国加紧撤侨行动。6

    • 苏丹武装部队宣布夺回陆军司令部控制权 已从相持转向清剿。7
  • 最近中文网络流传一种说法,称俄罗斯开始实施电子征兵令,当一个俄罗斯青年征兵码变红时,就需要前往征兵处报道。8 但澎湃新闻考证此事可能为假9

  • 北京长峰医院住院部东楼发生火情,21 人死亡。10


  1. 一季度中国GDP增长4.5%:经济如期转入回升轨道,消费有继续修复空间↩︎

  2. 卢沙野有哪些引起西方震惊的言论?↩︎

  3. 孟加拉国与俄罗斯同意以人民币形式结算核电站项目款项↩︎

  4. 评论丨SpaceX“星舰”发射失败,但马斯克并没有输↩︎

  5. DOTA 2 迎来 7.33“大展宏图”游戏更新:推出全新地图↩︎

  6. 苏丹冲突持续超过一周 各国加紧撤侨行动↩︎

  7. 苏丹武装部队宣布夺回陆军司令部控制权↩︎

  8. 俄罗斯的红码来了!征兵码变红的俄青年排队入伍,不去将寸步难行↩︎

  9. 明查|俄罗斯祭出“征兵码”?被赋红码青年在征兵处排长队报到?假↩︎

  10. 情况通报↩︎

Weekly-85

14 April 2023 at 22:47

时隔很久还是恢复 Weekly 的习惯!

1 头条

1.1 政经

  • 巴西总统卢拉访华:时隔 12 年再次担任巴西总统的卢拉于 12 至 15 日展开访华之旅,重点关注经贸领域的双边合作,以及和平解决乌克兰战争等方面的议题。1
    • 中华人民共和国和巴西联邦共和国关于深化全面战略伙伴关系的联合声明(全文)2

  • 美国机密文件泄露事件嫌疑人被捕,为一名 21 岁的空军国民警卫队成员 杰克·道格拉斯·特谢拉,他涉嫌泄露含有一系列国家安全机密的数十份高度机密文件,其中包括美国能够对俄罗斯进行的监视范围。3

    • 《纽约时报》称,这些文件还透露出华盛顿也一直在监视乌克兰、以色列和韩国等亲密盟国的领导人,这令美国与盟国的关系变得复杂。4
  • 德国外长贝尔伯克访问中国。4月13日,德国外交部长贝尔伯克抵达中国港口城市天津,开始她为期三天的中国之行。5

  • 传言美国海军“里根”号航空母舰发生严重核泄漏事故,目前“里根”号。6

近日,一则美国海军“里根”号航空母舰反应堆发生核泄漏事故的消息,突然在社交媒体上传开了,爆料人是一个叫“ALEX H”的推特用户,自称是“里根”号上的水兵。
  • 4 月 14 日晚,中国在境内实施了一次陆基中段反导拦截技术试验,试验达到了预期目的。7 这相信是2010年1月以来,中国官方第六次公开证实陆基中段反导拦截技术试验,并宣布试验取得成功。不过,有军事学者认为,中国实际试验的次数远不止这些。8

1.2 科技

  • 微软宣布开源 Deep Speed Chat,帮助用户轻松训练类ChatGPT等大语言模型。据悉,Deep Speed Chat是基于微软Deep Speed深度学习优化库开发而成,具备训练、强化推理等功能,还使用了RLHF(人工反馈机制的强化学习)技术,可将训练速度提升15倍以上,成本却大幅度降低。例如,一个130亿参数的类ChatGPT模型,只需1.25小时就能完成训练。9

2 其他

  • 在美国南部德克萨斯州的一个奶牛场发生爆炸并引起火灾后,统计有超过18000头奶牛死亡、一名牧场工人受重伤,这是当地有记录以来最严重的牛舍火灾。10
  • 苏丹非法军事组织快速支援部队星期六(4月15日)称,他们已占领总统府、陆军总司令布尔汉将军的住所、喀土穆机场和北部的麦洛维军事基地。11
    • 苏丹首都发生激烈交火,已经导致 25 人死亡,至少 183 人受伤,邻国决定关闭边界。12

  1. 巴西总统卢拉访华:经贸合作、乌克兰战争、一带一路是重点议题↩︎

  2. 中华人民共和国和巴西联邦共和国关于深化全面战略伙伴关系的联合声明(全文)↩︎

  3. 美国机密文件泄露事件嫌疑人被捕,为空军国民警卫队成员↩︎

  4. 五角大楼“泄密门”惹恼多个盟国,凸显美国对其他国家安全的破坏↩︎

  5. 德国外长贝尔伯克抵达天津↩︎

  6. 突发!美国海军“里根”号航空母舰发生严重核泄漏事故,面临报废?到底发生了什么?↩︎

  7. 中国成功实施陆基中段反导拦截技术试验↩︎

  8. 中国再次宣布成功实施陆基中段反导拦截技术试验↩︎

  9. 重磅!微软开源Deep Speed Chat,人人拥有ChatGPT!↩︎

  10. 美国德州一奶牛场爆炸 18000头牛死亡↩︎

  11. 苏丹非法军事组织称已占领总统府等多地点↩︎

  12. 苏丹首都发生激烈交火,已致 25 人死亡至少 183 人受伤,邻国决定关闭边界,具体情况如何?↩︎

读论文:一种音频事件识别方法

12 April 2023 at 15:43

本文的标题是 Adaptive Multi-scale Detection of Acoustic Events。这是清华大学电子系何亮老师团队在 2019 年的工作。

声音事件检测(AED或SED)的目标是预测给定音频段中目标事件的时间位置。这项任务在安全监测、声学预警和其他场景中起着重要作用。然而,数据不足和声学事件来源的多样性使AED任务成为一个艰巨的问题,特别是对于普遍采用数据驱动方法的情况。本文从分析声学事件的时频域特性入手,表明不同的声学事件具有不同的时频尺度特征。受到这个分析的启发,我们提出了一种自适应多尺度检测(AdaMD)方法。通过利用沙漏神经网络 (hourglass neural network) 和门控循环单元(gated recurrent unit, GRU)模块,我们的AdaMD在不同的时间和频率分辨率下产生多个预测。随后采用自适应训练算法,将多尺度预测相结合以增强整体能力。在2017年声学场景和事件的检测和分类(DCASE 2017)任务2、DCASE 2016任务3和DCASE 2017任务3上的实验结果表明,AdaMD在事件错误率(ER)和F1分数的指标上优于已发表的最先进竞争对手。我们收集的工厂机械数据集上的验证实验也证明了AdaMD的抗噪能力,提供了实际应用的证明。

1 Introduction

AED 问题的主要挑战:

  1. 数据极度不均衡;
  2. 事件具有多样化的特征;
  3. 时运频域尺度不一:这句话说的是不同的时间具有不同的时域长度,以定长音频输入为基础的检测模型的有效性存在问题;

AdaMD 的网络架构如上图所示。此网络由一个 CNN 网络和一个 RNN 网络组成。其中,CNN 部分的网络架构被称为 Hourglass,这种网络架构在计算机视觉领域被广泛应用于关键点检测,其优势在于其可以在多种时频分辨率的条件下进行特征提取。在 RNN 部分,作者采用了 Gate Recurrent Unit (GRU) 模块来处理 CNN 模型输出的每个通道,从而处理时域信息。GRU 的输出会经过一个上采样流程的处理,使得各个通道的输出具有相同的尺寸。

2 声音事件检测任务的类别

  1. 多事件检测:同一时间段内有多个事件发生,检测模块除了检测是否有事件发生,还需要给出事件的类别。
  2. 弱监督事件监测:理想情况下,标注数据应该包含事件的类别和起止时间。但是这样标注工作会比较多,如何在只标注的类别的情况下,让模型学习出事件的起止时间是一个挑战,目前这个问题巨解决的还不是很好;
  3. 异常事件检测:如何检测我们不知道的异常事件?

读论文:CADTransformer

11 April 2023 at 14:42

这次要读的文章是 CADTransformer: Panoptic Symbol Spotting Transformer for CAD Drawings

这篇论文介绍了一个名为CADTransformer的新框架,用于自动化CAD图纸中的全景符号识别任务。该任务需要识别和解析可数对象实例(如窗户、门、桌子等)和不可数的物品(如墙壁、栏杆等),并在CAD图纸中进行标记。该任务的主要难点在于图纸中符号的高度不规则的排序和方向。现有方法基于卷积神经网络(CNNs)和/或图神经网络(GNNs)来回归实例边界框并将预测结果转换为符号。相比之下,CADTransformer直接从CAD图形原始集合中进行标记,通过一对预测头同时优化线条级别的语义和实例符号识别。此外,该框架还通过几个可插拔的修改增强了主干网络,包括邻域感知自注意力、分层特征聚合和图形实体位置编码等。此外,该论文还提出了一种新的数据增强方法,称为随机层(Random Layer),通过CAD图纸的分层分离和重组来进行数据增强。最终,CADTransformer在最新发布的FloorPlanCAD数据集上,将先前的最先进水平从0.595提高到0.685,展示了该模型可以识别具有不规则形状和任意方向的符号。

1 Introduction

1.1 关于 CNN 网络的准确性问题

在 CAD 文件中对图元要素分类的任务在本文中被称为 Symbol Spotting。这类任务在建筑行业和其他工业领域都要广泛的应用前景。Symbol Spotting 任务分类的目标是具有几何特征或更多异质特征的图元要素,不同的是,传统的基于计算机视觉,尤其是基于 CNN 网络的图像分割任务,其分割的目标元素是同质化的像素。由于存在遮挡、非均匀聚集、制图风格差异等原因,Symbol Spotting 任务具有较大的难度。

传统的 Symbol Spotting 采用的是一种名为 Query-by-example,望文生义地来看,这些方法需要提前指定特定的符号构成,然后以匹配的方式寻找图面中与之类似的元素。这种方法显然无法适应现实中复杂多变的画图方式。近年来也涌现了一些基于机器学习的方法,他们提出使用 CNN 方法来解决 Symbol Spotting 问题。但是将 CAD 矢量数据本身转化成位图天然就引入了精度损失,这使得这些方法存在先天性不足。

以建筑行业为例,建筑 CAD 图纸具有“高对比度”(或者说“高灵敏度:)的特点,在一个 100 米乘 100 米的平面空间内,在围观层面上要求达到 1 毫米的精度。为了保持这个精度,我们必须将其转化成 10 万像素乘 10 万像素的位图,这个尺寸的图像处理起来代价过于高昂。而如果减少像素数量,则无法保持 1 毫米的精度。

这篇论文的团队在之前的发表的论文中提出了 FloorPlanCAD 数据集。基于这个数据集可以训练能够进行 Panoptic Segmentation Task 的模型。这类任务是综合了实例分割和语义分割的复合任务。在 CAD Symbol Spotting 场景,这意味着需要同时得到图元级别的类型归属和具有语音含义的每个建筑构件的识别。

本文的作者通过引入图卷积神经网络 (GCN) 来解决 Symbol Spotting 问题。GCN 网络可以给出每个图元的语义信息,同时还有一个 CNN 网络来给出“可数”构件的实例分割结果。

1.2 Transformer 的作用

基于 Transformer,可以在不预先定义图连接结构的情况下,基于注意力机制推断到 tokens 之间的全局关系 (Transformers reason global relationships across tokens without pre-defined graph connectivity, by instead learning with self-attention)。这使得 Transformer 可以在 panoptic symbol spotting 任务中替代 GCN 的作用。但是标准的 Transformer 在这类任务上的使用仍然存在一些挑战:

  1. Tokenization and position encoding of graph symbols:前者是编码的问题,后者则是典型的 ViT 引入的问题。典型的 ViT,即 Visual Transformer 会将每个图像划分成 或者 个 Patch,即 token,同时基于这个二维网格分布可以将这些 token 的顺序线性化,并组合在一起。不过在 CAD 数据中,图元数据的排列是无序的,且坐标数值属于连续的实数空间,并非离散化,这和栅格化的图像有根本性的不同。
  2. Inmense set of primitives in certain scenes. ViT 的全局注意力机制的复杂度相比于 token 的数量是四次方的,如果是处理 CAD 数据,由于图元的数量非常庞大,这个数量级的复杂度是无法处理的。
  3. Training data limitations. ViT 架构能够提供更多灵活性的同时,其对数据的需求量也更大了。

作者提出的 CADTransformer 旨在成为一个通用的框架,可以被轻易地和现有的 ViT 骨干网络整合起来。

CADTransformer 是作为一个完整的 ViT 处理 Pipe 的一部分被插入到图像处理流程中。

2 Methodology

2.1 Overview

一般意义上的 Panoptic symbol spotting 任务可以表述为 ,其中 表示一个基础图元。 分别是此图元的语义标签与实例索引。对于输入的 CAD 矢量图纸,我们首先将其拆解成基础图元要素(弧线、多段线等),并将图纸转换成栅格化后的位图。图元的嵌入的获取方式是将每个图元投影到一个预训练的 CNN 网络输出的二维特征图上。基于这个这样获取的嵌入,使用一个标注你的 Vision Transformer 以及作者提出的可插拔的附加网络,模型可学习到不同的 Token 之间的关系。Transformer 层后面跟着的是 Two-Branch heads 结构,我们可以通过这个结构来优化图元级预测的精度。本文提出的模型的整体结构如下图所示

具体而言,图元 Embeding 获取的方式是将图纸的栅格化图像输入 HRNetV2-W48 这个预训练网络。此网络是一个多级多分辨率的网络,我们取不同分辨率的分支,拼成一个 的张量。对于每个几何图元,我们将其中心点投影到这个张量的第二、三个维度张成的平面上,去第一个维度的特征值构成特征向量。这样最终得到所有 token 的 Embedding 就是 ,其中 是图元的数量, 是 Embedding 的维度。

2.2 Two-Branch heads

如前文所述,Two-Branch heads 提出的目的是优化预测精度。TBH 层以 Transformer 骨干网络输出的几何 Embedding 为输入。

2.2.1 语义分割

对于聚合后的 Transformer 特征输出 ,施加一个 MLP 层,可以生成一个语义分数 。损失函数使用交叉熵

2.2.2 实例分割

CAD 图纸场景中的实例分割结果无法简单地用 BoundingBox 来表示。作者提出了一个新的方法,不是在像素图像中预测 2D BoundingBox,而是针对每个图形实体预测一个偏移向量,以便将实例实体聚集在一个公共的实例中心周围。为了更好地将实例相关的图元聚类在一起,作者使用一系列的 MLP 来编码图元的 Embedding,进而产生 个偏移向量 。针对这个偏移量的训练使用 损失函数。

其中 是一个指示变量组成的向量,只有可数构件的图元参与这个计算。在经过这个 Offset 量处理之后,可以用聚类算法对偏移后的图元中心点进行聚类。

2.3 Improve Transformer Backbone Design for Better Spotting

ViT 骨干网络以上述编码层的输出,即 Tokenization Module 的输出作为输入,执行自注意力机制,并生成送往 Two-Branch Head 网络的特征表示。

2.3.1 Self-Attension with Neighbor Awareness

典型的 ViT 的架构由多个 Transformer 层组成,每层包含一个 self-attention 模块和一个前馈网络(Feed-Forward Network) 。自注意力机制的计算方式为:

其中 分别是 query, key 和 value 矩阵, 表示样本数量和隐藏层维度。

然而,图纸中图元的数量非常多,因此全局注意力复杂度太高。为了解决这个问题,作者提出以下改进方法。

2.3.2 Self-attention with k Neighbors

让注意力机制的计算只在图元最邻近的 k 个邻居之间进行计算,这样可以限制复杂度随着图纸规模增长的速度。这里的临近关系通过计算起始点和终止点的距离来实现。

其中 表示图元 的起始点和终止点, 是目标图元, 表示其他图元。增加注意力机制计算中考虑的临近的图元数量可以增加感知的范围。作者提出的 Transformer 主干网络划分成了若干个阶段,每经过一个阶段会提提高 的值。

2.3.3 Multi-resolution Feature Fusion

我们将不同阶段得到的特征输出整合起来,令

2.3.4 Graphic Entity Position Encoding

由于图元数据天然来自于二维欧式空间,因此可以的比较容易的定义位置编码:

2.4 Random Layer Augmentation

本章节介绍的是一种数据增强策略。这个策略很简单,就是利用 CAD 图纸的图层结构信息,将图层划分为是哪个类别,分别是 Thing layer, Stuff layer 和 Background Layer。我们让这些层内随机挑选出的若干层组成新的图纸。

解决在 Openvpn 客户端中部署的 Web 服务不可用的问题

4 April 2023 at 17:32

遇到这样一个蛋疼的问题,我有一台服务器,上面部署了一个 web 服务,同时我也想把这个服务器作为客户端连入一个 OpenVPN 虚拟网络,并且我希望服务器上的程序能够通过 OpenVPN 的网关来访问外部网络,这主要是为了隐藏服务器的身份。但是在 OpenVPN 连接之后,原有的 Web 服务将无法访问。通过调试分析可以法线,造成这一现象的原因是 Web 服务的响应包也被路由配置路由到 OpenVPN 的 tun0 接口中。这导致响应无法返回给原来的请求服务器。如何解决这个问题呢?

如果你在网络上搜索这类问题你会发现各种文章给出的方案一般都是为 OpenVPN 的配置文件添加 route-nopull 选项来阻止 OpenVPN 设置客户端路由,但是这会导致客户端的对外访问无法通过 VPN 进行。事实上,从 IP 路由的角度来看,如果我的服务器是 A,而某个访问 Web 服务的 IP 是 B,同时 B 也可能是 A 试图访问的外部的目标服务提供者。对传输层路由而言,A 给 B 的 Web 服务响应和 A 主动发往 B 的请求是无法区分的。从这个角度来看,要达成 inbound 和 outbound 流量分别路由似乎是不可能的。

但是其实我们有一个突破点:那就是 Web 服务总是在本机使用固定的端口和请求者通信,我们可以通过端口来区分 inbound 和 outbound 流量。首先我们输入下面的命令:

1
iptables -t mangle -A OUTPUT -p tcp -m multiport --sports 80,443 -j MARK --set-mark 1

在这个命令中,我们会让路由表观察从本地的 80 和 443 端口发出的包,并给这些包打上一个特定的标签(--set-mark 1 的作用)。然后输入下面的命令:

1
ip route add default via [gateway addr of eth0] dev eth0 table 100

这个命令会创建一个新的路由表 table 100,然后在这个表中加入一个默认路由,将流量送往原有的网关。然后再运行下面的命令:

1
ip rule add fwmark 1 table 100

这个命令会让带有标记 1 的包使用 table 100 路由表进行路由。运行完上面的配置后,web 服务就可以正常访问呢了。


以上方法是在 ChatGPT 的协助下摸索出来的,我的提问是:

如何通过 iptables 或者 route 命令,让访问 80 与 443 端口的包使用 tun0 接口,而访问其他端口的包使用 eth0 接口?

GPT 的回答是

可以使用iptables和route命令结合起来实现此目的。具体操作如下:

首先需要添加一条规则,将访问80和443端口的包标记为一个特定的标记值(比如"1"),使用tun0接口发送出去。iptables规则如下:

1
iptables -t mangle -A OUTPUT -p tcp -m multiport --dports 80,443 -j MARK --set-mark 1

接着需要添加一条规则,将标记为"1"的包使用tun0接口发送出去。route命令如下:

1
2
ip route add default dev tun0 table 100
ip rule add fwmark 1 table 100

最后需要添加一条规则,将所有未被标记的包使用eth0接口发送出去。route命令如下:

1
ip route add default dev eth0

以上三个步骤分别实现了:将访问80和443端口的包标记,并使用tun0接口发送出去;将标记为"1"的包使用tun0接口发送出去;将所有未被标记的包使用eth0接口发送出去。这样就实现了访问80和443端口的包使用tun0接口,访问其他端口的包使用eth0接口的效果。

介绍一种"新"的隧道技术: spiped

2 April 2023 at 16:47

在之前的文章SSH隧道:访问翻墙服务器的临时性手段 的时候介绍了一种使用 SSH 隧道进行端口映射,进而进行手动的流量混淆的技术。就稳定性来说这个方案是经受了考验的。在过去几年里,无论任何时候 SSH 隧道都是能够提供稳定可靠的链接的。不过,SSH 隧道的性能是一个问题。根据 Stackoverflow 上的 这个问题下的回答 来说:“当你通过TCP隧道传输TCP时,性能问题就会出现,因为您有两个层执行自适应纠正”。在这篇文章里面我介绍一个较新的方案。

1 简介

新的方案设计一个新的工具:spiped(github)。spiped是一种安全的、轻量级的加密通信工具,可用于在不安全的网络中传输敏感数据。它的设计目标是提供简单、易于配置和使用的加密通信,而不需要复杂的密钥管理或公钥基础设施。spiped使用对称密钥加密算法,可以在客户端和服务器之间建立加密通道。它可以用于保护各种类型的数据流,包括SSH、Telnet、HTTP和SMTP等协议。spiped的另一个优点是它可以在不同的平台上使用,包括Linux、FreeBSD、OpenBSD和Windows等操作系统。

2 使用方法

spiped 的使用非常简单。在 Ubuntu 上可以通过 apt 来安装。也可以自行编译。在完成安装以后,我们首先进入服务端,运行下面的命令生成一个秘钥:

1
dd if=/dev/urandom bs=32 count=1 of=keyfile

其中 keyfile 是生成的秘钥文件,注意将其保管到合适的位置。然后用下面的服务启动 spiped 服务端服务:

1
spiped -d -s '[0.0.0.0]:8025' -t '[127.0.0.1]:25' -k /path/to/keyfile

注意,不同于 ssh 使用同一个 TCP 链接来走所有的隧道流量,spiped 会为每个流创建一个独立的 TCP 链接。因此在做代理中介使用时,spiped 可能会需要创建数量较多的 TCP 链接。spiped-n 配置可以指定最大链接数量,这个数值默认是 100,对于代理应用来说这是不够的。你可以将其设置为 10000。

其中 -s 后的地址是服务端的监听地址,-t 的转发的目标地址,-d 表示将加密的流量从 -s 后的地址解密后发往 -t 后面的地址。这里我们可以填写 Shadowsocks 服务监听的地址。注意 Host 可以保持成 127.0.0.1 的形式。这样,我们可以在防火墙中把 Shadowssocks 的公网监听端口关闭,仅允许从加密隧道访问,这样可以提升安全性。

上面的命令在输入后会自动进入 Daemon 模式运行。

然后我们来到客户端(一般是跳板机),将服务端生成的秘钥文件下载到客户端,运行

1
spiped -e -s '[127.0.0.1]:25' -t $SERVERNAME:8025 -k /path/to/keyfile

其中 $SERVERNAME 是服务端的 IP 地址。注意用 [] 包裹的形式来填写。如果这里说的客户端也是 Shadowsocks 的机器,那么这里的源地址(-s 后的地址)可以写 127.0.0.1,但是如果是跳板机,那么应该写成 0.0.0.0

在上面的设置中,方括号内可以写入 IPv6 地址,但是注意 IPv6 的全网段监听地址就不能写 [0.0.0.0] 了,而是要写 [::]

客户端的程序也是会自动进入 Daemon 模式运行。

完成设置之后,访问客户端(跳板机)的 25 端口就相当于直接访问服务器的 25 端口。

3 后记

请注意,spiped:

  • 需要一个强密钥文件:通过-k选项指定的文件应该具有至少256位的熵。(dd if=/dev/urandom bs=32 count=1是你的好朋友。)
  • 需要来自/dev/urandom的强熵。(确保您的内核的随机数生成器在引导时被种子化!)
  • 不提供任何防止通过数据包计时泄露信息的保护:在spiped上运行telnet会保护密码不会被直接从网络中读取,但不会掩盖输入节奏。
  • 对于交互式会话,可以显着增加带宽使用:它以1024字节的数据包发送数据,并将较小的消息填充到此长度,因此如果不能与相邻字节合并,则1字节的写操作可能会扩展到1024字节。
  • 使用对称密钥——因此,任何可以连接到spiped“服务器”的人也可以冒充它。

如何使用 Proxifier 来进行流量代理控制

30 March 2023 at 22:33

Proxifier 是一款网络工具软件,可以帮助用户通过代理服务器进行网络访问,实现匿名化、翻墙、加速等功能。支持多种代理协议,规则管理,流量监控和日志记录等功能,提高网络连接的灵活性和智能化。注意 Proxifier 本身并不是一个代理软件,也不是一个 VPN 软件,其作用是控制其他其他软件访问代理的方式。

上一篇文章我提到了可以试用 Proxifier 来解决通过 Shadowsocks 代理访问呢 ChatGPT 频繁出现 Something Went Wrong 的问题,在本文中我们来展开讲一下具体的做法。

1 准备工作

1.1 软件下载

Proxifier 的官方网站是 https://www.proxifier.com/,其正版价格较贵,约 40 美元。不过,较早的 2.x 版本存在破解版。读者可以根据自己的经济状况来选择。

1.2 代理准备

如同开头提到,Proxifer 本身不是一个代理程序,读者应该自己提前准备好代理。这些代理中,无论是 Shadowsocks、V2Ray 还是其他常见的代理程序,都会在客户端本地重建一个 Socks5 代理,这一般都可以在代理客户端程序的配置或者服务器设置中看到。下图给出的是 Shadowsocks-NG 客户端在 macOS 中的系统配置,可以看到程序在本地 (localhost) 的 1086 端口创建了一个 Socks5 代理。

这个代理会被进一步转成 http 代理,从而被浏览器使用,这个转换过程是导致 ChatGPT 链接不稳定的元凶。接下来我们要使用 Proxifier 让 ChatGPT 直接使用 Socks5 代理。

2 设置 Proxifier

2.1 添加代理

我们以知乎上这篇文章为范本简要讲一下设置过程。首先我们需要将现有代理软件提供的 Socks5 服务告知 Proxifier,这通过添加代理服务器实现。

注意一般各种代理软件在本地创建的代理都是无需验证的,我们可以取消验证启用。输入设置之后,可以点击检查确认设置是否正确。

3 设置规则

为了让 ChatGPT 的访问能够通过 Proxifier 访问代理,我们需要配置 Proxifier 的代理规则。

添加代理规则的页面如下:

我们有两种方法引导 ChatGPT 的网络访问:

  1. 我们可以选择下载 ChatGPT Desktop 客户端,然后通过代理规则中应用程序过滤规则找到 ChatGPT 程序,然后让目标主机为空(这意味着所有的目标主机都会服从本规则);
  2. 将应用程序部分置空,然后在目标主机中填入 ChatGPT 访问的域名。经过我的试验发现,填入 chat.openai.com; challenges.cloudflare.com; *.openai.com; 即可。

事实上,因为 Proxifier 能够全局性地处理所有网络访问,因此,第二种方法对于使用 ChatGPT Desktop 的用户来说同样有效。下图是完整代理规则。

至此我们就完成了设置,你可能需要重启浏览器才能能让 Proxifier 成功地引导 ChatGPT 的流量。

使用 Shadowsocks 访问 ChatGPT 频繁出现 Something Went Wrong 问题的解决方法

28 March 2023 at 18:16

update at 2023.4.27:

Github 上有人做了一个开源的油猴脚本 KeepChatGPT 可以解决这个问题。在浏览器上这个脚本可以正常使用,但是注入到客户端时(尽管内部仍然是一个浏览器)会出现错误。

update at 2023.4.12:

在 Windows 上这套方法存在一定的问题。可能的原因是 Proxifier 的代理权限有时候会被 Shadowsocks 客户端,也就是 Privoxy 争抢。

由于国情原因我们使用使用 ChatGPT 需要使用各种形式的代理,这时我们肯能会发现在与 ChatGPT 对话的过程中可能会出现下面的错误:

Something went wrong. If this issue presists please contact us through our help center at help.openai.com

这种错误一般在我们再让网页空闲一段时间(通常是一分钟)后再次提问时出现。当然,去 OpenAI 的帮助中心是不会搜到什么有用的信息的。如果你去检索这个问题,你能得到的最好答案是在出现这个页面之后刷新页面,然后回到对话窗口继续对话即可。但是刷新后如果再次出现空闲窗口的情况,这个问题会反复出现,因此你在对话中就不得不反复地刷新页面,再加上每次页面重载你都需要去勾选 Cloudflare 的机器人验证框,这就会极大程度上拖慢我们的速度,影响产品体验。

这个问题是否可能是由于我翻墙访问导致的呢?我使用的翻墙软件是 Shadowsocks,经过一番搜索,一个 Github Issue 的讨论给了我灵感。

讨论地址:https://github.com/shadowsocks/shadowsocks-libev/issues/2149

尽管讨论的题目和 ChatGPT 没有直接关系,但是这个回复提到,Shadowsocks 在构建代理管道时,会在客户端使用 Privoxy 来讲 Socks5 代理转换成 http 代理,进而被浏览器使用。Privoxy 的配置中包含了 Socket 连接时长方面的控制。于是我查看了我本地的 Shadowsocks 使用的 Privoxy 的配置文件,这个文件在 MacOS 中位于 /Users/lena/Library/Application Support/ShadowsocksX-NG/privoxy.config 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
toggle  1
enable-remote-toggle 1
enable-remote-http-toggle 1
enable-edit-actions 0
enforce-blocks 0
buffer-limit 4096
forwarded-connect-retries 0
accept-intercepted-requests 0
allow-cgi-request-crunching 0
split-large-forms 0
keep-alive-timeout 5
socket-timeout 60

forward 192.168.*.*/ .
forward 10.*.*.*/ .
forward 127.*.*.*/ .
forward [FE80::/64] .
forward [::1] .
forward [FD00::/8] .
forward-socks5 / 127.0.0.1:1086 .

# Put user privoxy config line in this file.
# Ref: https://www.privoxy.org/user-manual/index.html

可以看到 keep-alive-timeoutsocket-timeout 这两个选项设定的超时时间都比较低。我做了下面两个尝试:

  1. 将这两个设置调高,并重启 Shadowsocks,但随后发现 Privoxy 重置了设置文件;
  2. 我手动 kill 掉正在运行的 Privoxy 并且手动使用更新后的 config 文件调起一个新的进程,但是仍然没有解决 Something went wrong 的问题;

至此,我也不想和 Privoxy 纠缠下去,既然问题出在 Socks5 代理转 Http 代理的环节,我们可以使用 Proxifier 这个软件直接使用 Shadowsocks 提供的 Socks5 代理,就可以很好地解决这个问题。

Proxifier 的教程网上非常多,我就不在这篇文章里赘述了。

Proxifier 的使用参加我的文章:如何使用 Proxifier 来进行流量代理控制

读论文: 基于图卷积网络实现的三维室内场景构建

23 February 2023 at 17:24

这次阅读的论文是 SceneHGN: Hierarchical Graph Networks for 3D Indoor Scene Generation with Fine-Grained Geometry,这是今年(2023)年发表在 IEEE Transactions on Pattern Analysis and Machine Intelligence 期刊上的文章,作者来自中科院。

作者在这篇文章里面提出 SceneHGH 网络来解决三维室内场景生成的问题。这个模型统筹考虑了房间的空间立体关系与每个待放置物体的形态细节。因此这个模型可以以细致的结合细节生成房间布局。为了让学习过程更加可控,作者引入了功能区 (Function Regions) 这一中间概念来串联起房间与待放置实体。为了确保模型的合理性(Plausibility),作者使用的基于图的表示形式同时包含了链接不同层级的父子节点的垂直边与连接同级别子节点的水平边。

作者使用的生成网络是一个基于可变自动编码器 (variational autoencoder, VAE) 条件式递归神经网络 (conditional recursive neural network, RvNN)。在给定放假你的边界条件的情况下,此网络可以生成具有良好几何表达形式的房间排布。

1 Introduction

在此章节中的行业背景我们暂且不表,主要关注其场景构建与假设条件。

现有的针对三维室内场景的排布工作往往是部分的,例如大部分工作着眼于生成家具的排布,但是并未包含家具的几何结构细节,或者只能从有限的现有家具几何形状中有选择性地布置。但是家具布局和家具的几何形态二者在排布问题上是存在关联的,将这二者拆解成独立的两个步骤可能导致形成一些次优结构。本文提出的架构是一个端到端的架构,可以将布局与几何结构统筹起来考虑。

当然,这种统筹方法会存在很多挑战。一个房间内的各个要素构成了一个天然的垂直层级结构:房间内包含多个家具对象,每个家具对象也可以由多个灵活子组件构成。房间内的对象数量可能很多(3D FRONT 数据集中的一个房间最多可以包含 188 个对象),这使得学习会比较困难。考虑到在大型房间中,部分对象之间内部会存在一定的关联(例如若干个椅子围绕着一个桌子摆放),作者引入了 Function Regions 的概念,如用餐区,沙发区等。功能区概念作为一个中间层在房间与具体的对象之间,作为一个衔接桥梁。

在生成一个室内场景时,房间的形状会预先给定,作者将房间形状拆解为单元格,这个单元格的编码被视为是房间生成过程的一个输入条件。为了确保模型的合理性,需要利用对象之间的丰富关系信息。这些关系包括垂直关系:功能区必须位于房间边界内,对象必须要在功能区内部;以及水平关系:对象的对称性,邻接关系,对象内的组件之间的对称性等。因此,作者提出的分层图网络是一个基于 VAE 的 RvNN 网络,此网络具有 4 层,分别是房间、功能区、对象以及对象组件。在图中,作者精心设计了节点之间的边来体现约束关系。训练如此大的网络也是富有挑战性的,作者提出了一种多阶段训练策略来保证续联的稳定性和有效性。

作者结合了多个开源数据集,包括 3D-FRONT,PartNet 等。

2 Related Works

暂略

3 Overview

3.1 分层图表示

作者提出将三维室内场景表示为一个分层树结构,分为房间、功能区、对象以及对象组件四个层次。

室内场景中的三维对象由多个组件组成,作者将对象组织为其组件的 n-ary 分层树结构,此结构中包含了各个组件的几何表示及其连接关系。每个对象的编码会包含其组件的结构信息,及其布局信息。

功能区划分的示例

在整个场景结构树中,作者同时编码了不同层级之间的对象的丰富关系。除了天然了父子垂直关系之外,同一层的节点之间也有水平关系(可以是多对多关系,这种关系表示为图中的 hyper-edge constraints)。

3.2 分层图网络

作者提出的网络架构包括一个房屋布局编码器(room layout encoder),一个场景层次结构编码器(scene hierachy encoder)以及一个场景层次结构解码器(scene heirachy decoder)。其中房间布局编码器以房间边界的分解梯度表示(deformation gradients of the floor boundary)作为输入条件,并提取处一个向量。此向量被用于解码器的输入条件。场景分层编码器完成了从房间级别逐级向下到功能区级别,对象级别并最终到对象组件几何级别,逐层地、递归地将房间多层体系映射到的通用隐藏空间(类似神经网络的隐藏层?)中(这句话很难翻译,原文是 The scene hierarchy encoder maps the indoor scene hierarchies from the room level to the functional region level, the object level, and finally down to the object part geometry level into a common space hierarchically and recursively),解码器的作用正好相反。

在编码过程中,家具级别的对象首先用一个预训练的 DSG-Net 编码、然后结合对象的特征及其在区域中的位置信息,功能域级别的特征被提取出来。最终,房间级别的特征政和路所有区域级别的信息。解码操作的过程为编码的逆操作。在编解码器的训练过程中,若干图消息传递操作被用来提取对象之间的关联与约束关系。下图展示了在场景生成过程中不同要对象之间的二元关系以及多对象之间的关系(hyper-edge)。

4 分层图表示(Hierarchical Graph Representation)

4.1 分层节点拆解

考虑一个三维室内场景,作者提出如下的分层结构,共分为四层,分别是房间层级、功能区域层级、家具层级以及对象组件几何层级。其中对象层级与对象组件几何层级在 PartNet 中有已经引入,而功能区层级为本文的创新点。每个层级的节点定义如下:

区域:根据家具对象的功能作用,功能区将整个室内场景的对象划分成若干个组,这样可以提高一致性,并更有利于学习过程。不同类型的房间通常有多重不同类型的功能区域。显式地构建这些区域的语义概念不仅为场景生产结果提供了许多有意义的语义标签,同时有利于更好地组织每个房间中的大量对象。另外,由于 RvNNs 模型的能力限制(capacity limit),作者也无法将同一个房间内的大量节点直接作为单一房间对象的子节点。

在实现过程中,作者使用 DBSCAN 空间聚类方法将每个场景划分成若干个更小的功能区域。DBSCAN 算法是一种基于密度的无参数聚类算法,分蔟的数量是自适应的,不需要预先指定。通过 DBSCAN 算法啊,所有的对象可以根据其空间临近性质进行分组,这种空间临近的性质,通常与其功能性相关。每个功能区的功能定义由其中面积最大的对象决定。对于一些的显然不合理的划分,作者会提出算法进行人为修正。

总结来看,对一个三维场景 ,有 ,其中 表示第 个分蔟(功能区域), 意味着功能区域之间的水平连接。

对象:在我们将室内空间划分为多个功能区域之后,每个功能区域内会包含多个家具对象。每个对象的形状 由一个 Mesh 几何对象描述,描述信息还包括其语义类别,以及空间位置信息。我们给出正式定义: ,其中 表示功能区中的第 个对象, 表示对象之间的水平关系。

对象组件:作者是会用 PartNet 形状分层结构来讲每个三维几何形状划分成具有与含义的组件树,将其组织为一个 n-ary 分层树结构,改结构的不同层表示了不同精细度的拆解结构。具体而言,每个对象 被拆解为若干个部分 ,这些部分由分层结构 组织起来,不同的部分之间存在二元或者 n-ary 关系 。每个组成部分 具有一个预定义的语义标签和一个 Mesh 形式的几何表示 。组件之间的关系集合遵循了 StructNet 中的做法。

4.2 Edge Relationships

在一个场景分层结构中,我们考虑两类节点关系,分别是:

  1. 父子节点之间的垂直关系;
  2. 同级别节点直接的水平关系;

相比于之前的工作,本文在二元关系的基础上引入多元关系。这种超关系(hyper edge)可以让我们生成的室内场景更加真实合理。

一个非常类似的工作是 GRAINS。在这个工作中,作者用三个关系来描述对象和墙体之间的关系,分别是 supporting, surrounding 以及 co-occurence。不同于 GEAINS,我们在这里采用了更加精确的关系描述,包括二元关系: adjancency (),translational symmetry (), reflective symmetry (), rotational symmetry (), 以及多元关系 parallel collinearity . n-fold rotational symmetry ()。

4.2.1 垂直关系

垂直关系描述了父子节点知己的关系,包括:

  • 房间跟节点与功能区域之间的关系;
  • 每个功能区域包含多个家具对象;
  • 每个家具对象由多个组件组成;

此外,我们还额外引入了两个垂直关系:直接将房间跟节点和对象层连接起来:

  • : 一个对象的倾转 bbox 需要与房间的边界对齐 (the oriented bounding box of an object may have to align with the room boundary in some scenes.);
  • :每个生成的对象需要位于房间边界内部;

这种跨层垂直边可以提高生成结果的合理性。

4.2.2 二元水平关系

水平关系发生在同级别的对象之间,用来描述具有同一个父节点的兄弟节点之间丰富关系与约束。基于形状生成领域的已有工作启发,我们定义了四种二元关系:adjancency (),translational symmetry (), reflective symmetry (), rotational symmetry ()。对于邻接关系,我们定义两个组件邻接,如果他们的最小间距低于 ,其中 为两个组件的外接圆的平均半径。对于对称关系,作者遵循一个已有工作来定义 。这些二元边于训练输入时在对象层级自动完成。注意两个节点之间可能存在多个二元关系。、

4.2.3 N-ary hyper-edges

二元关系还不足以充分描述复杂对象布局。例如在下图右侧,除了考虑的对象之间两两关系,最好能够考虑四把椅子环绕桌子布置的的多元关系。又如下图中右图,两个床头柜和床的方向线之间应该保持平行。

本文中我们引入两种多元关系:

  1. n 重旋转对称
  2. 平行关系:

多元关系发生一个功能区内的对象层层级。如果我们在任意两个对象之间构建二元关系(用以替代多元关系描述),这会形成一个致密网络,这对训练过程不利。

N 重旋转对称;考虑 个对象 ,如果满足下面的关系则称他们构成 N 重旋转对称关系:

其中 表示两个对象之间的 Chamfer 距离 表示旋转矩阵。 为旋转中心点, 为旋转角度。 为一个常量阈值。

平行共线性 考虑 个对象 ,如果他们满足下面的套件则称他们满足平行共线关系:

  • 所有对象的二维倾转 bbox 的主轴方向彼此平行(在二维面内);
  • 满足下列等式:

其中 为任意向量,而 为任意非负实数, 的倾转 bbox 的中心点 为以常量阈值。

5 分层图网络

如前文所属,本文中采用的网络是基于 VAE 的 RvNN 网络。SceneHGN 网络采用场景分层树 (从房间层级到对象组件层级) 作为输入,房间的边界 作为额外的输入条件。 需要通过一个编码器 映射到特征向量的 也经过一个编码器 映射到一个隐藏向量 。然后这两个向量被拼接起来,送入解码器 来重建 。为了训练 VAE 生成模型,我们使用 KL 散度增加了一个正则量,将所有场景的隐藏层空间映射到一个标注你的高斯分布。

由于我们是从 DSG-Net 中获取对象组件层级,因此编码器 $Enc_{PG} 和解码器 遵循了 DSG-Net。 利用组件的几何信息 ,其中 为对象 的每个组件的 deformation gradients 表示形式, 为其中心点,以及结构信息 ,将其映射到一个隐藏的特征空间 。解码器的作用阈值相反,它将特征向量映射回来

5.1 Room Boundary Layout VAE

FloorNet 的结构如下图所示。任意一个闭合的房间轮廓可以使用一个 deformed 2D unit square boundary 来表示。在本文中,一个单元方形边界由 596 个顶点和 596 个边组成。

Floor VAE。为了显示方便,图中的房间轮廓包含了 8 个点和 8 个边

为了高效地、精确地编码房间边界,尤其是变现处管教,我们使用了 2D non-rigid registration 技术,而非顶点坐标体系来表示一个 2D mesh。然后我们在这个 registered 2D mesh 上计算 ACAP deformation graidents。对于每个定点,我们最终得到 6 维特征 以及三维特征 (分别表示了缩放、剪切以及旋转)。

这段话有点东西啊,得找相关文献看看。

然后 矩阵被送入 FloorNet 编码器。FloorNet 的关键组件是一个图卷积网络。我们将整个 registered floor boundary 视为一个环装图,并在此基础上进行卷积操作来提取 2D mesh 的特征。在两轮卷积操作后,特征会通过一个 MLP 并输出一个隐藏层向量。我们执行两轮迭代,来将信息传递的给临近顶点,从而学习到邻接边的夹角(前面提到的着重学习拐角信息)。解码器的结构与编码器的相反。

5.2 Scene Hierachy Encoder

这个编码器包含两个部分,分别是一个递归编码器 ,实现从组件层到对象层的编码,以及 实现从对象层到整个房间层的编码。作者完全遵循了 DSG-Net 的结构来实现 ,本章节剩下的部分主要着介绍 这个编码器。

每个对象节点存储了三类信息,分别是:其结构和几何信息 ,这个信息通过 编码器提取;放置参数 $ 以及语义分类标签 。放置参数 包括了中心位置 ,缩放系数 ,以方向参数 (以 z 轴为旋转轴。对象特征编码器 将上述信息编码为特征向量

其中 是一个全连接网络,而 表示串联操作符。

对于非对象的节点 ,使用一个递归编码器 来提取其所有子节点的特征,并沿着节点之间的关系边来传递消息。对于表示多元关系的边,我们首先只将关系的类型的集成到对应节点的特征中,这个过程使用了一个 MLP 。这个 MLP 由两个全连接层和一个 Leaky ReLu 激活函数组成。然后我们在子图中执行两个消息传递操作来讲所有子节点的特征集中起来,这个过程通过一个以 Leaky ReLu 为激活函数的全连接层实现。最终得到的特征向量

其中 表示 的子节点。 为对象的语义标签, 为多元关系边的语义标签。

整个 不断重复直到得到根节点的特征。最终根节点的特征同一个全连接层映射到特征向量所处的隐藏空间。我们用 KL 散度加入一个正则量,使得的特征向量符合高斯分布。

5.3 Scene Hierachy Decoder

解码器以编码器输出的特征向量为输入,以空间边界特征为条件(condition),以递归的方式还原整个场景层级结构,解码器同样包含两个部分,分别是的递归式解码器 ,负责从对象特征描述中还原组件的几何结构,以及递归式解码器 ,负责接受根节点特征 以及条件输入 。对于对象解码, 解码器遵循了 DSG-Net 的设计,本章节主要介绍 .

如前文描述, 以房间节点未输入,并推断出其子节点,即功能区的特征,并递归式地解析处对象层级的特征信息。对于每一个解码步骤,我们假设每个父节点最多只有 10 个子节点,并且通过一个二分类网络(通过一个以 Sigmoid 为激活函数的 MLP 来实现)来预测节点存在的概率。同时我们也会预测出子节点的语义类型。对于对象节点,我们训练一个 MLP 来预测放置参数、分类语义标签以及对象特征,特征数据会被输入 来获取组件形态。放置参数估计网络由两个全连接网络,一个 Leaky ReLu 激活函数和一个 skip-link 组成。网络通过三个不同的全连接层来估计 , 。接下来我们通过节点的几何数据与特征来预测已有节点之间的关系。这些关系类型会与真值比较从而在训练阶段指导模型的超参数优化。

对于二元关系边的预测,我们对于任意两两节点组合,结合其特征判断每种边的存在概率。对于多元关系,我们通过注意力机制来预测一个 mask, 来表示哪些节点共享了一个多元边属性。利用节点之间的边连接关系,我们执行两轮消息传递过程来更新节点特征。最终得到的节点特征为:,其中 表示每个父节点 的子节点。综上,我们有

其中 ,其中 是子图中节点的数量, 是 hyper-edge 类型的数量。对于每一行,它预测了不同类型的 hyper-edge 出现的概率,最终推断的多元关系类型以 argmax 的操作给出。根据我们的定义,每个对象应该最多只有一个 hyper-edge。

5.4 Training and Losses

5.4.1 训练策略

根据之前的描述可以看出我么内购我们构架你的模型是一个由多个独立网络组成复杂体系,这个体系要想一次性训练到位是比较困难的。因此我们将训练分成两个阶段。首先我们训练从对象到组件级别的递归网络,然后训练整个网络。对于空间边界 VAE,我们在骨干网络之外单独对其进行训练。

5.4.2 损失函数

总损失函数如下:

其中 是整个数据集中的场景分布。重建损失 包括 eaf loss,sematic loss, edges/node existence loss, geometry loss, placement loss 以及部分 edge losses. 这里除了 placement loss, room-object edges 以及本文提出的 hyper-edge loss 之外,其他的损失定义都参照了 StructureNet。对于结构性损失,StructNet 中使用这一损失来确保生成的结构对象的合理性与真实性。不过在本文中,我们只对对象内的层级结构施加这一损失,而非整个场景层级。正则项 只在使得隐藏空间更加平滑,并有利于下游网络(生成与插值网络)的运作。在本文中我们设置常量 ,这是一个经验数值。

下面我们给出 Placement losses 和 edge losses:

Placement Parameter Reconstrucion Loss: 对于中心点和缩放系数我们采用 L2 Loss 来计算其损失。对于朝向角度来说,我么你发现在真实室内场景中,大多数情况下家具的朝向角度来自有限的几个值,一般是 ,因此我们选择用离散的值来预测家具的大致朝向,并生成一个残差偏移量来得拟合真值。因此 ,其中 为是一个估计向量,每个元素表示家具朝向指定方向的概率, 为预测出来的残差偏移量, 为真值。

Room-object Bindary Edge Loss:这个损失的主要目的是将预测出来的对象与房间的边界墙体之间对齐。在输入的室内场景数据中,房间边界和墙体基本对齐的。因此我们也鼓励对象的倾转 bbox 同水平面上的两个轴对齐。这一限制值对具有相关属性(即有 room-objects 边)的对象起作用。我们增加一个损失梁来估计对象的 box 和放假你的 box 沿着房间边界方向的距离,,其中 是所有预测出来的对象的集合。

为 Chamfer 距离。 为一个单元立方体的六个面的单位长度法向量, 为一个变换矩阵,将法向量旋转到的与对象的朝向 对齐。

Hyper-edge Loss:这个损失项的加入鼓励对象之间保留多元关系。本文引入了两种多元你关系。

对于满足 n 重旋转对称关系的 对象,我们可以定义损失函数:

其中 为所有对象的中心, 表示以竖直方向为轴以 为中心点,以 为角度的选转矩阵。注意由于在解码器作用之后对象之间不再保持旋转顺序,因此我们这里要尝试所有的顺序组合得到 Chamfer 距离的最小值。

对于共线平行关系(collinearly parallel hyper-edges),我们定义两个损失函数,第一个是:

这是对方向一致性的评估。第二个损失函数是:

其中 的倾转 bbox 的中点, 是所有相关对象的中点组成的点集的中点, 所有相关对象的平均相对位置。 表示将向量长度单元化。在计算 时,我们将对象中点的在水平面上的横纵坐标按照升序排序,从而避免相反向量被抵消。最终的损失函数是:

6 Experiments and applications

6.1 Scene Generation

前文训练的是一个面向室内场景的通用编解码系统,其隐藏层的特征向量,可以用于大量后续应用。三维场景生成就是一个典型的应用。这里提出的网络架构可以用于自由场景生成,但是在显示中,大部分的生成任务都有额外的限制条件输入(例如房间轮廓)。

论文的后续内容是结果对比和展示,不再整理。

BIM 中的几何表达方式

17 February 2023 at 16:31

三维几何表示时 BIM 模型的重要基础。这篇文章主要阐述 BIM 中几何模型设计原则与基本思想。

1 BIM 场景下的几何建模

一个建筑信息模型的包含的的建筑规划与维护所需要的所有相关信息。三维形态的几何苗猪相比于传统的二维图纸具有巨大的有事:

  1. 可以用统一模型描述整个建筑项目,而不需要拆解成多个图框与子项,进而可以确保不同部分的图纸内容的一致性;但仅凭三维模型,尚不足与生成与现行规范相符合的图纸,模型中还需要更多的信息,例如建筑类型,材料等;
  2. 三维模型可以更好地排查方案规划中的建筑元素冲突情况;
  3. 更便捷的算量计算;
  4. 更好地服务于后续计算与仿真;
  5. 能够更便捷地生成效果图;

一般而已,衡量一个 BIM 建模工具的几何能力的核心要素是其使用的几何建模型核心的质量。目前使用最为广泛的几何工具包括了 ACIS 和 ParaSolid。

2 Solid Modeling

建模三维实心几何体的方法有两种不同的方向:

  1. Explicit Modeling,即显式模型,此种方法通常是直接描述几何体的表面来描述实心体本身。这类方法通常也被称为 BRep (Boundary Representation);
  2. Implicit Modeling,即隐式模型,此方法是通过描述构造过程来描述几何体本身。

这两种方法在 BIM 建模工具中都会使用到,且都是 IFC 标准的一部分。

2.1 Explicit Modeling

2.1.1 BRep

Boundary Representation 是最为广泛使用的三维几何描述模型。其核心思想是构造一系列边界元素的树状结构。通常这种树状结构包含了点 (Vertex)、线 (Edge)、面 (Face)、体 (Body) 等。每个元素都是由更底层的子元素来描述。这个树状结构代表了几何体的拓扑结构,如下图所示:

整个拓扑结构我们可以用一个图数据结构来表示,这个图数据结构被称为 vertex-edge-face graph,简称为 vef 图。

在拓扑结构的基础上,还需要增加几何维度信息来才能充分表示几何体。如果偶几何体只包含了直线和平面,则只需要顶点的几何坐标信息就可以充分表示几何体。但是如果几何体中包含了曲线和曲面,则需要更多的几何描述信息。

用来描述拓扑信息的数据结构通是多个变长列表。体用围合它的面来表示,面则用围合它的边来表示,每个边由其起点和终点来表示。不过这种简单的数据结构只适用于简单的,没有孔洞和开口的几何体。为了表示更加复杂的几何体,我们必须要扩展这一数据结构。

ACIS 的几何数据结构设计。

上图给出了 ACIS 使用的几何数据结构设计,ACIS 在多种 CAD 和 BIM 软件中得到应用。在这个数据结构中,Body 有若干个彼此不连通的 Lump 组成、Lump 则由若干个 Shell 来描述(因此可以描述孔洞和开口)。每个 Shell 由若干个 Faces 来表示。每个 Face 由一个或者多个 Loop 来表示。由于每个 Face 可以有多个 Loop,因此可以表示含孔的多边形。

上述模型的一个重要特点是 Loop 本身并不直接指向边,而是通过 CoEdge 这个中间媒介。图中的下半部分显示了几何数据可以如何同拓扑数据整合起来。

2.1.2 基于三角面片的几何表达

这种方法是对 BRep 方法的简化。通过牺牲一定程度的精确度,可以将曲面表示为一系列的三角形。当然,三角形越小表示就越精确。三角面片表达方式通常用来做可视化。除了精度问题,三角面片表示方法也有更多的存储空间需求;

三角面片表达方式的底层数据结构的组织形式为 Indexed Face Set,其中每个顶点的坐标被存储为有序列表,每个三角面片由其在列表中的索引下表来定义。这种方法可以避免存储冗余的点坐标信息。同时避免因计算误差导致几何错误(三角面片之间出现错位)。

2.2 Implicit Modeling

如前面论述过的,Implicit 建模方法的核心思量是记录几何体被狗砸偶读过程,因此 Implicit Modeling 也被称为 procedural methods。BIM 和 CAD 软件在实践过程中会对生成的几何体提取快照,从而降低每次加载时的计算时间。

2.2.1 Cosntructive Solid Geometry (CSG)

CSG 是一个经典的面向构造过程的几何模型。CSG 模型在表示几何体时会选择一些基础元素(被称为 primitves),如立方体,圆柱体,棱锥结合几何布尔运算(交、并、差)等来创建更为复杂的几何体。

CSG 的一个例子。

表达能力的相对受限使得 CSG 模型在 BIM 场景中基本不会单独使用。

2.2.2 挤出和旋转方法

很多 CAD 或者 BIM 工具提供了通过挤出和旋转操作来代构造三维几何体的操作方法,其操作过程通常是将一个二维图形(通常是一个闭合曲线)沿着三维曲线移动。如果移动的路径是直线,则这个操作被称为 Extrustion,如果是曲线。则被称为 Sweep。作为出发点的二维图形在移动的过程中其朝向可以根据行进方向改变,亦可以保持不变。

旋转操作的定义非常直接,就是将一个二维梯形沿着一个三维上的轴进行旋转。

Lofting 是一种特殊的变种操作。在这个操作中用户需要定义两个面,程序会基于差值方法建立二者之间的过程,并在这一过程中构造出立方体(见上图)。

2.3 两种方法的对比

IFC: 工业基础类族定义的一些介绍

13 February 2023 at 18:22

我将 Industry Foundation Classes (IFC) 翻译为工业基础类族。IFC 是一套通用的建筑信息模型(BIM)的定义标准。本文对 IFC 提供一些基本的介绍,包括其数据模型的基本结构以及建筑元素的语言描述和几何表达。

本文参考了:https://publications.cms.bgu.tum.de/books/bim_2018/06_IFC_07.pdf

1 背景

构建建筑信息模型(BIM)的思想来源于我们试图将所有的建筑模型数据的数据交互操作中使用一致的建筑模型。这样可以避免手动添加模型中的已有数据(例如交圈的时候),并降低操作过程中的误差。除了方便模型构建过程中的数据交互,统一的 BIM 模型使得建筑模型交付过程变得简单而可靠。

目前市面上已经存在了很多不同的建筑设计软件,这些软件有的着眼于建筑物的几何设计,有的着眼于建筑分析和模拟,有的着眼于建筑操作维护,等等。然而,问题在于这些软件之间缺乏统一的数据交换协议,因此设计师在使用不同的软件时,需要讲相同的数据信息重复地手动输入到这些软件,这增加了工作量,并且提高了出错的概率。

为了解决这一问题,我们需要设计出满足这种需要的统一数据格式。此数据格式需要提供统一的、准确的几何描述;提供完备的建筑语义表达(包括建筑元素的分类树系统,以及这些元素之间的关系)。设计这样的数据格式存在诸多挑战,而且,在建筑领域,相比于其他工业领域,还存在下面这些挑战:

  • 一个建筑的设计和施工通常由不同公司负责;
  • 建筑设计通常由几个不同的阶段,不同的阶段由不同的团队完成;
  • 整个建筑项目的生命周期设计多个不同领域的专家,而这些专家可能分属不同的公司;
  • 不同公司之间的协作通常是 ad-hoc 方式(与之对应的是中央协调方式),且这种合作通常局限于项目周期内,而非双方具有长期的稳定合作关系;

简而言之,建筑行业的特征是由众多独立参与者共同参与的高度碎片化的过程,这意味着在这一过程中中会涉及多种不同的软件工具,同时也意味着统一标准较难施行。同时,权力机关在指定标准时也比较难以某个特定的软件作为统一标准,即若要推行统一标准,此标准应该是 Vendor-free 的。

为了达成此目标,国籍组织 buildingSMART 投入了数十年的时间来开发 Industry Foundation CLasses (IFC)。IFC 是一个复杂的数据交互格式,其可以以面向对象的方式描述一个建筑物的几何属性和语义信息。IFC 将建筑物拆解为构件元素和空间,把那个描述他们之间的复杂关联关系。此数据结构可以贯穿建筑设计的整个生命周期。需要注意的是 IFC 是一个类型设计标准,而非一个具体的软件,因此 IFC 的落地施行还需奥各个软件厂商进行针对性的实现。然而,完整的 IFC 标准实现旷日费时,因此 buildingSMART 提出了 Model View Definitions (MVD) 的概念。MVD 定义了在特定任务场景下,哪些 IFC 数据模型应该被实现(而非整个 IFC 模型)。

2 IFC 的历史

略。

3 EXPRESS - IFC 标注的数据模型语言

IFC 的研发采用了和 ISO 标准体系的相同技术手抖,即 EXPRESS 建模语言。EXPRESS 是一种声明式语言,可用于定义面向对象的数据模型结构。

EXPRESS 使用了 Entity(实体) 类型来对应面向对象理论中的类的概念。对于每个实体类型我们可以定义其具备的属性以及同其他实体的关系。EXPRESS 同样采用了面向对象的设计中的继承概念,这使得属性与关系可以应用到实体类型的子类型中。

两个类型 TypeA 和 TypeB 的对象之间的关系定义方式是赋予 TypeA 中定义类型为 TypeB 的属性。我们也可以以类似方式定义与之相对的反关系;

聚合数据类型 -- list, array, set 以及 bag 都是 EXPRESS 模型语言的一部分,这样方便我们定义一对多和多对多关系。EXPRESS 支持以 WHERE 关键字来定义保持数据一致性的算法条件。在 WHERE 语句块中包含了一个返回值为 Bool 类型的表达式,当此值为 True 时,其对应实例才会是有效的。

使用 EXPRESS 语言定你要数据模型的一个例子。

EXPRESS 中的 select 类型在继承树之外提供了一种额外的方法来讲多个实体类型赋予到一个高阶构型上。

取值范围局限于有限几个选择时,我们使用枚举类型来表示。例如 IfcBooleanOperator 可以取值为 UNION, INTERSECTION 或者 DIFFERENCE

除了文本表示以外,EXPRESS 定义了一种图示方法来表示数据模型。这种图示方法被称为 EXPRESS-G。

EXPRESS-G 的例子。

4 分层组织

为了改进了 IFC 模型的可维护性和可扩展性,该模型被构建成多个层。其基本思想是位于上层的元素可以索引其下方的元素,但是反之不可。这使得核心元素保持独立性。

4.1 Core layer

核心层包含了数据模型的大多数基础类型。他们可以被上层的所有元素索引。这些类定义了基础的结构,管架拿到关系以及可以被复用的通用概念。Kernal schema 中定义了 IFC 模型的核心概念,其包括一些非常基本的类:IfcRoot, IfcObject, IfcActor, IfcProcess, IfcProduct, IfcProject, IfcRelationship。在这些基础类型至善是是三个扩展包:Product Extension, Process ExtensionControl Extension,这些也是核心层的一部分。

Product Extension 中包含了一个建筑物中实体性的和空间对象的定义及其关系。这些子类包括: IfcBuildng, IfcBuildingStorey, IfcSpace, IfcElement, IfcBuildingElement, IfcOpenningElement(都是 IfcProduct 的子类),以及 IfcRelAssociatesMaterial, IfcRelFillsElement and IfcRelVoidsElements(都是 IfcRelationship 的子类)。

Process Extension 主要描述过程和操作,这个扩展包同时也提供了定义流程元素的依赖关系的手段。

Control Extension 定义了控制对象的基本类,如 IfcControlIfcPerformanceHistory,并可以将这些对象分配给物理和空间对象。

4.2 Introperability Layer

从共享层位位于核心层上方,起到核心层和应用领域之间的互操作(Interoperability)层的作用。这一层的类包括 IfcWall, IfcColumn, IfcBeam, IfcPlate, IfcWindow

4.3 Domain Layer

Domain Layer 中的类是高度特异化的类,往往只用于特定的领域。他们构成了继承树的叶子节点。这一层的类不能被其他层引用,在同一层的不同领域之间也不能互相引用。

4.4 Resource Layer

资源层是位于最底部的一层,可以在整个 IFC 模型中被使用。这一层中的类并非由 IfcRoot 导出,因此并不具备特定的 id 标识。不同于其他层的对象,这些类型并不能以独立的对象存在,而必须被其他 IfcRoot 的子类型对象引用。在这一层的对象中,最重要的资源类包括

  • Geometry Resource: 基础几何元素,包括点、向量、参数化曲线以及扫过的面(swept surfaces);
  • Topology Resource:包含了地表一个实心体的拓扑结构;
  • Geometry Model Resouce: 描述几何模型的类,如 IfcCsgSolid, IfcFacetBrep, IftSweptAreaSolid
  • Material Resource: 描述材料
  • Utility Resources: 用于描述 IFC 对象的从属关系和版本历史;

除了这些之外,资源层还具有一系列的 scheme 定义,例如 Cost, Measure, Datatime, Representation 等。

5 继承树

IFC 模型定义局部

如同任何其他面向对象设计的数据模型一样,继承树在 IFC 中扮演了关键的角色。上图展示了 IFC 模型继承树的部分结构。在本章节我们关注其中的若干重要类。

5.1 IfcRoot 及其直接子类

继承树的起点是 IfcRoot 类。除了资源层的类以外,其他所有层的对象都必须直接或者间接地继承自 IfcRoot。这个类提供了基础的对象身份功能(GUID),该身份标识可以唯一地确定一个对象,并检索其在变更历史上的位置。除此之外,每个对象可以具备一个名称和一段描述。

直接从 IfcRoot 派生出来的类是 IfcObjectDefinition, IfcPropertyDefinition 以及 IfcRelationship

IfcObjectDefinition 是所有代表物理对象、空间对象以及概念对象(conceptual elements)的类的祖先;这个类的是哪个子类是 IfcObject, IfcTypeObjectIfcContext (代表一般的项目信息)。

IfcRelationship 的子类代表了对象之间的关系。这个设计将关系本身的语义信息和对象自身的属性解耦开来。

IfcPropertyDefinition 定义了一个对象具有的,尚未定义在 IFC 数据模型中的属性。

5.2 IfcObject 及其直接子类

如前所述,IfcObject 代表了一个建筑项目中的各种具有实际物理含义的对象。它是 IFC 模型中六个重要类的父类:

  • IfcProduct: 代表一个物理对象或者空间类型;IfcProduct 对象可以被赋予一个几何形状表达,并被放置到项目坐标系中;
  • IfcProcess: 代表了在一个建筑项目中的一个处置流程(规划、建设、操作等)。这些流程具有时间维度;
  • IfcControl: 控制或者限制另一个对象的对象。这种控制形式可以是法律,指导、规范、边界条件或者其他限制条件;
  • IfcResource: 描述了作为一个流程的一部分的对象;
  • IfcActor: 描述建筑项目中人的参与;
  • IfcGroup: 对象的任意组合;

5.3 IfcProduct 及其直接子类

IfcProduct 能够表示所有和几何、空间上下文相关的对象。这类对象具有几何形态属性和空间位置属性。

IfcElemenetIfcProduct 一个子类,它是一系列描述物理对象的重要类型的父类,如 IfcBuildingElement(这个类是 IfcWall, IfcColumn, IfcWindow 等类的父类);

IfcSpatialElement 类,用来描述非物理形态的空间概念对象。其子类包括 IfcSite, IfcBuilding, IfcBuildingStorey, IfcSpace

IfcProxy 这个子类被用作占位符,用于表示不具备语义类型的对象。这些对象有时候也可以具有几何形态和位置信息。其子类包括 IfcAnnotation, IfcGrid, IfcPort

6 对象关系

6.1 一般概念

对象关系是 IFC 数据模型的重要方面。典型的关系类型可以是整体/部分关系,链接关系或者定义关系。IFC 将关系描述本身对象化,即在业务含义上具备一定关联关系的对象直接并不直接持有指向对方的引用,而是通过一个媒介对象来建立间接的链接,此媒介对象代表了关系本身。关系对象本身描述了方向的指向,在命名规范上,分别用 related...Elementrelating..Element 来代表关系的客体和主体。同时,为了检索反向关系,在主体和客体对象中会建立对应的关系属性。正向和反向关系相关属性指向的是同一个关系对象。

主体、客体与关系描述对象的关系示意图

关系对象都是 IfcRelationship 的子类的实例。相关类型的继承树系统见下方的示意图。IFC 中定义了下面六种基本关系:

  • IfcRelAssociation: 用于将一个外源信息(例如 classification, libraries or documents)关联到一个对象。
  • IfcRelDecompose: 用于表示对象的组分拆分的概念。这个关系描述了整体/部分的关系。其子类包括 IfcRelNests(被嵌套的对象内有顺序), IfcRelAggregate(聚合对象之间没有顺序), 以及 IfcVoidElement (对应开口 openning 关系)。
  • IfcRelDefines: 将一个对象关联到一个属性集定义(Property Set Definition)或者一个类型定义(Type Definition)。
  • IfcRelConnects: 描述两个对象之间的连接关系;
  • IfcRelDeclares: 代表了一个对象,其定义属性与相应上下文的关系、
  • IfcRelAssigns: 代表两个对象之间的一般意义上的链接关系。
IFC 数据模型中关系类型的继承树系统

6.2 空间聚合结构

IFC 模型对建筑的描述的一个重要底层概念是空间在不同层级上的聚合体系。所有具有空间语义含义的概念都派生自 IfcSpatialStructureElementIfcSite 用来表示建筑场地,IfcBuilding 用来表示建筑物,IfcBuildingStorey 表示特定的楼层,而 IfcSpace 用来表示独立的空间与走廊。IfcSpatialZone 表示一般意义上并不满足默认的建筑结构特征的空间区域,这些空间通过 IfcRelAggregates 关系组织起来。

空间结构体系的一个示例。

上图展示了空间结构体系的一个示例。上图中需要注意的是在被聚合(related)的 IfcSpatialStructureElement 的属性 CompositionType 被用来进一步描述聚合关系所。PARTIAL 表示整体、部分的关系,而 ELEMENT 则代表了简单的嵌入元素。

注意数据模型本身并明确定义每种类型应该出现在聚合体系中的什么位置,其数据规则应该有程序的开发者来定义和保证。这些规则可能包括不同类型的上下层限制与嵌套关系不能构成环路等。

为了描述建筑元素在空间聚合体系中的层级,我们使用了 IfcRelContainedInSpatialStructure 这个关系类型(如下图)。在大多数情况下,建筑元素都会链接到层上但是注意,任何一个建筑元素只能建立一个 IfcRelContainedInSpatialStructure 关系。如果一个元素纵跨多个楼测过,则需要使用 IfcReferencedInSpatialStructure 关系来关联到其他楼层。

IfcRelContainedInSpatialStructure 和 IfcReferencedInSpatialStructure 关系示例。

6.3 空间与边界元素的关系

BIM 场景中大量应用都需要获取空间元素与包围空间元素的边界元素之间的关系,这些边界元素可以是墙体,层以及天花板。为了建模这种关系,IFC 数据模型引入了 IfcRelSpaceBoundary 关系。其属性 RelatingSpace 指向目标空间,而 RelatedBuildingElement 则指向其边界元素。同时,我们可以使用 IfcConnectionGeometery 对象来表述空间和边界元素接触的表面形态。

空间边界总是从空间对象的视角进行阐述,根据次视角可以将空间边界划分成两个不同的层级。

  • Level 1 空间边界:不考虑边界元素对侧的变化情况。
  • Level 2 空间边界:考虑边界元素对侧的情况,再次划分成两个类型:
    • Level 2 Type-A: 在此边界对侧是一个空间;
    • Level 2 Type-B: 在此边界对侧是(另)一个建筑元素;
不同层级的边界定义。

对于空间边界更精细的定义方式需要根据应用场景来进行进一步设计;

6.4 指定材质

暂时我不关心这个问题,略过

7 几何表示

7.1 语义描述与几何表示的划分

IFC 模型严格区分了语义描述及其几何表示。语言描述表明了对象的语义身份,并且可以与一个或者多个几何表示相关联。因此对象身份将只与一个语义对象有关,而独立于其几何表示。

语义描述与几何表示的划分。

允许为同一个对象关联不同的几何表示,解决了在不同的应用场景下的不同需求。例如在一个纯显示程序中,立体对象只需要使用三角面表示即可完成渲染,但是在编辑场景中就需要使用高质量的 Brep 或者 CSG 表示。也可以为对象关联二维几何表达,使得 IFC 模型中可以保留二位图纸信息。

维护不同的几何表示之间的一致性是开发者的任务,IFC 标准并不解决这个问题。

7.2 几何描述形式

所有设计几何建模的类型都可以被划分到三个类别,分别是 Geometric Model Resource, Geometry Resource,或者 Topology Resource。在多数情况下这些数据结构的定义都符合 STEP 标准或者 X3D 标准。

所有的几何类都集成自抽象基类 IfcGemetricRepresentationItem。其子类可以分类成曲线表达 (IfcCurve 及其子类),曲面表达 (IfcSurface 及其子类),以及实体表达 (IfcSolidModel 及其紫烈)。几何表达的维度用 IfcGemetricRepresentationItemDim 属性表示。

7.2.1 点、向量与方向

  • IfcCartesianPoint
  • IfcCartesianPointList
  • IfcVector
  • IfcDirection

7.2.2 二维和三维曲线

一共有三个IfcCurve的子类可以用来描述线条,分别是 IfcBoundedCurve, IfcConic, IfcLine。自由曲线可以使用 IfcBSplineCurve 表示。IfcCompositeCurve 可以用来表示由多段曲线构成的复合曲线。

上述类型可以用于三维和二位的曲线表示,但是需要将表示维度的属性设置正确。

7.2.3 Bounding Box

Bounding Box 是高度简化的三维物体表达,通常被用来作为一个 Placeholder,或者连同一个更精细的几何表示一起使用。IfcBoundingBox 使用一个角点加上三个棱线长度来表示这个立方体。

7.2.4 面模型

面模型提供了描述有多个子面(sub-surfaces) 组成的复合曲面。疼吗通常被用来描述空间尺度交大的面(如地形)或者非常平整的面。三维实心体也可以使用他们的曲面表面来描述。

IFC 数据模型支持两种不同的表面模型变种(见下图)。IfcFaceBasedSurfaceModel 可以通过若干 IfcShell 对象来描述不含孔的实心体。这些壳对象可以开的 (IfcOpenShell),也可以是闭的 (IfcClosedShell)。

描述面模型的类

7.2.5 三角化曲面描述

通过三角面来描述立体对象是一种广为接受的形式,这种数据格式几乎可以被所有的可视化软件解读。其缺陷在于立体对象的描述精度收到三角形数量的限制,其数据量较大,且很多软件提供的对这种数据结构的编辑功能比较有限。

IFC 数据模型提供了 IfcTriagnulatedFaceSet 类来描述三角面形式的曲面描述。其基类是 IfcTessellatedFaceSet

IFC 实现了 Indexed Face Set。类 IfcTriangulatedFaceSet 的属性 Coordinates 是一个 IfcCartesianPointList3D 类型的对象,记录了所有三角形面片的坐标。同时每个面片还有一个法向量属性(Normal)。同时也可以链接颜色和纹理属性。

7.2.6 实心体模型

IFC 支持杜仲不同表示三维实体的方法,他们具有统一的基类 IfcSolidModel。这些类包括 IfcCsgSolid, IfcmanifoldSolidBRep, IfcSweptAreaSolid 以及 IfcSwiptDiskSolid

manifold: a collection of points forming a certain kind of set, such as those of a topologically closed surface or an analog of this in three or more dimensions.

7.2.7 边界表示

表示几何实体最有效、最灵活的方式是边界表示 (Boundary Representation, BRep)。IfcManifoldSolidBrep 的两个的子类 IfcFacetedBrepIfcAdvancedBrep 实现了两种典型的 BRep 数据结构。其中,前者只能描述平直的面(指每个子面都是平的),但是后者可以描述存在弯曲的情况。

但是这两个结构都只能描述实心体的外壳,因此无法描述包含孔洞的情况。为了兼容这种情况,IFC 引入了 IfcFacetedBrepWithVoidsIfcAdvancedBrepWithVoids 类。

如下图所示,IfcFacetedBrep 被用来表示具有平整表面的实心体。IfcFacetBrep 对象表示了整体,其 Outer 属性是一个类型为 IfcClosedShell 的对象,而这个对象由进一步引用了一系列的 IfcFace 对象。每个 IfcFace 对象可有任意多个IfcFaceBound 对象描述的封闭曲面。每个 IfcFaceBound 对象指向一个 IfcLoop 类型的对象,这个对象描述了一个点组成的列表。注意在曲面描述中,相邻曲面共享的顶点和边这些对象只会存在一份,此份数据可以被多次引用。

下图展示了使用 IfcAdvancedFace 表示复杂曲面的数据结构的例子。注意在这个例子中,IfcClosedShell 引用的是 IfcAdvancedFace 类型的曲面表示对象。不同于 IfcFace,这个类的对象本身具有几何描述属性,此属性为使用 NURBS 几何描述模型的 IfcBSplineSurface 类。在下方的各个层级的对象中,其几何描述都指向此描述模型中几何对象。

7.2.8 构造性实心几何表达 (Constructive Solid Geometry)

GSG 模型通过简单实心体元素的布尔操作来定义复杂实心体,这些操作包括并 (Union)、交 (Intersection)、差 (Difference)等操作。IFC 数据模型提供了 IfcCsgPrimitve3D 及其子类 IfcBlock, IfcRectangularPyramid, IfcRightCircularCone, IfcRightCircularCylinderIfcSphere 等类型类表示基础实心体元素。

IfcBooleanResult 被用来描述几何操作,其 Operator 属性可以在三个可选项中取值 -- UNION, INTERSECTIONDIFFERENCEFirstOperandSecondOperand 好随性则指向参与操作的元素,这些元素可以是 IfcSolidModel, IfcHalfSpaceSolid, IfcCsgPrimitive3D 或者 IfcBooleanResult。我们可以看出 IfcBooleanResult 可以递归式地使用,从而具备强大的表示能力。

7.2.9 裁切 (Clip)

裁切可以用来表示实心体被一个平面切开的过程。这个过程使用一个特殊的 CSG 操作来实现。第一个操作对象是待裁切元素(IfcSolidModel),第二个操作对象是一个特殊的半空间对象 IfcHalfSpaceSolid,操作符总是 DIFFERENCE

7.2.10 旋转、挤出和扫掠实体

IFC 中我们可以通过 IfcSweptAreaSolid 及其子类 IfcExtrudeAreaSolid, IfcRevolveAreaSolid, IfxFixedReferenceSweptAreaSoid 以及 IfcSurfaceCurveSweptAreaSolid。除了这一支类族以外,还有直接继承自 IfcSweptDiskSolidIfcSweptDiskSolid

每个操作的从基础元素形态以 IfcProfileDef 类型的对象给出(这是操作类的 SweptArea 属性)。IfcProfileDef 最常见子类是 IfcArbitraryClosedProfileDef,这个类通过引用一个 IfcCruve 对象来指定一个基础的闭合图形。

通过使用 IfcExtrudedAreaSolid,基础图形元素可以型一个指定的方向(ExtrudedDirection 属性)挤出指定的距离(Depth 属性)。如果是使用 IfcResolvedAreaSolid,则基础图形会被绕着指定的轴(Axis 属性) 旋转指定的角度 (Angle 属性)。

使用 IfcFixedReferenceSweptAreaSolid 类,我们可以表示一个基础图形沿着空间中的指定曲线 (Directrix 属性) 运动扫略过的实心体。其特征是在扫略过程中,基础图形的不会发生形变,其朝向和一个固定的引用向量 (FixedReference 属性)保持一致。

使用 IfcSectionedSpine 可以表示一组连续的的基础图形之间的差值(上图左侧)。其类型为 IfcCompositeCurve 的属性对象描述了合成曲线的轨迹。CorssSections 属性记录了参与差值的各个基础图形,每个图形的位置由 CrossSectonPosition 给出

IfcSweptDiskSolid 直接继承自 `IfcSolidModel。其使用的扫掠基础图形总是圆形,且在扫略过程中基础图形的朝向会随着轨迹的变化而改变。

7.3 相对定位

IFC 模型中的几何定位强烈地依赖于局部坐标系统。不同的坐标系统之间需要建立映射关系。例如墙体的定位坐标是相对于本层的坐标系统,而层的坐标系统可以映射到整个建筑物的坐标系。这个多层级的坐标系体系可以赋予模型更大的灵活性。IFC 中,我们将上述思想命名为 Local Placement。IFC 中定义了一系列派生自 IfcObjectPlacement 的类,如下图所示。

IfcLocalPlacemente 类继承自 IfcObjectPlacement,此类提供了两个属性:一个可选属性 PlacementRelTo 属性指向提供父级坐标系的对象(这个对象也是 IfcObjectPlacement 类型的,若为空,则对象的定位是相对于全局坐标系的),和 RelateivePlacement 的属性(定义了从父级坐标系到局部坐标系的变换)。这里的变换可以是二维 (IfcAxis2Placement3D) 的或者三维的 (IfcAxis2Placement3D)。下图展示了 IfcAxis2Placement3D 工作的原理。

相对定位体系的图示。

IfcLocalPlacement 构成递归体系和建筑空间对象之间的体系具有密切的联系。在一般实践中,只有 IfcSite 对象才会在全局坐标系中完成定位,而次级对象都以局部相对定位的方式来完成坐标描述。

除了定位系统以外,IFC 中还提供了对齐功能。IfcGrid 类提供了一种非常灵活的网格定义能力。此处所称的网格可以正方形的,辐条型的或者三角形的,如下图所示。非规则性的网格系统也能够被定义出来。基于网格的定位类 IfcGridPlacement 及其属性 PlacementLocation 可以描述在网格系统中的定位。

8 扩展功能:属性集及代理

对于建筑对象的基础关键属性,如门、墙的属性,我们可以提前在 IFC 规范中通过成员属性的方式给出定义。不过针对不同业场景,这些基础建筑元素还可能衍生出大量不同的属性,这些属性如果直接以属性形式定义在其类定义中,无疑会使得整个模型变得臃肿,同时,这些扩展属性类型我们也无法提前全部定义。为了解决这个问题,IFC 将属性分为两类:一类是直接定义在 IFC 模型中的静态属性,另一类是可以动态创建的属性。动态属性可以通过 IfcProperty 的子类来完成定义(通常是 IfcPropertySingleValue,这些属性可以根据需要而自由地谈加到实例中。动态属性的数量本身没有限制。新的对象属性的定义方式一简单的键值对形式进行。属性对象可以被 IfcPropertySet 组织起来并成组地被赋予给对象(通过 IfcRelDefinesByProperties 关系)。

属性集使用的例子

IfcProxy 系统是对动态属性集机制的一个补充。代理机制使得我们可以在程序运行的动态环境下的赋予一个类以语义含义。

9 建筑元素的类型化(Typification)

为了更加高效地描述在模型中频繁出现的对象,IFC 支持可复用对象(reusable types)。一个元素可以作为模板对象而被再次初始化,此时部分数据可以在新对象中被复用,而只有不同的部分需要被设置。IFC 支持在两种情况下的类型化。

9.1 语义对象化(Semantic typification)

若一个 IfcTypeObject 通过 IfRelDefinesByType 关系关联到一个对象,在对象被真正初始化之前,属性集对象会先被初始化并关联 IfcTypeObject 上,而具体的建筑元素对象通过建立和此对象的关系来索引扩展属性定义。

9.2 几何对象化(Geometric typification)

几何表示的复用可以通过 IfcMappedItems 来实现。同大多数 CAD 中引入的「块」概念类似,几何表示对象 IfcShapeReprestation 首先被常见出来,并存储在由一个局部坐标系定义的 IfcRepresentationMap 对象中。这个对象类似于 IfcPropertySet,可以被分配给一个 IfcTypedObject。以一个门的对象定义为例,当一个门被创建时,此对象会建立起对 IfcReprensentationMap 的引用,然后门对象会使用一个本地变换操作 IfcCartesianTransformationOperator 来得到自身的几何表示。

Python:在内存受限的情况下处理超大型 JSON 文件

27 December 2022 at 16:25

本文翻译自 Processing large JSON files in Python without running out of memory

在处理的大型 JSON 文件的时候很容易发生内存爆掉的问题。即便原始数据力量上能够为内存所容纳,但是由于 Python 在内存中的记录方式,其内存消耗会比原始数据的体积要更大。如果遇到了开启 SWAP 的计算机,即便内存不爆掉,如果程序的运行内存进入缓冲区,也会导致运行速度的急剧下降。解决这个问题的方法是流式解析 (Stream parsing),也被称为 lazy parsing, iterative parsing,或者 chunked parsing。

1 问题:Python 的内存低效的 JSON 加载方式

考虑这样一个例子:一个大小是 24MB 的 JSON 文件,这个文件的内容代表了一系列的 Github 事件:

1
2
3
[{"id":"2489651045","type":"CreateEvent","actor":{"id":665991,"login":"petroav","gravatar_id":"","url":"https://api.github.com/users/petroav","avatar_url":"https://avatars.githubusercontent.com/u/665991?"},"repo":{"id":28688495,"name":"petroav/6.828","url":"https://api.github.com/repos/petroav/6.828"},"payload":{"ref":"master","ref_type":"branch","master_branch":"master","description":"Solution to homework and assignments from MIT's 6.828 (Operating Systems Engineering). Done in my spare time.","pusher_type":"user"},"public":true,"created_at":"2015-01-01T15:00:00Z"},
...
]

我们的目标是找到指定用交互过的仓库,下面这个简单的 Python 程序可以达成这一目标:

1
2
3
4
5
6
7
8
9
10
11
12
import json

with open("large-file.json", "r") as f:
data = json.load(f)

user_to_repos = {}
for record in data:
user = record["actor"]["login"]
repo = record["repo"]["name"]
if user not in user_to_repos:
user_to_repos[user] = set()
user_to_repos[user].add(repo)

程序运行结果是用户名和仓库名的映射字典。当我们使用 File memory profile 分析的时候,我们可以得到如下结果:

原文中这里是一个可交互的图表,建议在原文链接中查看

观察内存峰值,我们可以看到两处主要的内存分配行为:

  1. 读取文件;
  2. 将读取的内容转换成 Unicode 字符串。

我们来看 Python 的 json 模块的实现可以发现,这个标准库中的 json.load() 函数会先把整个文件读入内存。

1
2
3
4
5
6
7
def load(fp, *, cls=None, object_hook=None, parse_float=None,
parse_int=None, parse_constant=None, object_pairs_hook=None, **kw):
"""Deserialize ``fp`` (a ``.read()``-supporting file-like object containing
a JSON document) to a Python object.
...
"""
return loads(fp.read(), ...)

注意上面记录到的是内存峰值的现象,所以后续创建字典对象时,其内存占用已经不是峰值处。整个程序执行过程中峰值是读取文件产生的。

有意思的是,尽管文件本身只有 24MB,但是读入内存之后其产生的内存峰值却远高于 24MB。为什么呢?

2 Python 的字符串内存表达方式

Python 的字符串表达经过优化可以使用较少的内存(这取决于字符串的内容)。首先,每个字符串都由于一个固有的开销 (overhead)。其次,如果字符串能够以 ASCII 编码表达,那么每个字符都只需要占用一个字节的内存。单如果有更多种类的字符需要表示,则每个字符占用的内存就上升到 4 个字节。我们来看下面的代码执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> import sys
>>> s = "a" * 1000
>>> len(s)
1000
>>> sys.getsizeof(s)
1049

>>> s2 = "❄" + "a" * 999
>>> len(s2)
1000
>>> sys.getsizeof(s2)
2074

>>> s3 = "💵" + "a" * 999
>>> len(s3)
1000
>>> sys.getsizeof(s3)
4076

这三个 case 中每个字符串的长度都是 1000,但是他们使用的内存大小是不同的,这与其内容有关。

3 流式解决方案

显而易见将整个文件加载进入内存是一种内存的浪费。如果文件的体积非常大,那么我们甚至无法将整个文件读入内存。

如果 JSON 文件是一些对象组成的列表,那么理论上我们可以分片进行加载。个很多 Python 库支持这种行为,这里我们采用 ijson 库。

1
2
3
4
5
6
7
8
9
10
11
import ijson

user_to_repos = {}

with open("large-file.json", "rb") as f:
for record in ijson.items(f, "item"):
user = record["actor"]["login"]
repo = record["repo"]["name"]
if user not in user_to_repos:
user_to_repos[user] = set()
user_to_repos[user].add(repo)

在之前的标准库版本中,当数据被读入内存之后,文件就会被关闭。但是在现在这个场景下文件必须被保持打开。 这是因为文件的内容只有部分被读取,后续内容的读取取决后续的迭代过程。

items() 接口在此处的接受一个查询字符串用来指定加载的对象。在这个例子中 "item" 输入表示返回每个顶层对象。你可以参见 ijson文档来查询接口细节。

采用上面的代码我们可以发现程序的内存峰值降低到了 3.6MB。

❌
❌