MIT 许可证 NuGet

首页 / 功能 / 拾取与编辑

拾取、高亮与编辑

一次点击解析到它落到的那个实例和那个三角形,然后对象自己高亮。 没有拾取渲染通道,没有 ID 缓冲回读,没有单独的高亮渲染路径—— 这就是拾取在全部四个后端上行为一致的原因。

一次拾取如何解析

ObjectPicker 是一个面板。把它加进场景, 注册应该可被拾取的控件,然后每帧驱动一次。每次拾取都在 CPU 上 跑同样的三步:

  1. Picking.ScreenPointToRay 用相机和 ExtendResolution 把指针位置 变成一条世界射线。
  2. 每个注册目标做一次粗略的有向包围盒测试,便宜到 足以拒绝几乎一切。
  3. 幸存者针对自己的拾取几何跑精确三角形测试。命中 距离是到最近三角形的世界空间距离,所有目标中最近的命中胜出。

因为最终测试精确到三角形,点击甜甜圈中间的那个洞 不会选中甜甜圈。拾取使用 LocalBoundsRaw,而不是膨胀后的剔除包围盒,所以 一次拾取永远不会命中动画角色周围的空气。

带信息面板的对象拾取 拾取 · 高亮
拾取结果、高亮和信息面板,全部由引擎部件组成。
picker = new ObjectPicker();
AddPanel(picker);

picker.Targets.Add(house);            // Mesh3DBase: Model or Mesh3D
picker.Targets.Add(robot);
picker.InstancedTargets.Add(rocks);   // InstancedMesh3DBase: per-instance hits
两条要紧的规则

注册是自愿加入的。 地面、墙、海面和天空盒应该留在 Targets 之外,否则它们会吞掉本意是 点给站在它们上面的东西的点击。

最后更新拾取器。 射线测试和高亮适配都读取 本帧最终的世界矩阵,所以 picker.Update 必须在每个持有 目标的面板更新完之后运行。

悬停、点击、锁定

悬停是即时且无状态的:移到一个目标上它就高亮,移开它就停止。 点击则改为锁定目标,且锁定在指针离开后仍然存活。

手势结果
指针悬停在目标上悬停高亮。Selected 报告命中。
点击目标锁定它。Mesh3DBase.Selected 或 MeshInstanceTransform.Selected 被设为 true。
点击另一个目标切换锁定。上一个目标的高亮在同一帧内被清除。
点击空白处清除锁定。
点击属性面板内部刻意不清除锁定。那个矩形算作面板交互。

一次点击是指在按点 20 像素内释放—— TouchService.IsReleased。更远的是拖拽, 不改变选择,这正是相机环绕可以从一个对象上开始的原因。触摸 屏幕没有悬停,所以按下即拾取、抬起即放开;同一条代码路径服务两者。

高亮是对象自身的属性

拾取器不绘制任何东西。它把值写进目标上的 Highlight,目标在自己的绘制过程中渲染自己的 高亮。这个面板完全不持有任何 GPU 资源。

HighlightStyle结果
None什么都不画,但选择与属性面板仍正常工作。对一块 150 米的草地来说这是正解——画包围盒会填满整个屏幕。
Bounds贴合原始包围盒的世界空间 AABB:半透明面加实心双色边。默认样式,在小中型模型上很清晰。
Wireframe贴合表面的壳加边条。避免大模型上出现巨型盒子。壳在首个启用帧惰性构建,之后保持常驻,全局禁用时零成本。
Outline有自己的颜色和像素宽度的屏幕空间轮廓线。不脉动。
robot.Highlight.Style = HighlightStyle.Outline;
robot.Highlight.OutlineColor = new Vector4(1f, 0.84f, 0.25f, 1f);
robot.Highlight.OutlineWidth = 3f;

// Or opt a large background body out entirely:
grass.Highlight.Style = HighlightStyle.None;

颜色,以及拾取器何时覆盖它们。 SurfaceColor、 EdgeColor 和 OutlineColor 在拾取器和每个目标上都存在。拾取器 的值是回退值:只在目标仍持有未改动的默认值时才会被应用。 在目标上设置颜色后它会保留,包括运行时做出的修改。

设置默认值说明
PulsePeriod1.2 s三角波的一次完整起落。
BoxAlpha0.3面透明度的峰值。每帧写入 SurfaceColor.W。
SurfaceColor白,0.3Bounds 和 Wireframe 的面色。W = 0 会把 Wireframe 降级为只剩边。
EdgeColor橙实心;不脉动。
OutlineColor亮金实心;不脉动。

目标自身的 Alpha 永不被碰。高亮是 附加几何,不是对对象的修改,所以被高亮的模型看起来还是它自己。 三个通道都在写入任何东西之前被清空,这就是运行时切换样式 永远不会留下过期盒子的原因。

Outline 的代价

屏幕空间描边是唯一需要自己通道的样式:位于 AfterScene compute 阶段和 Post 之间的条件性 OutlineMask 通道。 四个后端都实现了它;在一帧里没有任何东西要求描边时, 后端会在分配 mask 目标之前提前退出,所以这个通道不用时是免费的。 参见 渲染管线。

读取选择

一次拾取可以落在整个对象上,也可以落在实例化宿主内的某一个实例上, API 把这两种情况保持为可区分的,而不是把它们拍平。

成员它报告什么
Selected被聚焦的 Mesh3DBase;没有命中或焦点是实例时为 null。
SelectedInstance被聚焦的 MeshInstanceTransform。仅在逐实例命中时非空。
SelectedHost无论哪种情况下的宿主控件。null 表示没有焦点。
Locked、LockedInstance点击锁定的焦点做同样的拆分,它忽略悬停。
SelectedFocusFocus 结构本身:宿主加可选实例,带 IsEmpty。
Select(target)从代码锁定一个目标——比如你刚生成的东西。必要时会注册它。

锁定的焦点每帧重新校验:宿主不能被释放,实例必须仍在其宿主的列表里。 因此运行时重建实例列表会丢弃选择,而不是留下悬空引用。如果你自己调用 Select,请在本帧点击消费之后调用, 否则点击分支可能覆盖你刚设置的锁定。

属性面板

ObjectPanel 随拾取器一起提供。有东西被锁定时它 显示焦点,并让 PosX、 PosY、PosZ、 Width、Height、 Depth 和旋转可就地编辑,对任何有片段的物体 还加上当前动画片段及其索引。没有动画的目标显示一条短横线, 而不是空行。

旋转以度为单位编辑,归一化到 [0, 360),并按目标使用的形式写回—— Model 是绕 Y 的弧度, Mesh3D 和实例是 Y 轴四元数。偏航角通过旋转局部 +X 并取 atan2(-z, x) 从四元数恢复,这对纯 Y 旋转是精确的。

为什么旋转无需补偿

旋转支点就是锚点,而锚点是原始包围盒的几何中心。 由于世界矩阵总是把那个锚点映射到 PosX/PosY/PosZ,对象绕自己的中心旋转, 位置和尺寸都不变。这是统一摆放契约的唯一回报, 在你用过一个没有它的引擎之前很难体会。

面板是一个由 Texts、Shape 和 FrameButton 构建的普通 Panel。 如果它不是你要的编辑器,大约两百行就能替换掉, 拾取器暴露了它读取的一切。

已知缺口

  • 缺失 没有变换 gizmo。 编辑通过面板上的数字行进行, 而不是在视口里拖拽箭头。
  • 缺失 只支持单选。 一次一个焦点。没有橡皮筋框选, 没有多对象变换。
  • 缺失 没有撤销,也没有持久化。 编辑作用于活对象。写回 场景文件是你的活,因为引擎没有场景文件格式。
  • 缺失 2D 控件不在范围内。 精灵和文本通过 OnClick 按各自的矩形做命中测试;拾取器只处理 Mesh3DBase 和 InstancedMesh3DBase。