Unity3D中获取GameObject可见区域2D包围盒的高效实现问询
针对你开发深度学习标注工具时遇到的两个核心问题(误包含不可见对象、包围盒覆盖整个物体而非可见部分),这里有几个落地性很强的优化方向,结合Unity原生API就能解决:
1. 准确过滤相机视锥体外的对象
你当前仅通过判断物体中心的屏幕坐标z值是否小于0来排除不可见对象,这个逻辑太粗糙——比如物体部分在相机视野内但中心在视野外,或者中心在视野内但整个物体都在视野外的情况都无法准确判断。
优化方案:使用Unity的GeometryUtility.TestPlanesAABB来检测物体的包围盒是否与相机的视锥体相交,这是判断物体是否在相机可见范围内(或部分可见)的标准方法:
// 获取相机的视锥体平面数组 Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(camera); Renderer renderer = go.GetComponentInChildren<Renderer>(); // 检测物体的包围盒是否与视锥体相交(即是否有部分可见) if (!GeometryUtility.TestPlanesAABB(frustumPlanes, renderer.bounds)) { return new Rect(-1f, -1f, -1f, -1f); // 返回无效矩形表示完全不可见 }
替换你原来的camera.WorldToScreenPoint(...).z < 0判断逻辑,就能准确过滤掉完全不在相机视野内的对象。
2. 仅基于屏幕可见顶点生成包围盒
当前代码用了物体所有顶点来计算包围盒,导致即使部分顶点在屏幕外,也会被包含进去。我们需要过滤掉那些不在屏幕显示区域内的顶点,只基于可见的顶点生成包围盒。
优化方案:在遍历顶点时,先判断该顶点的屏幕坐标是否在屏幕有效范围内(z>0表示在相机前方,x/y在0到屏幕宽高之间):
List<Vector3> visibleVertices = new List<Vector3>(); MeshFilter meshFilter = go.GetComponentInChildren<MeshFilter>(); if (meshFilter == null || meshFilter.mesh.vertices.Length == 0) { return new Rect(-1f, -1f, -1f, -1f); } foreach (Vector3 localVertex in meshFilter.mesh.vertices) { Vector3 worldVertex = go.transform.TransformPoint(localVertex); Vector3 screenPos = camera.WorldToScreenPoint(worldVertex); // 过滤掉相机后方或屏幕外的顶点 if (screenPos.z > 0 && screenPos.x >= 0 && screenPos.x <= Screen.width && screenPos.y >= 0 && screenPos.y <= Screen.height) { // 转换为GUI坐标系(y轴翻转) screenPos.y = Screen.height - screenPos.y; visibleVertices.Add(screenPos); } } // 如果没有可见顶点,返回无效矩形 if (visibleVertices.Count == 0) { return new Rect(-1f, -1f, -1f, -1f); } // 基于可见顶点计算最小最大坐标 Vector3 min = visibleVertices[0]; Vector3 max = visibleVertices[0]; foreach (Vector3 pos in visibleVertices) { min = Vector3.Min(min, pos); max = Vector3.Max(max, pos); } // 生成最终包围盒并添加margin Rect rect = Rect.MinMaxRect(min.x, min.y, max.x, max.y); rect.xMin -= margin; rect.xMax += margin; rect.yMin -= margin; rect.yMax += margin; return rect;
这样生成的包围盒就只包含物体在屏幕上可见的部分了。
3. 处理遮挡情况(可选但对标注精度至关重要)
如果你的场景中存在物体互相遮挡(比如车轮被车身挡住),上面的方法还是会把被遮挡的顶点算进去,导致包围盒包含不可见区域。针对这种情况,可以增加顶点可见性检测:
优化方案:对每个顶点,从相机发射一条射线到顶点,检测是否有其他物体遮挡了该顶点。如果射线中途碰到了非目标物体的碰撞体,就过滤掉这个顶点:
// 注意:需要给目标物体和遮挡物体添加Collider,建议通过Layer分层优化检测 int targetLayer = go.layer; foreach (Vector3 localVertex in meshFilter.mesh.vertices) { Vector3 worldVertex = go.transform.TransformPoint(localVertex); Vector3 screenPos = camera.WorldToScreenPoint(worldVertex); if (screenPos.z <= 0) continue; // 从相机到顶点的射线 Ray ray = camera.ScreenPointToRay(new Vector3(screenPos.x, Screen.height - screenPos.y, 0)); if (Physics.Raycast(ray, out RaycastHit hit, screenPos.z)) { // 如果射线击中的物体不是目标物体的一部分,说明该顶点被遮挡 if (!hit.collider.gameObject.transform.IsChildOf(go.transform)) { continue; } } // 转换坐标并加入可见列表 screenPos.y = Screen.height - screenPos.y; visibleVertices.Add(screenPos); }
不过要注意,这种方法会增加性能开销,如果你的模型顶点很多,可以考虑简化顶点采样(比如只取网格的关键顶点,或者用包围盒的8个顶点来检测),或者通过Layer只检测可能的遮挡层,平衡精度和性能。
总结
这些优化步骤可以逐步实现:先解决视锥体过滤和屏幕内顶点过滤,解决你当前的两个核心问题;如果需要更高精度的标注,再添加遮挡检测逻辑。这样生成的包围盒会更贴合物体的实际可见区域,非常适合深度学习训练数据的标注需求。
内容的提问来源于stack exchange,提问作者n0s3

