351 lines
13 KiB
Python
351 lines
13 KiB
Python
#!/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("改进的点击检测测试完成") |