memoのPython和3D那点事

memoのPython和3D那点事

首先来说,python想要搞点啥3D的玩意,是真麻烦。可以撤了。

少侠别走!

虽然很艰难,我还是找到一些体验不错的python库,可以拿来用。
首先,就是这里。前提是需要有conda。我直接装了个miniconda。
我当前使用的是igl。用过的都知道,就是好用。加载模型啊(试了一圈模型加载库,igl的下脚料的功能都那么好用),计算法线啊(人家可是用来做计算几何的库),就是好用!

然后PyGLM也可以装一下,python下的glm库。投影矩阵啊,3维空间变换啊,四元数啊,老好用。

还有moderngl,更简单,更方便的OpenGL使用方法。

界面的话,我直接用的PySide6,C++用Qt,Python可以无缝衔接一下当然好啦!
备注一下,关于PySide6其实有官方的示例代码,安装好了之后,就在${YourPythonDir}Libsite-packagesPySide6examples,用vscode也好,用什么也好,直接用啥查啥,方便。之前查action连接槽,找了好久。。

上代码

代码没有别的含义,无非就是鼠标右键加载个模型,然后会显示出该模型特点视角下的法线图,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)
        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()
    # 这里有意思了。在win10下,我加了个这,multisample就没了。
    # 然而没有这个,在mac下会报错(抗锯齿也是没有)。娘希匹
    fmt.setVersion(3, 3) 
    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好用。

原文地址:https://www.cnblogs.com/daiday/p/14872894.html