【Python】使用 PyQt5 與 VTK 渲染 3D 物件 ( .obj )

【Python】使用 PyQt5 與 VTK 渲染 3D 物件 ( .obj )

安裝需要的函式庫

pip install vtk==9.4.0
pip install PyQt5==5.15.11

確認安裝的版本

pip show vtk
pip show PyQt5

名詞解釋

  • 中央小部件 ( central widget ) : 視窗中顯示內容的核心。
  • 渲染器 : 管理 3D 場景內容。
  • 渲染窗口 : 顯示渲染器產生的畫面。
  • 交互器 : 處理使用者的互動操作。

引入函式庫

有關 sys 或 QApplication、QVBoxLayout、QWidget 的介紹,可以前往以下文章觀看 :
【Python】使用 PyQt5 建構簡單視窗並顯示 “Hello World”

  • vtk :
    Visualization Toolkit 的縮寫,是一個用於 3D 渲染、影像處理和可視化的庫。

  • QMainWindow :
    這是 PyQt 中的一個主要視窗元件,是應用程式中最大的視窗,可以包含菜單欄、工具欄等元素。

  • QVTKRenderWindowInteractor :
    VTK 提供的 PyQt 配件,允許將 VTK 渲染視窗嵌入到 PyQt 應用程式中。這樣你就能在 PyQt 應用程式中顯示 3D 渲染視窗,並與之交互(例如旋轉、縮放、平移視圖)。
import sys
import vtk

from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget

from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor

定義一個 MainWindow 類別

繼承 QMainWindow 的所有屬性和方法

...
from vtkmodules.qt import QVTKRenderWindowInteractor

# 定義了一個 MainWindow 的類別,並且繼承自 QMainWindow。
class MainWindow(QMainWindow):
    def __init__(self):
        # 確保 MainWindow 類別能夠使用 QMainWindow 的所有屬性和方法。
        super().__init__()

設置視窗的標題和位置/大小

...
super().__init__()
self.setWindowTitle("OBJ 渲染與陰影效果") # 設定視窗的標題
self.setGeometry(100, 100, 800, 600) # 設定視窗的位置和大小

創建中央小部件和佈局

...
self.setGeometry(100, 100, 800, 600) # 設定視窗的位置和大小

# self 參數表示該小部件是自定義類別 MainWindow 的子元件
central_widget = QWidget(self)
central_widget.setFixedSize(800, 600)  # 設定固定尺寸為 800, 600

# 為中央小部件添加一個「垂直佈局管理器」
# 這意味著所有加入到這個布局的元件會垂直排列在 central_widget 中
layout = QVBoxLayout(central_widget)

創建一個 VTK 渲染視窗

...
layout = QVBoxLayout(central_widget)

# 將創建好的 VTK 渲染元件綁定到 central_widget
# 主要是用來建立父子關係和管理記憶體,但這並不會自動管理元件的排列、大小和位置
self.vtk_widget = QVTKRenderWindowInteractor(central_widget)

# 將 VTK 渲染元件添加到「垂直佈局管理器」進行排版與顯示
layout.addWidget(self.vtk_widget)

初始化 VTK 的渲染環境

...
layout.addWidget(self.vtk_widget)

# 建立一個渲染器,負責決定如何將這些元素物件渲染到畫面上
self.renderer = vtk.vtkRenderer() 

# 取得當前的相機
camera = self.renderer.GetActiveCamera()
camera.SetPosition(0, 0, 500) # 設定相機位置 (x, y, z)
camera.SetFocalPoint(0, 0, 0)  # 讓相機看向原點 (模型中心)
camera.SetViewUp(0, 1, 0)  # 保持相機上方朝向 +y 軸

# 取得 self.vtk_widget 內的渲染窗口,然後將剛剛建立的渲染器添加進去
# 添加渲染器後,窗口就會利用該渲染器來繪製 3D 場景
self.vtk_widget.GetRenderWindow().AddRenderer(self.renderer)

# 從渲染窗口中獲取「交互器」
# 交互器負責捕捉使用者的互動(例如滑鼠移動、點擊、鍵盤輸入)
# 並將這些操作轉換為相應的動作(如旋轉、縮放或平移 3D 場景)
self.iren = self.vtk_widget.GetRenderWindow().GetInteractor()
self.iren.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())

初始化並顯示 PyQt 視窗以及 VTK 渲染視窗

...
self.iren.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())

# 初始化並顯示 PyQt 視窗以及 VTK 渲染視窗
self.iren.Initialize()

# 告訴 VTK 開始渲染所有已經添加到渲染器中的內容(如模型、光源、陰影等)。
self.vtk_widget.GetRenderWindow().Render()
self.show()

MainWindow 類別定義「加載 obj 檔」的函式

def load_obj_model(self, filename):
  # 創建 OBJ 讀取器
  reader = vtk.vtkOBJReader()
  reader.SetFileName(filename)
  reader.Update() # 強制讀取器立即讀取並解析指定的 OBJ 檔案

  # VTK 的映射器,它負責將 3D 幾何數據(OBJ 檔案讀取後的結果)...
  # ...轉換成圖形管線(Graphics Pipeline)能夠理解的格式。
  mapper = vtk.vtkPolyDataMapper()
  mapper.SetInputData(reader.GetOutput())

  # 創建演員
  # 一個 vtkActor(演員) 物件,它代表 3D 場景中的一個可視化物件
  actor = vtk.vtkActor()

  # SetMapper 讓 actor 知道它應該渲染哪個 3D 物件
  # mapper 負責告訴 actor 該怎麼畫出 3D 物件
  # actor 只負責在場景中「演出」,但不處理細節。
  actor.SetMapper(mapper)

  # 將演員添加到渲染器
  self.renderer.AddActor(actor)

使用 PyQt5 與 VTK 渲染 3D 物件 ( .obj )

撰寫主程式 ( 使用自定義類別 : MainWindow )

if __name__ == "__main__":
  app = QApplication(sys.argv)
  window = MainWindow()
  window.load_obj_model("OBJ 檔案位置") # 請替換為你自己的 OBJ 檔案位置
  sys.exit(app.exec_())

完整程式碼

'''
vtk version   : 9.4.0
PyQt5 versoin : 5.15.11
'''
import sys
import vtk
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor

# 定義了一個 MainWindow 的類別,並且繼承自 QMainWindow。
class MainWindow(QMainWindow):
    def __init__(self):
        # 使得 MainWindow 類別能夠使用 QMainWindow 的所有屬性和方法。
        super().__init__()
        
        self.setWindowTitle("OBJ 渲染與陰影效果") # 設定視窗的標題
        self.setGeometry(100, 100, 800, 600) # 設定視窗的位置和大小

        # self 參數表示該小部件是自定義類別 MainWindow 的子元件
        central_widget = QWidget(self)
        central_widget.setFixedSize(800, 600)  # 設定固定尺寸為 800, 600

        # 為中央小部件添加一個「垂直佈局管理器」
        # 這意味著所有加入到這個布局的元件會垂直排列在 central_widget 中
        layout = QVBoxLayout(central_widget)

        # 將創建好的 VTK 渲染視窗綁定到 central_widget
        # 主要是用來建立父子關係和管理記憶體,但這並不會自動管理元件的排列、大小和位置
        self.vtk_widget = QVTKRenderWindowInteractor(central_widget)

        # 將 VTK 渲染視窗元件添加到「垂直佈局管理器」進行排版與顯示
        layout.addWidget(self.vtk_widget)

        # 建立一個渲染器,負責決定如何將這些元素物件渲染到畫面上
        self.renderer = vtk.vtkRenderer()

        # 取得當前的相機
        camera = self.renderer.GetActiveCamera()
        camera.SetPosition(0, 0, 500) # 設定相機位置 (x, y, z)
        camera.SetFocalPoint(0, 0, 0)  # 讓相機看向原點 (模型中心)
        camera.SetViewUp(0, 1, 0)  # 保持相機上方朝向 +y 軸

        # 取得 self.vtk_widget 內的渲染窗口,然後將剛剛建立的渲染器添加進去
        # 添加渲染器後,窗口就會利用該渲染器來繪製 3D 場景
        self.vtk_widget.GetRenderWindow().AddRenderer(self.renderer)

        # 從渲染窗口中獲取「交互器」
        # 交互器負責捕捉使用者的互動(例如滑鼠移動、點擊、鍵盤輸入)
        # 並將這些操作轉換為相應的動作(如旋轉、縮放或平移 3D 場景)
        self.iren = self.vtk_widget.GetRenderWindow().GetInteractor()
        self.iren.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())

        # 初始化並顯示 PyQt 視窗以及 VTK 渲染視窗
        self.iren.Initialize()

        # 告訴 VTK 開始渲染所有已經添加到渲染器中的內容(如模型、光源、陰影等)。
        self.vtk_widget.GetRenderWindow().Render()
        self.show()
    
    def load_obj_model(self, filename):
        # 創建 OBJ 讀取器
        reader = vtk.vtkOBJReader()
        reader.SetFileName(filename)
        reader.Update() # 強制讀取器立即讀取並解析指定的 OBJ 檔案

        # VTK 的映射器,它負責將 3D 幾何數據(OBJ 檔案讀取後的結果)...
        # ...轉換成圖形管線(Graphics Pipeline)能夠理解的格式。
        mapper = vtk.vtkPolyDataMapper()
        mapper.SetInputData(reader.GetOutput())

        # 創建演員
        # 一個 vtkActor(演員) 物件,它代表 3D 場景中的一個可視化物件
        actor = vtk.vtkActor()

        # SetMapper 讓 actor 知道它應該渲染哪個 3D 物件
        # mapper 負責告訴 actor 該怎麼畫出 3D 物件
        # actor 只負責在場景中「演出」,但不處理細節。
        actor.SetMapper(mapper)

        # 將演員添加到渲染器
        self.renderer.AddActor(actor)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.load_obj_model("OBJ 檔案位置") # 請替換為你自己的 OBJ 檔案位置
    sys.exit(app.exec_())

結果展示

OBJ 渲染與陰影效果

1 則留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *