今天开始记录一下整个魔方的具体绘制过程,虽然没有用到太多的很深的技术,但是做这个魔方的过程,确实是一个自我学习的过程,涉及到每一个细节,算法,然后到每一个小功能的实现,从在脑瓜里的一个偶然的想法,到最终实现它,从无到有,都很有成就感。
首要问题是绘制魔方的容器。看了一些教程,好多都是用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 循环,搞一搞就好了