1
0
forked from Rowland/EG
EG/scene/scene_manager.py
2025-07-02 09:49:59 +08:00

524 lines
21 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 os
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere,
BitMask32, TransparencyAttrib
)
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
class SceneManager:
"""场景管理器 - 统一管理场景中的所有元素"""
def __init__(self, world):
"""初始化场景管理器
Args:
world: 主程序world对象引用
"""
self.world = world
self.models = [] # 模型列表
print("✓ 场景管理系统初始化完成")
# ==================== 模型导入和处理 ====================
def importModel(self, filepath):
"""导入模型到场景"""
try:
print(f"\n=== 开始导入模型: {filepath} ===")
# 首先检查ModelPool中是否已有该模型
model = ModelPool.getModel(filepath, True)
if not model:
print("模型不在缓存中,开始加载...")
# 使用loader加载模型
model = self.world.loader.loadModel(filepath)
if not model:
print("加载模型失败")
return None
# 如果是ModelRoot节点,添加到ModelPool
if isinstance(model.node(), ModelRoot):
print("添加模型到ModelPool缓存")
model.node().setFullpath(Filename(filepath))
ModelPool.addModel(model.node())
else:
print("从ModelPool获取缓存的模型")
# 创建模型的副本
model = NodePath(model.copySubgraph())
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
# 将模型添加到场景
model.reparentTo(self.world.render)
# 处理FBX文件的单位转换
if filepath.lower().endswith('.fbx'):
print("处理FBX模型单位转换...")
# FBX使用厘米需要缩放到米
scale_factor = 0.01 # 将厘米转换为米
model.setScale(scale_factor)
# 调整位置使模型位于地面上
bounds = model.getBounds()
min_point = bounds.getMin()
# 将模型底部对齐到地面(y=0)
model.setZ(-min_point.getZ() * scale_factor)
else:
# 非FBX文件使用默认变换
model.setPos(0, 0, 0)
model.setHpr(0, 0, 0)
model.setScale(1, 1, 1)
# 创建并设置基础材质
print("\n=== 开始设置材质 ===")
self._applyMaterialsToModel(model)
# 添加文件标签用于保存/加载
model.setTag("file", model_name)
model.setTag("is_model_root", "1")
# 添加到模型列表
self.models.append(model)
# 更新场景树
self.updateSceneTree()
print(f"=== 模型导入成功: {model_name} ===\n")
return model
except Exception as e:
print(f"导入模型失败: {str(e)}")
return None
def _applyMaterialsToModel(self, model):
"""递归应用材质到模型的所有GeomNode"""
def apply_material(node_path, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {node_path.getName()}")
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
if isinstance(node_path.node(), GeomNode):
print(f"{indent}发现GeomNode处理材质")
geom_node = node_path.node()
# 检查所有几何体的状态
has_color = False
color = None
# 首先检查节点自身的状态
node_state = node_path.getState()
if node_state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType())
node_material = mat_attrib.getMaterial()
if node_material and node_material.hasDiffuse():
color = node_material.getDiffuse()
has_color = True
print(f"{indent}从节点材质获取颜色: {color}")
# 检查FBX特有的属性
for tag_key in node_path.getTagKeys():
print(f"{indent}发现标签: {tag_key}")
if "color" in tag_key.lower() or "diffuse" in tag_key.lower():
tag_value = node_path.getTag(tag_key)
print(f"{indent}颜色相关标签: {tag_key} = {tag_value}")
# 如果还没找到颜色,检查几何体
if not has_color:
for i in range(geom_node.getNumGeoms()):
geom = geom_node.getGeom(i)
state = geom_node.getGeomState(i)
# 检查顶点颜色
vdata = geom.getVertexData()
format = vdata.getFormat()
for j in range(format.getNumColumns()):
column = format.getColumn(j)
# InternalName对象需要使用getName()转换为字符串
column_name = column.getName().getName()
if "color" in column_name.lower():
print(f"{indent}发现顶点颜色数据: {column_name}")
# 这里可以读取顶点颜色,但先记录发现
# 检查材质属性
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
orig_material = mat_attrib.getMaterial()
if orig_material:
if orig_material.hasBaseColor():
color = orig_material.getBaseColor()
has_color = True
print(f"{indent}从基础颜色获取: {color}")
break
elif orig_material.hasDiffuse():
color = orig_material.getDiffuse()
has_color = True
print(f"{indent}从漫反射颜色获取: {color}")
break
# 检查颜色属性
if not has_color and state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
if not color_attrib.isOff():
color = color_attrib.getColor()
has_color = True
print(f"{indent}从颜色属性获取: {color}")
break
# 创建新材质
material = Material()
if has_color:
print(f"{indent}应用找到的颜色: {color}")
material.setDiffuse(color)
material.setBaseColor(color) # 同时设置基础颜色
node_path.setColor(color)
else:
print(f"{indent}使用默认颜色")
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
# 设置其他材质属性
material.setAmbient((0.2, 0.2, 0.2, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
# 应用材质
node_path.setMaterial(material)
print(f"{indent}几何体数量: {geom_node.getNumGeoms()}")
# 递归处理子节点
child_count = node_path.getNumChildren()
print(f"{indent}子节点数量: {child_count}")
for i in range(child_count):
child = node_path.getChild(i)
apply_material(child, depth + 1)
# 应用材质
print("\n开始递归应用材质...")
apply_material(model)
print("=== 材质设置完成 ===\n")
def importModelAsync(self, filepath):
"""异步导入模型"""
try:
# 创建异步加载请求
request = self.world.loader.makeAsyncRequest(filepath)
# 添加完成回调
def modelLoaded(task):
if task.isReady():
model = task.result()
if model:
# 处理加载完成的模型
self.processLoadedModel(model)
return task.done()
request.done_event = modelLoaded
# 开始异步加载
self.world.loader.loadAsync(request)
except Exception as e:
print(f"异步加载模型失败: {str(e)}")
def loadAnimatedModel(self, model_path, anims=None):
"""加载带动画的模型"""
try:
# 创建Actor对象
actor = Actor(model_path, anims)
if actor:
actor.reparentTo(self.world.render)
self.models.append(actor)
# 更新场景树
self.updateSceneTree()
return actor
except Exception as e:
print(f"加载动画模型失败: {str(e)}")
return None
# ==================== 材质和几何体处理 ====================
def processMaterials(self, model):
"""处理模型材质"""
if isinstance(model.node(), GeomNode):
# 创建基础材质
material = Material()
material.setAmbient((0.2, 0.2, 0.2, 1.0))
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
# 检查FBX材质
state = model.node().getGeomState(0)
if state.hasAttrib(MaterialAttrib.getClassType()):
fbx_material = state.getAttrib(MaterialAttrib.getClassType()).getMaterial()
if fbx_material:
# 复制FBX材质属性
material.setAmbient(fbx_material.getAmbient())
material.setDiffuse(fbx_material.getDiffuse())
material.setSpecular(fbx_material.getSpecular())
material.setShininess(fbx_material.getShininess())
# 应用材质
model.setMaterial(material)
def processModelGeometry(self, model):
"""处理模型几何体"""
# 创建EggData对象
egg_data = EggData()
# 处理顶点数据
vertex_pool = EggVertexPool("vpool")
egg_data.addChild(vertex_pool)
# 处理几何体
if isinstance(model.node(), GeomNode):
for i in range(model.node().getNumGeoms()):
geom = model.node().getGeom(i)
# 处理几何体数据
# ...
# ==================== 碰撞系统 ====================
def setupCollision(self, model):
"""为模型设置碰撞检测"""
# 创建碰撞节点
cNode = CollisionNode(f'modelCollision_{model.getName()}')
# 设置碰撞掩码
cNode.setIntoCollideMask(BitMask32.bit(2)) # 使用第2位作为模型的碰撞掩码
# 获取模型的边界
bounds = model.getBounds()
center = bounds.getCenter()
radius = bounds.getRadius()
# 添加碰撞球体
cSphere = CollisionSphere(center, radius)
cNode.addSolid(cSphere)
# 将碰撞节点附加到模型上
cNodePath = model.attachNewNode(cNode)
# cNodePath.show() # 取消注释可以显示碰撞体,用于调试
# ==================== 场景树管理 ====================
def updateSceneTree(self):
"""更新场景树显示 - 代理到interface_manager"""
if hasattr(self.world, 'interface_manager'):
return self.world.interface_manager.updateSceneTree()
else:
print("界面管理器未初始化,无法更新场景树")
# ==================== 场景保存和加载 ====================
def saveScene(self, filename):
"""保存场景到BAM文件"""
try:
print(f"\n=== 开始保存场景到: {filename} ===")
# 遍历所有模型,保存材质状态
for model in self.models:
# 获取当前状态
state = model.getState()
# 如果有材质属性,保存为标签
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
material = mat_attrib.getMaterial()
if material:
# 保存材质属性到标签
model.setTag("material_ambient", str(material.getAmbient()))
model.setTag("material_diffuse", str(material.getDiffuse()))
model.setTag("material_specular", str(material.getSpecular()))
model.setTag("material_emission", str(material.getEmission()))
model.setTag("material_shininess", str(material.getShininess()))
if material.hasBaseColor():
model.setTag("material_basecolor", str(material.getBaseColor()))
# 如果有颜色属性,保存为标签
if state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
if not color_attrib.isOff():
model.setTag("color", str(color_attrib.getColor()))
# 保存场景
success = self.world.render.writeBamFile(filename)
return success
except Exception as e:
print(f"保存场景时发生错误: {str(e)}")
return False
def loadScene(self, filename):
"""从BAM文件加载场景"""
try:
print(f"\n=== 开始加载场景: {filename} ===")
# 清除当前场景
print("\n清除当前场景...")
for model in self.models:
model.removeNode()
self.models.clear()
# 加载场景
scene = self.world.loader.loadModel(filename)
if not scene:
return False
# 遍历场景中的所有模型节点
def processNode(nodePath, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {nodePath.getName()}")
# 跳过render节点的递归
if nodePath.getName() == "render" and depth > 0:
print(f"{indent}跳过重复的render节点")
return
# 跳过光源节点
if nodePath.getName() in ["alight", "dlight"]:
print(f"{indent}跳过光源节点: {nodePath.getName()}")
return
# 跳过相机节点
if nodePath.getName() in ["camera", "cam"]:
print(f"{indent}跳过相机节点: {nodePath.getName()}")
return
if isinstance(nodePath.node(), ModelRoot):
print(f"{indent}找到模型根节点!")
# 清除现有材质状态
nodePath.clearMaterial()
nodePath.clearColor()
# 创建新材质
material = Material()
# 从标签恢复材质属性
def parseColor(color_str):
"""解析颜色字符串为Vec4"""
try:
# 移除LVecBase4f标记只保留数值
color_str = color_str.replace('LVecBase4f', '').strip('()')
r, g, b, a = map(float, color_str.split(','))
return Vec4(r, g, b, a)
except:
return Vec4(1, 1, 1, 1) # 默认白色
if nodePath.hasTag("material_ambient"):
material.setAmbient(parseColor(nodePath.getTag("material_ambient")))
if nodePath.hasTag("material_diffuse"):
material.setDiffuse(parseColor(nodePath.getTag("material_diffuse")))
if nodePath.hasTag("material_specular"):
material.setSpecular(parseColor(nodePath.getTag("material_specular")))
if nodePath.hasTag("material_emission"):
material.setEmission(parseColor(nodePath.getTag("material_emission")))
if nodePath.hasTag("material_shininess"):
material.setShininess(float(nodePath.getTag("material_shininess")))
if nodePath.hasTag("material_basecolor"):
material.setBaseColor(parseColor(nodePath.getTag("material_basecolor")))
# 应用材质
nodePath.setMaterial(material)
# 恢复颜色属性
if nodePath.hasTag("color"):
nodePath.setColor(parseColor(nodePath.getTag("color")))
# 将模型重新挂载到render下
nodePath.wrtReparentTo(self.world.render)
self.models.append(nodePath)
# 递归处理子节点
for child in nodePath.getChildren():
processNode(child, depth + 1)
print("\n开始处理场景节点...")
processNode(scene)
# 移除临时场景节点
scene.removeNode()
# 更新场景树
self.updateSceneTree()
print("=== 场景加载完成 ===\n")
return True
except Exception as e:
print(f"加载场景时发生错误: {str(e)}")
return False
# ==================== 模型管理 ====================
def deleteModel(self, model):
"""删除模型"""
try:
if model in self.models:
model.removeNode()
self.models.remove(model)
self.updateSceneTree()
print(f"删除模型: {model.getName()}")
return True
except Exception as e:
print(f"删除模型失败: {str(e)}")
return False
def clearAllModels(self):
"""清除所有模型"""
try:
for model in self.models:
model.removeNode()
self.models.clear()
self.updateSceneTree()
print("清除所有模型完成")
except Exception as e:
print(f"清除所有模型失败: {str(e)}")
def getModels(self):
"""获取模型列表"""
return self.models.copy()
def getModelCount(self):
"""获取模型数量"""
return len(self.models)
def findModelByName(self, name):
"""根据名称查找模型"""
for model in self.models:
if model.getName() == name:
return model
return None
# ==================== 帮助方法 ====================
def processLoadedModel(self, model):
"""处理加载完成的模型(用于异步加载回调)"""
if model:
# 添加到模型列表
self.models.append(model)
# 设置基础变换
model.setPos(0, 0, 0)
model.setHpr(0, 0, 0)
model.setScale(1, 1, 1)
# 应用材质
self.processMaterials(model)
# 更新场景树
self.updateSceneTree()
print(f"异步加载模型完成: {model.getName()}")