Merge remote-tracking branch 'origin/main_ch_eg' into main_ch_eg

# Conflicts:
#	RenderPipelineFile/config/daytime.yaml
#	ui/property_panel.py
This commit is contained in:
陈横 2025-08-21 11:35:17 +08:00
commit 0319b13f4a
8 changed files with 529 additions and 904 deletions

File diff suppressed because one or more lines are too long

BIN
core/TranslateArrowHandle.fbx Executable file

Binary file not shown.

View File

@ -11,7 +11,7 @@ from PIL.ImageChops import lighter
from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState,
DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib,
TransparencyAttrib)
TransparencyAttrib, Vec4)
from direct.task.TaskManagerGlobal import taskMgr
import math
@ -50,14 +50,14 @@ class SelectionSystem:
# 高亮相关
self.gizmoHighlightAxis = None
self.gizmo_colors = {
"x": (1*10, 0, 0, 1), # 红色
"y": (0, 1*10, 0, 1), # 绿色
"z": (0, 0, 1*10, 1) # 蓝色
"x": (1, 0, 0, 0), # 红色
"y": (0, 1, 0, 0), # 绿色
"z": (0, 0, 1, 0) # 蓝色
}
self.gizmo_highlight_colors = {
"x": (1.5*20, 1.5*20, 0, 1), # 黄色高亮
"y": (1.5*20, 1.5*20, 0, 1), # 黄色高亮
"z": (1.5*20, 1.5*20, 0, 1) # 黄色高亮
"x": (1, 1, 0, 0), # 黄色高亮
"y": (1, 1, 0, 0), # 黄色高亮
"z": (1, 1, 0, 0) # 黄色高亮
}
print("✓ 选择和变换系统初始化完成")
@ -217,6 +217,9 @@ class SelectionSystem:
return task.cont
self._last_selection_box_update = current_time
#检查目标节点是否已被删除
self.checkAndClearIfTargetDeleted()
if not self.selectionBox or not self.selectionBoxTarget:
return task.done # 结束任务
@ -300,11 +303,18 @@ class SelectionSystem:
# 只调用一次几何体创建
self.createGizmoGeometry()
# 只调用一次颜色设置
#只调用一次颜色设置
self.setGizmoAxisColor("x", self.gizmo_colors["x"])
self.setGizmoAxisColor("y", self.gizmo_colors["y"])
self.setGizmoAxisColor("z", self.gizmo_colors["z"])
self._updateGizmoScreenSize()
self._setupGizmoRendering()
# 现在才显示坐标轴
self.gizmo.show()
# 只启动一次更新任务
taskMgr.add(self.updateGizmoTask, "updateGizmo")
@ -319,321 +329,100 @@ class SelectionSystem:
if not self.gizmo:
return
# 创建X轴红色
x_lines = LineSegs("x_axis")
x_lines.setThickness(6.0)
x_lines.moveTo(0, 0, 0)
x_lines.drawTo(self.axis_length, 0, 0)
# 创建X轴箭头
x_lines.moveTo(self.axis_length - 0.5, -0.2, 0)
x_lines.drawTo(self.axis_length, 0, 0)
x_lines.drawTo(self.axis_length - 0.5, 0.2, 0)
x_geom = x_lines.create()
self.gizmoXAxis = self.gizmo.attachNewNode(x_geom)
self.gizmoXAxis.setName("gizmo_x_axis")
#self.gizmoXAxis.setLightOff()
# 创建Y轴绿色
y_lines = LineSegs("y_axis")
y_lines.setThickness(6.0)
y_lines.moveTo(0, 0, 0)
y_lines.drawTo(0, self.axis_length, 0)
# 创建Y轴箭头
y_lines.moveTo(-0.2, self.axis_length - 0.5, 0)
y_lines.drawTo(0, self.axis_length, 0)
y_lines.drawTo(0.2, self.axis_length - 0.5, 0)
y_geom = y_lines.create()
self.gizmoYAxis = self.gizmo.attachNewNode(y_geom)
self.gizmoYAxis.setName("gizmo_y_axis")
#self.gizmoYAxis.setLightOff()
model_paths = [
"core/TranslateArrowHandle.fbx",
"EG/core/TranslateArrowHandle.fbx",
]
arrow_model = None
for path in model_paths:
try:
arrow_model = self.world.loader.loadModel(path)
if arrow_model:
print(f"成功加载模型: {path}")
break
except:
continue
self.gizmoXAxis = self.gizmo.attachNewNode("gizmo_x_axis")
x_arrow = arrow_model.copyTo(self.gizmoXAxis)
x_arrow.setName("x_arrow")
x_arrow.setHpr(0,-90,0)
x_arrow.setScale(0.1,0.05,0.05)
x_arrow.setPos(0,0,0)
self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis")
y_arrow = arrow_model.copyTo(self.gizmoYAxis)
y_arrow.setName("y_arrow")
y_arrow.setHpr(90,0,0)
y_arrow.setScale(0.1,0.05,0.05)
y_arrow.setPos(0,0,0)
# 创建Z轴蓝色
z_lines = LineSegs("z_axis")
z_lines.setThickness(6.0)
z_lines.moveTo(0, 0, 0)
z_lines.drawTo(0, 0, self.axis_length)
# 创建Z轴箭头
z_lines.moveTo(-0.2, 0, self.axis_length - 0.5)
z_lines.drawTo(0, 0, self.axis_length)
z_lines.drawTo(0.2, 0, self.axis_length - 0.5)
z_geom = z_lines.create()
self.gizmoZAxis = self.gizmo.attachNewNode(z_geom)
self.gizmoZAxis.setName("gizmo_z_axis")
#self.gizmoZAxis.setLightOff()
self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis")
z_arrow = arrow_model.copyTo(self.gizmoZAxis)
z_arrow.setName("z_arrow")
# 旋转箭头使其指向Z轴正方向
z_arrow.setHpr(0, 0, -90) # 根据需要调整旋转
z_arrow.setScale(0.1,0.05,0.05)
z_arrow.setPos(0, 0, 0)
# 确保坐标轴不被光照影响
#self.gizmo.setLightOff()
# 使用最强的渲染设置,确保坐标轴绝对不会被遮挡
self.gizmo.setBin("gui-popup", 0) # 使用最高的GUI渲染层
self.gizmo.setDepthTest(False) # 完全禁用深度测试
self.gizmo.setDepthWrite(False) # 禁用深度写入
self.gizmo.setTwoSided(True) # 双面渲染
# 创建强制前景渲染状态
from panda3d.core import RenderModeAttrib, TransparencyAttrib
foreground_state = RenderState.make(
DepthTestAttrib.make(DepthTestAttrib.MNone), # 完全不进行深度测试
TransparencyAttrib.make(TransparencyAttrib.MAlpha) # 启用透明度混合
)
#self.gizmo.setState(foreground_state)
# 对每个坐标轴设置独立的最高渲染优先级
self.gizmoXAxis.setBin("gui-popup", 10)
self.gizmoXAxis.setDepthTest(False)
self.gizmoXAxis.setDepthWrite(False)
self.gizmoXAxis.setLightOff()
self.gizmoXAxis.setState(foreground_state)
self.gizmoYAxis.setBin("gui-popup", 20)
self.gizmoYAxis.setDepthTest(False)
self.gizmoYAxis.setDepthWrite(False)
self.gizmoYAxis.setLightOff()
self.gizmoYAxis.setState(foreground_state)
self.gizmoZAxis.setBin("gui-popup", 30)
self.gizmoZAxis.setDepthTest(False)
self.gizmoZAxis.setDepthWrite(False)
self.gizmoZAxis.setLightOff()
self.gizmoZAxis.setState(foreground_state)
# 初始化高亮状态
self.gizmoHighlightAxis = None
# 立即设置初始颜色,确保创建时就有正确的颜色
self.setGizmoAxisColor("z", self.gizmo_colors["z"])
# 设置初始颜色
self.setGizmoAxisColor("x", self.gizmo_colors["x"])
self.setGizmoAxisColor("y", self.gizmo_colors["y"])
self.setGizmoAxisColor("z", self.gizmo_colors["z"])
print(f"✓ 坐标轴几何体创建完成,长度={self.axis_length}")
# 为 RenderPipeline 环境设置正确的渲染状态
self._setupRenderPipelineCompatibleGizmo()
self._setupEmissiveMaterials()
#设置渲染属性,解决模型遮挡和阴影问题
self._setupGizmoRendering()
except Exception as e:
print(f"创建坐标轴几何体失败: {str(e)}")
def _setupEmissiveMaterials(self):
try:
from panda3d.core import Material,Vec4
materials ={
"x":(Vec4(1,0,0,1),Vec4(2.0,0,0,1)),
"y":(Vec4(0,1,0,1),Vec4(0,2.0,0,1)),
"z":(Vec4(0,0,1,1),Vec4(0,0,2.0,1))
}
axis_nodes ={
"x":self.gizmoXAxis,
"y":self.gizmoYAxis,
"z":self.gizmoZAxis
}
for axis,(base_color,emission_color) in materials.items():
if axis_nodes[axis]:
material=Material(f"gizmo_{axis}_material")
material.setBaseColor(base_color)
material.setEmission(emission_color)
material.setRoughness(1.0)
material.setMetallic(0.0)
axis_nodes[axis].setMaterial(material)
except Exception as e:
print(f"自发光材质设置失败: {str(e)}")
def _setupGizmoRendering(self):
"""设置坐标轴渲染属性"""
try:
# 设置渲染优先级,确保在最前面显示
self.gizmo.setBin("gui-popup", 1000)
self.gizmo.setDepthTest(False)
self.gizmo.setDepthWrite(False)
self.gizmo.setLightOff()
axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis]
# 为每个轴设置独立的渲染属性
for i, axis_node in enumerate([self.gizmoXAxis, self.gizmoYAxis, self.gizmoZAxis]):
for axis_node in axis_nodes:
if axis_node:
axis_node.setBin("gui-popup", 1001 + i)
axis_node.setDepthTest(False)
axis_node.setDepthWrite(False)
axis_node.setLightOff()
except Exception as e:
print(f"设置坐标轴渲染失败!!!!!!: {str(e)}")
def _setupRenderPipelineCompatibleGizmo(self):
"""为 RenderPipeline 环境设置兼容的坐标轴渲染 - 激进修复版本"""
try:
from panda3d.core import (ShaderAttrib, MaterialAttrib, RenderModeAttrib,
AntialiasAttrib, TransparencyAttrib, CullFaceAttrib,
RescaleNormalAttrib, TextureAttrib)
# 第一步:完全隔离坐标轴,避免被任何系统处理
self._isolateGizmoFromRenderPipeline()
# 第二步:使用最简单的渲染方式
self._setupMinimalGizmoRendering()
# 第三步:强制设置每个轴的独立渲染
self._forceAxisIndependentRendering()
except Exception as e:
# 使用最后的备用方案
self._setupUltimateGizmoFallback()
def _isolateGizmoFromRenderPipeline(self):
"""完全隔离坐标轴,避免被 RenderPipeline 处理"""
try:
# 设置所有可能的隔离标签
isolation_tags = [
("no_shadow", "1"),
("no_lighting", "1"),
("no_material", "1"),
("no_shader", "1"),
("no_fog", "1"),
("no_texture", "1"),
("gui_element", "1"),
("bypass_rp", "1"),
("fixed_pipeline", "1"),
("ignore_all", "1")
]
for tag, value in isolation_tags:
self.gizmo.setTag(tag, value)
# 完全禁用所有高级功能,使用最高优先级
self.gizmo.setShaderOff(10000)
self.gizmo.setLightOff(10000)
self.gizmo.setFogOff(10000)
self.gizmo.setMaterialOff(10000)
self.gizmo.setTextureOff(10000)
# 禁用所有可能的渲染特性
from panda3d.core import RenderModeAttrib, CullFaceAttrib
# self.gizmo.setRenderModeWireframe()
# self.gizmo.setTwoSided(True)
self.gizmo.setRenderMode(RenderModeAttrib.MFilled)
self.gizmo.setTwoSided(True)
self.gizmo.setColorScale(2.0,2.0,2.0,1.0)
for axis_node in [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis]:
if axis_node:
axis_node.setTag("emissive","1")
axis_node.setTag("unlit","1")
axis_node.setColorScale(2.0,2.0,2.0,1.0)
except Exception as e:
print(f" ❌ 隔离失败: {e}")
def _setupMinimalGizmoRendering(self):
"""设置最简单的渲染方式"""
try:
from panda3d.core import (ShaderAttrib, MaterialAttrib, TextureAttrib,
CullFaceAttrib, RescaleNormalAttrib)
# 使用最高优先级的 GUI 渲染 bin
self.gizmo.setBin("gui-popup", 10000)
self.gizmo.setDepthTest(False)
self.gizmo.setDepthWrite(False)
# 创建最简单的渲染状态
minimal_state = RenderState.make(
ShaderAttrib.makeOff(10000), # 完全禁用着色器
MaterialAttrib.makeOff(), # 禁用材质
TextureAttrib.makeOff(), # 禁用纹理
CullFaceAttrib.make(CullFaceAttrib.MCullNone), # 禁用面剔除
RescaleNormalAttrib.makeOff() # 禁用法线重缩放
)
self.gizmo.setState(minimal_state, 10000)
except Exception as e:
print(f" ❌ 最简渲染设置失败: {e}")
def _forceAxisIndependentRendering(self):
"""强制设置每个轴的独立渲染"""
try:
# 轴配置
axis_configs = [
(self.gizmoXAxis, "X轴", (1.0, 0.0, 0.0, 1.0), 0),
(self.gizmoYAxis, "Y轴", (0.0, 1.0, 0.0, 1.0), 0),
(self.gizmoZAxis, "Z轴", (0.0, 0.0, 1.0, 1.0), 0)
]
for axis_node, name, color, priority in axis_configs:
if axis_node:
# 每个轴都完全独立设置
self._setupSingleAxisRendering(axis_node, name, color, 0)
except Exception as e:
print(f" ❌ 独立轴渲染设置失败: {e}")
def _setupSingleAxisRendering(self, axis_node, name, color, priority):
"""为单个轴设置完全独立的渲染"""
try:
from panda3d.core import (LVecBase4f, RenderState, ColorAttrib,
TransparencyAttrib, LColor, AntialiasAttrib,
RenderModeAttrib, CullFaceAttrib, AuxBitplaneAttrib,
LightRampAttrib)
# 转换颜色为LColor并增加亮度
base_color = LColor(*color)
emissive_color = LColor(base_color[0], base_color[1], base_color[2], 1.0)
# 完全禁用所有高级渲染功能
axis_node.clearShader()
axis_node.clearTexture()
axis_node.clearMaterial()
#禁用光照和阴影
axis_node.setLightOff()
axis_node.setShaderOff()
axis_node.setFogOff()
axis_node.setAttrib(RenderModeAttrib.make(RenderModeAttrib.MWireframe, 6.0))
axis_node.setAttrib(AntialiasAttrib.make(AntialiasAttrib.MLine))
axis_node.setBin("gui-popup", 0)
axis_node.setDepthTest(False)
#设置渲染层级,确保大多数对象之前渲染
axis_node.setBin("fixed",30)
axis_node.setDepthWrite(False)
axis_node.setTwoSided(True)
# 强制设置自发光颜色
axis_node.setColor(*color)
axis_node.setColorScale(1.0, 1.0, 1.0, 1.0) # 增加整体亮度
except Exception as e:
print("{} 轴渲染设置失败: {}".format(name, str(e)))
raise e
def _setupUltimateGizmoFallback(self):
"""最后的备用方案 - 最简单的渲染"""
try:
# 最简单的设置
self.gizmo.setLightOff()
self.gizmo.setFogOff()
self.gizmo.setBin("gui-popup", 20000)
self.gizmo.setDepthTest(False)
self.gizmo.setDepthWrite(False)
# 直接设置颜色,不使用复杂的渲染状态
axis_node.setDepthTest(False)
arrow_nodes = []
if self.gizmoXAxis:
self.gizmoXAxis.setColor(1, 0, 0, 1)
self.gizmoXAxis.setLightOff()
self.gizmoXAxis.setBin("gui-popup", 20001)
self.gizmoXAxis.setDepthTest(False)
x_arrow = self.gizmoXAxis.find("x_arrow")
if x_arrow:
arrow_nodes.append(x_arrow)
if self.gizmoYAxis:
self.gizmoYAxis.setColor(0, 1, 0, 1)
self.gizmoYAxis.setLightOff()
self.gizmoYAxis.setBin("gui-popup", 20002)
self.gizmoYAxis.setDepthTest(False)
y_arrow = self.gizmoYAxis.find("y_arrow")
if y_arrow:
arrow_nodes.append(y_arrow)
if self.gizmoZAxis:
self.gizmoZAxis.setColor(0, 0, 1, 1)
self.gizmoZAxis.setLightOff()
self.gizmoZAxis.setBin("gui-popup", 20003)
self.gizmoZAxis.setDepthTest(False)
z_arrow = self.gizmoZAxis.find("z_arrow")
if z_arrow:
arrow_nodes.append(z_arrow)
for arrow_node in arrow_nodes:
if arrow_node:
arrow_node.setLightOff()
arrow_node.setShaderOff()
arrow_node.setFogOff()
arrow_node.setBin("fixed",31)
arrow_node.setDepthWrite(False)
arrow_node.setDepthTest(False)
#启用透明度S
arrow_node.setTransparency(TransparencyAttrib.MAlpha)
if self.gizmo:
self.gizmo.setLightOff()
self.gizmo.setShaderOff()
self.gizmo.setFogOff()
self.gizmo.setBin("fixed",29)
self.gizmo.setDepthWrite(False)
self.gizmo.setDepthTest(False)
except Exception as e:
print(f"❌ 最后备用方案也失败: {e}")
print(f"设置坐标轴渲染属性失败: {str(e)}")
def updateGizmoTask(self, task):
"""坐标轴更新任务 - 包含固定大小功能"""
@ -648,6 +437,9 @@ class SelectionSystem:
return task.cont
self._last_gizmo_update = current_time
#检查目标节点是否已被删除
self.checkAndClearIfTargetDeleted()
if not self.gizmo or not self.gizmoTarget:
return task.done
@ -665,25 +457,6 @@ class SelectionSystem:
else:
# 只在必要时更新位置和朝向
self._updateGizmoPositionAndOrientation()
# # 更新坐标轴位置,始终在目标节点中心
# minPoint = Point3()
# maxPoint = Point3()
# if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render):
# # 计算中心点
# center = Point3((minPoint.x + maxPoint.x) * 0.5,
# (minPoint.y + maxPoint.y) * 0.5,
# (minPoint.z + maxPoint.z) * 0.5)
# self.gizmo.setPos(center)
#
# # 【关键修复】:更新坐标轴朝向以跟踪父节点的变化
# parent_node = self.gizmoTarget.getParent()
# if parent_node and parent_node != self.world.render:
# # 子节点:坐标轴朝向跟随父节点
# parent_hpr = parent_node.getHpr()
# self.gizmo.setHpr(parent_hpr)
# else:
# # 顶级模型:使用世界坐标系朝向
# self.gizmo.setHpr(0, 0, 0)
# 【新功能】:动态调整坐标轴大小,保持固定的屏幕大小
self._updateGizmoScreenSize()
@ -753,8 +526,8 @@ class SelectionSystem:
self.gizmo.setScale(scale_factor)
# 限制缩放范围,避免过大或过小
min_scale = 0.1
max_scale = 10.0
min_scale = 0.08
max_scale = 100.0
final_scale = max(min_scale, min(max_scale, scale_factor))
if final_scale != scale_factor:
@ -785,11 +558,114 @@ class SelectionSystem:
self.gizmoStartPos = None
def setGizmoAxisColor(self, axis, color):
"""设置坐标轴颜色 - RenderPipeline 兼容版本"""
try:
from panda3d.core import AntialiasAttrib,TransparencyAttrib
# def setGizmoAxisColor(self, axis, color):
# """设置坐标轴颜色 - RenderPipeline 兼容版本"""
# try:
# from panda3d.core import AntialiasAttrib,TransparencyAttrib
#
# axis_nodes = {
# "x": self.gizmoXAxis,
# "y": self.gizmoYAxis,
# "z": self.gizmoZAxis
# }
#
# if axis in axis_nodes and axis_nodes[axis]:
# axis_node = axis_nodes[axis]
#
# axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3])
# axis_node.setColorScale(color[0]*10.0,color[1]*10.0,color[2]*10.0,color[3])
# axis_node.setShaderOff(10000)
# axis_node.setLightOff()
# axis_node.setMaterialOff()
# axis_node.setTextureOff()
# axis_node.setFogOff()
#
# except Exception as e:
# print(f"设置坐标轴颜色失败: {str(e)}")
# # 回退到简单的颜色设置
# try:
# if axis in axis_nodes and axis_nodes[axis]:
# axis_nodes[axis].setColor(*color)
# except:
# pass
def setGizmoAxisColor(self, axis, color):
"""使用材质设置坐标轴颜色 - RenderPipeline兼容版本"""
try:
from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib
# 获取对应的轴节点
axis_nodes = {
"x": self.gizmoXAxis,
"y": self.gizmoYAxis,
"z": self.gizmoZAxis
}
if axis not in axis_nodes or not axis_nodes[axis]:
return
axis_node = axis_nodes[axis]
# 查找箭头模型节点
arrow_node = None
if axis == "x":
arrow_node = axis_node.find("x_arrow")
elif axis == "y":
arrow_node = axis_node.find("y_arrow")
elif axis == "z":
arrow_node = axis_node.find("z_arrow")
if not arrow_node:
print(f"未找到{axis}轴的箭头模型")
return
# 创建或获取材质
mat = Material()
# 设置材质属性 - 使用自发光确保在RenderPipeline下可见
mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3]))
mat.setDiffuse(Vec4(0, 0, 0, 1))
#mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光
mat.setEmission(Vec4(1,1,1,1.0)) # 自发光
mat.set_roughness(1)
# 应用材质
arrow_node.setMaterial(mat, 1)
# 设置透明度
if color[3] < 1.0:
arrow_node.setTransparency(TransparencyAttrib.MAlpha)
else:
arrow_node.setTransparency(TransparencyAttrib.MNone)
arrow_node.setLightOff() # 禁用光照影响
arrow_node.setShaderOff() # 禁用着色器
arrow_node.setFogOff() # 禁用雾效果
arrow_node.setBin("fixed",31)
#arrow_node.setDepthWrite(False)
#arrow_node.setDepthTest(True)
# 保存材质引用以便后续修改
if axis == "x":
self.xMat = mat
elif axis == "y":
self.yMat = mat
elif axis == "z":
self.zMat = mat
axis_node.setLightOff()
axis_node.setShaderOff()
axis_node.setFogOff()
axis_node.setBin("fixed", 30)
#axis_node.setDepthWrite(False)
#axis_node.setDepthTest(True)
except Exception as e:
print(f"设置坐标轴颜色失败: {str(e)}")
# 回退到简单颜色设置
try:
axis_nodes = {
"x": self.gizmoXAxis,
"y": self.gizmoYAxis,
@ -797,22 +673,7 @@ class SelectionSystem:
}
if axis in axis_nodes and axis_nodes[axis]:
axis_node = axis_nodes[axis]
axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3])
axis_node.setColorScale(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3])
axis_node.setShaderOff(10000)
axis_node.setLightOff(10000)
axis_node.setMaterialOff(10000)
axis_node.setTextureOff(1000)
axis_node.setFogOff(10000)
except Exception as e:
print(f"设置坐标轴颜色失败: {str(e)}")
# 回退到简单的颜色设置
try:
if axis in axis_nodes and axis_nodes[axis]:
axis_nodes[axis].setColor(*color)
axis_nodes[axis].setColor(color[0], color[1], color[2], color[3])
except:
pass
@ -1050,38 +911,10 @@ class SelectionSystem:
# ==================== 高亮和交互 ====================
def updateGizmoHighlight(self, mouseX, mouseY):
"""更新坐标轴高亮状态"""
if not self.gizmo or self.isDraggingGizmo:
return
import time
current_time = time.time()
if not hasattr(self,'_last_highlight_time'):
self._last_highlight_time = 0
if current_time - self._last_highlight_time<0.05:
return
self._last_highlight_time = current_time
hoveredAxis = self._detectHoveredAxis(mouseX, mouseY)
if not hasattr(self,'_hover_stability_counter'):
self._hover_stability_counter = {}
self._last_detected_axis = None
if hoveredAxis !=self._last_detected_axis:
self._hover_stability_counter[hoveredAxis]=1
self._last_detected_axis = hoveredAxis
else:
self._hover_stability_counter[hoveredAxis] = self._hover_stability_counter.get(hoveredAxis,0)+1
if self._hover_stability_counter.get(hoveredAxis,0)>=2:
if hoveredAxis != self.gizmoHighlightAxis:
self._updateAxisHighlight(hoveredAxis)
# 检测鼠标悬停的轴(使用相同的检测逻辑但不输出调试信息)
hoveredAxis = None
def detectGizmoAxisAtMouse(self, mouseX, mouseY):
"""统一的坐标轴检测方法 - 同时用于高亮和点击检测"""
if not self.gizmo or not self.gizmoTarget:
return None
try:
# 获取坐标轴中心的世界坐标
@ -1124,60 +957,78 @@ class SelectionSystem:
y_screen = worldToScreen(y_end)
z_screen = worldToScreen(z_end)
# 只要坐标轴中心在屏幕内,就进行检测
if gizmo_screen:
click_threshold = 25
# 如果坐标轴中心不在屏幕内返回None
if not gizmo_screen:
return None
def isNearLine(mousePos, start, end, threshold):
# 设置检测阈值
click_threshold = 35 # 统一使用25像素的检测阈值
# 更准确的点到线段距离计算方法
def distanceToLineSegment(mousePos, start, end):
import math
A = mousePos[1] - start[1]
B = start[0] - mousePos[0]
C = (end[1] - start[1]) * mousePos[0] + (start[0] - end[0]) * mousePos[1] + end[0] * start[1] - start[0] * end[1]
mx, my = mousePos
x1, y1 = start
x2, y2 = end
length = math.sqrt((end[0] - start[0])**2 + (end[1] - start[1])**2)
if length == 0:
return False
# 线段向量
dx = x2 - x1
dy = y2 - y1
distance = abs(C) / length
t = ((mousePos[0] - start[0]) * (end[0] - start[0]) +
(mousePos[1] - start[1]) * (end[1] - start[1])) / (length * length)
# 线段长度平方
length_sq = dx * dx + dy * dy
return distance < threshold and 0 <= t <= 1
if length_sq == 0:
# 线段退化为点
return math.sqrt((mx - x1) ** 2 + (my - y1) ** 2)
# 投影参数
t = max(0, min(1, ((mx - x1) * dx + (my - y1) * dy) / length_sq))
# 投影点坐标
proj_x = x1 + t * dx
proj_y = y1 + t * dy
# 返回点到投影点的距离
return math.sqrt((mx - proj_x) ** 2 + (my - proj_y) ** 2)
mouse_pos = (mouseX, mouseY)
# 分别检测每个轴,为在屏幕外的轴端点提供替代方案
# 按优先级检测轴Z > X > Y
# 检测各个轴 - 按优先级检测Z > X > Y
axes_to_check = [
("z", z_screen),
("x", x_screen),
("y", y_screen)
]
# 对于轴端点在屏幕外的情况,使用较短的轴段进行检测
def getAxisScreenPoint(axis_name, axis_screen_end):
if axis_screen_end:
return axis_screen_end
# 如果端点在屏幕外,使用轴长度的一半作为检测点
if axis_name == "x":
half_end = gizmo_world_pos + Vec3(self.axis_length * 0.5, 0, 0)
elif axis_name == "y":
half_end = gizmo_world_pos + Vec3(0, self.axis_length * 0.5, 0)
elif axis_name == "z":
half_end = gizmo_world_pos + Vec3(0, 0, self.axis_length * 0.5)
return worldToScreen(half_end)
for axis_name, axis_end in axes_to_check:
if axis_end:
distance = distanceToLineSegment(mouse_pos, gizmo_screen, axis_end)
if distance < click_threshold:
return axis_name
# 获取有效的检测点(优先使用完整轴,备用使用半轴)
z_detect_point = getAxisScreenPoint("z", z_screen)
x_detect_point = getAxisScreenPoint("x", x_screen)
y_detect_point = getAxisScreenPoint("y", y_screen)
if z_detect_point and isNearLine(mouse_pos, gizmo_screen, z_detect_point, click_threshold):
hoveredAxis = "z"
elif x_detect_point and isNearLine(mouse_pos, gizmo_screen, x_detect_point, click_threshold):
hoveredAxis = "x"
elif y_detect_point and isNearLine(mouse_pos, gizmo_screen, y_detect_point, click_threshold):
hoveredAxis = "y"
# 如果没有检测到返回None
return None
except Exception as e:
pass # 静默处理错误,避免频繁输出
# 静默处理错误,避免频繁输出
return None
# 如果高亮状态发生变化
def updateGizmoHighlight(self, mouseX, mouseY):
"""更新坐标轴高亮状态"""
if not self.gizmo or self.isDraggingGizmo:
return
# 使用统一的检测方法
hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY)
# 简化稳定性检测逻辑
if not hasattr(self, '_last_detected_axis'):
self._last_detected_axis = None
# 如果检测结果发生变化,立即更新高亮
if hoveredAxis != self._last_detected_axis:
# 更新轴的高亮状态
if hoveredAxis != self.gizmoHighlightAxis:
# 恢复之前高亮的轴
if self.gizmoHighlightAxis:
@ -1189,6 +1040,8 @@ class SelectionSystem:
self.gizmoHighlightAxis = hoveredAxis
self._last_detected_axis = hoveredAxis
def _detectHoveredAxis(self, mouseX, mouseY):
"""检测鼠标悬停的轴 - 提取为独立方法"""
# 将原来 updateGizmoHighlight 中的检测逻辑移到这里
@ -1221,6 +1074,10 @@ class SelectionSystem:
return
self.isDraggingGizmo = True
# 使用当前高亮的轴,如果有的话
if self.gizmoHighlightAxis:
self.dragGizmoAxis = self.gizmoHighlightAxis
else:
self.dragGizmoAxis = axis
self.dragStartMousePos = (mouseX, mouseY)
@ -1228,7 +1085,25 @@ class SelectionSystem:
self.gizmoTargetStartPos = self.gizmoTarget.getPos()
self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置
print(f"开始拖拽 {axis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})")
# 确保正在拖动的轴保持高亮状态
if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors:
# 先将所有轴恢复为正常颜色
for axis_name in self.gizmo_colors.keys():
if axis_name != self.dragGizmoAxis:
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
# 然后将当前拖动的轴设置为高亮颜色
self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_highlight_colors[self.dragGizmoAxis])
self.gizmoHighlightAxis = self.dragGizmoAxis
elif axis and axis in self.gizmo_colors:
for axis_name in self.gizmo_colors.keys():
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis])
self.gizmoHighlightAxis = axis
print(
f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})")
except Exception as e:
print(f"开始坐标轴拖拽失败: {str(e)}")
@ -1454,6 +1329,10 @@ class SelectionSystem:
def stopGizmoDrag(self):
"""停止坐标轴拖拽"""
print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}")
if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors:
self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_colors[self.dragGizmoAxis])
# 不要将 gizmoHighlightAxis 设置为 None保持当前高亮轴的状态
# self.gizmoHighlightAxis = None
self.isDraggingGizmo = False
self.dragGizmoAxis = None
@ -1507,3 +1386,10 @@ class SelectionSystem:
def hasSelection(self):
"""检查是否有选中的节点"""
return self.selectedNode is not None
def checkAndClearIfTargetDeleted(self):
if self.gizmoTarget and self.gizmoTarget.isEmpty():
self.clearGizmo()
if self.selectionBoxTarget and self.selectionBoxTarget.isEmpty():
self.clearSelectionBox()

View File

@ -370,9 +370,6 @@ class MyWorld(CoreWorld):
"""更新属性面板显示 - 代理到property_panel"""
return self.property_panel.updatePropertyPanel(item)
# def addAnimationPanel(self,originmodel,filepath):
# return self.property_panel._addAnimationPanel(originmodel,filepath)
def updateGUIPropertyPanel(self, gui_element):
"""更新GUI元素属性面板 - 代理到property_panel"""
return self.property_panel.updateGUIPropertyPanel(gui_element)
@ -380,6 +377,9 @@ class MyWorld(CoreWorld):
def removeActorForModel(self,model):
return self.property_panel.removeActorForModel( model)
def updateNodeVisibilityAfterDrag(self,item):
return self.property_panel.updateNodeVisibilityAfterDrag(item)
# ==================== 工具管理代理 ====================
def setCurrentTool(self, tool):

View File

@ -631,6 +631,7 @@ class SceneManager:
# 将碰撞节点附加到模型上
cNodePath = model.attachNewNode(cNode)
#cNodePath.hide()
# cNodePath.show() # 取消注释可以显示碰撞体,用于调试
# ==================== 场景树管理 ====================

View File

@ -1,5 +1,6 @@
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu
from PyQt5.QtCore import Qt
from PyQt5.sip import delete
from panda3d.core import GeomNode, ModelRoot
@ -26,8 +27,11 @@ class InterfaceManager:
def onTreeItemClicked(self, item, column):
"""处理树形控件项目点击事件"""
if not item:
self.world.selection.updateSelection(None)
return
self.world.property_panel.updatePropertyPanel(item)
# 获取节点对象
nodePath = item.data(0, Qt.UserRole)
if nodePath:
@ -36,7 +40,7 @@ class InterfaceManager:
self.world.selection.updateSelection(nodePath)
# 更新属性面板
self.world.property_panel.updatePropertyPanel(item)
#self.world.property_panel.updatePropertyPanel(item)
print(f"树形控件点击: {item.text(0)}")
else:
@ -77,6 +81,9 @@ class InterfaceManager:
if self.isModelOrChild(item):
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
else:
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
# 显示菜单
menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
@ -94,10 +101,31 @@ class InterfaceManager:
try:
# 从场景中移除
self.world.property_panel.removeActorForModel(nodePath)
if hasattr(nodePath,'getPythonTag'):
light_object = nodePath.getPythonTag('rp_light_object')
if light_object and hasattr(self.world,'render_pipeline'):
self.world.render_pipeline.remove_light(light_object)
if hasattr(self.world,'selection'):
if self.world.selection.selectedNode == nodePath:
self.world.selection.clearSelectionBox()
self.world.selection.clearGizmo()
self.world.selection.selectedNode = None
self.world.selection.selectedObject = None
if nodePath in self.world.Spotlight:
self.world.Spotlight.remove(nodePath)
if nodePath in self.world.Pointlight:
self.world.Pointlight.remove(nodePath)
nodePath.removeNode()
if hasattr(self.world,'selection'):
self.world.selection.checkAndClearIfTargetDeleted()
# 如果是模型根节点,从模型列表中移除
if item.parent().text(0) == "模型":
#if item.parent().text(0) == "模型":
if nodePath in self.world.models:
self.world.models.remove(nodePath)
@ -240,3 +268,34 @@ class InterfaceManager:
for i in range(item.childCount()):
self._restore_expanded(item.child(i),path)
def syncVisibilityDownward(self):
from collections import deque
if not self.world.models:
return
q = deque()
for root in self.world.models:
q.append(root)
while q:
node = q.popleft()
visible = node.getPythonTag("visible")
if visible is None:
visible = True
if not visible:
stack = [node]
while stack:
cur = stack.pop()
cur.hide()
for child in cur.getChildren():
stack.append(child)
continue
node.show()
for child in node.getChildren():
q.append(child)

View File

@ -1,3 +1,4 @@
from collections import deque
from traceback import print_exc
from types import new_class
from typing import Hashable
@ -21,6 +22,25 @@ class PropertyPanelManager:
self._propertyLayout = None
self._actor_cache={}
# 定义紧凑样式
self.compact_style = """
QDoubleSpinBox {
min-width: 45px;
}
QPushButton {
min-width: 10px;
}
QComboBox {
min-width: 60px;
}
QLineEdit {
min-width: 60px;
}
QCheckBox {
min-width: 20px;
}
"""
def setPropertyLayout(self, layout):
"""设置属性面板布局引用"""
print("开始设置属性布局")
@ -50,6 +70,20 @@ class PropertyPanelManager:
if item.widget():
item.widget().deleteLater()
def updateNodeVisibilityAfterDrag(self, item):
"""拖拽结束后更新节点的可见性状态"""
node = item.data(0, Qt.UserRole)
if not node:
return
# 当节点被拖拽后,需要根据新父节点的状态来更新可见性
self._syncEffectiveVisibility(node)
self._syncSceneVisibility()
def _syncSceneVisibility(self):
scene_root = self.world.render
self._syncEffectiveVisibility(scene_root)
def updatePropertyPanel(self, item):
"""更新属性面板显示"""
if not self._propertyLayout or not self._propertyLayout.parent():
@ -58,6 +92,10 @@ class PropertyPanelManager:
self.clearPropertyPanel()
# 应用紧凑样式到属性面板容器
if self._propertyLayout.parent():
self._propertyLayout.parent().setStyleSheet(self.compact_style)
itemText = item.text(0)
# 如果点击的是场景根节点,显示提示信息
@ -68,15 +106,34 @@ class PropertyPanelManager:
self._propertyLayout.addWidget(tipLabel)
return
model = item.data(0, Qt.UserRole)
user_visible = True
if model:
user_visible = model.getPythonTag("user_visible")
if user_visible is None:
user_visible = True
model.setPythonTag("user_visible", True)
self.name_group = QGroupBox("物体名称")
name_layout = QHBoxLayout()
self.active_check = QCheckBox()
self.active_check.setChecked(True) # 默认激活
# 根据模型的实际可见性状态设置复选框
self.active_check.setChecked(user_visible)
self.name_input = QLineEdit(itemText)
name_layout.addWidget(self.active_check)
name_layout.addWidget(self.name_input)
self.name_group.setLayout(name_layout)
self._propertyLayout.addWidget(self.name_group)
if model:
try:
self.active_check.stateChanged.disconnect()
except TypeError:
pass
self.active_check.stateChanged.connect(
lambda state,m = model:self._setUserVisible(m,state == Qt.Checked)
)
# nameLabel = QLabel("名称:")
# nameEdit = QLineEdit(itemText)
# self._propertyLayout.addRow(nameLabel, nameEdit)
@ -107,6 +164,59 @@ class PropertyPanelManager:
if propertyWidget:
propertyWidget.update()
def _setUserVisible(self,node,visible):
node.setPythonTag("user_visible",visible)
self._syncEffectiveVisibility(node)
def _syncEffectiveVisibility(self, start_node):
"""广度优先,确保父隐藏则子一定隐藏"""
# 获取起始节点的父节点
parent_node = start_node.getParent()
# 确定父节点的有效可见性
parent_effective_visible = True
if parent_node:
parent_effective_visible = parent_node.getPythonTag("effective_visible")
if parent_effective_visible is None:
parent_effective_visible = True
q = deque([(start_node, parent_effective_visible)]) # (node, parent_effective_visible)
while q:
node, parent_eff = q.popleft()
user = node.getPythonTag("user_visible")
if user is None:
user = True
eff = parent_eff and user
node.setPythonTag("effective_visible", eff)
# 特殊处理:检查是否为碰撞体节点
if node.getName().startswith("modelCollision_"):
node.hide()
else:
if eff:
node.show()
else:
node.hide()
for child in node.getChildren():
q.append((child, eff))
def _toggleModelVisibility(self, model, state):
"""切换模型可见性状态"""
try:
# 用我们自己维护的可见性接口,而不是直接 show/hide
visible = (state == Qt.Checked)
self._setUserVisible(model, visible)
collision_nodes = model.findAllMatches("**/modelCollision_*")
for collision_node in collision_nodes:
collision_node.hide()
except Exception as e:
print(f"切换模型可见性失败: {str(e)}")
def refreshModelValues(self, nodePath):
"""实时刷新模型所有属性数值(相对/世界位置、旋转、缩放)"""
if not nodePath or self._propertyLayout is None:
@ -185,7 +295,7 @@ class PropertyPanelManager:
# 设置位置控件属性
for pos_widget in [self.pos_x, self.pos_y, self.pos_z]:
pos_widget.setRange(-1000, 1000)
pos_widget.setRange(-1000000.0, 1000000.0)
self.pos_x.setValue(relativePos.getX())
self.pos_y.setValue(relativePos.getY())
@ -234,7 +344,7 @@ class PropertyPanelManager:
# 设置世界位置控件属性
for world_pos_widget in [self.world_pos_x, self.world_pos_y, self.world_pos_z]:
world_pos_widget.setRange(-1000, 1000)
world_pos_widget.setRange(-1000000.0, 1000000.0)
world_pos_widget.setReadOnly(True)
self.world_pos_x.setValue(worldPos.getX())
@ -253,7 +363,7 @@ class PropertyPanelManager:
# 设置旋转控件属性
for rot_widget in [self.rot_h, self.rot_p, self.rot_r]:
rot_widget.setRange(-180, 180)
rot_widget.setRange(-360, 360)
self.rot_h.setValue(model.getH())
self.rot_p.setValue(model.getP())
@ -4182,14 +4292,6 @@ class PropertyPanelManager:
# 忽略 Actor 加载错误,很多模型都不是角色动画
print(f"[信息] 该模型不包含骨骼动画: {actor_error}")
# 只有在没有骨骼动画时才检测非骨骼动画
if not has_skeletal_anim:
non_skeletal_anims = self._detectNonSkeletalAnimations(origin_model)
if non_skeletal_anims and self._validateNonSkeletalAnimations(origin_model, non_skeletal_anims):
self._buildNonSkeletalUI(origin_model, non_skeletal_anims, animation_layout)
has_animation = True
print(f"[信息] 检测到非骨骼动画: {list(non_skeletal_anims.keys())}")
# 如果都没有动画
if not has_animation:
no_anim_label = QLabel("此模型无动画")
@ -4861,434 +4963,7 @@ except Exception as e:
origin_model.setPythonTag("anim_speed",speed)
print(f"[动画] 速度设为: {speed} ({display_name})")
def _detectNonSkeletalAnimations(self, origin_model):
"""检测模型中的非骨骼动画"""
animations = {}
try:
print(f"[调试] 开始检测非骨骼动画: {origin_model}")
# 1. 精确检测 AnimBundle (非骨骼动画)
bundle_nodes = origin_model.findAllMatches("**/+AnimBundleNode")
print(f"[调试] 找到 AnimBundleNode 数量: {bundle_nodes.getNumPaths()}")
if not bundle_nodes.isEmpty():
for i, bundle_node in enumerate(bundle_nodes):
try:
bundle = bundle_node.node().getBundle()
print(f"[调试] AnimBundle #{i}: {bundle}")
if bundle and hasattr(bundle, 'getNumFrames'):
num_frames = bundle.getNumFrames()
print(f"[调试] Bundle 帧数: {num_frames}")
# 只有真正有多帧的才认为是动画
if num_frames > 1:
anim_names = []
# 尝试获取动画名称
try:
if hasattr(bundle, 'getName') and bundle.getName():
anim_names.append(bundle.getName())
else:
anim_names.append(f"Animation_{i}")
print(f"[调试] 检测到有效帧动画: {anim_names}, {num_frames}")
except Exception as name_error:
anim_names.append(f"Animation_{i}")
print(f"[调试] 使用默认动画名: Animation_{i}")
animations['transform'] = {
'bundle': bundle,
'names': anim_names,
'node': bundle_node,
'frames': num_frames
}
else:
print(f"[调试] Bundle 只有 {num_frames} 帧,不认为是动画")
except Exception as bundle_error:
print(f"[调试] 处理 bundle 失败: {bundle_error}")
# 2. 仅对 GLB 文件进行特殊检测
filepath = origin_model.getTag("model_path")
if filepath and filepath.lower().endswith('.glb') and not animations:
print(f"[调试] GLB 文件特殊检测: {filepath}")
# 检查是否有任何动画相关的节点名称
all_nodes = origin_model.findAllMatches("**")
anim_indicators = []
for node in all_nodes:
node_name = node.getName().lower()
# 查找典型的动画节点命名模式
if any(keyword in node_name for keyword in ['anim', 'key', 'frame', 'action', 'timeline']):
anim_indicators.append(node.getName())
print(f"[调试] 发现可能的动画节点: {node.getName()}")
# 只有在明确找到动画指示器时才创建动画条目
if anim_indicators:
animations['glb_keyframe'] = {
'bundle': None,
'names': ['GLB_Animation'],
'node': origin_model,
'type': 'glb_manual',
'indicators': anim_indicators
}
print(f"[调试] 创建 GLB 动画条目,基于指示器: {anim_indicators}")
except Exception as e:
print(f"检测非骨骼动画失败: {e}")
print(f"[调试] 最终检测结果: {animations}")
return animations if animations else None
def _validateNonSkeletalAnimations(self, origin_model, animations):
"""验证检测到的非骨骼动画是否真的可以播放"""
try:
print(f"[验证] 开始验证非骨骼动画: {list(animations.keys())}")
for anim_type, anim_data in animations.items():
if anim_type == 'transform':
# 验证变换动画
bundle = anim_data.get('bundle')
if bundle:
# 检查是否真的有可播放的动画数据
if hasattr(bundle, 'getNumFrames'):
frames = bundle.getNumFrames()
if frames <= 1:
print(f"[验证] 变换动画帧数不足: {frames}")
return False
# 检查是否有有效的动画通道
try:
if hasattr(bundle, 'getNumChannels'):
channels = bundle.getNumChannels()
if channels == 0:
print(f"[验证] 无有效动画通道")
return False
except:
pass
elif anim_type == 'glb_keyframe':
# 验证 GLB 动画指示器
indicators = anim_data.get('indicators', [])
if not indicators:
print(f"[验证] GLB 动画无有效指示器")
return False
print(f"[验证] 动画验证通过")
return True
except Exception as e:
print(f"[验证] 动画验证失败: {e}")
return False
def _buildNonSkeletalUI(self, origin_model, animations, layout):
"""构建非骨骼动画控制UI"""
from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox, QTabWidget
# 动画信息
info_text = f"非骨骼动画数量: {len(animations)}"
info_label = QLabel(info_text)
info_label.setStyleSheet("color:#888;font-size:10px;")
layout.addWidget(QLabel("信息:"), 0, 0)
layout.addWidget(info_label, 0, 1, 1, 3)
# 如果有多种类型的动画,使用标签页
if len(animations) > 1:
tab_widget = QTabWidget()
for anim_type, anim_data in animations.items():
tab = QWidget()
tab_layout = QVBoxLayout(tab)
self._buildAnimationTypeUI(tab_layout, origin_model, anim_type, anim_data)
tab_widget.addTab(tab, self._getAnimTypeDisplayName(anim_type))
self._propertyLayout.addRow("动画类型:", tab_widget)
else:
# 只有一种类型,直接显示
anim_type, anim_data = next(iter(animations.items()))
self._buildAnimationTypeUI(self._propertyLayout, origin_model, anim_type, anim_data)
# 存储动画信息供控制使用
if not hasattr(self, '_non_skeletal_cache'):
self._non_skeletal_cache = {}
self._non_skeletal_cache[origin_model] = animations
def _buildAnimationTypeUI(self, layout, origin_model, anim_type, anim_data):
"""为特定动画类型构建UI"""
from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox
if anim_type == 'transform':
# 变换动画控制
self.ns_transform_combo = QComboBox()
self.ns_transform_combo.addItems(anim_data['names'])
layout.addRow("变换动画:", self.ns_transform_combo)
elif anim_type == 'texture':
# 纹理动画控制
self.ns_texture_combo = QComboBox()
self.ns_texture_combo.addItems(anim_data['stages'])
layout.addRow("纹理动画:", self.ns_texture_combo)
elif anim_type == 'material':
# 材质动画控制
self.ns_material_combo = QComboBox()
self.ns_material_combo.addItems(anim_data['properties'])
layout.addRow("材质动画:", self.ns_material_combo)
elif anim_type == 'lerp':
# Lerp动画控制
self.ns_lerp_combo = QComboBox()
self.ns_lerp_combo.addItems(anim_data['intervals'])
layout.addRow("Lerp动画", self.ns_lerp_combo)
# 通用控制按钮
btn_box = QWidget()
btn_lay = QHBoxLayout(btn_box)
for txt, cmd in (("播放", "play"), ("暂停", "pause"), ("停止", "stop"), ("循环", "loop")):
btn = QPushButton(txt)
btn.clicked.connect(lambda _, c=cmd, t=anim_type: self._controlNonSkeletalAnimation(origin_model, t, c))
btn_lay.addWidget(btn)
layout.addRow("控制:", btn_box)
# 播放速度
speed_spinbox = QDoubleSpinBox()
speed_spinbox.setRange(0.1, 5.0)
speed_spinbox.setSingleStep(0.1)
speed_spinbox.setValue(1.0)
speed_spinbox.valueChanged.connect(lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v))
layout.addRow("播放速度:", speed_spinbox)
def _getAnimTypeDisplayName(self, anim_type):
"""获取动画类型的显示名称"""
names = {
'transform': '变换动画',
'glb_keyframe': 'GLB关键帧动画',
'texture': '纹理动画',
'material': '材质动画',
'lerp': 'Lerp动画'
}
return names.get(anim_type, anim_type)
def _controlNonSkeletalAnimation(self, origin_model, anim_type, command):
"""控制非骨骼动画播放"""
try:
if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache:
return
animations = self._non_skeletal_cache[origin_model]
if anim_type not in animations:
return
anim_data = animations[anim_type]
if anim_type == 'transform':
self._controlTransformAnimation(origin_model, anim_data, command)
elif anim_type == 'glb_keyframe':
self._controlGLBKeyframeAnimation(origin_model, anim_data, command)
elif anim_type == 'texture':
self._controlTextureAnimation(origin_model, anim_data, command)
elif anim_type == 'material':
self._controlMaterialAnimation(origin_model, anim_data, command)
elif anim_type == 'lerp':
self._controlLerpAnimation(origin_model, anim_data, command)
print(f"[非骨骼动画] {anim_type} {command}")
except Exception as e:
print(f"控制非骨骼动画失败: {e}")
def _controlTransformAnimation(self, origin_model, anim_data, command):
"""控制变换动画"""
try:
bundle = anim_data['bundle']
bundle_node = anim_data['node']
print(f"[调试] 控制变换动画: {command}, Bundle: {bundle}")
if command == 'play':
# 方法1: 通过 AnimBundle 直接播放
if hasattr(bundle, 'play'):
bundle.play()
print(f"[动画] 通过 bundle.play() 播放")
# 方法2: 通过 AnimControl 播放
elif hasattr(bundle_node.node(), 'getAnimControl'):
controls = bundle_node.node().getAnimControls()
if controls:
controls[0].play()
print(f"[动画] 通过 AnimControl 播放")
# 方法3: 通过启用动画节点
else:
bundle_node.node().setPlayRate(1.0)
print(f"[动画] 设置播放速率为 1.0")
elif command == 'pause':
if hasattr(bundle, 'pause'):
bundle.pause()
elif hasattr(bundle_node.node(), 'getAnimControl'):
controls = bundle_node.node().getAnimControls()
if controls:
controls[0].pause()
else:
bundle_node.node().setPlayRate(0.0)
elif command == 'stop':
if hasattr(bundle, 'stop'):
bundle.stop()
elif hasattr(bundle_node.node(), 'getAnimControl'):
controls = bundle_node.node().getAnimControls()
if controls:
controls[0].stop()
else:
bundle_node.node().setPlayRate(0.0)
# 重置到第一帧
if hasattr(bundle, 'setFrame'):
bundle.setFrame(0)
elif command == 'loop':
if hasattr(bundle, 'loop'):
bundle.loop()
elif hasattr(bundle_node.node(), 'getAnimControl'):
controls = bundle_node.node().getAnimControls()
if controls:
controls[0].loop(True)
controls[0].play()
else:
bundle_node.node().setPlayRate(1.0)
except Exception as e:
print(f"[错误] 控制变换动画失败: {e}")
import traceback
traceback.print_exc()
def _controlGLBKeyframeAnimation(self, origin_model, anim_data, command):
"""控制 GLB 关键帧动画"""
try:
print(f"[调试] 控制 GLB 动画: {command}")
# 尝试通过 AnimControlCollection 控制
from panda3d.core import AnimControlCollection
# 方法1: 查找 AnimControlCollection
anim_collection = AnimControlCollection()
origin_model.getAnimControls(anim_collection)
if anim_collection.getNumAnims() > 0:
for i in range(anim_collection.getNumAnims()):
anim_control = anim_collection.getAnim(i)
print(f"[调试] 找到动画控制: {anim_control.getName()}")
if command == 'play':
anim_control.setPlayRate(1.0)
print(f"[GLB动画] 播放: {anim_control.getName()}")
elif command == 'pause':
anim_control.setPlayRate(0.0)
print(f"[GLB动画] 暂停: {anim_control.getName()}")
elif command == 'stop':
anim_control.setPlayRate(0.0)
anim_control.setFrame(0)
print(f"[GLB动画] 停止: {anim_control.getName()}")
elif command == 'loop':
anim_control.setPlayRate(1.0)
anim_control.loop(True)
print(f"[GLB动画] 循环: {anim_control.getName()}")
return
# 方法2: 通过自动播放任务
if command == 'play':
origin_model.setPlayRate(1.0)
print(f"[GLB动画] 设置播放速率为 1.0")
elif command == 'pause':
origin_model.setPlayRate(0.0)
print(f"[GLB动画] 暂停播放")
elif command == 'stop':
origin_model.setPlayRate(0.0)
print(f"[GLB动画] 停止播放")
elif command == 'loop':
origin_model.setPlayRate(1.0)
print(f"[GLB动画] 循环播放")
except Exception as e:
print(f"[错误] 控制 GLB 动画失败: {e}")
def _controlTextureAnimation(self, origin_model, anim_data, command):
"""控制纹理动画"""
# 纹理动画通常通过 LerpInterval 实现
print(f"[纹理动画] {command} - 功能待实现")
def _controlMaterialAnimation(self, origin_model, anim_data, command):
"""控制材质动画"""
# 材质动画通常通过修改材质属性实现
print(f"[材质动画] {command} - 功能待实现")
def _controlLerpAnimation(self, origin_model, anim_data, command):
"""控制Lerp动画"""
# 通过 LerpInterval 控制
print(f"[Lerp动画] {command} - 功能待实现")
def _setNonSkeletalAnimationSpeed(self, origin_model, anim_type, speed):
"""设置非骨骼动画播放速度"""
try:
if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache:
return
animations = self._non_skeletal_cache[origin_model]
if anim_type not in animations:
return
anim_data = animations[anim_type]
if anim_type == 'transform':
bundle = anim_data['bundle']
if hasattr(bundle, 'setPlayRate'):
bundle.setPlayRate(speed)
print(f"[非骨骼动画] {anim_type} 速度设为: {speed}")
except Exception as e:
print(f"设置非骨骼动画速度失败: {e}")
def _detectPlayer(self,origin_model):
filepath = origin_model.getTag("model_path")
if filepath:
try:
actor = Actor(filepath)
if actor.getAnimNames():
actor.cleanup();actor.removeNode()
return ("actor",None)
except:
pass
bundle_np = origin_model.find("**/+AnimBundleNode")
if not bundle_np.isEmpty():
ctrl = bundle_np.node().getBundle().bind(origin_model.node(),PartGroup.PART_Whole)
return ("bundle",ctrl)
return None
def _buildBundleUI(self,origin_model,ctrl):
from PyQt5.QtWidgets import QLabel,QPushButton,QHBoxLayout,QWidget,QSlider
title = QLabel("骨骼动画控制")
title.setStyleSheet("color:#FF8C00;font-weight:bold;font-size:14px;margin-top:10px;")
self._propertyLayout.addRow(title)
btn_box = QWidget()
lay = QHBoxLayout(btn_box)
for txt , fn in (("播放",ctrl.play),
("暂停",ctrl.stop),
("重置",lambda:ctrl.pose(0))):
btn = QPushButton(txt)
btn.clicked.connect(fn)
lay.addWidget(btn)
self._propertyLayout.addRow("控制:",btn_box)
slider = QSlider()
slider.setOrientation(1)
slider.setRange(0,int(ctrl.getNumFrames()))
slider.valueChanged.connect(ctrl.pose)
self._propertyLayout.addRow("帧:",slider)
self._actor_cache[origin_model] = ("bundle",ctrl)
def _dispatchAnimCommand(self,origin_model,cmd):
cache = self._actor_cache.get(origin_model)
@ -5323,20 +4998,6 @@ except Exception as e:
elif isinstance(cmd,tuple) and cmd[0] == "speed":
actor.setPlayRate(cmd[1], anim_name)
elif kind == "bundle":
ctrl = player
if cmd == "play":
ctrl.play()
elif cmd == "pause":
ctrl.stop()
elif cmd == "stop":
ctrl.stop()
ctrl.pose(0)
elif cmd == "loop":
ctrl.loop(True)
elif isinstance(cmd,tuple) and cmd[0] =="speed":
ctrl.setPlayRate(cmd[1])
def removeActorForModel(self, model):
"""删除 model 对应的 Actor如果存在"""
actor = self._actor_cache.pop(model, None)

View File

@ -348,6 +348,24 @@ class CustomTreeWidget(QTreeWidget):
event.ignore()
return
# # 检查是否是有效的父子关系
# if self.isValidParentChild(dragged_item, target_item):
# # 保存当前的世界坐标
# world_pos = dragged_node.getPos(self.world.render)
#
# # 更新场景图中的父子关系
# dragged_node.wrtReparentTo(target_node)
#
# # 接受拖放事件,更新树形控件
# super().dropEvent(event)
#
# #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item)
# # 更新属性面板
# self.world.updatePropertyPanel(dragged_item)
# self.world.property_panel._syncEffectiveVisibility(dragged_node)
print(f"dragged_node: {dragged_node}, target_node: {target_node}")
# 记录拖拽前的父节点
@ -394,7 +412,7 @@ class CustomTreeWidget(QTreeWidget):
# 事后验证:确保节点仍在"场景"根节点下
self._ensureUnderSceneRoot(dragged_item)
self.world.property_panel._syncEffectiveVisibility(dragged_node)
event.accept()
# try: