1
0
forked from Rowland/EG

1.修改整体布局(有BUG未修改完成)

This commit is contained in:
陈横 2025-08-28 15:46:38 +08:00
parent 0319b13f4a
commit 4f680a3da4
18 changed files with 4737 additions and 744 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
__pycache__/
*.pyc
.idea/
*.pyc

View File

@ -121,13 +121,26 @@ class InternalLightManager(object):
print("ERROR: Could not detach light, light was not attached!")
return
self._lights.free_slot(light.get_slot())
self.gpu_remove_light(light)
light.remove_slot()
# 保存必要信息,避免过早清理
light_slot = light.get_slot()
has_shadows = light.get_casts_shadows()
if light.get_casts_shadows():
# 处理shadow sources如果有的话
if has_shadows:
num_sources = light.get_num_shadow_sources()
if num_sources > 0:
# 关键修复先保存第一个source的slot再清理
first_source = light.get_shadow_source(0)
first_source_slot = first_source.get_slot() # 保存slot值
for i in range(light.get_num_shadow_sources()):
# 先发送GPU移除命令在清理之前
cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources)
cmd_remove.push_int(first_source_slot) # 使用保存的slot值
cmd_remove.push_int(num_sources)
self._cmd_list.add_command(cmd_remove)
# 然后清理所有shadow sources
for i in range(num_sources):
source = light.get_shadow_source(i)
if source.has_slot():
self._shadow_sources.free_slot(source.get_slot())
@ -135,11 +148,16 @@ class InternalLightManager(object):
self._shadow_manager.get_atlas().free_region(source.get_region())
source.clear_region()
self.gpu_remove_consecutive_sources(
light.get_shadow_source(0), light.get_num_shadow_sources())
# 清理light的shadow sources
light.clear_shadow_sources()
# 发送GPU移除light命令
self.gpu_remove_light(light)
# 最后释放light slot和清理light信息
self._lights.free_slot(light_slot)
light.remove_slot()
def gpu_remove_consecutive_sources(self, first_source, num_sources):
cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources)
cmd_remove.push_int(first_source.get_slot())

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

945
core/collision_manager.py Normal file
View File

@ -0,0 +1,945 @@
import time
from datetime import datetime
from panda3d.core import (
CollisionTraverser, CollisionHandlerQueue, CollisionNode,
CollisionRay, CollisionSphere, BitMask32, Point3, Vec3
)
from direct.task.TaskManagerGlobal import taskMgr
class CollisionManager:
"""统一的碰撞检测管理器"""
def __init__(self, world):
self.world = world
self.traverser = CollisionTraverser("main_traverser")
self.queue = CollisionHandlerQueue()
# 碰撞掩码定义
self.MASKS = {
# === 基础碰撞掩码定义 ===
# 每个掩码使用不同的位(bit)来标识,可以进行位运算组合
'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测
'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测
'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测
'MODEL_COLLISION': BitMask32.bit(3), # 模型碰撞 - 通用模型间碰撞检测
}
# 碰撞体形状类型
self.COLLISION_SHAPES = {
'SPHERE': 'sphere',
'BOX': 'box',
'CAPSULE': 'capsule',
'PLANE': 'plane',
'POLYGON': 'polygon',
'MESH': 'mesh'
}
# 掩码组合定义
self.MASK_COMBINATIONS = {
}
# 碰撞分组管理
self.collision_groups = {}
self.group_enabled = {}
# 碰撞回调系统
self.collision_callbacks = {}
self.collision_filters = {}
# 空间分割系统(八叉树)
self.spatial_partitioning_enabled = False
self.octree = None
self.octree_max_depth = 6
self.octree_max_objects = 10
# 连续碰撞检测
self.ccd_enabled = False
self.ccd_threshold = 10.0 # 速度阈值
# 性能监控
self.performance_monitor = CollisionPerformanceMonitor()
# 模型间碰撞检测配置
self.model_collision_enabled = False
self.collision_detection_frequency = 0.1
self.collision_distance_threshold = 0
self.last_collision_check = 0
self.collision_history = []
self.max_collision_history = 100
# 碰撞状态跟踪 - 避免重复打印
self.active_collisions = set() # 当前活跃的碰撞对
# 模型间碰撞检测任务
self.collision_task = None
print("✅ 碰撞检测管理器初始化完成")
def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
"""启用/禁用模型间碰撞检测
Args:
enable: 是否启用碰撞检测
frequency: 检测频率
threshold: 碰撞距离阈值
"""
self.model_collision_enabled = enable
self.collision_detection_frequency = frequency
self.collision_distance_threshold = threshold
if enable:
print(f"✅ 启用模型间碰撞检测 - 频率: {frequency}s, 阈值: {threshold}")
self._startCollisionDetectionTask()
else:
print("❌ 禁用模型间碰撞检测")
self._stopCollisionDetectionTask()
def _startCollisionDetectionTask(self):
"""启动碰撞检测任务"""
if self.collision_task:
taskMgr.remove(self.collision_task)
def collisionDetectionTask(task):
try:
self.detectModelCollisions()
return task.again
except Exception as e:
print(f"❌ 碰撞检测任务异常: {str(e)}")
return task.again
self.collision_task = taskMgr.doMethodLater(
self.collision_detection_frequency,
collisionDetectionTask,
"modelCollisionDetection"
)
def _stopCollisionDetectionTask(self):
"""停止碰撞检测任务"""
if self.collision_task:
taskMgr.remove(self.collision_task)
self.collision_task = None
def detectModelCollisions(self, specific_models=None, log_results=True):
"""检测模型间碰撞"""
if not self.model_collision_enabled and specific_models is None:
return []
start_time = time.perf_counter()
current_time = datetime.now()
models_to_check = specific_models if specific_models else self.world.models
if len(models_to_check) < 2:
return []
collision_results = []
checked_pairs = set()
current_collision_pairs = set()
# 遍历所有模型对
for i, model_a in enumerate(models_to_check):
for j, model_b in enumerate(models_to_check[i+1:], i+1):
pair_key = tuple(sorted([id(model_a), id(model_b)]))
if pair_key in checked_pairs:
continue
checked_pairs.add(pair_key)
collision_info = self._checkModelPairCollision(model_a, model_b, current_time)
if collision_info:
collision_results.append(collision_info)
current_collision_pairs.add(pair_key)
# 只有新碰撞才打印日志
if log_results and pair_key not in self.active_collisions:
self._logCollisionInfo(collision_info)
print(f"🆕 新碰撞检测到!")
# 检测碰撞结束的情况
ended_collisions = self.active_collisions - current_collision_pairs
for pair_key in ended_collisions:
print(f"✅ 碰撞结束: 模型对 {pair_key}")
# 更新活跃碰撞状态
self.active_collisions = current_collision_pairs
# 记录性能和历史
detection_time = time.perf_counter() - start_time
self.performance_monitor.recordModelCollisionDetection(
detection_time, len(models_to_check), len(collision_results))
if collision_results:
self.collision_history.extend(collision_results)
if len(self.collision_history) > self.max_collision_history:
self.collision_history = self.collision_history[-self.max_collision_history:]
return collision_results
def _checkModelPairCollision(self, model_a, model_b, timestamp):
"""使用Panda3D内置碰撞系统检查两个模型是否碰撞"""
try:
# 检查模型是否有碰撞节点
collision_node_a = self._findCollisionNode(model_a)
collision_node_b = self._findCollisionNode(model_b)
if not collision_node_a or not collision_node_b:
return None
# 使用Panda3D的碰撞遍历器
temp_traverser = CollisionTraverser()
temp_queue = CollisionHandlerQueue()
# 设置碰撞检测
temp_traverser.addCollider(collision_node_a, temp_queue)
# 执行碰撞检测
temp_traverser.traverse(model_b)
if temp_queue.getNumEntries() > 0:
# 有碰撞,获取碰撞信息
entry = temp_queue.getEntry(0)
center_a = model_a.getPos(self.world.render)
center_b = model_b.getPos(self.world.render)
distance = (center_b - center_a).length()
return {
'timestamp': timestamp,
'model_a': {
'name': model_a.getName(),
'center': center_a,
'radius': 0, # 不再需要手动计算半径
'node': model_a
},
'model_b': {
'name': model_b.getName(),
'center': center_b,
'radius': 0,
'node': model_b
},
'collision_point': entry.getSurfacePoint(self.world.render),
'distance': distance,
'penetration_depth': 0, # Panda3D会自动处理
'collision_normal': entry.getSurfaceNormal(self.world.render)
}
return None
except Exception as e:
print(f"❌ 检测模型对碰撞失败 ({model_a.getName()} vs {model_b.getName()}): {str(e)}")
return None
def _findCollisionNode(self, model):
"""查找模型的碰撞节点"""
for child in model.getChildren():
if isinstance(child.node(), CollisionNode):
return child
return None
def _logCollisionInfo(self, collision_info):
"""输出碰撞信息日志"""
timestamp = collision_info['timestamp'].strftime('%H:%M:%S.%f')[:-3]
model_a = collision_info['model_a']
model_b = collision_info['model_b']
print(f"🔥 [{timestamp}] 检测到碰撞:")
print(f" 模型A: {model_a['name']} (中心: {model_a['center']}, 半径: {model_a['radius']:.2f})")
print(f" 模型B: {model_b['name']} (中心: {model_b['center']}, 半径: {model_b['radius']:.2f})")
print(f" 碰撞点: {collision_info['collision_point']}")
print(f" 中心距离: {collision_info['distance']:.3f}")
print(f" 穿透深度: {collision_info['penetration_depth']:.3f}")
print(f" 碰撞法向: {collision_info['collision_normal']}")
def getCollisionHistory(self, limit=None):
"""获取碰撞历史记录
Args:
limit: 返回记录数量限制
Returns:
list: 碰撞历史记录
"""
if limit:
return self.collision_history[-limit:]
return self.collision_history.copy()
def clearCollisionHistory(self):
"""清除碰撞历史记录"""
self.collision_history.clear()
print("✅ 碰撞历史记录已清除")
def getCollisionStatistics(self):
"""获取碰撞统计信息"""
if not self.collision_history:
return {"total_collisions": 0, "unique_pairs": 0, "most_frequent_pair": None}
# 统计碰撞对
collision_pairs = {}
for collision in self.collision_history:
pair_key = tuple(sorted([
collision['model_a']['name'],
collision['model_b']['name']
]))
collision_pairs[pair_key] = collision_pairs.get(pair_key, 0) + 1
most_frequent_pair = max(collision_pairs.items(), key=lambda x: x[1])
return {
"total_collisions": len(self.collision_history),
"unique_pairs": len(collision_pairs),
"most_frequent_pair": {
"models": most_frequent_pair[0],
"count": most_frequent_pair[1]
},
"collision_pairs": collision_pairs
}
def createCollisionShape(self, model, shape_type='auto', **kwargs):
"""为模型创建指定类型的碰撞体
Args:
model: 模型节点
shape_type: 碰撞体类型 ('auto', 'sphere', 'box', 'capsule', 'plane', 'polygon')
**kwargs: 形状特定参数
Returns:
CollisionSolid: 创建的碰撞体
"""
from panda3d.core import (
CollisionSphere, CollisionBox, CollisionCapsule,
CollisionPlane, CollisionPolygon, Plane, Vec3
)
bounds = model.getBounds()
if bounds.isEmpty():
# 默认小球体
return CollisionSphere(Point3(0, 0, 0), 1.0)
center = bounds.getCenter()
radius = bounds.getRadius()
# 自动选择最适合的形状
if shape_type == 'auto':
shape_type = self._determineOptimalShape(model, bounds)
if shape_type == 'sphere':
return CollisionSphere(center, kwargs.get('radius', radius))
elif shape_type == 'box':
# 创建包围盒
min_point = bounds.getMin()
max_point = bounds.getMax()
return CollisionBox(min_point, max_point)
elif shape_type == 'capsule':
# 创建胶囊体(适合角色)
height = kwargs.get('height', (bounds.getMax().z - bounds.getMin().z))
radius = kwargs.get('radius', min(bounds.getRadius() * 0.5, height * 0.3))
point_a = Point3(center.x, center.y, bounds.getMin().z + radius)
point_b = Point3(center.x, center.y, bounds.getMax().z - radius)
return CollisionCapsule(point_a, point_b, radius)
elif shape_type == 'plane':
# 创建平面(适合地面、墙面)
normal = kwargs.get('normal', Vec3(0, 0, 1))
point = kwargs.get('point', center)
plane = Plane(normal, point)
return CollisionPlane(plane)
elif shape_type == 'polygon':
# === 创建多边形碰撞体 ===
# CollisionPolygon特点
# - 单个平面多边形,所有顶点必须共面
# - 适用于:地板、墙面、简单平台等平面区域
# - 性能较好,但只能表示平面形状
#
# Mesh碰撞体特点
# - 由多个三角形组成的复杂3D形状
# - 适用于:复杂地形、建筑物、不规则物体
# - 性能较差,但可以表示任意复杂形状
vertices = kwargs.get('vertices', [])
if len(vertices) >= 3:
# 将顶点转换为Point3对象
# 要求:所有顶点必须在同一平面上
collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices])
return collision_poly
else:
print("⚠️ 多边形至少需要3个顶点回退到球体")
return CollisionSphere(center, radius)
def _determineOptimalShape(self, model, bounds):
"""根据模型特征自动确定最适合的碰撞体形状"""
# 获取模型尺寸比例
size = bounds.getMax() - bounds.getMin()
max_dim = max(size.x, size.y, size.z)
min_dim = min(size.x, size.y, size.z)
# 根据模型名称和比例判断
model_name = model.getName().lower()
# 角色类模型使用胶囊体
if any(keyword in model_name for keyword in ['character', 'human', 'player', 'npc']):
return 'capsule'
# 建筑、箱子类使用包围盒
if any(keyword in model_name for keyword in ['building', 'box', 'cube', 'wall', 'door']):
return 'box'
# 地面、平台使用平面
if any(keyword in model_name for keyword in ['ground', 'floor', 'platform', 'terrain']):
return 'plane'
# 根据尺寸比例判断
aspect_ratio = max_dim / min_dim if min_dim > 0 else 1
if aspect_ratio > 3: # 细长物体用胶囊体
return 'capsule'
elif aspect_ratio < 1.5: # 接近球形用球体
return 'sphere'
else: # 其他用包围盒
return 'box'
def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']):
"""创建鼠标射线"""
# 组合掩码
combined_mask = BitMask32.allOff()
for mask_type in mask_types:
if mask_type in self.MASKS:
combined_mask |= self.MASKS[mask_type]
# 坐标转换
near_point, far_point = self.world.event_handler.robustCoordinateConversion(
screen_x, screen_y)
if not near_point:
return None
# 创建射线节点
ray_node = CollisionNode('mouse_ray')
ray_node.setFromCollideMask(combined_mask)
ray_node.setIntoCollideMask(BitMask32.allOff())
# 创建射线
direction = far_point - near_point
direction.normalize()
ray = CollisionRay(near_point, direction)
ray_node.addSolid(ray)
return ray_node, near_point, far_point
def performRaycast(self, ray_node, scene_root=None):
"""执行射线检测"""
if scene_root is None:
scene_root = self.world.render
# 创建临时节点路径
ray_np = self.world.cam.attachNewNode(ray_node)
try:
# 清除之前的结果
self.queue.clearEntries()
# 添加碰撞器
self.traverser.addCollider(ray_np, self.queue)
# 执行遍历
start_time = time.perf_counter()
self.traverser.traverse(scene_root)
detection_time = time.perf_counter() - start_time
# 记录性能
self.performance_monitor.recordDetection(
detection_time, self.queue.getNumEntries())
# 处理结果
results = []
if self.queue.getNumEntries() > 0:
self.queue.sortEntries()
for i in range(self.queue.getNumEntries()):
entry = self.queue.getEntry(i)
results.append({
'hit_pos': entry.getSurfacePoint(scene_root),
'hit_normal': entry.getSurfaceNormal(scene_root),
'hit_node': entry.getIntoNodePath(),
'distance': entry.getT()
})
return results
finally:
# 清理资源
self.traverser.removeCollider(ray_np)
ray_np.removeNode()
def getCollisionMask(self, mask_type):
"""获取碰撞掩码"""
return self.MASKS.get(mask_type, BitMask32.allOff())
def createCollisionGroup(self, group_name, mask_types, enabled=True):
"""创建碰撞分组
Args:
group_name: 分组名称
mask_types: 掩码类型列表
enabled: 是否启用
"""
combined_mask = BitMask32.allOff()
for mask_type in mask_types:
if mask_type in self.MASKS:
combined_mask |= self.MASKS[mask_type]
self.collision_groups[group_name] = {
'mask': combined_mask,
'types': mask_types,
'objects': []
}
self.group_enabled[group_name] = enabled
print(f"✅ 创建碰撞分组: {group_name}, 掩码: {mask_types}")
def addToCollisionGroup(self, group_name, model, collision_node):
"""将模型添加到碰撞分组"""
if group_name in self.collision_groups:
self.collision_groups[group_name]['objects'].append({
'model': model,
'collision_node': collision_node
})
def enableCollisionGroup(self, group_name, enabled=True):
"""启用/禁用碰撞分组"""
if group_name in self.group_enabled:
self.group_enabled[group_name] = enabled
# 更新分组中所有对象的碰撞状态
if group_name in self.collision_groups:
for obj in self.collision_groups[group_name]['objects']:
collision_node = obj['collision_node']
if enabled:
collision_node.show()
else:
collision_node.hide()
print(f"{'✅ 启用' if enabled else '❌ 禁用'}碰撞分组: {group_name}")
def registerCollisionCallback(self, mask_a, mask_b, callback_func, filter_func=None):
"""注册碰撞回调函数
Args:
mask_a: 第一个掩码类型
mask_b: 第二个掩码类型
callback_func: 碰撞回调函数 callback(collision_info)
filter_func: 过滤函数 filter(model_a, model_b) -> bool
"""
key = tuple(sorted([mask_a, mask_b]))
if key not in self.collision_callbacks:
self.collision_callbacks[key] = []
self.collision_callbacks[key].append(callback_func)
if filter_func:
if key not in self.collision_filters:
self.collision_filters[key] = []
self.collision_filters[key].append(filter_func)
print(f"✅ 注册碰撞回调: {mask_a} <-> {mask_b}")
def _executeCollisionCallbacks(self, collision_info):
"""执行碰撞回调"""
model_a = collision_info['model_a']['node']
model_b = collision_info['model_b']['node']
# 获取模型的掩码类型
mask_a = self._getModelMaskType(model_a)
mask_b = self._getModelMaskType(model_b)
if mask_a and mask_b:
key = tuple(sorted([mask_a, mask_b]))
# 检查过滤条件
if key in self.collision_filters:
for filter_func in self.collision_filters[key]:
if not filter_func(model_a, model_b):
return
# 执行回调
if key in self.collision_callbacks:
for callback_func in self.collision_callbacks[key]:
try:
callback_func(collision_info)
except Exception as e:
print(f"❌ 碰撞回调执行失败: {e}")
def _getModelMaskType(self, model):
"""获取模型的掩码类型"""
# 从模型标签或属性中获取掩码类型
if model.hasTag('collision_mask_type'):
return model.getTag('collision_mask_type')
return None
def enableSpatialPartitioning(self, enabled=True, max_depth=6, max_objects=10):
"""启用空间分割优化
Args:
enabled: 是否启用
max_depth: 八叉树最大深度
max_objects: 每个节点最大对象数
"""
self.spatial_partitioning_enabled = enabled
self.octree_max_depth = max_depth
self.octree_max_objects = max_objects
if enabled:
self._buildOctree()
print(f"✅ 启用空间分割优化 - 深度: {max_depth}, 对象数: {max_objects}")
else:
self.octree = None
print("❌ 禁用空间分割优化")
def _buildOctree(self):
"""构建八叉树"""
if not hasattr(self.world, 'models') or not self.world.models:
return
# 计算场景边界
min_bound = Point3(float('inf'), float('inf'), float('inf'))
max_bound = Point3(float('-inf'), float('-inf'), float('-inf'))
for model in self.world.models:
bounds = model.getBounds()
if not bounds.isEmpty():
model_min = bounds.getMin()
model_max = bounds.getMax()
min_bound.x = min(min_bound.x, model_min.x)
min_bound.y = min(min_bound.y, model_min.y)
min_bound.z = min(min_bound.z, model_min.z)
max_bound.x = max(max_bound.x, model_max.x)
max_bound.y = max(max_bound.y, model_max.y)
max_bound.z = max(max_bound.z, model_max.z)
# 创建八叉树根节点
self.octree = OctreeNode(min_bound, max_bound, 0, self.octree_max_depth, self.octree_max_objects)
# 将模型插入八叉树
for model in self.world.models:
self.octree.insert(model)
def detectModelCollisionsOptimized(self, specific_models=None, log_results=True):
"""使用空间分割优化的碰撞检测"""
if not self.spatial_partitioning_enabled or not self.octree:
return self.detectModelCollisions(specific_models, log_results)
start_time = time.perf_counter()
current_time = datetime.now()
models_to_check = specific_models if specific_models else self.world.models
if len(models_to_check) < 2:
return []
collision_results = []
current_collision_pairs = set()
# 使用八叉树进行优化检测
for model in models_to_check:
# 获取可能碰撞的模型列表
potential_collisions = self.octree.query(model)
for other_model in potential_collisions:
if model == other_model:
continue
pair_key = tuple(sorted([id(model), id(other_model)]))
if pair_key in current_collision_pairs:
continue
collision_info = self._checkModelPairCollision(model, other_model, current_time)
if collision_info:
collision_results.append(collision_info)
current_collision_pairs.add(pair_key)
# 执行回调
self._executeCollisionCallbacks(collision_info)
# 只有新碰撞才打印日志
if log_results and pair_key not in self.active_collisions:
self._logCollisionInfo(collision_info)
print(f"🆕 新碰撞检测到!")
# 更新活跃碰撞状态
ended_collisions = self.active_collisions - current_collision_pairs
for pair_key in ended_collisions:
print(f"✅ 碰撞结束: 模型对 {pair_key}")
self.active_collisions = current_collision_pairs
# 记录性能
detection_time = time.perf_counter() - start_time
self.performance_monitor.recordModelCollisionDetection(
detection_time, len(models_to_check), len(collision_results))
return collision_results
def cleanup(self):
"""清理碰撞管理器资源"""
self._stopCollisionDetectionTask()
self.collision_history.clear()
self.active_collisions.clear()
print("✅ 碰撞管理器已清理")
def setupAdvancedCollision(self, model, shape_type='auto', mask_type='SELECTABLE',
group_name=None, **shape_kwargs):
"""高级碰撞设置方法
Args:
model: 模型节点
shape_type: 碰撞体形状类型
mask_type: 掩码类型
group_name: 碰撞分组名称
**shape_kwargs: 形状特定参数
Returns:
collision_node_path: 碰撞节点路径
"""
# 创建碰撞节点
cNode = CollisionNode(f'collision_{model.getName()}')
# 设置掩码
if mask_type in self.MASKS:
cNode.setIntoCollideMask(self.MASKS[mask_type])
# 设置模型标签用于回调识别
model.setTag('collision_mask_type', mask_type)
# 创建碰撞体
collision_solid = self.createCollisionShape(model, shape_type, **shape_kwargs)
cNode.addSolid(collision_solid)
# 附加到模型
cNodePath = model.attachNewNode(cNode)
# 添加到分组
if group_name:
if group_name not in self.collision_groups:
self.createCollisionGroup(group_name, [mask_type])
self.addToCollisionGroup(group_name, model, cNodePath)
# 根据调试设置决定是否显示
if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
cNodePath.show()
else:
cNodePath.hide()
print(f"✅ 为 {model.getName()} 设置高级碰撞 - 形状: {shape_type}, 掩码: {mask_type}")
return cNodePath
def example_usage(self):
"""碰撞系统使用示例"""
print("\n=== 碰撞系统使用示例 ===")
# 1. 创建碰撞分组
self.createCollisionGroup('characters', ['CHARACTER', 'DYNAMIC_OBJECT'])
self.createCollisionGroup('environment', ['STATIC_GEOMETRY', 'TERRAIN'])
self.createCollisionGroup('pickups', ['PICKUP_ITEM', 'INTERACTIVE'])
# 2. 注册碰撞回调
def character_pickup_callback(collision_info):
print(f"角色拾取物品: {collision_info['model_a']['name']} -> {collision_info['model_b']['name']}")
def character_environment_callback(collision_info):
print(f"角色碰撞环境: {collision_info['model_a']['name']} 碰到 {collision_info['model_b']['name']}")
self.registerCollisionCallback('CHARACTER', 'PICKUP_ITEM', character_pickup_callback)
self.registerCollisionCallback('CHARACTER', 'STATIC_GEOMETRY', character_environment_callback)
# 3. 启用空间分割优化
self.enableSpatialPartitioning(enabled=True, max_depth=6, max_objects=8)
# 4. 为模型设置不同类型的碰撞体
# model1 = ... # 假设已有模型
# self.setupAdvancedCollision(model1, 'capsule', 'CHARACTER', 'characters')
# self.setupAdvancedCollision(model2, 'box', 'PICKUP_ITEM', 'pickups')
# self.setupAdvancedCollision(model3, 'plane', 'TERRAIN', 'environment')
print("✅ 碰撞系统示例配置完成")
class CollisionPerformanceMonitor:
"""碰撞检测性能监控器"""
def __init__(self):
self.detection_times = []
self.collision_counts = []
self.model_collision_times = [] # 新增:模型间碰撞检测时间
self.model_collision_data = [] # 新增:模型间碰撞数据
self.max_records = 100
def recordDetection(self, detection_time, collision_count):
"""记录射线检测性能"""
self.detection_times.append(detection_time)
self.collision_counts.append(collision_count)
# 限制记录数量
if len(self.detection_times) > self.max_records:
self.detection_times.pop(0)
self.collision_counts.pop(0)
def recordModelCollisionDetection(self, detection_time, model_count, collision_count):
"""记录模型间碰撞检测性能"""
self.model_collision_times.append(detection_time)
self.model_collision_data.append({
'model_count': model_count,
'collision_count': collision_count,
'timestamp': time.time()
})
# 限制记录数量
if len(self.model_collision_times) > self.max_records:
self.model_collision_times.pop(0)
self.model_collision_data.pop(0)
def getAverageTime(self):
"""获取平均射线检测时间"""
if not self.detection_times:
return 0
return sum(self.detection_times) / len(self.detection_times)
def getAverageModelCollisionTime(self):
"""获取平均模型间碰撞检测时间"""
if not self.model_collision_times:
return 0
return sum(self.model_collision_times) / len(self.model_collision_times)
def getPerformanceReport(self):
"""获取完整性能报告"""
report = []
# 射线检测性能
if self.detection_times:
avg_time = self.getAverageTime()
max_time = max(self.detection_times)
avg_collisions = sum(self.collision_counts) / len(self.collision_counts)
report.append("=== 射线检测性能 ===")
report.append(f"平均检测时间: {avg_time*1000:.2f}ms")
report.append(f"最大检测时间: {max_time*1000:.2f}ms")
report.append(f"平均碰撞数量: {avg_collisions:.1f}")
report.append(f"检测次数: {len(self.detection_times)}")
# 模型间碰撞检测性能
if self.model_collision_times:
avg_model_time = self.getAverageModelCollisionTime()
max_model_time = max(self.model_collision_times)
avg_model_count = sum(d['model_count'] for d in self.model_collision_data) / len(self.model_collision_data)
total_collisions = sum(d['collision_count'] for d in self.model_collision_data)
report.append("\n=== 模型间碰撞检测性能 ===")
report.append(f"平均检测时间: {avg_model_time*1000:.2f}ms")
report.append(f"最大检测时间: {max_model_time*1000:.2f}ms")
report.append(f"平均模型数量: {avg_model_count:.1f}")
report.append(f"总碰撞次数: {total_collisions}")
report.append(f"检测次数: {len(self.model_collision_times)}")
return "\n".join(report) if report else "没有性能数据"
class OctreeNode:
"""八叉树节点"""
def __init__(self, min_bound, max_bound, depth, max_depth, max_objects):
self.min_bound = min_bound
self.max_bound = max_bound
self.depth = depth
self.max_depth = max_depth
self.max_objects = max_objects
self.objects = []
self.children = []
self.is_leaf = True
def insert(self, model):
"""插入模型到八叉树"""
if not self._contains(model):
return False
if self.is_leaf:
self.objects.append(model)
# 如果超过最大对象数且未达到最大深度,则分割
if len(self.objects) > self.max_objects and self.depth < self.max_depth:
self._subdivide()
# 重新分配对象到子节点
remaining_objects = []
for obj in self.objects:
inserted = False
for child in self.children:
if child.insert(obj):
inserted = True
break
if not inserted:
remaining_objects.append(obj)
self.objects = remaining_objects
else:
# 尝试插入到子节点
inserted = False
for child in self.children:
if child.insert(model):
inserted = True
break
if not inserted:
self.objects.append(model)
return True
def query(self, model):
"""查询可能与指定模型碰撞的模型列表"""
if not self._contains(model):
return []
result = list(self.objects)
if not self.is_leaf:
for child in self.children:
result.extend(child.query(model))
return result
def _contains(self, model):
"""检查模型是否在此节点范围内"""
bounds = model.getBounds()
if bounds.isEmpty():
return False
model_min = bounds.getMin()
model_max = bounds.getMax()
return (model_max.x >= self.min_bound.x and model_min.x <= self.max_bound.x and
model_max.y >= self.min_bound.y and model_min.y <= self.max_bound.y and
model_max.z >= self.min_bound.z and model_min.z <= self.max_bound.z)
def _subdivide(self):
"""分割节点为8个子节点"""
center = Point3(
(self.min_bound.x + self.max_bound.x) * 0.5,
(self.min_bound.y + self.max_bound.y) * 0.5,
(self.min_bound.z + self.max_bound.z) * 0.5
)
# 创建8个子节点
subdivisions = [
(self.min_bound, center),
(Point3(center.x, self.min_bound.y, self.min_bound.z), Point3(self.max_bound.x, center.y, center.z)),
(Point3(self.min_bound.x, center.y, self.min_bound.z), Point3(center.x, self.max_bound.y, center.z)),
(Point3(center.x, center.y, self.min_bound.z), Point3(self.max_bound.x, self.max_bound.y, center.z)),
(Point3(self.min_bound.x, self.min_bound.y, center.z), Point3(center.x, center.y, self.max_bound.z)),
(Point3(center.x, self.min_bound.y, center.z), Point3(self.max_bound.x, center.y, self.max_bound.z)),
(Point3(self.min_bound.x, center.y, center.z), Point3(center.x, self.max_bound.y, self.max_bound.z)),
(center, self.max_bound)
]
for min_b, max_b in subdivisions:
child = OctreeNode(min_b, max_b, self.depth + 1, self.max_depth, self.max_objects)
self.children.append(child)
self.is_leaf = False

View File

@ -45,11 +45,44 @@ class GUIManager:
# ==================== GUI元素创建方法 ====================
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮"""
"""创建2D GUI按钮 - 支持多选创建和GUI父子关系优化版本"""
try:
from direct.gui.DirectGui import DirectButton
from PyQt5.QtCore import Qt
# 将3D坐标转换为2D屏幕坐标
print(f"🔘 开始创建GUI按钮位置: {pos}, 文本: {text}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 使用CustomTreeWidget的方法获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_gui_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_buttons = []
# 为每个有效的父节点创建GUI按钮
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
button_name = f"GUIButton_{len(self.gui_elements)}"
# 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
if tree_widget.is_gui_element(parent_node):
# 父节点是GUI元素 - 作为子GUI挂载
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
parent_gui_node = parent_node # 直接挂载到GUI元素
print(f"📎 挂载到GUI父节点: {parent_node.getName()}")
else:
# 父节点是普通3D节点 - 使用屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
parent_gui_node = None # 使用默认的aspect2d
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
button = DirectButton(
text=text,
@ -60,29 +93,107 @@ class GUIManager:
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
rolloverSound=None,
clickSound=None
clickSound=None,
parent=parent_gui_node # 设置GUI父节点
)
# 为GUI元素添加标识
# 设置节点标签
button.setTag("gui_type", "button")
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
button.setTag("gui_text", text)
button.setTag("is_gui_element", "1")
button.setTag("is_scene_element", "1")
button.setTag("created_by_user", "1")
button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
button.setName(button_name)
# 如果有GUI父节点建立引用关系
if parent_gui_node:
parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
button.setTag("gui_parent_id", parent_id)
# 添加到GUI元素列表
self.gui_elements.append(button)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建GUI按钮: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return button
print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}")
# 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(button, parent_item, "GUI_BUTTON")
if qt_item:
created_buttons.append((button, qt_item))
else:
created_buttons.append((button, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建GUI按钮失败: {str(e)}")
continue
# 处理创建结果
if not created_buttons:
print("❌ 没有成功创建任何GUI按钮")
return None
# 选中最后创建的按钮并更新场景树
if created_buttons:
last_button, last_qt_item = created_buttons[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_button, last_qt_item)
print(f"🎉 总共创建了 {len(created_buttons)} 个GUI按钮")
# 返回值处理
if len(created_buttons) == 1:
return created_buttons[0][0]
else:
return [button for button, _ in created_buttons]
except Exception as e:
print(f"❌ 创建GUI按钮过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签"""
"""创建2D GUI标签 - 支持多选创建和GUI父子关系优化版本"""
try:
from direct.gui.DirectGui import DirectLabel
from PyQt5.QtCore import Qt
# 将3D坐标转换为2D屏幕坐标
print(f"🏷️ 开始创建GUI标签位置: {pos}, 文本: {text}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 使用CustomTreeWidget的方法获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_gui_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_labels = []
# 为每个有效的父节点创建GUI标签
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
label_name = f"GUILabel_{len(self.gui_elements)}"
# 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
if tree_widget.is_gui_element(parent_node):
# 父节点是GUI元素 - 作为子GUI挂载
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
parent_gui_node = parent_node
print(f"📎 挂载到GUI父节点: {parent_node.getName()}")
else:
# 父节点是普通3D节点 - 使用屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
parent_gui_node = None
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
label = DirectLabel(
text=text,
@ -90,29 +201,107 @@ class GUIManager:
scale=size,
frameColor=(0, 0, 0, 0), # 透明背景
text_fg=(1, 1, 1, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
parent=parent_gui_node # 设置GUI父节点
)
# 为GUI元素添加标识
# 设置节点标签
label.setTag("gui_type", "label")
label.setTag("gui_id", f"label_{len(self.gui_elements)}")
label.setTag("gui_text", text)
label.setTag("is_gui_element", "1")
label.setTag("is_scene_element", "1")
label.setTag("created_by_user", "1")
label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
label.setName(label_name)
# 如果有GUI父节点建立引用关系
if parent_gui_node:
parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
label.setTag("gui_parent_id", parent_id)
# 添加到GUI元素列表
self.gui_elements.append(label)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建GUI标签: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return label
print(f"✅ 为 {parent_item.text(0)} 创建GUI标签成功: {label_name}")
# 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(label, parent_item, "GUI_LABEL")
if qt_item:
created_labels.append((label, qt_item))
else:
created_labels.append((label, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建GUI标签失败: {str(e)}")
continue
# 处理创建结果
if not created_labels:
print("❌ 没有成功创建任何GUI标签")
return None
# 选中最后创建的标签并更新场景树
if created_labels:
last_label, last_qt_item = created_labels[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_label, last_qt_item)
print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签")
# 返回值处理
if len(created_labels) == 1:
return created_labels[0][0]
else:
return [label for label, _ in created_labels]
except Exception as e:
print(f"❌ 创建GUI标签过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框"""
"""创建2D GUI文本输入框 - 支持多选创建和GUI父子关系优化版本"""
try:
from direct.gui.DirectGui import DirectEntry
from PyQt5.QtCore import Qt
# 将3D坐标转换为2D屏幕坐标
print(f"📝 开始创建GUI输入框位置: {pos}, 占位符: {placeholder}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 使用CustomTreeWidget的方法获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_gui_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_entries = []
# 为每个有效的父节点创建GUI输入框
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
entry_name = f"GUIEntry_{len(self.gui_elements)}"
# 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
if tree_widget.is_gui_element(parent_node):
# 父节点是GUI元素 - 作为子GUI挂载
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
parent_gui_node = parent_node
print(f"📎 挂载到GUI父节点: {parent_node.getName()}")
else:
# 父节点是普通3D节点 - 使用屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
parent_gui_node = None
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
entry = DirectEntry(
text="",
@ -123,26 +312,95 @@ class GUIManager:
initialText=placeholder,
numLines=1,
width=12,
focus=0
focus=0,
parent=parent_gui_node # 设置GUI父节点
)
# 为GUI元素添加标识
# 设置节点标签
entry.setTag("gui_type", "entry")
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
entry.setTag("gui_placeholder", placeholder)
entry.setTag("is_gui_element", "1")
entry.setTag("is_scene_element", "1")
entry.setTag("created_by_user", "1")
entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
entry.setName(entry_name)
# 如果有GUI父节点建立引用关系
if parent_gui_node:
parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
entry.setTag("gui_parent_id", parent_id)
# 添加到GUI元素列表
self.gui_elements.append(entry)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return entry
print(f"✅ 为 {parent_item.text(0)} 创建GUI输入框成功: {entry_name}")
# 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(entry, parent_item, "GUI_ENTRY")
if qt_item:
created_entries.append((entry, qt_item))
else:
created_entries.append((entry, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建GUI输入框失败: {str(e)}")
continue
# 处理创建结果
if not created_entries:
print("❌ 没有成功创建任何GUI输入框")
return None
# 选中最后创建的输入框并更新场景树
if created_entries:
last_entry, last_qt_item = created_entries[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_entry, last_qt_item)
print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框")
# 返回值处理
if len(created_entries) == 1:
return created_entries[0][0]
else:
return [entry for entry, _ in created_entries]
except Exception as e:
print(f"❌ 创建GUI输入框过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D空间文本"""
"""创建3D空间文本 - 支持多选创建,优化版本"""
try:
from panda3d.core import TextNode
from PyQt5.QtCore import Qt
print(f"📄 开始创建3D文本位置: {pos}, 文本: {text}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_texts = []
# 为每个有效的父节点创建3D文本
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
text_name = f"GUI3DText_{len(self.gui_elements)}"
textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
textNode.setText(text)
@ -150,64 +408,194 @@ class GUIManager:
if self.world.getChineseFont():
textNode.setFont(self.world.getChineseFont())
textNodePath = self.world.render.attachNewNode(textNode)
# 挂载到选中的父节点
textNodePath = parent_node.attachNewNode(textNode)
textNodePath.setPos(*pos)
textNodePath.setScale(size)
textNodePath.setColor(1, 1, 0, 1)
textNodePath.setBillboardAxis() # 让文本总是面向相机
textNodePath.setName(text_name)
# 为GUI元素添加标识
# 设置节点标签
textNodePath.setTag("gui_type", "3d_text")
textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}")
textNodePath.setTag("gui_text", text)
textNodePath.setTag("is_gui_element", "1")
textNodePath.setTag("is_scene_element", "1")
textNodePath.setTag("created_by_user", "1")
# 添加到GUI元素列表
self.gui_elements.append(textNodePath)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建3D文本: {text} (世界位置: {pos})")
return textNodePath
print(f"✅ 为 {parent_item.text(0)} 创建3D文本成功: {text_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(textNodePath, parent_item, "GUI_3DTEXT")
if qt_item:
created_texts.append((textNodePath, qt_item))
else:
created_texts.append((textNodePath, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建3D文本失败: {str(e)}")
continue
# 处理创建结果
if not created_texts:
print("❌ 没有成功创建任何3D文本")
return None
# 选中最后创建的文本并更新场景树
if created_texts:
last_text, last_qt_item = created_texts[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_text, last_qt_item)
print(f"🎉 总共创建了 {len(created_texts)} 个3D文本")
# 返回值处理
if len(created_texts) == 1:
return created_texts[0][0]
else:
return [text_np for text_np, _ in created_texts]
except Exception as e:
print(f"❌ 创建3D文本过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建3D虚拟屏幕"""
"""创建3D虚拟屏幕 - 支持多选创建,优化版本"""
try:
from panda3d.core import CardMaker, TransparencyAttrib, TextNode
from PyQt5.QtCore import Qt
# 创建虚拟屏幕
print(f"🖥️ 开始创建虚拟屏幕,位置: {pos}, 尺寸: {size}, 文本: {text}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_screens = []
# 为每个有效的父节点创建虚拟屏幕
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
screen_name = f"VirtualScreen_{len(self.gui_elements)}"
# 创建虚拟屏幕几何体
cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
virtualScreen = self.world.render.attachNewNode(cm.generate())
virtualScreen.setPos(*pos)
virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2)
# 创建挂载节点 - 挂载到选中的父节点
virtual_screen = parent_node.attachNewNode(cm.generate())
virtual_screen.setPos(*pos)
virtual_screen.setName(screen_name)
virtual_screen.setColor(0.2, 0.2, 0.2, 0.8)
virtual_screen.setTransparency(TransparencyAttrib.MAlpha)
# 在虚拟屏幕上添加文本
screenText = TextNode(f'screen-text-{len(self.gui_elements)}')
screenText.setText(text)
screenText.setAlign(TextNode.ACenter)
if self.world.getChineseFont():
screenText.setFont(self.world.getChineseFont())
screenTextNP = virtualScreen.attachNewNode(screenText)
screenTextNP.setPos(0, 0.01, 0)
screenTextNP.setScale(0.3)
screenTextNP.setColor(0, 1, 0, 1)
screen_text_node = self._create_screen_text(virtual_screen, text, len(self.gui_elements))
# 为GUI元素添加标识
virtualScreen.setTag("gui_type", "virtual_screen")
virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}")
virtualScreen.setTag("gui_text", text)
virtualScreen.setTag("is_gui_element", "1")
# 设置节点标签
virtual_screen.setTag("gui_type", "virtual_screen")
virtual_screen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}")
virtual_screen.setTag("gui_text", text)
virtual_screen.setTag("is_gui_element", "1")
virtual_screen.setTag("is_scene_element", "1")
virtual_screen.setTag("created_by_user", "1")
self.gui_elements.append(virtualScreen)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
# 添加到GUI元素列表
self.gui_elements.append(virtual_screen)
print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})")
return virtualScreen
print(f"✅ 为 {parent_item.text(0)} 创建虚拟屏幕成功: {screen_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(virtual_screen, parent_item, "GUI_VirtualScreen")
if qt_item:
created_screens.append((virtual_screen, qt_item))
else:
created_screens.append((virtual_screen, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建虚拟屏幕失败: {str(e)}")
continue
# 处理创建结果
if not created_screens:
print("❌ 没有成功创建任何虚拟屏幕")
return None
# 选中最后创建的虚拟屏幕
if created_screens:
last_screen_np, last_qt_item = created_screens[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕")
# 返回值处理
if len(created_screens) == 1:
return created_screens[0][0] # 单个屏幕返回NodePath
else:
return [screen_np for screen_np, _ in created_screens] # 多个屏幕返回列表
except Exception as e:
print(f"❌ 创建虚拟屏幕过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _create_screen_text(self, virtual_screen, text, screen_index):
"""为虚拟屏幕创建文本节点"""
try:
from panda3d.core import TextNode
screen_text = TextNode(f'screen-text-{screen_index}')
screen_text.setText(text)
screen_text.setAlign(TextNode.ACenter)
# 设置中文字体
if hasattr(self.world, 'getChineseFont') and self.world.getChineseFont():
screen_text.setFont(self.world.getChineseFont())
# 创建文本节点路径并设置属性
screen_text_np = virtual_screen.attachNewNode(screen_text)
screen_text_np.setPos(0, 0.01, 0) # 稍微向前偏移避免Z-fighting
screen_text_np.setScale(0.3)
screen_text_np.setColor(0, 1, 0, 1) # 绿色文本
print(f"✅ 虚拟屏幕文本创建成功: {text}")
return screen_text_np
except Exception as e:
print(f"❌ 创建虚拟屏幕文本失败: {str(e)}")
return None
def _get_tree_widget(self):
"""安全获取树形控件"""
try:
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'treeWidget')):
return self.world.interface_manager.treeWidget
except AttributeError:
pass
return None
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
"""创建2D GUI滑块"""
@ -232,6 +620,7 @@ class GUIManager:
self.gui_elements.append(slider)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
pass # CH
self.world.updateSceneTree()
print(f"✓ 创建GUI滑块: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")

BIN
icons/move_tool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
icons/rotate_tool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
icons/scale_tool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
icons/select_tool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

34
main.py
View File

@ -1,4 +1,7 @@
import warnings
from demo.video_integration import VideoManager
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
@ -63,6 +66,9 @@ class MyWorld(CoreWorld):
# 初始化GUI管理系统
self.gui_manager = GUIManager(self)
# 初始化视频管理
self.video_manager = VideoManager(self)
# 初始化场景管理系统
self.scene_manager = SceneManager(self)
@ -85,7 +91,18 @@ class MyWorld(CoreWorld):
#self.material_editor = None
# 初始化碰撞管理器
from core.collision_manager import CollisionManager
self.collision_manager = CollisionManager(self)
# 调试选项
self.debug_collision = True # 是否显示碰撞体
# 默认启用模型间碰撞检测(可选)
self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
print("✓ MyWorld 初始化完成")
print("✅ 碰撞管理器已初始化")
# ==================== 兼容性属性 ====================
@ -673,6 +690,23 @@ class MyWorld(CoreWorld):
"streaming_status": self.getALVRStreamingStatus() if self.isALVRConnected() else None
}
# 添加碰撞管理相关的代理方法
def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
"""启用模型间碰撞检测"""
return self.collision_manager.enableModelCollisionDetection(enable, frequency, threshold)
def detectModelCollisions(self, specific_models=None, log_results=True):
"""检测模型间碰撞"""
return self.collision_manager.detectModelCollisions(specific_models, log_results)
def getCollisionHistory(self, limit=None):
"""获取碰撞历史"""
return self.collision_manager.getCollisionHistory(limit)
def getCollisionStatistics(self):
"""获取碰撞统计"""
return self.collision_manager.getCollisionStatistics()
# ==================== 项目管理功能代理 ====================
# 以下函数代理到project_manager模块的对应功能

View File

@ -7,6 +7,8 @@
"""
import os
from PyQt5.QtCore import Qt
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere,
@ -33,13 +35,12 @@ class SceneManager:
self.Spotlight = []
self.Pointlight = []
print("✓ 场景管理系统初始化完成")
# ==================== 模型导入和处理 ====================
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
"""导入模型到场景
"""导入模型到场景 - 只在根节点下创建
Args:
filepath: 模型文件路径
@ -48,14 +49,16 @@ class SceneManager:
auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持
"""
try:
print(f"\n=== 开始导入模型: {filepath} ===")
print(f"\n💾 开始导入模型: {os.path.basename(filepath)}")
print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}")
print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}")
# 预处理文件路径和转换
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
# 检查是否需要转换为GLB以获得更好的动画支持
# 检查是否需要转换为GLB
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
print(f"🔄 检测到需要转换的格式尝试转换为GLB...")
converted_path = self._convertToGLBWithProgress(filepath)
@ -73,69 +76,94 @@ class SceneManager:
else:
print(f"⚠️ 转换失败,使用原始文件")
# 总是重新加载模型以确保材质信息完整
# 不使用ModelPool缓存避免材质信息丢失问题
# 直接在render根节点下创建模型
print("--- 在根节点下创建模型实例 ---")
# 加载模型
print("直接从文件加载模型...")
model = self.world.loader.loadModel(filepath)
if not model:
print("加载模型失败")
print("加载模型失败")
return None
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
# 将模型添加到场景
# 将模型挂载到render根节点
model.reparentTo(self.world.render)
# 保存原始路径和转换后的路径
# 设置标签和路径信息
model.setTag("model_path", filepath)
model.setTag("original_path", original_filepath)
model.setTag("file", model.getName())
model.setTag("is_model_root", "1")
model.setTag("is_scene_element", "1")
model.setTag("created_by_user", "1")
if filepath != original_filepath:
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
model.setTag("converted_to_glb", "true")
# 可选的单位转换主要针对FBX
# 应用处理选项
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
print("应用FBX单位转换厘米到米...")
self._applyUnitConversion(model, 0.01)
model.setTag("unit_conversion_applied", "true")
# 智能缩放标准化处理FBX子节点的大缩放值
if normalize_scales and filepath.lower().endswith('.fbx'):
print("标准化FBX模型缩放层级...")
self._normalizeModelScales(model)
model.setTag("scale_normalization_applied", "true")
# 调整模型位置到地面
self._adjustModelToGround(model)
# 创建并设置基础材质
print("\n=== 开始设置材质 ===")
print("设置材质...")
self._applyMaterialsToModel(model)
# 设置碰撞检测(重要!用于选择功能)
print("\n=== 设置碰撞检测 ===")
print("设置碰撞检测...")
self.setupCollision(model)
# 添加文件标签用于保存/加载
model.setTag("file", model_name)
model.setTag("is_model_root", "1")
# 记录应用的处理选项
if apply_unit_conversion:
model.setTag("unit_conversion_applied", "true")
if normalize_scales:
model.setTag("scale_normalization_applied", "true")
# 添加到模型列表
self.models.append(model)
# 更新场景树
self.updateSceneTree()
print(f"✅ 创建模型成功: {model.getName()}")
print(f"=== 模型导入成功: {model_name} ===\n")
# 获取树形控件并添加到Qt树中
tree_widget = self._get_tree_widget()
if tree_widget:
# 找到根节点项
root_item = None
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
root_item = item
break
if root_item:
qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE")
if qt_item:
tree_widget.setCurrentItem(qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(model, qt_item)
print("✅ Qt树节点添加成功")
else:
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
else:
print("⚠️ 未找到根节点项无法添加到Qt树")
else:
print("⚠️ 无法访问树形控件")
print(f"🎉 模型导入完成")
return model
except Exception as e:
print(f"导入模型失败: {str(e)}")
print(f"❌ 导入模型过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
# def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
@ -614,25 +642,66 @@ class SceneManager:
# ==================== 碰撞系统 ====================
def setupCollision(self, model):
"""为模型设置碰撞检测"""
"""为模型设置碰撞检测(增强版本)"""
try:
# 创建碰撞节点
cNode = CollisionNode(f'modelCollision_{model.getName()}')
# 设置碰撞掩码
cNode.setIntoCollideMask(BitMask32.bit(2)) # 使用第2位作为模型的碰撞掩码
cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择
# 如果启用了模型间碰撞检测,添加额外的掩码
if (hasattr(self.world, 'collision_manager') and
self.world.collision_manager.model_collision_enabled):
# 同时设置模型间碰撞掩码
current_mask = cNode.getIntoCollideMask()
model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION
cNode.setIntoCollideMask(current_mask | model_collision_mask)
print(f"{model.getName()} 启用模型间碰撞检测")
# 获取模型的边界
bounds = model.getBounds()
if bounds.isEmpty():
print(f"⚠️ 模型 {model.getName()} 边界为空,使用默认碰撞体")
# 使用默认的小球体
cSphere = CollisionSphere(Point3(0, 0, 0), 1.0)
else:
center = bounds.getCenter()
radius = bounds.getRadius()
# 添加碰撞球体
cSphere = CollisionSphere(center, radius)
# 确保半径不为零
if radius <= 0:
radius = 1.0
print(f"⚠️ 模型 {model.getName()} 半径为零,使用默认半径 1.0")
#
# # 添加碰撞球体
# cSphere = CollisionSphere(center, radius)
cSphere = self.world.collision_manager.createCollisionShape(model, 'polygon')
cNode.addSolid(cSphere)
# 将碰撞节点附加到模型上
cNodePath = model.attachNewNode(cNode)
#cNodePath.hide()
# cNodePath.show() # 取消注释可以显示碰撞体,用于调试
# 根据调试设置决定是否显示碰撞体
if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
cNodePath.show()
else:
cNodePath.hide()
# 为模型添加碰撞相关标签
model.setTag("has_collision", "true")
model.setTag("collision_radius", str(radius if 'radius' in locals() else 1.0))
print(f"✅ 为模型 {model.getName()} 设置碰撞检测完成")
return cNodePath
except Exception as e:
print(f"❌ 为模型 {model.getName()} 设置碰撞检测失败: {str(e)}")
import traceback
traceback.print_exc()
return None
# ==================== 场景树管理 ====================
@ -890,99 +959,489 @@ class SceneManager:
print(f"异步加载模型完成: {model.getName()}")
def createSpotLight(self, pos=(0, 0, 0)):
from RenderPipelineFile.rpcore import SpotLight, RenderPipeline
from panda3d.core import Vec3,NodePath
"""创建聚光灯 - 支持多选创建,优化版本"""
try:
from RenderPipelineFile.rpcore import SpotLight
from QPanda3D.Panda3DWorld import get_render_pipeline
from panda3d.core import Vec3, NodePath
from PyQt5.QtCore import Qt
print(f"🔆 开始创建聚光灯,位置: {pos}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_lights = []
render_pipeline = get_render_pipeline()
# 创建一个挂载节点(你控制的)
light_np = NodePath("SpotlightAttachNode")
light_np.reparentTo(self.world.render)
#light_np.setPos(*pos)
self.half_energy = 5000
self.lamp_fov = 70
self.lamp_radius = 1000
light = SpotLight()
light.direction = Vec3(0, 0, -1) # 光照方向
light.fov = self.lamp_fov # 光源角度(类似手电筒)
light.set_color_from_temperature(5 * 1000.0) # 色温K
light.energy = self.half_energy # 光照强度
light.radius = self.lamp_radius # 影响范围
light.casts_shadows = True # 是否投射阴影
light.shadow_map_resolution = 256 # 阴影分辨率
light.setPos(*pos)
#light_np.setPos(*pos)
#light_np = render_pipeline.add_light(light, parent=self.world.render)
render_pipeline.add_light(light) # 添加到渲染管线
# 为每个有效的父节点创建聚光灯
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
light_name = f"Spotlight_{len(self.Spotlight)}"
light_np.setName(light_name) # 设置唯一名称
#light_np.reparentTo(self.world.render) # 挂载到场景根节点
# 创建挂载节点 - 挂载到选中的父节点
light_np = NodePath(light_name)
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
light_np.setPos(*pos)
# 创建聚光灯对象
light = SpotLight()
light.direction = Vec3(0, 0, -1)
light.fov = 70
light.set_color_from_temperature(5 * 1000.0)
light.energy = 5000
light.radius = 1000
light.casts_shadows = True
light.shadow_map_resolution = 256
# 设置光源的世界坐标位置
world_pos = light_np.getPos(self.world.render)
light.setPos(world_pos)
# 添加到渲染管线
render_pipeline.add_light(light)
# 设置节点属性和标签
light_np.setTag("light_type", "spot_light")
light_np.setTag("is_scene_element", "1")
light_np.setTag("light_energy", str(light.energy))
light_np.setTag("created_by_user", "1")
# 保存光源对象引用
light_np.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.Spotlight.append(light_np)
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✅ 为 {parent_item.text(0)} 创建聚光灯成功: {light_name}")
#print("nikan"+light_np.getHpr())
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
if qt_item:
created_lights.append((light_np, qt_item))
else:
created_lights.append((light_np, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}")
continue
# 处理创建结果
if not created_lights:
print("❌ 没有成功创建任何聚光灯")
return None
# 选中最后创建的光源
if created_lights:
last_light_np, last_qt_item = created_lights[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_lights)} 个聚光灯")
# 返回值处理
if len(created_lights) == 1:
return created_lights[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_lights] # 多个光源返回列表
except Exception as e:
print(f"❌ 创建聚光灯过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createPointLight(self, pos=(0, 0, 0)):
from RenderPipelineFile.rpcore import PointLight, RenderPipeline
"""创建点光源 - 支持多选创建,优化版本"""
try:
from RenderPipelineFile.rpcore import PointLight
from QPanda3D.Panda3DWorld import get_render_pipeline
from panda3d.core import Vec3, NodePath
from PyQt5.QtCore import Qt
print(f"💡 开始创建点光源,位置: {pos}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_lights = []
render_pipeline = get_render_pipeline()
# 创建一个挂载节点(你控制的)
light_np = NodePath("PointlightAttachNode")
light_np.reparentTo(self.world.render)
# 为每个有效的父节点创建点光源
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
light_name = f"Pointlight_{len(self.Pointlight)}"
light = PointLight()
light.setPos(*pos)
# 创建挂载节点 - 挂载到选中的父节点
light_np = NodePath(light_name)
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
light_np.setPos(*pos)
# 创建点光源对象
light = PointLight()
# 设置光源的世界坐标位置
world_pos = light_np.getPos(self.world.render)
light.setPos(world_pos)
light.energy = 5000
light.radius = 1000
light.inner_radius = 0.4
light.set_color_from_temperature(5 * 1000.0) # 色温K
light.casts_shadows = True # 是否投射阴影
light.shadow_map_resolution = 256 # 阴影分辨率
light.set_color_from_temperature(5 * 1000.0)
light.casts_shadows = True
light.shadow_map_resolution = 256
render_pipeline.add_light(light) # 添加到渲染管线
light_name = f"Pointlight{len(self.Pointlight)}"
light_np.setName(light_name) # 设置唯一名称
#light_np = NodePath(f"PointLight_{len(self.Pointlight)}")
#light_np.reparentTo(self.world.render)
#light_np.setPos(*pos)
# 添加到渲染管线
render_pipeline.add_light(light)
# 设置节点属性和标签
light_np.setTag("light_type", "point_light")
light_np.setTag("is_scene_element", "1")
light_np.setTag("light_energy", str(light.energy))
light_np.setTag("created_by_user", "1")
# 保存光源对象引用(重要!用于属性面板)
# 保存光源对象引用
light_np.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.Pointlight.append(light_np)
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✅ 为 {parent_item.text(0)} 创建点光源成功: {light_name}")
return light,light_np
# 在Qt树形控件中添加对应节点
qt_item =tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
if qt_item:
created_lights.append((light_np, qt_item))
else:
created_lights.append((light_np, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建点光源失败: {str(e)}")
continue
# 处理创建结果
if not created_lights:
print("❌ 没有成功创建任何点光源")
return None
# 选中最后创建的光源
if created_lights:
last_light_np, last_qt_item = created_lights[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_lights)} 个点光源")
# 返回值处理
if len(created_lights) == 1:
return created_lights[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_lights] # 多个光源返回列表
except Exception as e:
print(f"❌ 创建点光源过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _get_tree_widget(self):
"""安全获取树形控件"""
try:
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'treeWidget')):
return self.world.interface_manager.treeWidget
except AttributeError:
pass
return None
def _importModelSingle(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
"""传统单一模型导入方法(兼容性保留)"""
try:
print(f"\n=== 使用传统模式导入模型: {filepath} ===")
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
# 检查是否需要转换为GLB
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
print(f"🔄 检测到需要转换的格式尝试转换为GLB...")
converted_path = self._convertToGLBWithProgress(filepath)
if converted_path:
print(f"✅ 转换成功: {converted_path}")
filepath = converted_path
try:
from PyQt5.QtWidgets import QMessageBox
original_ext = os.path.splitext(original_filepath)[1].upper()
QMessageBox.information(None, "转换成功",
f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!")
except:
pass
else:
print(f"⚠️ 转换失败,使用原始文件")
# 加载模型
print("直接从文件加载模型...")
model = self.world.loader.loadModel(filepath)
if not model:
print("加载模型失败")
return None
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
# 将模型添加到场景
model.reparentTo(self.world.render)
# 设置标签和路径信息
model.setTag("model_path", filepath)
model.setTag("original_path", original_filepath)
model.setTag("file", model_name)
model.setTag("is_model_root", "1")
model.setTag("is_scene_element", "1")
model.setTag("created_by_user", "1")
if filepath != original_filepath:
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
model.setTag("converted_to_glb", "true")
# 应用处理选项
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
print("应用FBX单位转换厘米到米...")
self._applyUnitConversion(model, 0.01)
model.setTag("unit_conversion_applied", "true")
if normalize_scales and filepath.lower().endswith('.fbx'):
print("标准化FBX模型缩放层级...")
self._normalizeModelScales(model)
model.setTag("scale_normalization_applied", "true")
# 调整模型位置到地面
self._adjustModelToGround(model)
# 创建并设置基础材质
print("\n=== 开始设置材质 ===")
self._applyMaterialsToModel(model)
# 设置碰撞检测(重要!用于选择功能)
print("\n=== 设置碰撞检测 ===")
self.setupCollision(model)
# 添加到模型列表
self.models.append(model)
# 更新场景树
self.updateSceneTree()
print(f"=== 模型导入成功: {model_name} ===\n")
return model
except Exception as e:
print(f"导入模型失败: {str(e)}")
return None
# def createSpotLight(self, pos=(0, 0, 0)):
# """创建聚光灯 - 使用统一的create_item方法"""
# try:
# # 调用CustomTreeWidget的create_item方法创建聚光灯节点
# if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
# tree_widget = self.world.interface_manager.treeWidget
# if tree_widget and hasattr(tree_widget, 'create_item'):
# # 创建聚光灯节点
# created_nodes = tree_widget.create_item("spot_light")
#
# if created_nodes:
# # 获取创建的节点
# light_np, qt_item = created_nodes[0]
#
# # 设置位置(如果指定了非默认位置)
# if pos != (0, 0, 0):
# light_np.setPos(*pos)
# # 同时更新光源对象的位置
# light_obj = light_np.getPythonTag("rp_light_object")
# if light_obj:
# light_obj.setPos(*pos)
#
# print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}")
# return light_np
# else:
# print("❌ create_item创建聚光灯失败")
# return None
# else:
# print("❌ 无法访问树形控件的create_item方法")
# return None
# else:
# print("❌ 无法访问界面管理器或树形控件")
# return None
#
# except Exception as e:
# print(f"❌ 创建聚光灯时发生错误: {str(e)}")
# import traceback
# traceback.print_exc()
# return None
# def createSpotLight(self, pos=(0, 0, 0)):
# from RenderPipelineFile.rpcore import SpotLight, RenderPipeline
# from panda3d.core import Vec3,NodePath
#
# render_pipeline = get_render_pipeline()
#
# # 创建一个挂载节点(你控制的)
# light_np = NodePath("SpotlightAttachNode")
# light_np.reparentTo(self.world.render)
# #light_np.setPos(*pos)
#
# self.half_energy = 5000
# self.lamp_fov = 70
# self.lamp_radius = 1000
#
# light = SpotLight()
# light.direction = Vec3(0, 0, -1) # 光照方向
# light.fov = self.lamp_fov # 光源角度(类似手电筒)
# light.set_color_from_temperature(5 * 1000.0) # 色温K
# light.energy = self.half_energy # 光照强度
# light.radius = self.lamp_radius # 影响范围
# light.casts_shadows = True # 是否投射阴影
# light.shadow_map_resolution = 256 # 阴影分辨率
# light.setPos(*pos)
# #light_np.setPos(*pos)
#
# #light_np = render_pipeline.add_light(light, parent=self.world.render)
# render_pipeline.add_light(light) # 添加到渲染管线
#
# light_name = f"Spotlight_{len(self.Spotlight)}"
#
# light_np.setName(light_name) # 设置唯一名称
# #light_np.reparentTo(self.world.render) # 挂载到场景根节点
#
# light_np.setTag("light_type", "spot_light")
# light_np.setTag("is_scene_element", "1")
# light_np.setTag("light_energy", str(light.energy))
#
# light_np.setPythonTag("rp_light_object", light)
#
# self.Spotlight.append(light_np)
#
# if hasattr(self.world, 'updateSceneTree'):
# self.world.updateSceneTree()
#
# #print("nikan"+light_np.getHpr())
# def createPointLight(self, pos=(0, 0, 0)):
# """创建点光源 - 使用统一的create_item方法"""
# try:
# # 调用CustomTreeWidget的create_item方法创建点光源节点
# if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
# tree_widget = self.world.interface_manager.treeWidget
# if tree_widget and hasattr(tree_widget, 'create_item'):
# # 创建点光源节点
# created_nodes = tree_widget.create_item("point_light")
#
# if created_nodes:
# # 获取创建的节点
# light_np, qt_item = created_nodes[0]
#
# # 设置位置(如果指定了非默认位置)
# if pos != (0, 0, 0):
# light_np.setPos(*pos)
# # 同时更新光源对象的位置
# light_obj = light_np.getPythonTag("rp_light_object")
# if light_obj:
# light_obj.setPos(*pos)
#
# print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}")
# return light_np
# else:
# print("❌ create_item创建点光源失败")
# return None
# else:
# print("❌ 无法访问树形控件的create_item方法")
# return None
# else:
# print("❌ 无法访问界面管理器或树形控件")
# return None
#
# except Exception as e:
# print(f"❌ 创建点光源时发生错误: {str(e)}")
# import traceback
# traceback.print_exc()
# return None
# def createPointLight(self, pos=(0, 0, 0)):
# from RenderPipelineFile.rpcore import PointLight, RenderPipeline
# from panda3d.core import Vec3, NodePath
#
# render_pipeline = get_render_pipeline()
#
# # 创建一个挂载节点(你控制的)
# light_np = NodePath("PointlightAttachNode")
# light_np.reparentTo(self.world.render)
#
#
# light = PointLight()
# light.setPos(*pos)
# light_np.setPos(*pos)
# light.energy = 5000
# light.radius = 1000
# light.inner_radius = 0.4
# light.set_color_from_temperature(5 * 1000.0) # 色温K
# light.casts_shadows = True # 是否投射阴影
# light.shadow_map_resolution = 256 # 阴影分辨率
#
# render_pipeline.add_light(light) # 添加到渲染管线
#
# light_name = f"Pointlight{len(self.Pointlight)}"
#
# light_np.setName(light_name) # 设置唯一名称
#
# #light_np = NodePath(f"PointLight_{len(self.Pointlight)}")
# #light_np.reparentTo(self.world.render)
# #light_np.setPos(*pos)
#
# light_np.setTag("light_type", "point_light")
# light_np.setTag("is_scene_element", "1")
# light_np.setTag("light_energy", str(light.energy))
#
# # 保存光源对象引用(重要!用于属性面板)
# light_np.setPythonTag("rp_light_object", light)
#
# self.Pointlight.append(light_np)
#
# if hasattr(self.world, 'updateSceneTree'):
# self.world.updateSceneTree()
#
# return light,light_np
# ==================== GLB 转换方法 ====================
#===================================================
def _shouldConvertToGLB(self, filepath):
"""判断是否应该转换为GLB格式"""
ext = os.path.splitext(filepath)[1].lower()

View File

@ -18,8 +18,8 @@ class InterfaceManager:
self.treeWidget = treeWidget
# 添加右键菜单
self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu)
# self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu)
# self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu)
# 更新场景树
self.world.scene_manager.updateSceneTree()
@ -155,10 +155,12 @@ class InterfaceManager:
# 创建场景根节点
sceneRoot = QTreeWidgetItem(self.treeWidget, ['场景'])
sceneRoot.setData(0, Qt.UserRole, self.world.render)
sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT")
# 添加相机节点
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
cameraItem.setData(0, Qt.UserRole, self.world.cam)
cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE")
print("添加相机节点")
# # 添加模型节点组
@ -223,6 +225,7 @@ class InterfaceManager:
if hasattr(self.world, 'ground') and self.world.ground:
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
groundItem.setData(0, Qt.UserRole, self.world.ground)
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
# 展开所有节点
#self.treeWidget.expandAll()

View File

@ -8,13 +8,15 @@
"""
import sys
from PyQt5.QtGui import QKeySequence, QIcon
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea,
QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout,
QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget)
from PyQt5.QtCore import Qt, QDir, QTimer
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget
QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QFrame)
from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget, CustomAssetsTreeWidget, CustomConsoleDockWidget
class MainWindow(QMainWindow):
"""主窗口类"""
@ -22,10 +24,12 @@ class MainWindow(QMainWindow):
def __init__(self, world):
super().__init__()
self.world = world
self.world.main_window = self # 关键让world对象能访问主窗口
self.setupCenterWidget() # 创建中间部分Panda3D
self.setupMenus() # 创建菜单栏
self.setupDockWindows()
self.setupToolbar()
# self.setupToolbar()
self.connectEvents()
# 移动窗口到屏幕中央
@ -44,6 +48,9 @@ class MainWindow(QMainWindow):
self.pandaWidget = CustomPanda3DWidget(self.world)
self.setCentralWidget(self.pandaWidget)
# 创建内嵌工具栏
self.setupEmbeddedToolbar()
def move_center(self):
"""设置窗口居中显示"""
self.setGeometry(50, 50, 1920, 1080)
@ -54,6 +61,204 @@ class MainWindow(QMainWindow):
int(screen.height() / 2 - self.height() / 2),
)
def setupEmbeddedToolbar(self):
"""创建Unity风格的内嵌工具栏"""
# 创建工具栏容器
self.embeddedToolbar = QFrame(self.pandaWidget)
self.embeddedToolbar.setObjectName("UnityToolbar")
self.embeddedToolbar.setStyleSheet("""
QFrame#UnityToolbar {
background-color: rgba(240, 240, 240, 180);
border: 1px solid rgba(200, 200, 200, 200);
border-radius: 4px;
}
QToolButton {
background-color: rgba(250, 250, 250, 150);
border: none;
color: #333333;
padding: 5px;
border-radius: 3px;
}
QToolButton:hover {
background-color: rgba(220, 220, 220, 200);
}
QToolButton:checked {
background-color: rgba(100, 150, 220, 180);
color: white;
}
QToolButton:pressed {
background-color: rgba(80, 130, 200, 200);
}
""")
# 水平布局
self.toolbarLayout = QHBoxLayout(self.embeddedToolbar)
self.toolbarLayout.setContentsMargins(5, 5, 5, 5)
self.toolbarLayout.setSpacing(2)
# 创建工具按钮组
self.toolGroup = QButtonGroup()
# 选择工具
self.selectTool = QToolButton()
self.selectTool.setIcon(QIcon("icons/select_tool.png")) # 使用图标资源
self.selectTool.setText('选择')
self.selectTool.setIconSize(QSize(16, 16))
self.selectTool.setCheckable(True)
self.selectTool.setToolTip("选择工具 (Q)")
# self.selectTool.setShortcut(QKeySequence("Q"))
self.toolGroup.addButton(self.selectTool)
self.toolbarLayout.addWidget(self.selectTool)
# 移动工具
self.moveTool = QToolButton()
self.moveTool.setIcon(QIcon("icons/move_tool.png"))
self.moveTool.setText('移动')
self.moveTool.setIconSize(QSize(16, 16))
self.moveTool.setCheckable(True)
self.moveTool.setToolTip("移动工具 (W)")
# self.moveTool.setShortcut(QKeySequence("W"))
self.toolGroup.addButton(self.moveTool)
self.toolbarLayout.addWidget(self.moveTool)
# 旋转工具
self.rotateTool = QToolButton()
self.rotateTool.setIcon(QIcon("icons/rotate_tool.png"))
self.rotateTool.setText('旋转')
self.rotateTool.setIconSize(QSize(16, 16))
self.rotateTool.setCheckable(True)
self.rotateTool.setToolTip("旋转工具 (E)")
# self.rotateTool.setShortcut(QKeySequence("E"))
self.toolGroup.addButton(self.rotateTool)
self.toolbarLayout.addWidget(self.rotateTool)
# 缩放工具
self.scaleTool = QToolButton()
self.scaleTool.setIcon(QIcon("icons/scale_tool.png"))
self.scaleTool.setText('缩放')
self.scaleTool.setIconSize(QSize(16, 16))
self.scaleTool.setCheckable(True)
self.scaleTool.setToolTip("缩放工具 (R)")
# self.scaleTool.setShortcut(QKeySequence("R"))
self.toolGroup.addButton(self.scaleTool)
self.toolbarLayout.addWidget(self.scaleTool)
# 添加分隔符
separator = QFrame()
separator.setFrameShape(QFrame.VLine)
separator.setFrameShadow(QFrame.Sunken)
separator.setStyleSheet("background-color: rgba(240, 240, 240, 255);")
separator.setFixedWidth(1)
self.toolbarLayout.addWidget(separator)
# 设置位置到左上角
self.embeddedToolbar.move(10, 10)
self.embeddedToolbar.adjustSize()
self.embeddedToolbar.show()
# 连接事件
self.toolGroup.buttonClicked.connect(self.onToolChanged)
# 设置工具栏拖拽事件
self.embeddedToolbar.mousePressEvent = self.toolbarMousePressEvent
self.embeddedToolbar.mouseMoveEvent = self.toolbarMouseMoveEvent
self.embeddedToolbar.mouseReleaseEvent = self.toolbarMouseReleaseEvent
# 默认选择"选择"工具
self.selectTool.setChecked(True)
self.world.setCurrentTool("选择")
def toolbarMousePressEvent(self, event):
"""工具栏鼠标按下事件"""
if event.button() == Qt.LeftButton:
self.toolbarDragging = True
self.dragStartPos = event.globalPos()
self.toolbarStartPos = self.embeddedToolbar.pos()
event.accept()
def toolbarMouseMoveEvent(self, event):
"""工具栏鼠标移动事件"""
if self.toolbarDragging and event.buttons() == Qt.LeftButton:
# 计算新位置
delta = event.globalPos() - self.dragStartPos
new_pos = self.toolbarStartPos + delta
# 边界检测
panda_rect = self.pandaWidget.geometry()
toolbar_size = self.embeddedToolbar.size()
# 限制在Panda3D区域内
new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width())))
new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height())))
self.embeddedToolbar.move(new_pos)
event.accept()
def toolbarMouseReleaseEvent(self, event):
"""工具栏鼠标释放事件"""
if event.button() == Qt.LeftButton and self.toolbarDragging:
self.toolbarDragging = False
# 自动吸附到最近的预设位置
self.snapToNearestPosition()
event.accept()
def snapToNearestPosition(self):
"""自动吸附到最近的预设位置"""
current_pos = self.embeddedToolbar.pos()
panda_rect = self.pandaWidget.geometry()
toolbar_size = self.embeddedToolbar.size()
margin = 10
# 定义所有预设位置
positions = {
"top_center": QPoint(
(panda_rect.width() - toolbar_size.width()) // 2,
margin
),
"top_left": QPoint(margin, margin),
"top_right": QPoint(
panda_rect.width() - toolbar_size.width() - margin,
margin
),
"bottom_center": QPoint(
(panda_rect.width() - toolbar_size.width()) // 2,
panda_rect.height() - toolbar_size.height() - margin
),
"bottom_left": QPoint(
margin,
panda_rect.height() - toolbar_size.height() - margin
),
"bottom_right": QPoint(
panda_rect.width() - toolbar_size.width() - margin,
panda_rect.height() - toolbar_size.height() - margin
)
}
# 找到最近的位置
min_distance = float('inf')
nearest_position = "top_center"
for pos_name, pos_point in positions.items():
distance = ((current_pos.x() - pos_point.x()) ** 2 +
(current_pos.y() - pos_point.y()) ** 2) ** 0.5
if distance < min_distance:
min_distance = distance
nearest_position = pos_name
# 更新位置并平滑移动
self.toolbarPosition = nearest_position
target_pos = positions[nearest_position]
# 简单的平滑移动动画
from PyQt5.QtCore import QPropertyAnimation, QEasingCurve
self.toolbarAnimation = QPropertyAnimation(self.embeddedToolbar, b"pos")
self.toolbarAnimation.setDuration(200)
self.toolbarAnimation.setStartValue(current_pos)
self.toolbarAnimation.setEndValue(target_pos)
self.toolbarAnimation.setEasingCurve(QEasingCurve.OutCubic)
self.toolbarAnimation.start()
def setupMenus(self):
"""创建菜单栏"""
menubar = self.menuBar()
@ -93,39 +298,19 @@ class MainWindow(QMainWindow):
self.sunsetAction = self.toolsMenu.addAction('光照编辑')
self.pluginAction = self.toolsMenu.addAction('图形编辑')
# 创建菜单
# 统一创建菜单 - 关键修改
self.createMenu = menubar.addMenu('创建')
self.createEnptyaddAction = self.createMenu.addAction('空对象')
self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象')
self.setupCreateMenuActions() # 统一创建菜单动作
self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
self.createGUIaddMenu = self.createMenu.addMenu('GUI')
self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
self.createGUIaddMenu.addSeparator()
self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
self.createLightaddMenu = self.createMenu.addMenu('光源')
self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
# GUI菜单
# GUI菜单 - 复用创建菜单的动作
self.guiMenu = menubar.addMenu('GUI')
self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式')
self.guiMenu.addSeparator()
# self.createButtonAction = self.guiMenu.addAction('创建按钮')
# self.createLabelAction = self.guiMenu.addAction('创建标签')
# self.createEntryAction = self.guiMenu.addAction('创建输入框')
self.guiMenu.addAction(self.createButtonAction)
self.guiMenu.addAction(self.createLabelAction)
self.guiMenu.addAction(self.createEntryAction)
self.guiMenu.addSeparator()
# self.create3DTextAction = self.guiMenu.addAction('创建3D文本')
self.guiMenu.addAction(self.create3DTextAction)
# self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕')
self.guiMenu.addAction(self.createVirtualScreenAction)
# 脚本菜单
@ -144,32 +329,95 @@ class MainWindow(QMainWindow):
self.helpMenu = menubar.addMenu('帮助')
self.aboutAction = self.helpMenu.addAction('关于')
def setupCreateMenuActions(self):
"""统一设置创建菜单的所有动作 - 避免重复代码"""
# 基础对象
self.createEnptyaddAction = self.createMenu.addAction('空对象')
# 3D对象子菜单
self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象')
# 可以在这里添加更多3D对象类型
# 3D GUI子菜单
self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
# GUI子菜单
self.createGUIaddMenu = self.createMenu.addMenu('GUI')
self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
self.createGUIaddMenu.addSeparator()
self.createVideoScreen = self.createGUIaddMenu.addAction('创建视频屏幕')
self.createSphericalVideo = self.createGUIaddMenu.addAction('创建球形视频')
self.createGUIaddMenu.addSeparator()
self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
# 光源子菜单
self.createLightaddMenu = self.createMenu.addMenu('光源')
self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
# 统一连接信号到处理方法
self.connectCreateMenuActions()
def connectCreateMenuActions(self):
"""统一连接创建菜单的信号到处理方法"""
# 连接到world对象的创建方法
# self.createEnptyaddAction.triggered.connect(self.world.createEmptyObject)
self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
# self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
# self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo)
# self.createVirtualScreenAction.triggered.connect(self.world.createVirtualScreen)
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
# # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen(
# # pos=(0, 0, 2),
# # size=(4, 3),
# # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}"
# # ))
# # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo())
# self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video())
def getCreateMenuActions(self):
"""获取所有创建菜单动作的字典 - 供右键菜单使用"""
return {
'createEmpty': self.createEnptyaddAction,
'create3DText': self.create3DTextAction,
'createButton': self.createButtonAction,
'createLabel': self.createLabelAction,
'createEntry': self.createEntryAction,
'createVideoScreen': self.createVideoScreen,
'createSphericalVideo': self.createSphericalVideo,
'createVirtualScreen': self.createVirtualScreenAction,
'createSpotLight': self.createSpotLightAction,
'createPointLight': self.createPointLightAction,
}
def setupDockWindows(self):
"""创建停靠窗口"""
# 创建左侧停靠窗口(层级窗口)
self.leftDock = QDockWidget("层级", self)
self.leftDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.treeWidget = CustomTreeWidget(self.world)
self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用
self.leftDock.setWidget(self.treeWidget)
# self.leftDock.setMinimumWidth(300)
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock)
# 创建右侧停靠窗口(属性窗口)
self.rightDock = QDockWidget("属性", self)
self.rightDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
# 创建属性面板的主容器和布局
self.propertyContainer = QWidget()
self.propertyContainer.setObjectName("PropertyContainer")
self.propertyLayout = QVBoxLayout(self.propertyContainer)
# self.propertyLayout = QFormLayout(self.propertyContainer)
# 添加初始提示信息
tipLabel = QLabel("")
tipLabel.setStyleSheet("color: gray;") # 使用灰色字体
# self.propertyLayout.addRow(tipLabel)
self.propertyLayout.addWidget(tipLabel)
# 创建滚动区域并设置属性
@ -185,41 +433,69 @@ class MainWindow(QMainWindow):
# 设置属性面板到世界对象
self.world.setPropertyLayout(self.propertyLayout)
# 创建脚本管理停靠窗口
self.scriptDock = QDockWidget("脚本管理", self)
self.scriptDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.setupScriptPanel()
# 创建脚本面板的主容器和布局(与属性面板相同结构)
self.scriptContainer = QWidget()
self.scriptContainer.setObjectName("ScriptContainer")
self.scriptLayout = QVBoxLayout(self.scriptContainer)
# 创建滚动区域并设置属性
self.scriptScrollArea = QScrollArea()
self.scriptScrollArea.setWidgetResizable(True)
self.scriptScrollArea.setWidget(self.scriptContainer)
# 设置滚动区域为停靠窗口的主部件
self.scriptDock.setWidget(self.scriptScrollArea)
self.scriptDock.setMinimumWidth(300)
# 设置脚本面板内容
self.setupScriptPanel(self.scriptLayout)
self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock)
# 将右侧停靠窗口设为标签形式
self.tabifyDockWidget(self.rightDock, self.scriptDock)
# # 创建底部停靠窗口(资源窗口)
# self.bottomDock = QDockWidget("资源", self)
# self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea)
#
# # 创建文件系统模型
# self.fileModel = QFileSystemModel()
# self.fileModel.setRootPath(QDir.homePath()) # 设置为用户主目录
#
# # 创建树形视图显示文件系统
# self.fileView = CustomFileView(self.world)
# self.fileView.setModel(self.fileModel)
# self.fileView.setRootIndex(self.fileModel.index(QDir.homePath())) # 设置为用户主目录索引
#
# # 设置列宽
# self.fileView.setColumnWidth(0, 250) # 名称列
# self.fileView.setColumnWidth(1, 100) # 大小列
# self.fileView.setColumnWidth(2, 100) # 类型列
# self.fileView.setColumnWidth(3, 150) # 修改日期列
#
# # 设置视图属性
# self.fileView.setMinimumHeight(200) # 设置最小高度
#
# self.bottomDock.setWidget(self.fileView)
# self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
# 创建底部停靠窗口(资源窗口)
self.bottomDock = QDockWidget("资源", self)
self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea)
# 创建文件系统模型
self.fileModel = QFileSystemModel()
self.fileModel.setRootPath(QDir.homePath()) # 设置为用户主目录
# 创建树形视图显示文件系统
self.fileView = CustomFileView(self.world)
self.fileView.setModel(self.fileModel)
self.fileView.setRootIndex(self.fileModel.index(QDir.homePath())) # 设置为用户主目录索引
# 设置列宽
self.fileView.setColumnWidth(0, 250) # 名称列
self.fileView.setColumnWidth(1, 100) # 大小列
self.fileView.setColumnWidth(2, 100) # 类型列
self.fileView.setColumnWidth(3, 150) # 修改日期列
# 设置视图属性
self.fileView.setMinimumHeight(200) # 设置最小高度
self.fileView = CustomAssetsTreeWidget(self.world)
self.bottomDock.setWidget(self.fileView)
self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
# 创建底部停靠控制台
self.consoleDock = QDockWidget("控制台", self)
self.consoleView = CustomConsoleDockWidget(self.world)
self.consoleDock.setWidget(self.consoleView)
self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
def setupToolbar(self):
"""创建工具栏"""
self.toolbar = self.addToolBar('工具栏')
@ -276,12 +552,8 @@ class MainWindow(QMainWindow):
self.selectTool.setChecked(True)
self.world.setCurrentTool("选择")
def setupScriptPanel(self):
def setupScriptPanel(self, layout):
"""创建脚本管理面板"""
# 创建主容器
scriptContainer = QWidget()
layout = QVBoxLayout(scriptContainer)
# 脚本状态组
statusGroup = QGroupBox("脚本系统状态")
statusLayout = QVBoxLayout()
@ -389,9 +661,6 @@ class MainWindow(QMainWindow):
# 添加拉伸以填充剩余空间
layout.addStretch()
# 设置到停靠窗口
self.scriptDock.setWidget(scriptContainer)
# 初始化脚本列表
self.refreshScriptsList()
@ -414,24 +683,32 @@ class MainWindow(QMainWindow):
# 连接GUI编辑模式事件
self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode())
# 连接创建事件
# 连接光源创建按钮事件
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
# 连接GUI创建按钮事件
self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
#self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
# 连接工具栏GUI创建按钮事件
self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton())
self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel())
self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText())
self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight())
self.createPointLight.clicked.connect(lambda :self.world.createPointLight())
# # 连接创建事件
# # 连接光源创建按钮事件
# self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
# self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
# # 连接GUI创建按钮事件
# self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
# self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
# self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
# self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
# #self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
#
# # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen(
# # pos=(0, 0, 2),
# # size=(4, 3),
# # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}"
# # ))
# # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo())
#
# self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video())
#
# # 连接工具栏GUI创建按钮事件
# self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton())
# self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel())
# self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText())
# self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight())
# self.createPointLight.clicked.connect(lambda :self.world.createPointLight())
# 连接树节点点击信号
# self.treeWidget.itemClicked.connect(self.world.onTreeItemClicked)

View File

@ -504,6 +504,7 @@ class PropertyPanelManager:
success = self.world.gui_manager.editGUIElement(gui_element, "text", text)
if success:
# 更新场景树显示的名称
pass #CH
self.world.scene_manager.updateSceneTree()
textEdit.textChanged.connect(updateText)

File diff suppressed because it is too large Load Diff