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()