一次点击解析到它落到的那个实例和那个三角形,然后对象自己高亮。 没有拾取渲染通道,没有 ID 缓冲回读,没有单独的高亮渲染路径—— 这就是拾取在全部四个后端上行为一致的原因。
ObjectPicker 是一个面板。把它加进场景,
注册应该可被拾取的控件,然后每帧驱动一次。每次拾取都在 CPU 上
跑同样的三步:
Picking.ScreenPointToRay 用相机和
ExtendResolution 把指针位置
变成一条世界射线。
因为最终测试精确到三角形,点击甜甜圈中间的那个洞
不会选中甜甜圈。拾取使用
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 在拾取器和每个目标上都存在。拾取器
的值是回退值:只在目标仍持有未改动的默认值时才会被应用。
在目标上设置颜色后它会保留,包括运行时做出的修改。
| 设置 | 默认值 | 说明 |
|---|---|---|
PulsePeriod | 1.2 s | 三角波的一次完整起落。 |
BoxAlpha | 0.3 | 面透明度的峰值。每帧写入 SurfaceColor.W。 |
SurfaceColor | 白,0.3 | Bounds 和 Wireframe 的面色。W = 0 会把 Wireframe 降级为只剩边。 |
EdgeColor | 橙 | 实心;不脉动。 |
OutlineColor | 亮金 | 实心;不脉动。 |
目标自身的 Alpha 永不被碰。高亮是
附加几何,不是对对象的修改,所以被高亮的模型看起来还是它自己。
三个通道都在写入任何东西之前被清空,这就是运行时切换样式
永远不会留下过期盒子的原因。
屏幕空间描边是唯一需要自己通道的样式:位于 AfterScene compute 阶段和 Post
之间的条件性 OutlineMask 通道。
四个后端都实现了它;在一帧里没有任何东西要求描边时,
后端会在分配 mask 目标之前提前退出,所以这个通道不用时是免费的。
参见 渲染管线。
一次拾取可以落在整个对象上,也可以落在实例化宿主内的某一个实例上, API 把这两种情况保持为可区分的,而不是把它们拍平。
| 成员 | 它报告什么 |
|---|---|
Selected | 被聚焦的 Mesh3DBase;没有命中或焦点是实例时为 null。 |
SelectedInstance | 被聚焦的 MeshInstanceTransform。仅在逐实例命中时非空。 |
SelectedHost | 无论哪种情况下的宿主控件。null 表示没有焦点。 |
Locked、LockedInstance | 点击锁定的焦点做同样的拆分,它忽略悬停。 |
SelectedFocus | Focus 结构本身:宿主加可选实例,带 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。
如果它不是你要的编辑器,大约两百行就能替换掉,
拾取器暴露了它读取的一切。
OnClick 按各自的矩形做命中测试;拾取器只处理
Mesh3DBase 和
InstancedMesh3DBase。