在Google Geospatial示例中,如何检测HitTest是否命中指定Mesh对象?
解决Google Geospatial中自定义Mesh的HitTest命中检测问题
问题根源
你当前的代码逻辑存在两个核心问题:
- ARCore的
frame.hitTest()仅检测现实世界的Trackable对象(比如平面、特征点),你自己创建的自定义3D Mesh不属于ARCore的Trackable范畴,所以不会出现在hitResultList中。 - 直接用
hit.equals(virtualObjectMesh)对比完全无效——HitResult是ARCore返回的现实空间检测结果,和你的Mesh对象不是同一类型,不可能相等。
正确解决方案
要检测触摸是否命中自定义Mesh,需要通过屏幕射线与Mesh空间范围的相交检测来实现,步骤如下:
1. 提前记录Mesh的关键信息
创建Mesh时,保存关联的EarthAnchor(用于获取世界坐标),以及Mesh的包围盒(用于快速碰撞检测):
// 自定义数据类存储Mesh相关信息 data class TrackedVirtualMesh( val earthAnchor: EarthAnchor, val mesh: Mesh, // 预计算Mesh的局部包围盒(AABB) val localBoundingBox: BoundingBox ) // 创建Mesh时初始化 val virtualMesh = TrackedVirtualMesh( earthAnchor = yourEarthAnchor, mesh = yourVirtualMesh, localBoundingBox = calculateMeshBoundingBox(yourVirtualMesh) )
2. 触摸事件中执行射线相交检测
将屏幕触摸坐标转换为世界空间射线,再判断射线是否与Mesh的世界空间包围盒(或三角面)相交:
if (motionEvent != null) { // 从触摸位置创建世界空间射线 val ray = frame.createRay(motionEvent.x, motionEvent.y) // 遍历所有自定义Mesh for (trackedMesh in yourVirtualMeshList) { // 获取Anchor的世界姿态,将Mesh局部包围盒转换到世界空间 val anchorPose = trackedMesh.earthAnchor.pose val worldBoundingBox = trackedMesh.localBoundingBox.transform(anchorPose) // 先做快速包围盒检测 if (ray.intersects(worldBoundingBox)) { // 可选:执行高精度三角面相交检测(需要遍历Mesh的三角面计算) if (rayIntersectsMesh(ray, trackedMesh.mesh, anchorPose)) { Log.d(TAG, "命中自定义Mesh!") break } } } motionEvent = null }
3. 辅助函数实现
- 计算Mesh局部包围盒:
private fun calculateMeshBoundingBox(mesh: Mesh): BoundingBox { var minX = Float.MAX_VALUE var minY = Float.MAX_VALUE var minZ = Float.MAX_VALUE var maxX = Float.MIN_VALUE var maxY = Float.MIN_VALUE var maxZ = Float.MIN_VALUE // 遍历Mesh所有顶点,计算最小/最大坐标 val vertices = mesh.vertices for (i in 0 until vertices.size step 3) { val x = vertices[i] val y = vertices[i+1] val z = vertices[i+2] minX = min(minX, x) minY = min(minY, y) minZ = min(minZ, z) maxX = max(maxX, x) maxY = max(maxY, y) maxZ = max(maxZ, z) } return BoundingBox(minX, minY, minZ, maxX, maxY, maxZ) }
- 射线与三角面相交检测(可选,用于高精度场景):
private fun rayIntersectsMesh(ray: Ray, mesh: Mesh, pose: Pose): Boolean { val vertices = mesh.vertices val indices = mesh.indices // 遍历所有三角面 for (i in 0 until indices.size step 3) { val v0Idx = indices[i] * 3 val v1Idx = indices[i+1] * 3 val v2Idx = indices[i+2] * 3 // 获取局部顶点坐标,转换到世界空间 val v0 = pose.transformPoint(vertices[v0Idx], vertices[v0Idx+1], vertices[v0Idx+2]) val v1 = pose.transformPoint(vertices[v1Idx], vertices[v1Idx+1], vertices[v1Idx+2]) val v2 = pose.transformPoint(vertices[v2Idx], vertices[v2Idx+1], vertices[v2Idx+2]) // 射线与三角面相交检测(使用Möller-Trumbore算法) if (rayIntersectsTriangle(ray, v0, v1, v2)) { return true } } return false } // Möller-Trumbore射线三角相交算法实现 private fun rayIntersectsTriangle(ray: Ray, v0: FloatArray, v1: FloatArray, v2: FloatArray): Boolean { // 标准算法实现:计算射线与三角面的交点参数,判断是否在三角面范围内 val edge1 = floatArrayOf(v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]) val edge2 = floatArrayOf(v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]) val h = crossProduct(ray.direction, edge2) val a = dotProduct(edge1, h) if (a > -0.00001 && a < 0.00001) return false val f = 1/a val s = floatArrayOf(ray.origin[0]-v0[0], ray.origin[1]-v0[1], ray.origin[2]-v0[2]) val u = f * dotProduct(s, h) if (u < 0 || u > 1) return false val q = crossProduct(s, edge1) val v = f * dotProduct(ray.direction, q) if (v < 0 || u + v > 1) return false val t = f * dotProduct(edge2, q) return t > 0.00001 } // 辅助:计算向量叉乘 private fun crossProduct(a: FloatArray, b: FloatArray): FloatArray { return floatArrayOf( a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0] ) } // 辅助:计算向量点乘 private fun dotProduct(a: FloatArray, b: FloatArray): Float { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] }
补充说明
如果使用Filament(Google推荐的AR渲染引擎),可以直接使用Filament的Raycaster类来简化Mesh碰撞检测,无需手动实现三角面相交逻辑,效率和准确性更高。
内容的提问来源于stack exchange,提问作者Luca Rocchi
相关产品推荐
相关产品推荐

