LibGDX 3D:无法精准检测相机射线击中的方块网格面
解决LibGDX中第一人称射线检测方块面不准确的问题
嘿,我看你已经把方块定位的逻辑搞定了,卡在了精准检测方块面这一步——这种局部检测的问题通常都是变换空间或者顶点数据对应关系的小疏漏,咱们一步步来捋:
问题根源分析
你当前的实现有两个关键问题导致检测范围只有角落:
- 网格变换的方式不对:直接对整个Mesh做
transform()会把所有顶点都转换到世界空间,但每个NodePart对应的是原始Mesh的一部分索引,你没有考虑到Node自身的局部变换(比如方块的位置偏移其实是Node的变换,不是ModelInstance的全局变换),导致顶点位置和实际渲染的位置有偏差。 - 射线检测的空间不匹配:你在世界空间用变换后的Mesh做检测,但
Intersector.intersectRayTriangles对顶点数据的格式要求很严格,如果你的Mesh顶点包含法线、UV等额外属性,必须确保位置数据在顶点数组的最开头,且顶点大小计算正确。
优化后的解决方案
更好的思路是把射线转换到模型的局部空间,而不是变换整个Mesh——这样既避免了Mesh复制的性能开销,又能直接用原始Mesh的顶点/索引数据做检测,准确性更高。
完整修正代码
public void detectObjectPointed(int screenX, int screenY, PerspectiveCamera camera){ Ray ray = camera.getPickRay(1280/2, 720/2+BLOCK_SIZE); float closestDistance = -1f; BlockBase3D closestBlock = null; Vector3 tempIntersection = new Vector3(); BoundingBox tempBox = new BoundingBox(); // 重置之前选中的方块材质 if (mBlockPointed != null){ for (int i = 0; i < mBlockPointed.getModelInstance().nodes.get(0).parts.size; i++) { mBlockPointed.getModelInstance().nodes.get(0).parts.get(i).material.set(mOriginalMaterials.get(i)); } mBlockPointed.setIsShowingBoundingBox(false); mBlockPointed = null; mOriginalMaterials.clear(); } // 第一步:找到射线指向的最近方块 for (BlockBase3D block : mBlocksArray) { tempBox.set(block.getBoundingBox()); tempBox.mul(block.getModelInstance().transform); float distanceSquared = ray.origin.dst2(block.getCenterPosition()); // 跳过比当前最近方块更远的方块,优化性能 if (closestDistance >= 0 && distanceSquared > closestDistance) continue; if (Intersector.intersectRayBounds(ray, tempBox, tempIntersection)) { closestBlock = block; closestDistance = distanceSquared; } } mBlockPointed = closestBlock; if (mBlockPointed == null) return; // 标记选中的方块包围盒 mBlockPointed.setIsShowingBoundingBox(true); ModelInstance instance = mBlockPointed.getModelInstance(); Mesh originalMesh = instance.model.meshes.get(0); int vertexSize = originalMesh.getVertexSize() / 4; // 每个顶点的float数量 // 第二步:遍历每个方块面(NodePart),检测射线是否相交 for (Node node : instance.nodes) { // 计算当前Node的世界变换矩阵,并求逆得到从世界空间到局部空间的变换 Matrix4 nodeWorldTransform = node.globalTransform.cpy().mul(instance.transform); Matrix4 worldToLocal = nodeWorldTransform.inv(); // 将世界空间的射线转换到当前Node的局部空间 Ray localRay = new Ray(); worldToLocal.prj(ray.origin, localRay.origin); worldToLocal.prj(ray.direction, localRay.direction).nor(); for (NodePart part : node.parts) { MeshPart meshPart = part.meshPart; // 备份原始材质 mOriginalMaterials.add(part.material.copy()); // 准备索引数组:只获取当前MeshPart对应的索引 short[] indices = new short[meshPart.size]; originalMesh.getIndices(meshPart.offset, meshPart.size, indices, 0); // 准备顶点数组:直接用原始Mesh的顶点,因为我们已经把射线转换到局部空间了 float[] vertices = new float[originalMesh.getNumVertices() * vertexSize]; originalMesh.getVertices(vertices); // 检测射线与当前MeshPart的三角形相交 if (Intersector.intersectRayTriangles(localRay, vertices, indices, vertexSize, null)) { // 标记选中的面 part.material.set(mSelectionMaterial); } } } }
关键修改点说明
射线空间转换:
- 计算每个Node的世界变换矩阵,然后求逆得到
worldToLocal矩阵,把世界空间的射线转换到Node的局部空间。这样我们就可以直接使用原始Mesh的顶点数据,不用修改Mesh。 - 注意:
prj()方法会同时处理位置和方向,但方向需要重新归一化,因为逆矩阵可能会缩放方向向量。
- 计算每个Node的世界变换矩阵,然后求逆得到
使用Node的全局变换:
- ModelInstance的
transform是全局变换,而每个Node还有自己的globalTransform(局部变换叠加全局变换),所以正确的世界变换应该是node.globalTransform.mul(instance.transform)。
- ModelInstance的
性能优化:
- 避免每次检测都复制Mesh,改用射线空间转换,减少内存分配和计算开销。
- 提前跳过更远的方块,减少不必要的检测。
顶点数据兼容性:
- 确保你的Mesh顶点数据的第一个属性是位置(x,y,z),这是
Intersector.intersectRayTriangles默认读取的位置数据。如果你的Mesh顶点格式不同(比如位置在其他偏移量),需要修改方法调用,指定位置属性的偏移量(不过LibGDX默认的模型导出通常都是位置在前)。
- 确保你的Mesh顶点数据的第一个属性是位置(x,y,z),这是
测试建议
- 可以在检测到相交时,输出当前选中的NodePart的名称(如果模型导出时给每个面命名了),确认是否正确对应了指向的面。
- 暂时关闭方块的旋转缩放,先测试平移的情况,确保基础逻辑正确后再加入复杂变换。
内容的提问来源于stack exchange,提问作者safanth
相关产品推荐
相关产品推荐

