EG/core/world.py

1269 lines
49 KiB
Python
Raw Permalink 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.

import math
import sys
import os
import warnings
from direct.actor.Actor import Actor
warnings.filterwarnings("ignore", category=DeprecationWarning)
from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight,
Point3, WindowProperties, Material, LColor, loadPrcFileData)
from direct.showbase.ShowBase import ShowBase
from direct.showbase.ShowBaseGlobal import globalClock
from scene.scene_manager import SceneManager
# 设置 RenderPipelineFile 路径。打包后可通过环境变量显式指定项目根目录,
# 避免 AppImage / 独立发行目录的布局差异导致资源定位错误。
project_root = os.environ.get("EG_PROJECT_ROOT") or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
render_pipeline_path = os.path.join(project_root, "RenderPipelineFile")
sys.path.insert(0, render_pipeline_path)
from RenderPipelineFile.rpcore import RenderPipeline
from ssbo_component.ssbo_editor import SSBOEditor
# 从渲染管线工具模块导入全局函数
from core.render_pipeline_utils import get_render_pipeline, set_render_pipeline
# 尝试导入插件管理器(如果存在)
try:
from plugins.plugin_manager import PluginManager
PLUGIN_SUPPORT = True
except ImportError:
PLUGIN_SUPPORT = False
print("⚠ 插件管理器未找到,插件功能将不可用")
class CoreWorld(ShowBase):
"""核心世界功能类 - 负责基础的3D世界设置和核心功能"""
def __init__(self, width=1380, height=750, is_fullscreen=False, clear_color=LColor(0, 0.5, 0, 1)):
global _global_render_pipeline
# 初始化基础属性
self.qtWidget = None # Qt部件引用用于获取准确的渲染区域尺寸
# 设置基本配置
loadPrcFileData("", "show-frame-rate-meter 0")
loadPrcFileData("", "window-type onscreen")
loadPrcFileData("", f"win-size {width} {height}")
loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
# Performance preset to match the standalone smooth runner
loadPrcFileData("", "threading-model /Draw")
loadPrcFileData("", "pstats-gpu-timing #f")
loadPrcFileData("", "gl-debug #f")
loadPrcFileData("", "gl-debug-object-labels #f")
# VR性能优化配置
loadPrcFileData("", "prefer-single-buffer true")
loadPrcFileData("", "gl-force-flush false")
loadPrcFileData("", "sync-video false")
loadPrcFileData("", "support-stencil false")
loadPrcFileData("", "clock-mode non-real-time")
loadPrcFileData("", "support-threads false")
if is_fullscreen:
loadPrcFileData("", "fullscreen #t")
# 创建渲染管线。打包后 Nuitka 会把 rpcore/rplibs 平铺到发行目录,
# 自动探测到的 base_path 可能变成 dist 根目录,导致找不到
# RenderPipelineFile/config/pipeline.yaml这里显式指回真实管线目录。
self.render_pipeline = RenderPipeline()
rp_base_dir = os.path.join(project_root, "RenderPipelineFile")
rp_config_dir = os.path.join(rp_base_dir, "config")
if os.path.isdir(rp_base_dir):
self.render_pipeline.mount_mgr.base_path = rp_base_dir
if os.path.isdir(rp_config_dir):
self.render_pipeline.mount_mgr.config_dir = rp_config_dir
self.render_pipeline.pre_showbase_init()
# 强制开启多线程支持 (Video播放必需)
loadPrcFileData("force_threads", "support-threads #f")
# 初始化 ShowBase
ShowBase.__init__(self)
# 创建渲染管线
self.render_pipeline.create(self)
set_render_pipeline(self.render_pipeline)
# 设置相机
self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam
# 设置相机控制参数
self.cameraSpeed = 20.0
self.cameraRotateSpeed = 10.0
# 鼠标控制相关变量
self.lastMouseX = 0
self.lastMouseY = 0
self.mouseRightPressed = False
# 初始化自定义鼠标控制器
from core.CustomMouseController import CustomMouseController
self.mouse_controller = CustomMouseController(self)
self.mouse_controller.setUp()
# 初始化插件管理器(如果支持)
if PLUGIN_SUPPORT:
self.plugin_manager = PluginManager(self)
else:
self.plugin_manager = None
# 添加错误处理钩子
self.accept("transform_state_error", self._handle_transform_error)
# 初始化世界
self._setupResourcePaths()
self._setupCamera()
self._setupLighting()
self._setupGround()
self._loadFont()
def _handle_transform_error(self):
"""处理TransformState相关的错误"""
try:
from panda3d.core import TransformState, RenderState
TransformState.clear_cache()
RenderState.clear_cache()
print("已清理TransformState和RenderState缓存")
except Exception as e:
print(f"清理缓存时出错: {e}")
def _setupResourcePaths(self):
"""设置Panda3D资源搜索路径确保能正确找到Resources文件夹中的模型和贴图"""
try:
import os
from panda3d.core import getModelPath, DSearchPath, Filename
# 获取项目根目录
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
resources_dir = os.path.join(project_root, "Resources")
# 确保Resources目录存在
if not os.path.exists(resources_dir):
os.makedirs(resources_dir, exist_ok=True)
print(f"✓ 创建Resources目录: {resources_dir}")
# 添加Resources目录到Panda3D模型搜索路径
model_path = getModelPath()
resources_filename = Filename.from_os_specific(resources_dir)
# 检查路径是否已存在,避免重复添加
if not model_path.findFile(resources_filename):
model_path.appendDirectory(resources_filename)
print(f"✓ 添加Resources到模型搜索路径: {resources_dir}")
# 添加core目录到搜索路径
core_dir = os.path.join(project_root, "core")
core_filename = Filename.from_os_specific(core_dir)
if not model_path.findFile(core_filename):
model_path.appendDirectory(core_filename)
print(f"✓ 添加core目录到模型搜索路径: {core_dir}")
# 添加RenderPipeline目录到搜索路径
rp_dir = os.path.join(project_root, "RenderPipelineFile")
rp_filename = Filename.from_os_specific(rp_dir)
if not model_path.findFile(rp_filename):
model_path.appendDirectory(rp_filename)
print(f"✓ 添加RenderPipeline目录到模型搜索路径: {rp_dir}")
# 添加RenderPipeline data目录到搜索路径
rp_data_dir = os.path.join(rp_dir, "data")
rp_data_filename = Filename.from_os_specific(rp_data_dir)
if not model_path.findFile(rp_data_filename):
model_path.appendDirectory(rp_data_filename)
print(f"✓ 添加RenderPipeline data目录到模型搜索路径: {rp_data_dir}")
# 同时添加各个子目录到搜索路径
subdirs = ['models', 'textures', 'animations', 'icons', 'materials']
for subdir in subdirs:
subdir_path = os.path.join(resources_dir, subdir)
if os.path.exists(subdir_path):
subdir_filename = Filename.from_os_specific(subdir_path)
if not model_path.findFile(subdir_filename):
model_path.appendDirectory(subdir_filename)
print(f"✓ 添加子目录到搜索路径: {subdir}")
else:
# 创建不存在的子目录
os.makedirs(subdir_path, exist_ok=True)
subdir_filename = Filename.from_os_specific(subdir_path)
model_path.appendDirectory(subdir_filename)
print(f"✓ 创建并添加子目录: {subdir}")
# 设置纹理搜索路径
try:
from panda3d.core import getTexturePath
texture_path = getTexturePath()
if not texture_path.findFile(resources_filename):
texture_path.appendDirectory(resources_filename)
except ImportError:
# 新版本 Panda3D 中 getTexturePath 可能不可用
print(" 注意: getTexturePath 不可用,使用默认纹理路径")
for subdir in ['textures', 'materials', 'icons']:
subdir_path = os.path.join(resources_dir, subdir)
if os.path.exists(subdir_path):
subdir_filename = Filename.from_os_specific(subdir_path)
if not texture_path.findFile(subdir_filename):
texture_path.appendDirectory(subdir_filename)
print(f"✓ 资源路径设置完成")
print(f" 项目根目录: {project_root}")
print(f" Resources目录: {resources_dir}")
except Exception as e:
print(f"⚠️ 设置资源路径失败: {e}")
def diagnose_fbx_loading(self, fbx_path):
"""诊断FBX加载状态"""
print("=== FBX加载诊断 ===")
# 检查文件存在
import os
if not os.path.exists(fbx_path):
print("❌ FBX文件不存在")
return None
try:
# 尝试加载
actor = Actor(fbx_path)
# 检查动画
anims = actor.getAnimNames()
print(f"✓ 找到 {len(anims)} 个动画: {anims}")
# 检查PartBundle
bundles = actor.getPartBundles()
print(f"✓ 找到 {len(bundles)} 个PartBundle")
# 测试动画控制器
if anims:
control = actor.getAnimControl(anims[0])
if control:
print(f"✓ 动画控制器创建成功: {anims[0]}")
return actor
else:
print(f"❌ 动画控制器创建失败: {anims[0]}")
return actor
except Exception as e:
print(f"❌ FBX加载异常: {e}")
return None
def diagnose_actor_animation(self,model_path, anim_path, anim_name="walk"):
print("=== 开始诊断 Actor 动画问题 ===")
import os
from direct.actor.Actor import Actor
from panda3d.core import Loader
from panda3d.core import Filename
# 检查文件存在性
print(f"1. 文件检查:")
print(f" 模型文件存在: {os.path.exists(model_path)}")
print(f" 动画文件存在: {os.path.exists(anim_path)}")
if not os.path.exists(model_path):
print("❌ 模型文件不存在,请检查路径")
return None
if not os.path.exists(anim_path):
print("❌ 动画文件不存在,请检查路径")
return None
# 2. 检查模型结构
print(f"2. 模型结构检查:")
try:
# 先用普通loader加载检查结构
temp_model = self.loader.loadModel(model_path)
if temp_model is None:
print("❌ 无法加载模型文件")
return None
# 检查Character节点
bundleNP = temp_model.find("**/+Character")
if bundleNP.isEmpty():
print("❌ 模型不包含Character节点 - 这是动画模型必需的")
print(" 建议: 确保模型文件是正确的角色模型,包含骨骼结构")
return None
else:
print("✓ 模型包含Character节点")
temp_model.removeNode() # 清理临时模型
except Exception as e:
print(f"❌ 模型加载失败: {e}")
return None
# 3. 检查动画文件结构
print(f"3. 动画文件检查:")
try:
temp_anim = self.loader.loadModel(anim_path)
if temp_anim is None:
print("❌ 无法加载动画文件")
return None
# 检查AnimBundleNode
animBundleNP = temp_anim.find('**/+AnimBundleNode')
if animBundleNP.isEmpty():
print("❌ 动画文件不包含AnimBundleNode")
print(" 建议: 确保动画文件是正确导出的动画数据")
return None
else:
print("✓ 动画文件包含AnimBundleNode")
temp_anim.removeNode() # 清理临时动画
except Exception as e:
print(f"❌ 动画文件加载失败: {e}")
# 4. 尝试创建Actor
print(f"4. Actor创建测试:")
# 方法1: 构造函数方式
try:
print(" 尝试方法1: 构造函数加载")
actor = Actor(model_path, {anim_name: anim_path})
print(f" 动画列表: {actor.getAnimNames()}")
# 强制同步绑定
print(" 尝试强制绑定动画...")
actor.bindAnim(anim_name, allowAsyncBind=False)
control = actor.getAnimControl(anim_name)
if control is not None:
print("✓ 方法1成功 - 动画控制器创建成功")
return actor
else:
print("❌ 方法1失败 - 动画控制器为None")
except Exception as e:
print(f"❌ 方法1异常: {e}")
# 方法2: 分步加载
try:
print(" 尝试方法2: 分步加载")
actor = Actor()
actor.loadModel(model_path)
actor.loadAnims({anim_name: anim_path})
# 强制绑定
actor.bindAnim(anim_name, allowAsyncBind=False)
control = actor.getAnimControl(anim_name)
if control is not None:
print("✓ 方法2成功 - 动画控制器创建成功")
return actor
else:
print("❌ 方法2失败 - 动画控制器为None")
except Exception as e:
print(f"❌ 方法2异常: {e}")
# 5. 深度诊断
print(f"5. 深度诊断:")
try:
actor = Actor()
actor.loadModel(model_path)
# 检查PartBundle
bundles = actor.getPartBundles()
print(f" PartBundle数量: {len(bundles)}")
if len(bundles) == 0:
print("❌ 没有找到PartBundle - 模型可能不是正确的角色模型")
return None
# 检查动画绑定过程
print(" 尝试手动绑定动画...")
actor.loadAnims({anim_name: anim_path})
# 获取详细的绑定信息
actor_info = actor.getActorInfo()
print(f" Actor信息: {actor_info}")
return None
except Exception as e:
print(f"❌ 深度诊断异常: {e}")
return None
print("❌ 所有方法都失败了")
return None
def _setYCModel(self):
model = self.loader.loadModel("/home/tiger/文档/Tzjyc_GLTF/tzjyc.gltf")
model.reparentTo(self.render)
model.setScale(0.25)
model.setPos(-8, 42, 0)
model.setHpr(0, 90, 0)
def _setupCamera(self):
"""设置相机位置和朝向"""
self.cam.setPos(0, -50, 20)
self.cam.lookAt(0, 0, 0)
self.camLens.setFov(80)
self.cam.setTag("is_scene_element", "1")
self.cam.setTag("tree_item_type", "CAMERA_NODE")
print("✓ 相机设置完成")
def _setupLighting(self):
"""设置基础光照系统"""
# 环境光
alight = AmbientLight('alight')
alight.setColor((0.2, 0.2, 0.2, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
# 定向光(模拟太阳光)
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0) # 设置光照方向
self.render.setLight(dlnp)
# 保存光源引用
self.ambient_light = alnp
self.directional_light = dlnp
print("✓ 光照系统设置完成")
def _setupGround(self):
"""创建地板"""
cm = CardMaker('ground')
cm.setFrame(-50, 50, -50, 50)
# 创建地板节点
self.ground = self.render.attachNewNode(cm.generate())
self.ground.setP(-90)
self.ground.setZ(-1.0)
self.ground.setColor(0.8, 0.8, 0.8, 1)
self.ground.setTag("is_scene_element", "1")
self.ground.setTag("tree_item_type", "SCENE_NODE")
# 创建支持贴图的材质
mat = Material()
mat.setName("GroundMaterial")
color = LColor(1, 1, 1, 0.8)
mat.set_base_color(color)
mat.set_roughness(1) # 设置合适的初始粗糙度
mat.set_metallic(0.5) # 设置较低的初始金属性
self.ground.set_material(mat)
# #创建第二个相同的地面,位置稍有偏移
# self.ground2 = self.render.attachNewNode(cm.generate())
# self.ground2.setH(-90)
# self.ground2.setZ(-1.0)
# self.ground2.setX(50) # 在X轴方向偏移
# self.ground2.setZ(49) # 在X轴方向偏移
# self.ground2.setColor(0.8, 0.8, 0.8, 1)
# self.ground2.set_material(mat)
# self.ground2.setTag("is_scene_element", "1")
# self.ground2.setTag("tree_item_type", "SCENE_NODE")
#
# # 创建第三个相同的地面,位置在另一个方向
# self.ground3 = self.render.attachNewNode(cm.generate())
# self.ground3.setH(90)
# self.ground3.setZ(-1.0)
# self.ground3.setX(-50) # 在X轴负方向偏移
# self.ground3.setZ(49) # 在X轴负方向偏移
# self.ground3.setColor(0.8, 0.8, 0.8, 1)
# self.ground3.set_material(mat)
# self.ground3.setTag("is_scene_element", "1")
# self.ground3.setTag("tree_item_type", "SCENE_NODE")
#
# self.ground4 = self.render.attachNewNode(cm.generate())
# # self.ground3.setR(90)
# self.ground4.setZ(-1.0)
# self.ground4.setY(50) # 在X轴负方向偏移
# self.ground4.setZ(49) # 在X轴负方向偏移
# self.ground4.setColor(0.8, 0.8, 0.8, 1)
# self.ground4.set_material(mat)
# self.ground4.setTag("is_scene_element", "1")
# self.ground4.setTag("tree_item_type", "SCENE_NODE")
#
# self.ground5 = self.render.attachNewNode(cm.generate())
# self.ground5.setP(180)
# self.ground5.setZ(-1)
# self.ground5.setY(-50) # 在X轴负方向偏移
# self.ground5.setZ(49) # 在X轴负方向偏移
# self.ground5.setColor(0.8, 0.8, 0.8, 1)
# self.ground5.set_material(mat)
# self.ground5.setTag("is_scene_element", "1")
# self.ground5.setTag("tree_item_type", "SCENE_NODE")
#
# self.ground6 = self.render.attachNewNode(cm.generate())
# self.ground6.setP(90)
# self.ground6.setZ(-1)
# self.ground6.setZ(99) # 在X轴负方向偏移
# self.ground6.setColor(0.8, 0.8, 0.8, 1)
# self.ground6.set_material(mat)
# self.ground6.setTag("is_scene_element", "1")
# self.ground6.setTag("tree_item_type", "SCENE_NODE")
# 应用默认PBR效果确保支持贴图
try:
if hasattr(self, 'render_pipeline') and self.render_pipeline:
self.render_pipeline.set_effect(
self.ground,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
# # 为其他两个地面也应用相同的效果
# self.render_pipeline.set_effect(
# self.ground2,
# "effects/default.yaml",
# {
# "normal_mapping": True,
# "render_gbuffer": True,
# "alpha_testing": False,
# "parallax_mapping": False,
# "render_shadow": True,
# "render_envmap": True
# },
# 50
# )
# self.render_pipeline.set_effect(
# self.ground3,
# "effects/default.yaml",
# {
# "normal_mapping": True,
# "render_gbuffer": True,
# "alpha_testing": False,
# "parallax_mapping": False,
# "render_shadow": True,
# "render_envmap": True
# },
# 50
# )
# self.render_pipeline.set_effect(
# self.ground4,
# "effects/default.yaml",
# {
# "normal_mapping": True,
# "render_gbuffer": True,
# "alpha_testing": False,
# "parallax_mapping": False,
# "render_shadow": True,
# "render_envmap": True
# },
# 50
# )
# self.render_pipeline.set_effect(
# self.ground5,
# "effects/default.yaml",
# {
# "normal_mapping": True,
# "render_gbuffer": True,
# "alpha_testing": False,
# "parallax_mapping": False,
# "render_shadow": True,
# "render_envmap": True
# },
# 50
# )
# self.render_pipeline.set_effect(
# self.ground6,
# "effects/default.yaml",
# {
# "normal_mapping": True,
# "render_gbuffer": True,
# "alpha_testing": False,
# "parallax_mapping": False,
# "render_shadow": True,
# "render_envmap": True
# },
# 50
# )
# print("✓ 地板PBR效果已应用")
else:
print("⚠️ RenderPipeline未初始化地板将使用基础渲染")
except Exception as e:
print(f"⚠️ 地板PBR效果应用失败: {e}")
print("✓ 地板创建完成(支持材质贴图)")
# def _loadFont(self):
# """加载中文字体"""
# try:
# self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
# if not self.chinese_font:
# print("警告: 无法加载中文字体,将使用默认字体")
# else:
# print("✓ 中文字体加载成功")
# except:
# print("警告: 无法加载中文字体,将使用默认字体")
# self.chinese_font = None
def _loadFont(self):
"""加载中文字体 - 跨平台Panda3D 友好路径"""
try:
import os
import platform
from pathlib import Path
from panda3d.core import Filename
self.chinese_font = None # 初始化
# --- 平台与项目根 ---
system = platform.system().lower()
project_root = Path(__file__).resolve().parent.parent # 上两级
# --- 候选字体路径(按平台) ---
if system == "windows":
win_dir = os.environ.get("WINDIR") or r"C:\Windows"
win_fonts_dir = Path(win_dir) / "Fonts"
font_candidates = [
project_root / "RenderPipelineFile" / "data" / "font" / "msyh.ttc",
win_fonts_dir / "msyh.ttc",
win_fonts_dir / "msyh.ttf",
win_fonts_dir / "simhei.ttf",
win_fonts_dir / "simsun.ttc",
]
elif system == "darwin": # macOS
font_candidates = [
Path("/System/Library/Fonts/PingFang.ttc"),
Path("/System/Library/Fonts/STHeiti.ttc"),
Path("/Library/Fonts/Songti.ttc"),
]
else: # Linux / 其他
font_candidates = [
Path("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"),
Path("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"),
Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"),
]
# --- 逐个尝试加载 ---
for os_path in font_candidates:
os_path_str = str(os_path)
if not os.path.exists(os_path_str):
# 文件确实不存在就跳过
continue
# 1) 先把 OS 路径转换成 Panda 内部路径(/d/... 或 /usr/...
panda_fn = Filename.fromOsSpecific(os_path_str)
panda_fn.makeTrueCase() # Windows 上修正大小写(更稳)
panda_internal = panda_fn.getFullpath() # 取“内部样式”的字符串
# 2) 打印便于对照 Panda 的日志
try:
print(f"[FontLoader] 尝试加载: {panda_internal}")
except Exception:
pass
try:
# 3) 传“字符串”给 loader.loadFont关键修复点
font = self.loader.loadFont(panda_internal)
if font:
self.chinese_font = font
print("✓ 中文字体加载成功:", os_path_str)
break
else:
print("[FontLoader] 返回空字体对象,继续尝试下一个候选...")
except TypeError as te:
# 某些旧版绑定只接受 str不接受 Filename此处已传 str若仍报错再兜底一次
try:
font = self.loader.loadFont(str(panda_fn)) # 退一步str(Filename)
if font:
self.chinese_font = font
print("✓ 中文字体加载成功(备用API):", os_path_str)
break
else:
print("[FontLoader] 备用API仍返回空字体对象继续…")
except Exception as te2:
print(f"[FontLoader] 加载失败(TypeError),继续下一个: {os_path},原因: {te2}")
except Exception as e:
print(f"[FontLoader] 加载失败,继续下一个: {os_path},原因: {e}")
# --- 兜底 ---
if not self.chinese_font:
print("警告: 无法加载中文字体,将使用默认字体(可能导致中文显示不全)")
self.chinese_font = None
except Exception as e:
print(f"警告: 加载中文字体时发生错误: {e}")
self.chinese_font = None
def setQtWidget(self, widget):
"""设置Qt部件引用"""
self.qtWidget = widget
print(f"✓ 设置Qt部件引用: {widget}")
def getWindowSize(self):
"""获取准确的窗口尺寸"""
if self.qtWidget:
# 优先使用Qt部件的实际尺寸
width, height = self.qtWidget.getActualSize()
if width > 0 and height > 0:
return width, height
# 备用方案使用Panda3D窗口尺寸
if hasattr(self, 'win') and self.win:
width = self.win.getXSize()
height = self.win.getYSize()
# print(f"从Panda3D窗口获取尺寸!!!!!!!!!!!!!!!!!!!!!: {width} x {height}")
return width, height
# 最后的默认值
print("使用默认窗口尺寸: 800 x 600")
return 800, 600
# ==================== 相机控制功能 ====================
def wheelForward(self, data=None):
"""处理滚轮向前滚动(前进)"""
# 获取相机的前向向量
forward = self.cam.getMat().getRow3(1)
# 计算移动距离
distance = self.cameraSpeed * globalClock.getDt()
# 更新相机位置
currentPos = self.cam.getPos()
newPos = currentPos + forward * distance
self.cam.setPos(newPos)
def wheelBackward(self, data=None):
"""处理滚轮向后滚动(后退)"""
# 获取相机的前向向量
forward = self.cam.getMat().getRow3(1)
# 计算移动距离
distance = self.cameraSpeed * globalClock.getDt()
# 更新相机位置
currentPos = self.cam.getPos()
newPos = currentPos - forward * distance
self.cam.setPos(newPos)
def moveCamera(self, x, y, z):
"""移动相机位置(垂直移动)"""
# 获取相机的上向量
upVector = self.cam.getMat().getRow3(2)
# 计算移动距离
distance = self.cameraSpeed * globalClock.getDt()
# 更新相机位置
currentPos = self.cam.getPos()
newPos = currentPos + upVector * z * distance
self.cam.setPos(newPos)
# ==================== 鼠标事件处理 ====================
def mousePressEventRight(self, evt):
"""处理鼠标右键按下事件"""
#print("右键按下")
self.mouseRightPressed = True
self.lastMouseX = evt['x']
self.lastMouseY = evt['y']
#
# # 通过 Qt 窗口隐藏光标并捕获鼠标
# try:
# if hasattr(self, 'qtWidget') and self.qtWidget:
# from PyQt5.QtCore import Qt
# self.qtWidget.setCursor(Qt.BlankCursor)
# # 捕获鼠标,使其无法离开窗口
# self.qtWidget.grabMouse()
# except Exception as e:
# print(f"通过 Qt 隐藏光标时出错: {e}")
def mouseReleaseEventRight(self, evt):
"""处理鼠标右键释放事件"""
#print("右键释放")
self.mouseRightPressed = False
# # 恢复 Qt 窗口光标并释放鼠标捕获
# try:
# if hasattr(self, 'qtWidget') and self.qtWidget:
# from PyQt5.QtCore import Qt
# self.qtWidget.unsetCursor() # 恢复默认光标
# # 释放鼠标捕获
# self.qtWidget.releaseMouse()
# except Exception as e:
# print(f"恢复 Qt 光标时出错: {e}")
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件 - 只处理相机旋转"""
if not evt:
return
if self.mouseRightPressed:
# 计算鼠标移动距离
dx = evt.get('x', 0) - self.lastMouseX
dy = evt.get('y', 0) - self.lastMouseY
# 计算旋转角度
rotateSpeed = self.cameraRotateSpeed * globalClock.getDt()
# 更新相机朝向
currentH = self.cam.getH()
currentP = self.cam.getP()
# 限制俯仰角度在-90到90度之间
newP = max(-90, min(90, currentP - dy * rotateSpeed))
self.cam.setH(currentH - dx * rotateSpeed)
self.cam.setP(newP)
# 更新鼠标位置
self.lastMouseX = evt.get('x', 0)
self.lastMouseY = evt.get('y', 0)
# ==================== 其他基础功能 ====================
def getChineseFont(self):
"""获取中文字体"""
return self.chinese_font
def getGroundNode(self):
"""获取地板节点"""
return self.ground
def getAmbientLight(self):
"""获取环境光"""
return self.ambient_light
def getDirectionalLight(self):
"""获取定向光"""
return self.directional_light
def start_day_night_cycle(self, duration_seconds=10.0):
"""让天空盒在 duration_seconds 秒内从 0:00 过渡到 24:00"""
self._cycle_start_time = self.taskMgr.globalClock.get_real_time()
self._cycle_duration = duration_seconds
self.taskMgr.add(self._day_night_cycle_task, "day_night_cycle_task")
def _day_night_cycle_task(self, task):
elapsed = self.taskMgr.globalClock.get_real_time() - self._cycle_start_time
t = (elapsed % self._cycle_duration) / self._cycle_duration # 始终在 0~1 循环
self.render_pipeline.daytime_mgr.time = 24.0 * t
return task.cont
def set_daytime(self, time_str):
"""设置时间"""
try:
if hasattr(self, 'render_pipeline') and self.render_pipeline:
self.render_pipeline.daytime_mgr.time = time_str
print(f"当前时间设置为: {time_str}")
else:
print(f"⚠️ RenderPipeline 不可用,无法设置时间: {time_str}")
except Exception as e:
print(f"设置时间失败: {e}")
def get_render_pipeline(self):
"""获取 RenderPipeline 实例"""
return getattr(self, 'render_pipeline', None)
def _setupSkybox(self):
# 加载天空盒模型
self.skybox = self.loader.loadModel("data/builtin_models/skybox/skybox.bam")
self.skybox.reparentTo(self.camera) # 绑定到相机
self.skybox.setScale(500)
self.skybox.setBin('background', 0)
self.skybox.setDepthWrite(False)
self.skybox.setLightOff()
self.skybox.setCompass() # 始终朝向固定
print("✓ 静态天空盒加载完成")
def createDirectionalLight(self):
from RenderPipelineFile.rpcore import light_manager
from panda3d.core import DirectionalLight, Vec3
dlight = DirectionalLight("1")
dlight_np = self.render.attachNewNode(dlight)
#light_manager.add_light(dlight)
dlight_np.setHpr(45,-45,0)
self.render.setLight(dlight_np)
dlight.setColor((1,1,1,1))
dlight.setShadowCaster(True,2048,2048)
print("平行光创建完成")
# dlight.direction = Vec3(0, 0, -1) # 光照方向
# dlight.fov = self.lamp_fov # 光源角度(类似手电筒)
# dlight.set_color_from_temperature(1 * 1000.0) # 色温K
# dlight.energy = self.half_energy # 光照强度
# dlight.radius = self.lamp_radius # 影响范围
# dlight.casts_shadows = True # 是否投射阴影
# dlight.shadow_map_resolution = 256 # 阴影分辨率
# dlight.setPos(0,0,10)
def check_material_editor_connection(self):
"""检查材质编辑器连接状态"""
try:
# 确保 RenderPipeline 已完全初始化
if not hasattr(self, 'render_pipeline') or not self.render_pipeline:
print("RenderPipeline 未初始化")
return False
# 检查网络监听器是否存在
if not hasattr(self.render_pipeline, '_listener'):
print("NetworkCommunication 监听器未初始化")
return False
from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication
import tempfile
import os
import time
# 使用唯一的测试文件名
import uuid
test_filename = f"test_materials_{uuid.uuid4().hex[:8]}.data"
temp_path = os.path.join(tempfile.gettempdir(), test_filename)
print(f"测试材质编辑器连接,文件路径: {temp_path}")
# 确保测试文件不存在
if os.path.exists(temp_path):
os.remove(temp_path)
# 发送导出命令
NetworkCommunication.send_async(
NetworkCommunication.MATERIAL_PORT,
f"dump_materials {temp_path}"
)
# 大幅增加等待时间,因为网络命令处理可能有延迟
for i in range(60): # 等待最多30秒
time.sleep(0.5)
if os.path.exists(temp_path):
# 等待文件写入完成
time.sleep(1.0)
try:
with open(temp_path, 'r') as f:
content = f.read().strip()
print(f"材质编辑器连接测试成功,文件内容: {len(content)} 字符")
os.remove(temp_path)
return True
except:
print("文件读取失败,继续等待...")
continue
if i % 20 == 0 and i > 0: # 每10秒打印一次状态
print(f"等待材质文件创建... ({i // 2}s)")
print("材质编辑器连接测试超时")
return False
except Exception as e:
print(f"材质编辑器连接测试出错: {e}")
return False
def create_material_editor_widget(self):
"""创建材质编辑器组件"""
try:
# 确保 RenderPipeline 已完全初始化
if not hasattr(self, 'render_pipeline') or not self.render_pipeline:
print("RenderPipeline 未初始化")
return None
# 检查网络连接
if not self.check_material_editor_connection():
print("无法连接到材质编辑器网络服务")
return None
# 创建材质编辑器组件
# 使用纯 Panda3D 实现,不依赖 PyQt5
print("材质编辑器组件已创建(使用 Panda3D 原生实现)")
material_widget.setLayout(layout)
return material_widget
except Exception as e:
print(f"创建材质编辑器失败: {e}")
return None
def _delayed_material_test(self, task):
"""延迟执行的材质编辑器连接测试"""
print("开始延迟材质编辑器连接测试...")
success = self.check_material_editor_connection()
if success:
print("✓ 材质编辑器连接正常")
else:
print("✗ 材质编辑器连接失败")
return task.done
def launch_day_time_editor(self):
# 检查是否已经启动
if hasattr(self, '_day_time_editor_process') and self._day_time_editor_process:
if self._day_time_editor_process.poll() is None: # 进程仍在运行
print("Day Time Editor 已经在运行")
return True
import subprocess
import os
import sys
try:
if not hasattr(self.world,'render_pipeline') or not self.world.render_pipeline:
print("错误renderpipeline未初始化")
return False
base_path = self.world.render_pipeline.mount_mgr.base_path
editor_path = os.path.join(base_path,"toolkit/day_time_editor/main.py")
if not os.path.exists(editor_path):
print("错误文件不存在")
return False
self._day_time_editor_process = subprocess.Popen([sys.executable, editor_path])
print("Day Time Editor 已启动")
return True
except Exception as e:
print(f"启动 time editor失败")
def setup_material_editor_network(self):
"""设置材质编辑器网络通信"""
from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication
def handle_material_requests(message):
"""处理材质编辑器的网络请求"""
try:
print(f"收到材质编辑器请求: {message}")
parts = message.strip().split()
if not parts:
return
command = parts[0]
if command == "dump_materials":
# 处理导出材质列表请求
path = parts[1] if len(parts) > 1 else ""
if path:
self.export_materials_to_file(path)
elif command.startswith("update_material"):
# 处理更新材质请求
data = message[len("update_material "):].strip()
self.update_material_from_editor(data.split())
except Exception as e:
print(f"处理材质编辑器请求失败: {e}")
# 注册网络消息处理器
try:
NetworkCommunication.listen_threaded(
NetworkCommunication.MATERIAL_PORT,
handle_material_requests
)
print("✓ 材质编辑器网络通信已设置")
except Exception as e:
print(f"设置材质编辑器网络通信失败: {e}")
def export_materials_to_file(self, path):
"""导出材质列表到文件"""
try:
print(f"导出材质列表到: {path}")
materials = []
# 收集所有材质
def collect_materials(node):
if node.hasMaterial():
material = node.getMaterial()
if material:
materials.append(material)
for child in node.getChildren():
collect_materials(child)
collect_materials(self.render)
# 写入文件
with open(path, "w") as f:
for i, material in enumerate(materials):
name = f"{i}-{material.getName() or 'unnamed'}"
# 获取材质属性,使用默认值如果不存在
base_color = material.getBaseColor() if hasattr(material, 'getBaseColor') else Vec4(0.6, 0.6, 0.6, 1.0)
roughness = material.getRoughness() if hasattr(material, 'getRoughness') else 0.5
metallic = material.getMetallic() if hasattr(material, 'getMetallic') else 0.0
# 写入材质数据
f.write(("{} " * 11).format(
name,
base_color.x,
base_color.y,
base_color.z,
roughness,
0.5, # refractive_index
metallic,
0, # shading_model
1.0, # normal_strength
0.0, # param1
0.0 # param2
) + "\n")
print(f"✓ 成功导出 {len(materials)} 个材质到 {path}")
except Exception as e:
print(f"导出材质列表失败: {e}")
def update_material_from_editor(self, data):
"""从编辑器更新材质"""
try:
if len(data) < 11:
print(f"材质数据不完整: {data}")
return
name_parts = data[0].split("-")
if len(name_parts) < 2:
print(f"材质名称格式错误: {data[0]}")
return
index = int(name_parts[0])
name = "-".join(name_parts[1:])
# 收集所有材质
materials = []
def collect_materials(node):
if node.hasMaterial():
material = node.getMaterial()
if material:
materials.append(material)
for child in node.getChildren():
collect_materials(child)
collect_materials(self.render)
# 查找匹配的材质
if index < len(materials):
material = materials[index]
# 更新材质属性
material.setBaseColor(Vec4(float(data[1]), float(data[2]), float(data[3]), 1.0))
material.setRoughness(float(data[4]))
material.setMetallic(float(data[6]))
print(f"✓ 更新材质 {name}: 颜色=({data[1]}, {data[2]}, {data[3]}), 粗糙度={data[4]}, 金属度={data[6]}")
else:
print(f"未找到索引为 {index} 的材质")
except Exception as e:
print(f"更新材质失败: {e}")
def launch_material_editor(self):
"""启动材质编辑器"""
import subprocess
import os
import sys
try:
if not self.render_pipeline:
print("错误renderpipeline未初始化")
return False
base_path = self.render_pipeline.mount_mgr.base_path
editor_path = os.path.join(base_path,"toolkit/material_editor/main.py")
if not os.path.exists(editor_path):
print("错误文件不存在")
return False
self._material_editor_process = subprocess.Popen([sys.executable, editor_path])
print("Material Editor 已启动")
return True
except Exception as e:
print(f"启动 Material editor失败")
def create_sample_materials(self):
"""创建一些示例材质供编辑器使用"""
from panda3d.core import Material, Vec4, CardMaker
# 创建几个测试几何体,每个都有不同的材质
sample_materials = [
{"name": "MetalMaterial", "color": Vec4(0.7, 0.7, 0.8, 1.0), "metallic": True, "roughness": 0.2},
{"name": "PlasticMaterial", "color": Vec4(0.8, 0.2, 0.2, 1.0), "metallic": False, "roughness": 0.8},
{"name": "GlassMaterial", "color": Vec4(0.9, 0.9, 1.0, 0.3), "metallic": False, "roughness": 0.1},
{"name": "WoodMaterial", "color": Vec4(0.6, 0.4, 0.2, 1.0), "metallic": False, "roughness": 0.7},
{"name": "TestPlaneMaterial", "color": Vec4(0.8, 0.8, 0.8, 1.0), "metallic": False, "roughness": 1.0}
]
for i, mat_data in enumerate(sample_materials):
# 创建几何体
if mat_data["name"] == "TestPlaneMaterial":
# 创建一个大的测试平面类似Blender中的平面
cm = CardMaker('test_plane')
cm.setFrame(-5, 5, -5, 5) # 更大的平面
geom_node = self.render.attachNewNode(cm.generate())
geom_node.setPos(0, 15, 0) # 放在前面显眼位置
geom_node.setP(-90) # 水平放置
print("✓ 创建大型测试平面,适合测试粗糙度贴图")
else:
cm = CardMaker(f'sample_geom_{i}')
cm.setFrame(-1, 1, -1, 1)
geom_node = self.render.attachNewNode(cm.generate())
geom_node.setPos(i * 3 - 6, 10, 1)
geom_node.setP(-90) # 水平放置
# 创建材质并确保名称正确设置
material = Material()
material.setName(mat_data["name"]) # 明确设置材质名称
material.setBaseColor(mat_data["color"])
material.setRoughness(mat_data["roughness"])
material.setMetallic(1.0 if mat_data["metallic"] else 0.0)
# 应用材质
geom_node.setMaterial(material)
print(f"✓ 创建示例材质: {mat_data['name']}")
# 延迟一下确保材质完全创建
import time
time.sleep(0.1)
def load_test_models_with_materials(self):
"""加载一些测试模型以提供更多材质供编辑"""
try:
# 你可以在这里加载你的模型文件
# test_model = self.loader.loadModel("models/your_model.gltf")
# if test_model:
# test_model.reparentTo(self.render)
# test_model.setPos(5, 0, 0)
# test_model.setScale(1.0)
# print(f"✓ 测试模型已加载")
# 创建示例材质
self.create_sample_materials()
except Exception as e:
print(f"加载测试模型失败: {e}")
# 创建示例材质作为备选
self.create_sample_materials()
def launch_plugin_configurator(self):
"""启动材质编辑器"""
import subprocess
import os
import sys
try:
if not self.render_pipeline:
print("错误renderpipeline未初始化")
return False
base_path = self.render_pipeline.mount_mgr.base_path
editor_path = os.path.join(base_path, "toolkit/plugin_configurator/main.py")
if not os.path.exists(editor_path):
print("错误文件不存在")
return False
self._material_editor_process = subprocess.Popen([sys.executable, editor_path])
print("plugin_configurator 已启动")
return True
except Exception as e:
print(f"启动plugin_configurator失败")