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

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