#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys from direct.showbase.ShowBase import ShowBase from panda3d.core import ( Point3, Point2, Vec3, Vec4, CardMaker, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, CollisionSphere, BitMask32, PerspectiveLens ) class ImprovedPickingTest(ShowBase): def __init__(self): ShowBase.__init__(self) # 相机设置 self.cam.setPos(-3.87655, -188.38084, 82.602684) self.cam.lookAt(0, 0, 0) print("=== 改进的点击检测测试 ===") # 创建测试对象 self.createTestObjects() # 测试各种点击检测方法 self.testAllPickingMethods() def createTestObjects(self): """创建测试对象""" self.objects = [] # 创建几个测试立方体 positions = [ (0, 10, 3, "中心立方体"), (-5, 10, 3, "左侧立方体"), (5, 10, 3, "右侧立方体"), (0, 10, 8, "上方立方体"), (0, 15, 3, "远处立方体") ] colors = [ (1, 0, 0, 1), # 红色 (0, 1, 0, 1), # 绿色 (0, 0, 1, 1), # 蓝色 (1, 1, 0, 1), # 黄色 (1, 0, 1, 1), # 紫色 ] for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)): # 创建立方体 cm = CardMaker(f"cube_{i}") cm.setFrame(-1, 1, -1, 1) cube = self.render.attachNewNode(cm.generate()) cube.setPos(x, y, z) cube.setScale(2, 2, 2) cube.setColor(*color) cube.setBillboardAxis() cube.setName(name) # 为每个对象添加球形碰撞体(用于精确的碰撞检测) collider = cube.attachNewNode(CollisionNode(f"cube_collider_{i}")) collider.node().addSolid(CollisionSphere(0, 0, 0, 2.0)) collider.node().setFromCollideMask(BitMask32.bit(0)) # 存储对象信息 obj_info = { 'node': cube, 'name': name, 'world_pos': Point3(x, y, z), 'radius': 2.0, 'collider': collider } self.objects.append(obj_info) print(f"创建 {name} 在位置 ({x}, {y}, {z})") # 方法1:改进的射线检测(从相机发出) def pickByRayFromCamera(self, mouseX, mouseY): """使用从相机发出的射线进行检测""" print(f"\n=== 方法1: 从相机发出的射线检测 ===") # 获取窗口尺寸 winWidth = self.win.getXSize() winHeight = self.win.getYSize() # 转换为归一化坐标 normalizedX = (2.0 * mouseX / winWidth) - 1.0 normalizedY = 1.0 - (2.0 * mouseY / winHeight) print(f"鼠标位置: ({mouseX}, {mouseY})") print(f"归一化坐标: ({normalizedX:.6f}, {normalizedY:.6f})") # 获取相机镜头 lens = self.cam.node().getLens() # 计算射线在相机坐标系中的方向 # 使用镜头的逆投影来获取正确的射线方向 near_point = Point3() far_point = Point3() if lens.extrude(Point2(normalizedX, normalizedY), near_point, far_point): # 转换到世界坐标系 near_world = self.cam.getRelativePoint(self.render, near_point) far_world = self.cam.getRelativePoint(self.render, far_point) # 计算射线方向 ray_origin = near_world ray_direction = (far_world - near_world).normalized() print(f"射线起点 (相机近平面): {near_world}") print(f"射线方向: {ray_direction}") # 手动检测与对象的交集 return self.intersectRayWithObjects(ray_origin, ray_direction) else: print("无法生成射线") return None def intersectRayWithObjects(self, ray_origin, ray_direction): """手动计算射线与对象的交集""" closest_obj = None closest_distance = float('inf') for obj_info in self.objects: # 计算射线与球体的交点 center = obj_info['world_pos'] radius = obj_info['radius'] # 射线到球心的向量 oc = ray_origin - center # 二次方程系数 a = ray_direction.dot(ray_direction) b = 2.0 * oc.dot(ray_direction) c = oc.dot(oc) - radius * radius # 判别式 discriminant = b * b - 4 * a * c if discriminant >= 0: # 有交点,计算距离 sqrt_discriminant = discriminant ** 0.5 t1 = (-b - sqrt_discriminant) / (2 * a) t2 = (-b + sqrt_discriminant) / (2 * a) # 取最近的正值交点 t = t1 if t1 > 0 else t2 if t > 0: distance = t if distance < closest_distance: closest_distance = distance closest_obj = obj_info print(f"{obj_info['name']}: 交点距离 {distance:.2f}") else: print(f"{obj_info['name']}: 在射线后方") else: print(f"{obj_info['name']}: 无交点") if closest_obj: print(f"选中: {closest_obj['name']}") else: print("没有选中任何对象") return closest_obj # 方法2:基于屏幕投影的检测 def pickByProjection(self, mouseX, mouseY): """使用屏幕投影进行检测""" print(f"\n=== 方法2: 屏幕投影检测 ===") closest_obj = None closest_distance = float('inf') for obj_info in self.objects: # 将3D位置投影到屏幕 screenPos = self.worldToScreen(obj_info['world_pos']) if screenPos: screenX, screenY = screenPos # 计算屏幕半径 screenRadius = self.getObjectScreenRadius(obj_info) # 计算鼠标到对象中心的距离 dx = mouseX - screenX dy = mouseY - screenY distance = (dx * dx + dy * dy) ** 0.5 print(f"{obj_info['name']}: 屏幕位置({screenX:.1f}, {screenY:.1f}), 半径{screenRadius:.1f}, 距离{distance:.1f}") # 检查是否在点击范围内 if distance <= screenRadius and distance < closest_distance: closest_distance = distance closest_obj = obj_info else: print(f"{obj_info['name']}: 不在屏幕内") if closest_obj: print(f"选中: {closest_obj['name']}") else: print("没有选中任何对象") return closest_obj def worldToScreen(self, worldPos): """将世界坐标转换为屏幕坐标""" lens = self.cam.node().getLens() camPos = self.cam.getRelativePoint(self.render, worldPos) screenPoint = Point2() if lens.project(camPos, screenPoint): winWidth = self.win.getXSize() winHeight = self.win.getYSize() pixelX = (screenPoint.getX() + 1.0) * winWidth * 0.5 pixelY = (1.0 - screenPoint.getY()) * winHeight * 0.5 return pixelX, pixelY return None def getObjectScreenRadius(self, obj_info): """计算对象在屏幕上的半径""" worldPos = obj_info['world_pos'] worldRadius = obj_info['radius'] centerScreen = self.worldToScreen(worldPos) if not centerScreen: return 0 edgePos = worldPos + Vec3(worldRadius, 0, 0) edgeScreen = self.worldToScreen(edgePos) if edgeScreen: return abs(edgeScreen[0] - centerScreen[0]) return 0 # 方法3:混合检测(投影预选 + 射线精确) def pickByHybrid(self, mouseX, mouseY): """使用混合方法进行检测""" print(f"\n=== 方法3: 混合检测(投影预选 + 射线精确)===") # 第一步:使用投影快速预选 candidates = [] for obj_info in self.objects: screenPos = self.worldToScreen(obj_info['world_pos']) if screenPos: screenX, screenY = screenPos screenRadius = self.getObjectScreenRadius(obj_info) dx = mouseX - screenX dy = mouseY - screenY distance = (dx * dx + dy * dy) ** 0.5 # 使用更大的容差进行预选 tolerance = screenRadius * 1.5 if distance <= tolerance: candidates.append((obj_info, distance)) print(f"预选候选: {obj_info['name']}, 屏幕距离: {distance:.1f}") if not candidates: print("投影预选: 没有候选对象") return None # 第二步:对候选对象使用精确射线检测 print("对候选对象进行精确射线检测:") winWidth = self.win.getXSize() winHeight = self.win.getYSize() normalizedX = (2.0 * mouseX / winWidth) - 1.0 normalizedY = 1.0 - (2.0 * mouseY / winHeight) lens = self.cam.node().getLens() near_point = Point3() far_point = Point3() if lens.extrude(Point2(normalizedX, normalizedY), near_point, far_point): near_world = self.cam.getRelativePoint(self.render, near_point) far_world = self.cam.getRelativePoint(self.render, far_point) ray_origin = near_world ray_direction = (far_world - near_world).normalized() # 只检测候选对象 closest_obj = None closest_distance = float('inf') for obj_info, _ in candidates: center = obj_info['world_pos'] radius = obj_info['radius'] oc = ray_origin - center a = ray_direction.dot(ray_direction) b = 2.0 * oc.dot(ray_direction) c = oc.dot(oc) - radius * radius discriminant = b * b - 4 * a * c if discriminant >= 0: sqrt_discriminant = discriminant ** 0.5 t1 = (-b - sqrt_discriminant) / (2 * a) t2 = (-b + sqrt_discriminant) / (2 * a) t = t1 if t1 > 0 else t2 if t > 0 and t < closest_distance: closest_distance = t closest_obj = obj_info print(f" {obj_info['name']}: 精确射线距离 {t:.2f}") if closest_obj: print(f"最终选中: {closest_obj['name']}") else: print("精确检测: 没有有效交点") return closest_obj return None def testAllPickingMethods(self): """测试所有点击检测方法""" print(f"\n=== 测试所有点击检测方法 ===") # 测试点击位置(模拟点击左侧立方体附近) test_clicks = [ (372, 265, "左侧立方体附近"), (399, 265, "中心立方体附近"), (425, 265, "右侧立方体附近"), ] for mouseX, mouseY, description in test_clicks: print(f"\n{'='*60}") print(f"测试位置: {description} ({mouseX}, {mouseY})") print(f"{'='*60}") # 方法1:从相机发出的射线 result1 = self.pickByRayFromCamera(mouseX, mouseY) # 方法2:屏幕投影 result2 = self.pickByProjection(mouseX, mouseY) # 方法3:混合方法 result3 = self.pickByHybrid(mouseX, mouseY) # 比较结果 print(f"\n--- 结果比较 ---") print(f"射线检测: {result1['name'] if result1 else '无'}") print(f"投影检测: {result2['name'] if result2 else '无'}") print(f"混合检测: {result3['name'] if result3 else '无'}") if __name__ == "__main__": app = ImprovedPickingTest() print("改进的点击检测测试完成")