EG/demo/improved_picking_test.py
2025-07-02 09:49:59 +08:00

351 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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