视图、边界和裁剪

简介

本指南介绍了景观中视图边界和裁剪的工作原理。这个 指南介绍了如何设置视图边界,如何解读命令 以及视图边界对现有风景子系统的影响。

概念

设置视图边界

嵌入器必须为视图和视图持有者创建一对令牌, 也会在其视图中为嵌入的 ViewHolder 分配空间来布局。 这是通过在嵌入视图的 ViewHolder 上设置边界来实现的。接收者 在视图上设置视图边界时,必须对其调用 SetViewProperties 相应的 ViewHolder。您可以在之前或之后调用 SetViewProperties 视图本身已创建并链接到 ViewHolder,因此您不必 不必考虑设置顺序边界本身已设置 通过在 3D 空间中指定其最小和最大点 (xyz) 来调整数据。

边界范围和边衬区

ViewProperties 结构体集中的 bounding_box 属性用于设置 边界。最小和最大边界表示 轴对齐的边界框的最大坐标点。

insets_from_mininsets_from_max 属性提供了一个提示, 边界框和边衬区之间的区域可能会被遮挡。 风景区并不使用边衬区值来确定边界框, 而是

{ bounding_box.min + inset_from_min, bounding_box.max - inset_from_max }

可能会被其祖先视图遮挡。遮盖的原因和规则 每个产品都有自己的专属空间。

示例

// Create a pair of tokens to register a view and view holder in
// the scene graph.
auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();

// Create the actual view and view holder.
scenic::View view(session, std::move(view_token), "View");
scenic::ViewHolder view_holder(session, std::move(view_holder_token),
                               "ViewHolder");
// Set the bounding box dimensions on the view holder.
view_holder.SetViewProperties({.bounding_box{.min{0, 0, -200}, .max{500, 500, 0}},
                               .inset_from_min{20, 30, 0},
                               .inset_from_max{20, 30, 0}});

上面的代码会创建一个 View 和 ViewHolder 对,其边界从 (0, 0, -200) 开始并延伸到 (500, 500, 0)。边界本身始终 与轴对齐。

坐标系

风景区的 3D 系统由多个坐标空间组成。他们可以 大致可分为以下几类:图层空间、世界空间和本地空间。空间 点在每个坐标系中都具有独立的坐标。转换 矩阵描述了如何在两个坐标空间之间映射空间点;该 将系统 A 中点的坐标乘以转换矩阵, 求该坐标系在 B 系中的坐标(或者求出矩阵在 )。

所有风景坐标系都采用右手方式。

坐标空间图

层空间(屏幕空间)

层空间是与屏幕上的某个区域相对应的 2D 坐标空间。 - 原点:左上角,X 轴指向右侧,Y 轴指向下方。轴对齐 和显示屏。 - 尺寸:通常等于显示屏尺寸(全屏图层)。

相机转换

镜头转换矩阵将坐标从世界空间转换为图层 太空。

此转换实际上包含几个步骤。

首先,我们将从世界空间转换为中间空间,称为相机。 太空。这是规范化的三维坐标系,范围为 -1 到 1, 此坐标系采用右手方式,X 轴为 Y 轴向下, Z 轴指向 屏幕”。镜头在世界空间中的位置和方向决定了 转换。

接下来,投影转换矩阵将决定如何将镜头 将空间转换为层空间的二维空间。这种转换 用于指明要执行的投影类型:正交或透视 投影。常见的选择是正交,即现实世界基本上是 在 Z 轴方向上展平,同时保持 X 轴和 Y 轴。

最后,可能会应用裁剪空间转换来缩放和转换 2D 空间以实现放大。1

世界空间

世界空间是场景图根视图的放置位置, 相机已定位。因此,整个场景图都嵌入在这个空间中, 以及如何将相机空间与场景联系起来。

转换节点

场景图中的每个节点都有一个与之关联的本地空间。此聊天室 由该节点的转换矩阵定义,该矩阵将坐标从 节点的本地空间复制到其父级的本地空间。这个矩阵 取决于节点的位置、旋转和缩放比例 。

将每个节点的本地空间与世界空间相乘, 将场景图转换矩阵向上转换,终止于场景图的根部 (其转换矩阵是从根的局部空间到 世界空间)。这个相乘的矩阵称为节点的“全局转换”。

节点的转换矩阵由平移、缩放比例和 该节点的轮替。应用的顺序是先缩放,然后是旋转,最后是旋转 翻译。因为这些值都可以写成 以及矩阵的乘法顺序很重要, 然后可以将完整转换写为:

parent_local_point = translation matrix * rotation_matrix * scale_matrix * local_point

示例

我们的场景图由两个节点(Node1 和 Node2)组成。 Node1 是 Node2 的子级,P1 是 Node1 本地空间中的一个点。 Node1 和 Node2 都具有旋转、缩放和平移功能。 要获取 P1 的世界空间坐标,然后依次组合所有父项的转换: p1_world_position = (translation_node2 * rotation_node2 * scale_node2) * (translation_node1 * rotation_node1 * scale_node1) * p1_local_position

本地空间(景观空间)2

由于并非每个会话都充分了解完整的场景图,因此 所有操作均根据其视图的局部坐标空间操作。这是 Session 定义其节点的转换,以及输入坐标 。

  • 来源:“后角”视图。视图的 Z 维度通常设置为 为 -1000 到 0(这是以左手坐标开始的副作用) 系统),因此 View 的来源位于 但 X 和 Y 值最小。通常与轴对齐 世界空间。

查看边界

视图边界以本地坐标形式指定,其世界空间位置 取决于视图节点的全局转换。

输入坐标源自图层空间,通常对应于像素空间, 以屏幕左上角的原点表示。输入系统 与合成器和相机协同工作,以从输入设备坐标映射到 世界空间 射线投放和点击测试

示例

// Create a view and view-holder token pair.
auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();
scenic::View view(session, std::move(view_token), "View");
scenic::ViewHolder view_holder(session, std::move(view_holder_token),
                               "ViewHolder");

// Add the view holder as a child of the scene.
scene.AddChild(view_holder);

// Translate the view holder and set view bounds.
view_holder.SetTranslation(100, 100, 200);
view_holder.SetViewProperties({.bounding_box{.max{500, 500, 200}}});

在上面的代码中,Local Space 中的视图边界的最小值和最大值均为 (0, 0, 0)(500, 500, 200),但由于父节点由 (100, 100, 200) 世界空间中的视图边界实际上会有一个世界 空间上下限为 (100, 100, 200)(600, 600, 400) 。不过,视图本身并看不到这些世界空间边界, 只在自己的本地空间中处理边界。

中心几何图形

几何图形(如 RoundedRectangle)的质心是其 中心,而对于视图,其边界的质心是其最小值 坐标。这意味着,如果一个视图和一个作为其子对象的圆角矩形, 这两个视图具有相同的平移,即圆角矩形的中心 将以视图边界的最小坐标渲染。要解决此问题,请 再在形状节点上将另一个平移移动到视图 范围。

置中几何图形图

调试线框渲染

为帮助调试视图边界,您可以在 线框模式,查看您的视图在“世界空间”中的准确位置。这个 可使用风景命令按视图应用 功能:

// This command turns on wireframe rendering of the specified
// view, which can aid in debugging.
struct SetEnableDebugViewBoundsCmd {
    uint32 view_id;
    bool display_bounds;
};

此命令接受 view idbool,用于切换 视图边界。默认的显示颜色为白色 通过在SetViewHolderBoundsColorCmd 指定的 ViewHolder:

// This command determines the color to be set on a view holder’s debug
// wireframe bounding box.
struct SetViewHolderBoundsColorCmd {
    uint32 view_holder_id;
    ColorRgbValue color;
};

射线投放和命中测试

通过射线投射将图层空间坐标映射到场景几何图形以及进行点击测试 坐标。最终,输入的内容通过“本地视图”传递给视图 坐标。如坐标系中所述, 部分,视图局部坐标由 视图节点,该节点从世界空间映射到本地空间。

热门射线

注入触摸输入事件时,我们需要了解两件事情:1.哪个(些)客户端 将输入事件发送到什么位置?2. 输入事件的坐标是什么 客户局部坐标系中的相对位置?

输入坐标从图层空间转换为世界空间涉及 输入系统、合成器层和相机。

输入坐标空格

为了确定将事件发送到哪个客户端,我们需要执行点击测试。这是 方法是将光线(在“世界空间”中)投影到场景中,看看 相交,然后将输入发送到最近的命中对象。当我们点击 我们可以记下该点在场景中的位置。然后计算 将命中对象所属视图的世界空间转换为本地空间;以及 使用它以本地坐标向客户端发送输入事件。

原始输入坐标是以屏幕像素为单位的二维坐标。 输入系统和合成器就一种惯例达成一致,如上图所示, 三维(蓝色)的设备坐标,其中可视深度为 1, 近平面的 z = 0,远平面的 z = -1。了解了这一点 输入系统通过计算世界空间坐标来构造命中射线 (通过反转相机转换),则将它们设置为 原点,Z 为 0,方向为 (0, 0, 1),朝向场景。

那么,在世界空间中,上述命中射线从 (x, y, -1000) 开始。 方向为 (0, 0, 1000)

规则

执行点击测试时,Skys 会针对 ViewNode 的边界运行测试 然后再确定射线是否应该继续检查其子项 节点。

  • 如果一条光线完全错过了某个视图的边界框,那么没有对象是 该视图将被覆盖

  • 如果一条射线与边界框相交,则只有边框内存在的几何图形 那么,射线的进出范围就是边界框的进出范围, 视为命中。例如,无法命中裁剪几何图形。

如果忘记为视图设置边界,以子项形式存在的任何几何图形 无法命中该视图。这是因为,相应边界将为 null, 所以这个空间将无限小,这也意味着 渲染到屏幕上。

在调试模式下,null 边界框将触发 中的 FXL_DCHECK escher::BoundingBox 类,指明边界框尺寸需要 大于或等于 2。

边缘情况

射线与边界框的一侧垂直, 不会算作撞击由于构成这 6 个平面的 边界框本身就是裁剪平面,因此任何对象 直接位于裁剪平面上,也会被裁剪。

碰撞

当光线投射同时检测到两次或更多命中时,就会发生冲突 距离。冲突表示可命中的目标相互重叠, 位置相同的位置。这被视为不正确的行为 而且在发生冲突时,Skys 不提供命中测试订单保证。 客户端必须防止冲突。

发生冲突的方式有两种:

  • 同一视图中的节点之间发生冲突。主视图必须确保 元素在视图中的正确位置。

  • 不同视图中的节点之间发生冲突。父视图必须阻止 其子项的剪裁边界之间的交集。

遵循这些规则也是最佳做法,以避免视觉元素出现 Z-fighting 内容。

检测到冲突时,系统会通过 会话 ID 和资源 ID。


  1. 处理放大情况更好的解决方案是直接将缩放节点插入图表,然后直接处理。这种方法会造成一些问题,导致应用难以判断其大小和应分配的内存量(因为发出指标事件时会进行缩放),因此引入裁剪空间转换作为一种解决方法。由于输入目前在相机空间中处理,因此有一个不幸的副作用,即滑动手势在放大下看起来会变小。 

  2. 请勿混淆 3D 图形的常见“模型-视图-投影”表示法中的“视图空间”,我们在这里将其称为“相机空间”。