DarkString
DarkString

PICO / SPATIAL SDK 6.1.9

PICO 地面检测实战:线框 Debug、日志与异常恢复

Gunner · · Kotlin / Full Space

真机提示“环境检测暂不可用”,不一定是系统没找到地面。Flatnation 的一次故障发生在应用自己的高度校验:候选平面被拒绝,异常又中断了检测。下面给现有 Full Space 场景补上三件东西:可见网格、坐标日志和失败重试。

地面 Debug 效果示意:设置中开启显示检测地面后,房间地板上叠加青绿色三角线框;它表示 FLOOR 锚点,不表示业务校验成功
效果示意 / 开启开关后,检测到的 FLOOR 网格以青绿色线框显示。此图为概念示意,非真机截图;界面布局与房间仅用于说明。点击查看大图。
地面检测流程:系统锚点分别进入 Debug 显示和业务校验;失败后继续检测
01 / Debug 与业务校验分开,被拒绝的平面也能看见 · 原创示意图,非头显截图。点击放大。

1. 先确认是谁拒绝了地面

真机日志出现 Floor is not level in room coordinates。异常来自 BureauEnvironment.kt 的这一段:

Kotlin · 项目原有高度校验
val ys = anchor.vertices.map { surface.convertPositionTo(it, parent).y }
require(ys.all { it.isFinite() } && ys.max() - ys.min() < .04f) {
    "Floor is not level in room coordinates"
}

4 厘米是项目阈值。失败可能是坐标非有限,也可能是转换后的高度差过大;旧日志没有数值,不能直接归因于权限或扫描失败。

先确认 Full Space 前提。按官方平面检测指南订阅更新后再启动 PlaneTrackingManager。6.1.9 PlaneAnchor 文档对 transform 的原文是:

“The transform of the anchor in the current Stage coordinate space”

即位姿在 Stage 空间,位置单位为米。它不能单独证明 vertices 的坐标空间约定:需要把原始顶点、锚点位姿和转换后的高度一起核对。

坐标诊断:对照 Stage 锚点位姿、父节点坐标转换及 rawY 和 roomY 高度范围
02 / 对比转换前后高度,排查坐标假设 · 原创示意图,非头显截图。点击放大。

2. 设置中开启地面线框

用进程内状态控制开关,默认关闭。把下面的 Switch 放进已有设置页面,场景协程收集 enabled:开启时绘制缓存锚点,关闭时清理实体。

Kotlin · 设置页核心片段(省略 imports)
object EnvironmentDebug {
    val enabled = MutableStateFlow(false)
}

// Compose 设置内容中
val debugEnabled by EnvironmentDebug.enabled.collectAsState()
Switch(
    checked = debugEnabled,
    onCheckedChange = { EnvironmentDebug.enabled.value = it }
)

只筛选 FLOOR 语义,直接加载锚点网格。青绿色表示“被识别的地面”,不表示通过放置校验。下面摘自项目,依赖类内的 physics、debugPlanes 和 debugMaterial;并非独立示例。

Kotlin · BureauEnvironment.updateDebug
private fun updateDebug(anchor:PlaneAnchor) {
    if(!EnvironmentDebug.enabled.value) return
    debugPlanes.remove(anchor.anchorUUID)?.let { if(it.valid) it.destroy() }
    if(anchor.semantics!=SemanticLabelType.FLOOR) return
    val parent=physics?.takeIf { it.valid } ?: return
    try {
        val ink=debugMaterial ?: UnlitMaterial.create().apply {
            toGlobal();setBaseColor(Color4(0f,1f,.8f,1f))
            setPolygonFillMode(PolygonFillMode.LINE)
        }.also { debugMaterial=it }
        val mesh=MeshResource.loadFromPlaneAnchor(anchor.anchorUUID)
        val entity=ModelEntity(mesh,ink)
        parent.addChild(entity)
        debugPlanes[anchor.anchorUUID]=entity
        entity.setName("DebugDetectedFloor")
        entity.components[TransformComponent::class.java]!!.apply {
            setPosition(parent.convertPositionFrom(anchor.transform.position,null))
            setQuaternion(parent.convertRotationFrom(anchor.transform.rotation.toQuat(),null))
        }
    } catch(failure:Exception) { Log.w(LifeWorldScene.TAG,"Debug floor mesh failed id=${anchor.anchorUUID}",failure) }
}

位置和旋转都转到父节点空间;UnlitMaterial 避免依赖光照,LINE 显示网格边。这里使用项目编译通过的 loadFromPlaneAnchor,移植时以锁定的 SDK 版本为准。

锚点 REMOVED 时销毁对应实体;关闭 Debug、停止场景时销毁全部网格,再 close 材质。当前实现逐次重建实体,高频更新下应再验证性能;跟踪离开 RUNNING 时也应清理或标记旧网格,避免把过期结果当成当前结果。

3. 记录数值,让失败停在当前候选

日志至少保留 UUID、语义、方向、尺寸、锚点位姿、顶点数量,以及 rawY / roomY 范围。只抽样少量顶点,不要整份网格刷屏。下面放在高度校验前,候选已通过非空几何筛选:

Kotlin · 坐标日志(精简版)
val ys = anchor.vertices.map { surface.convertPositionTo(it, parent).y }
Log.i(TAG, "Floor id=${anchor.anchorUUID} " +
    "semantic=${anchor.semantics} orientation=${anchor.planeOrientation} " +
    "extent=${anchor.boundingBoxSize} pose=${anchor.transform} " +
    "vertices=${anchor.vertices.size} " +
    "rawY=${anchor.vertices.minOf { it.y }}..${anchor.vertices.maxOf { it.y }} " +
    "roomY=${ys.min()}..${ys.max()} samples=${anchor.vertices.take(3)}")

在候选处理内部 catch 校验异常:销毁临时 surface、加入 rejected、重置稳定性采样,继续等待其他候选。收到该 UUID 的新几何数据后,再移除拒绝标记重试。不要让异常逃出 findGround(),结束整个检测协程。

4. 真机上按这个顺序验证

Shell · 指定真机采集日志
adb devices -l
adb -s <真机序列号> logcat -v threadtime -s FlatnationPico > floor-debug.log
  1. 进入 Full Space 场景,开启“显示检测地面”,环顾地面。
  2. 没有线框:先查跟踪状态、FLOOR 锚点和 Debug mesh 异常。
  3. 有线框却放置失败:按 UUID 对照候选日志,检查非有限值和 roomY 跨度。
  4. 触发候选拒绝后,确认仍能收到更新并重新校验;关闭开关、退出场景后确认网格消失。

验证边界:该版本已通过构建与单元测试,并安装到真机。本文修复了“单次候选失败终止检测”的恢复路径;头显线框视觉验收和原始高度异常根因仍待确认。