python + Qt5 + opengl 绘制魔方(一)

今天开始记录一下整个魔方的具体绘制过程,虽然没有用到太多的很深的技术,但是做这个魔方的过程,确实是一个自我学习的过程,涉及到每一个细节,算法,然后到每一个小功能的实现,从在脑瓜里的一个偶然的想法,到最终实现它,从无到有,都很有成就感。

首要问题是绘制魔方的容器。看了一些教程,好多都是用OpenGL的GLUT库在一个独立的窗口中显示模型,无法添加一些按钮等进行交互,个人感觉不太方便,于是考虑用QT Designer设计加载一个Widget进行显示。

一、绘图环境准备

python的环境设置:pycharm + pyQT5,方法网上很多,可参考

3D图形库,可用的有不少,根据自己喜好即可,可安装OpenGL库

pip install pyopengl

pip直接安装会默认32位的,如果64位系统会出现错误,先下载对应的whl,然后安装

pip install PyOpenGL-3.1.6-cp38-cp38-win_amd64.whl

创建一个RubikCube_Widget 类,从 QtWidgets.QOpenGLWidget继承

from PyQt5 import QtWidgets
from OpenGL.GL import *
from OpenGL.GLU import *

class RubikCube_Widget(QtWidgets.QOpenGLWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

 在QT Designer中添加一个Widget,并提升为RubikCube_Widget

 UI文件通过PY-UIC直接生成py文件,创建主窗口类

from Rubik_Cube import Ui_mainWindow

class RubikCubeWindow(QtWidgets.QMainWindow, Ui_mainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    rubikcubeFrom = RubikCubeWindow()
    rubikcubeFrom.show()
    app.exec_()

 然后对整个绘图空间进行初始化设置。基本的OpenGL操作,不过多解释,详细参考OpenGL的基础,重写RubikCube_Widget 类的initializeGL函数

    def initializeGL(self):
        # 初始化画布
        glClearColor(0.0, 0.0, 0.0, 1.0)  # 设置画布背景色
        glEnable(GL_DEPTH_TEST)           # 开启深度测试
        glDepthFunc(GL_LEQUAL)           # 设置深度测试函数

        # 清除屏幕 深度缓存
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        # 设置投影
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()

        k = 400 / 400
        glFrustum(-0.5*k, 0.5*k, -0.5, 0.5, 1.0, 20.0)      # 透视投影

        # 设置缩放
        glScale(1.0, 1.0, 1.0)

        # 设置视点
        gluLookAt(
            0.9, 1.9, 0.0,
            0.0, 0.0, 0.0,
            0.0, 1.0, 0.0
        )

        # 设置视口
        glViewport(0, 0, 400, 400)

 运行一下,如下图,已经创建了一个OpenGL的绘图窗口。

接下来我们就可以在以上绘图窗口中进行自由绘图了。

二、从画一个方块开始

一个魔方的基本元素就是一个个小方块,所以首要问题是要解决一个方块的绘制。在画方块之前,为了看一下参考坐标系,我们可以在窗口中画出x,y,z坐标轴看一下效果。

定义一个画坐标轴的函数,并重写RubikCube_Widget 类的paintGL函数

    def drawAxis(self):
        glBegin(GL_LINES)               # 开始绘制
        # x轴
        glColor4f(1.0, 0.0, 0.0, 1.0)   # 红色不透明
        glVertex3f(-0.5, 0.0, 0.0)      # 设置x轴起始点 +
        glVertex3f(0.5, 0.0, 0.0)       # 设置x轴结束点  -

        # y轴
        glColor4f(0.0, 1.0, 0.0, 1.0)   # 绿色不透明
        glVertex3f(0.0, -0.5, 0.0)      # 设置x轴起始点 +
        glVertex3f(0.0, 0.5, 0.0)       # 设置x轴结束点  -

        # z轴
        glColor4f(0.0, 0.0, 1.0, 1.0)   # 蓝色不透明
        glVertex3f(0.0, 0.0, -0.5)      # 设置x轴起始点 +
        glVertex3f(0.0, 0.0, 0.5)       # 设置x轴结束点  -

        glEnd()                         # 结束绘制

    def paintGL(self):
        self.drawAxis()

 运行一下,如下图,可以看到,我们的参考坐标系正在屏幕的正中心,也就是我们需要绘制的魔方的中心点位置:

 接下来可以准备画一个方块(正方体),并将它显示在坐标系的正中心。首先我们需要定义方块的顶点坐标,然后通过调用OpenGL的面绘图函数,重复绘制方块的6个面就OK了。顶点定义的方式可自行参考相关资料,为了处理方便,定义了一系列单位坐标的顶点,然后乘以边长的一半 L/2,即可绘制出边长为L的方块。

    def drawCube(self):
        L = 0.2 # 边长
        # 定义6个面的顶点的坐标集,每个面4个点
        vertex_cube = [
            [[-1, -1,  1], [ 1, -1,  1], [ 1,  1,  1], [-1,  1,  1]],  # 0 F
            [[-1, -1, -1], [-1,  1, -1], [ 1,  1, -1], [ 1, -1, -1]],  # 1 B
            [[-1,  1, -1], [-1,  1,  1], [ 1,  1,  1], [ 1,  1, -1]],  # 2 U
            [[-1, -1, -1], [ 1, -1, -1], [ 1, -1,  1], [-1, -1,  1]],  # 3 D
            [[ 1, -1, -1], [ 1,  1, -1], [ 1,  1,  1], [ 1, -1,  1]],  # 4 R
            [[-1, -1, -1], [-1, -1,  1], [-1,  1,  1], [-1,  1, -1]]   # 5 L
        ]
        for i in range(6):
            glBegin(GL_QUADS)
            for j in range(4):
                glVertex3f(vertex_cube[i][j][0] * L/2,
                           vertex_cube[i][j][1] * L/2,
                           vertex_cube[i][j][2] * L/2)
            glEnd()

运行一下如图,一个方块就画好了。但是几个面都是蓝色,看起来很不爽,我们需要对它进行纹理贴图处理。

 方块的贴图,OPenGL也给出了很简单的解决方案。

首先需要自己准备 6 张魔方的6面【R,G,B,Y,O,W】颜色材质贴图,.bmp格式的。加载后进行纹理绑定即可。同时修改drawCube函数

此处需要用到PIL库的Image.open方法,PIL库自行pip安装 即可

# from PIL import Image  需要

  
    def LoadTexture(self):
        # 提前准备好的6个图片
        imgFiles = ["texure\\" + str(i) + '.bmp' for i in range(1, 7)]
        for i in range(6):
            img = Image.open(imgFiles[i])
            width, height = img.size
            img = img.tobytes('raw', 'RGBX', 0, -1)
            glGenTextures(2)
            glBindTexture(GL_TEXTURE_2D, i)
            glTexImage2D(GL_TEXTURE_2D, 0, 4,
                         width, height, 0, GL_RGBA,
                         GL_UNSIGNED_BYTE, img)
            glTexParameterf(GL_TEXTURE_2D,
                            GL_TEXTURE_WRAP_S, GL_CLAMP)
            glTexParameterf(GL_TEXTURE_2D,
                            GL_TEXTURE_WRAP_T, GL_CLAMP)
            glTexParameterf(GL_TEXTURE_2D,
                            GL_TEXTURE_WRAP_S, GL_REPEAT)
            glTexParameterf(GL_TEXTURE_2D,
                            GL_TEXTURE_WRAP_T, GL_REPEAT)
            glTexParameterf(GL_TEXTURE_2D,
                            GL_TEXTURE_MAG_FILTER, GL_NEAREST)
            glTexParameterf(GL_TEXTURE_2D,
                            GL_TEXTURE_MIN_FILTER, GL_NEAREST)
            glTexEnvf(GL_TEXTURE_ENV,
                      GL_TEXTURE_ENV_MODE, GL_DECAL)

    def drawCube(self):
        L = 0.4 # 边长
        # 定义6个面的顶点的坐标集,每个面4个点
        vertex_cube = [
            [[-1, -1,  1], [ 1, -1,  1], [ 1,  1,  1], [-1,  1,  1]],  # 0 F
            [[-1, -1, -1], [-1,  1, -1], [ 1,  1, -1], [ 1, -1, -1]],  # 1 B
            [[-1,  1, -1], [-1,  1,  1], [ 1,  1,  1], [ 1,  1, -1]],  # 2 U
            [[-1, -1, -1], [ 1, -1, -1], [ 1, -1,  1], [-1, -1,  1]],  # 3 D
            [[ 1, -1, -1], [ 1,  1, -1], [ 1,  1,  1], [ 1, -1,  1]],  # 4 R
            [[-1, -1, -1], [-1, -1,  1], [-1,  1,  1], [-1,  1, -1]]   # 5 L
        ]

        # 6各面对应的纹理坐标
        cube_texture = [
            [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
            [[1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
            [[0.0, 1.0], [0.0, 0.0], [1.0, 0.0], [1.0, 1.0]],
            [[1.0, 1.0], [0.0, 1.0], [0.0, 0.0], [1.0, 0.0]],
            [[1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
            [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
        
        glEnable(GL_TEXTURE_2D)

        for i in range(6):
            glBindTexture(GL_TEXTURE_2D, i)
            glBegin(GL_QUADS)
            for j in range(4):
                glTexCoord2f(cube_texture[i][j][0],
                             cube_texture[i][j][1])
                glVertex3f(vertex_cube[i][j][0] * L/2,
                           vertex_cube[i][j][1] * L/2,
                           vertex_cube[i][j][2] * L/2)
            glEnd()

尝试运行一下,可以得到如下图的一个小方块。

三、画出整个魔方

接下来需要将上面的单个方块复制 N个 ,排列起来,就可以得到一个静止的魔方。如图所示

如果想更精确一点,也可以将遮挡面都定义成一种颜色,比如说灰色材质。这样在转动的过程中感觉更真实一点。代码就不贴了,一顿for 循环,搞一搞就好了

  • 7
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值