You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
348 lines
14 KiB
348 lines
14 KiB
import vtk
|
|
import os
|
|
from typing import Dict, List, Optional
|
|
|
|
class OBJWithMTLViewer:
|
|
def __init__(self):
|
|
"""初始化OBJ+MTL查看器"""
|
|
self.reader = vtk.vtkOBJReader()
|
|
self.actors_dict: Dict[str, vtk.vtkActor] = {} # 材质名 -> Actor
|
|
self.material_visibility: Dict[str, bool] = {} # 材质名 -> 是否可见
|
|
self.current_material_filter: Optional[str] = None # 当前筛选的材质
|
|
|
|
# 创建渲染器
|
|
self.renderer = vtk.vtkRenderer()
|
|
self.renderer.SetBackground(0.1, 0.2, 0.3)
|
|
self.renderer.SetBackground2(0.3, 0.4, 0.5)
|
|
self.renderer.GradientBackgroundOn()
|
|
|
|
# 创建渲染窗口
|
|
self.render_window = vtk.vtkRenderWindow()
|
|
self.render_window.AddRenderer(self.renderer)
|
|
self.render_window.SetSize(1200, 800)
|
|
self.render_window.SetWindowName("OBJ with MTL Viewer")
|
|
|
|
# 创建交互器
|
|
self.interactor = vtk.vtkRenderWindowInteractor()
|
|
self.interactor.SetRenderWindow(self.render_window)
|
|
|
|
# 设置交互样式
|
|
style = vtk.vtkInteractorStyleTrackballCamera()
|
|
self.interactor.SetInteractorStyle(style)
|
|
|
|
# 添加坐标轴
|
|
self.axes = vtk.vtkAxesActor()
|
|
self.axes_widget = vtk.vtkOrientationMarkerWidget()
|
|
self.axes_widget.SetOutlineColor(0.93, 0.57, 0.13)
|
|
self.axes_widget.SetOrientationMarker(self.axes)
|
|
self.axes_widget.SetInteractor(self.interactor)
|
|
self.axes_widget.SetViewport(0.0, 0.0, 0.2, 0.2)
|
|
self.axes_widget.EnabledOn()
|
|
self.axes_widget.InteractiveOn()
|
|
|
|
# 创建文本显示当前模式
|
|
self.text_actor = vtk.vtkTextActor()
|
|
self.text_actor.SetInput("显示模式: 所有材质")
|
|
self.text_actor.GetTextProperty().SetColor(1, 1, 1)
|
|
self.text_actor.GetTextProperty().SetFontSize(16)
|
|
self.text_actor.SetPosition(20, 10)
|
|
self.renderer.AddViewProp(self.text_actor)
|
|
|
|
# 创建材质信息显示
|
|
self.material_info_actor = vtk.vtkTextActor()
|
|
self.material_info_actor.GetTextProperty().SetColor(1, 1, 0.8)
|
|
self.material_info_actor.GetTextProperty().SetFontSize(14)
|
|
self.material_info_actor.SetPosition(20, 750)
|
|
self.renderer.AddViewProp(self.material_info_actor)
|
|
|
|
def load_file(self, obj_path: str, mtl_path: str = None):
|
|
"""加载OBJ文件(可指定MTL路径)"""
|
|
if not os.path.exists(obj_path):
|
|
raise FileNotFoundError(f"OBJ文件不存在: {obj_path}")
|
|
|
|
# 设置MTL文件路径(如果未指定,则使用同名mtl文件)
|
|
if mtl_path is None:
|
|
base_name = os.path.splitext(obj_path)[0]
|
|
mtl_path = base_name + ".mtl"
|
|
|
|
self.reader.SetFileName(obj_path)
|
|
|
|
# VTK的OBJReader会自动在同一目录查找同名mtl文件
|
|
# 但如果mtl在不同目录,需要额外处理
|
|
if os.path.exists(mtl_path):
|
|
# 复制mtl文件到obj同一目录(如果不在同一目录)
|
|
obj_dir = os.path.dirname(obj_path)
|
|
if os.path.dirname(mtl_path) != obj_dir:
|
|
import shutil
|
|
target_mtl = os.path.join(obj_dir, os.path.basename(mtl_path))
|
|
shutil.copy2(mtl_path, target_mtl)
|
|
print(f"已将MTL文件复制到: {target_mtl}")
|
|
|
|
self.reader.Update()
|
|
|
|
# 处理读取的数据
|
|
self._process_obj_data()
|
|
|
|
def _process_obj_data(self):
|
|
"""处理OBJ数据,按材质分组创建Actor"""
|
|
# 清除已有的actor
|
|
for actor in self.actors_dict.values():
|
|
self.renderer.RemoveActor(actor)
|
|
self.actors_dict.clear()
|
|
|
|
# 获取poly数据
|
|
poly_data = self.reader.GetOutput()
|
|
|
|
if poly_data is None or poly_data.GetNumberOfCells() == 0:
|
|
print("警告: 没有读取到多边形数据")
|
|
return
|
|
|
|
# 检查是否有材质信息
|
|
cell_data = poly_data.GetCellData()
|
|
material_ids = cell_data.GetArray("MaterialIds")
|
|
|
|
if material_ids is None:
|
|
# 如果没有材质信息,创建单个actor
|
|
self._create_single_actor(poly_data, "default_material")
|
|
else:
|
|
# 按材质ID分离多边形
|
|
self._create_actors_by_material(poly_data, material_ids)
|
|
|
|
# 重置相机
|
|
self.renderer.ResetCamera()
|
|
self._update_display_text()
|
|
|
|
def _create_single_actor(self, poly_data: vtk.vtkPolyData, material_name: str):
|
|
"""创建单个材质actor"""
|
|
mapper = vtk.vtkPolyDataMapper()
|
|
mapper.SetInputData(poly_data)
|
|
mapper.ScalarVisibilityOff()
|
|
|
|
actor = vtk.vtkActor()
|
|
actor.SetMapper(mapper)
|
|
|
|
# 设置随机颜色
|
|
import random
|
|
actor.GetProperty().SetColor(random.random(), random.random(), random.random())
|
|
actor.GetProperty().SetSpecular(0.5)
|
|
actor.GetProperty().SetSpecularPower(20)
|
|
|
|
self.actors_dict[material_name] = actor
|
|
self.material_visibility[material_name] = True
|
|
self.renderer.AddActor(actor)
|
|
|
|
def _create_actors_by_material(self, poly_data: vtk.vtkPolyData, material_ids):
|
|
"""按材质ID创建多个actor"""
|
|
# 获取唯一的材质ID
|
|
unique_ids = set()
|
|
for i in range(material_ids.GetNumberOfTuples()):
|
|
unique_ids.add(int(material_ids.GetValue(i)))
|
|
|
|
# 为每个材质ID创建actor
|
|
for mat_id in unique_ids:
|
|
# 提取该材质的多边形
|
|
selection_node = vtk.vtkSelectionNode()
|
|
selection_node.SetFieldType(vtk.vtkSelectionNode.CELL)
|
|
selection_node.SetContentType(vtk.vtkSelectionNode.INDICES)
|
|
|
|
# 选择属于当前材质的单元格
|
|
ids = vtk.vtkIdTypeArray()
|
|
for i in range(material_ids.GetNumberOfTuples()):
|
|
if int(material_ids.GetValue(i)) == mat_id:
|
|
ids.InsertNextValue(i)
|
|
|
|
selection_node.SetSelectionList(ids)
|
|
|
|
selection = vtk.vtkSelection()
|
|
selection.AddNode(selection_node)
|
|
|
|
extractor = vtk.vtkExtractSelection()
|
|
extractor.SetInputData(0, poly_data)
|
|
extractor.SetInputData(1, selection)
|
|
extractor.Update()
|
|
|
|
extracted = vtk.vtkPolyData()
|
|
extracted.ShallowCopy(extractor.GetOutput())
|
|
|
|
if extracted.GetNumberOfCells() > 0:
|
|
mapper = vtk.vtkPolyDataMapper()
|
|
mapper.SetInputData(extracted)
|
|
mapper.ScalarVisibilityOff()
|
|
|
|
actor = vtk.vtkActor()
|
|
actor.SetMapper(mapper)
|
|
|
|
# 设置材质属性
|
|
self._apply_material_properties(actor, mat_id)
|
|
|
|
material_name = f"material_{mat_id}"
|
|
self.actors_dict[material_name] = actor
|
|
self.material_visibility[material_name] = True
|
|
self.renderer.AddActor(actor)
|
|
|
|
def _apply_material_properties(self, actor: vtk.vtkActor, mat_id: int):
|
|
"""应用材质属性(简化版本)"""
|
|
# 在实际应用中,这里应该从MTL文件读取材质属性
|
|
# 这里使用预设的颜色方案
|
|
colors = [
|
|
(1.0, 0.0, 0.0), # 红
|
|
(0.0, 1.0, 0.0), # 绿
|
|
(0.0, 0.0, 1.0), # 蓝
|
|
(1.0, 1.0, 0.0), # 黄
|
|
(1.0, 0.0, 1.0), # 紫
|
|
(0.0, 1.0, 1.0), # 青
|
|
(0.5, 0.5, 0.5), # 灰
|
|
(1.0, 0.5, 0.0), # 橙
|
|
]
|
|
|
|
color_idx = mat_id % len(colors)
|
|
actor.GetProperty().SetColor(*colors[color_idx])
|
|
actor.GetProperty().SetAmbient(0.1)
|
|
actor.GetProperty().SetDiffuse(0.7)
|
|
actor.GetProperty().SetSpecular(0.5)
|
|
actor.GetProperty().SetSpecularPower(30)
|
|
|
|
# 启用光照
|
|
actor.GetProperty().LightingOn()
|
|
|
|
def show_all_materials(self):
|
|
"""显示所有材质"""
|
|
self.current_material_filter = None
|
|
for material_name, actor in self.actors_dict.items():
|
|
actor.SetVisibility(True)
|
|
self.material_visibility[material_name] = True
|
|
self._update_display_text()
|
|
self.render_window.Render()
|
|
|
|
def show_only_material(self, material_name: str):
|
|
"""只显示指定材质"""
|
|
if material_name not in self.actors_dict:
|
|
print(f"警告: 材质 '{material_name}' 不存在")
|
|
return
|
|
|
|
self.current_material_filter = material_name
|
|
for name, actor in self.actors_dict.items():
|
|
if name == material_name:
|
|
actor.SetVisibility(True)
|
|
self.material_visibility[name] = True
|
|
else:
|
|
actor.SetVisibility(False)
|
|
self.material_visibility[name] = False
|
|
self._update_display_text()
|
|
self.render_window.Render()
|
|
|
|
def toggle_material_visibility(self, material_name: str):
|
|
"""切换材质的可见性"""
|
|
if material_name in self.actors_dict:
|
|
current = self.material_visibility.get(material_name, True)
|
|
new_visibility = not current
|
|
self.actors_dict[material_name].SetVisibility(new_visibility)
|
|
self.material_visibility[material_name] = new_visibility
|
|
self._update_display_text()
|
|
self.render_window.Render()
|
|
|
|
def _update_display_text(self):
|
|
"""更新显示文本"""
|
|
if self.current_material_filter:
|
|
self.text_actor.SetInput(f"显示模式: 仅显示 {self.current_material_filter}")
|
|
else:
|
|
visible_count = sum(1 for v in self.material_visibility.values() if v)
|
|
total_count = len(self.actors_dict)
|
|
self.text_actor.SetInput(f"显示模式: 所有材质 ({visible_count}/{total_count}可见)")
|
|
|
|
# 更新材质信息
|
|
info_text = "材质列表:\n"
|
|
for i, (mat_name, is_visible) in enumerate(self.material_visibility.items()):
|
|
status = "✓" if is_visible else "✗"
|
|
info_text += f"{i+1}. {mat_name} [{status}]\n"
|
|
self.material_info_actor.SetInput(info_text)
|
|
|
|
def setup_keyboard_controls(self):
|
|
"""设置键盘控制"""
|
|
def key_press(obj, event):
|
|
key = obj.GetKeySym()
|
|
|
|
# 数字键1-9切换材质显示
|
|
if key in [f"{i}" for i in range(1, 10)]:
|
|
idx = int(key) - 1
|
|
materials = list(self.actors_dict.keys())
|
|
if idx < len(materials):
|
|
self.toggle_material_visibility(materials[idx])
|
|
|
|
# A键显示所有材质
|
|
elif key == "a" or key == "A":
|
|
self.show_all_materials()
|
|
|
|
# S键切换显示模式:单个材质循环
|
|
elif key == "s" or key == "S":
|
|
materials = list(self.actors_dict.keys())
|
|
if materials:
|
|
if not self.current_material_filter:
|
|
self.show_only_material(materials[0])
|
|
else:
|
|
current_idx = materials.index(self.current_material_filter)
|
|
next_idx = (current_idx + 1) % len(materials)
|
|
self.show_only_material(materials[next_idx])
|
|
|
|
# H键显示帮助信息
|
|
elif key == "h" or key == "H":
|
|
print("\n=== 键盘控制 ===")
|
|
print("A: 显示所有材质")
|
|
print("S: 循环切换显示单个材质")
|
|
print("1-9: 切换对应编号材质的可见性")
|
|
print("鼠标左键: 旋转模型")
|
|
print("鼠标右键: 平移模型")
|
|
print("鼠标滚轮: 缩放")
|
|
print("R: 重置视图")
|
|
print("H: 显示此帮助信息")
|
|
print("================\n")
|
|
|
|
# R键重置视图
|
|
elif key == "r" or key == "R":
|
|
self.renderer.ResetCamera()
|
|
self.render_window.Render()
|
|
|
|
# 绑定键盘事件
|
|
self.interactor.AddObserver("KeyPressEvent", key_press)
|
|
|
|
def run(self):
|
|
"""运行查看器"""
|
|
if not self.actors_dict:
|
|
print("警告: 没有加载任何模型数据")
|
|
|
|
self.setup_keyboard_controls()
|
|
|
|
# 开始交互
|
|
self.interactor.Initialize()
|
|
self.render_window.Render()
|
|
|
|
print("查看器已启动!")
|
|
print("按 'H' 键查看帮助信息")
|
|
|
|
self.interactor.Start()
|
|
|
|
if __name__ == "__main__":
|
|
# 创建查看器实例
|
|
viewer = OBJWithMTLViewer()
|
|
|
|
# 加载OBJ文件(自动查找同目录同名MTL文件)
|
|
obj_file = "halfpatch_final.obj" # 替换为您的OBJ文件路径
|
|
|
|
try:
|
|
viewer.load_file(obj_file)
|
|
|
|
# 可选:如果MTL文件在不同目录,可以指定路径
|
|
# mtl_file = "path/to/your/model.mtl"
|
|
# viewer.load_file(obj_file, mtl_file)
|
|
|
|
# 运行查看器
|
|
viewer.run()
|
|
|
|
except FileNotFoundError as e:
|
|
print(f"错误: {e}")
|
|
print("请确保OBJ文件存在,并且MTL文件在同一目录下")
|
|
|
|
except Exception as e:
|
|
print(f"发生错误: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|