1. 程式人生 > 其它 >memoのPython和3D那點事

memoのPython和3D那點事

memoのPython和3D那點事

首先來說,python想要搞點啥3D的玩意,是真麻煩。可以撤了。

少俠別走!

雖然很艱難,我還是找到一些體驗不錯的python庫,可以拿來用。
首先,就是這裡。前提是需要有conda。我直接裝了個miniconda。
我當前使用的是igl。用過的都知道,就是好用。載入模型啊(試了一圈模型載入庫,igl的下腳料的功能都那麼好用),計演算法線啊(人家可是用來做計算幾何的庫),就是好用!

然後PyGLM也可以裝一下,python下的glm庫。投影矩陣啊,3維空間變換啊,四元數啊,老好用。

還有moderngl,更簡單,更方便的OpenGL使用方法。

介面的話,我直接用的PySide6,C++用Qt,Python可以無縫銜接一下當然好啦!

上程式碼

程式碼沒有別的含義,無非就是滑鼠右鍵載入個模型,然後會顯示出該模型特點視角下的法線圖,ctrl+s儲存一下。
第一次寫費了點功夫,這也要查,那也要查,怎麼給action連結槽,怎麼載入obj模型,怎麼新增uniform。折騰一遍之後,感覺還是渾身輕鬆。

import sys
from typing import List
import glm
import igl
import numpy as np
import moderngl as mgl
from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import QKeyEvent, QContextMenuEvent, QAction, QImage, QSurfaceFormat
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import QMenu, QFileDialog, QApplication

vertex_shader = """
#version 330
in vec3 position;
in vec3 normal;
out vec3 vColor;
uniform mat4 uProjMat;
uniform mat4 uViewMat;
void main() {
    vColor = vec3((normal + 1.0) * 0.5);
    gl_Position = uProjMat * uViewMat * vec4(position, 1);
}
"""

fragment_shader = """
#version 330
in vec3 vColor;
out vec4 fColor;
void main() {
    fColor = vec4(vColor, 1);
}
"""


class MyGLWidget(QOpenGLWidget):
    vao = None
    menu = None
    loadAction = None

    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        self.loadAction = QAction("Load Obj Model")
        self.loadAction.triggered.connect(self.slotLoadModel) # 給這個action的訊號新增槽。這裡我找了好久。。官方都沒有個示例,文件也不咋地,真是服了
        self.menu = QMenu()
        self.menu.addAction(self.loadAction)

    def initializeGL(self) -> None:
        print("~~~~~ initialize GL ~~~~~")
        gl = mgl.create_context()
        gl.multisample = True
        gl.enable(gl.CULL_FACE)
        gl.enable(gl.DEPTH_TEST)

    def resizeGL(self, w: int, h: int) -> None:
        print("~~~~~ resizing ~~~~~")
        pass

    def paintGL(self) -> None:
        print("~~~~~ painting ~~~~~")
        gl = mgl.create_context()
        gl.clear(0, 0, 0)
        if self.vao:
            self.vao.render()

    def keyReleaseEvent(self, event: QKeyEvent):
        # 按下esc或者q鍵退出
        if event.key() in (Qt.Key_Q, Qt.Key_Escape): 
            self.close()
        # 按下Ctrl+S,儲存當前畫面
        elif event.key() == Qt.Key_S:
            if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
                self.slotCapture()

    def contextMenuEvent(self, event: QContextMenuEvent):
        self.menu.exec(event.globalPos())

    @Slot()
    def slotLoadModel(self):
        try:
            (fileName, selectedFilter) = QFileDialog.getOpenFileName(self, "Select File", "",
                                                                     "Wavefront Obj File (*.obj)")
            if not fileName:
                raise Exception("canceled")

            # 載入obj模型,igl真好用
            (v, _, _, f, *_) = igl.read_obj(filename=fileName, dtype="f4")
            # 繞著x軸旋轉90度。自己看著轉吧
            for i, x in enumerate(v):
                v[i] = glm.rotateX(x, glm.half_pi())
            # 這裡計算了一下包圍盒,然後對模型做了平移加縮放,無非是想左下角為0點,最大尺寸限制到1,1,1
            (bb, _) = igl.bounding_box(v)
            maxLen = np.max(bb[0] - bb[7])
            v = (v - bb[7]) / maxLen

            (bb, _) = igl.bounding_box(v)
            size = bb[0] - bb[7]

            # 計算個法線跟玩似的
            n = igl.per_vertex_normals(v, f, 0)

            self.makeCurrent()
            gl = mgl.create_context()

            vbo = gl.buffer(v.tobytes())
            nbo = gl.buffer(n.tobytes())
            ibo = gl.buffer(f.tobytes())

            program = gl.program(vertex_shader=vertex_shader, fragment_shader=fragment_shader)
            projMat: mgl.Uniform = program["uProjMat"]
            projMat.write(glm.ortho(0, size[0], 0, size[1], -2, 2))
            viewMat: mgl.Uniform = program["uViewMat"]
            viewMat.write(glm.lookAt(glm.vec3(0, 0, 1), glm.vec3(0, 0, 0), glm.vec3(0, 1, 0)))

            self.vao = gl.vertex_array(
                program=program,
                content=[
                    (vbo, "3f", "position"),
                    (nbo, "3f", "normal"),
                ],
                index_buffer=ibo
            )

        except Exception as e:
            print(e)

    @Slot()
    def slotCapture(self):
        try:
            (fileName, _) = QFileDialog.getSaveFileName(self, "Save Capture File", "", "PNG Format (*.png)")
            if not fileName:
                raise Exception("canceled")

            img: QImage = self.grabFramebuffer()
            img.save(fileName)

        except Exception as e:
            print(e)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    
    # 這裡是全域性設定
    fmt = QSurfaceFormat()
    fmt.setVersion(3, 3) # 這裡有意思了。在win10下,我加了個這,multisample就沒了。然而沒有這個,在mac下會報錯(抗鋸齒也是沒有)。娘希匹
    fmt.setProfile(QSurfaceFormat.CoreProfile)
    fmt.setSamples(4)
    fmt.setDepthBufferSize(24)
    fmt.setStencilBufferSize(8)
    QSurfaceFormat.setDefaultFormat(fmt)

    widget = MyGLWidget(None)
    widget.resize(512, 512)
    widget.show()

    sys.exit(app.exec())

我在win10和big sur上都測試了,還不錯。
最後,CLion還能搞搞python,還真就比vscode好用。