几年前的元宵节前夕,我曾经写过一篇使用WxGL绘制3D走马灯的文章,受到很多读者的关注。今年的元宵节又要到了,这几天陆续收到许多读者私信,反映说那篇制作花灯的博文中的代码无法运行了,希望能尽快更新代码。
的确,由于WxGL的版本一直在不断更新,我以前写的很多3D的例子几乎都不可用了。正好借着今晚WxGL发布0.9.7版的热情,先更新一下3D花灯的代码吧。
如下所示,还可以加上自己喜欢的图案、文字等。
一台安装了Python环境的电脑,Python环境需要安装以下模块。
如果没有上述模块,请参考下面的命令安装。
pip install numpy
pip install pillow
pip install wxgl
NumPy和pillow是Python旗下最常用的科学计算库和图像处理库,属于常用模块。WxGL是一个基于PyOpenGL的跨平台三维数据绘图工具包,提供类似Matplotlib风格的应用方式。WxGL也可以集成到wxPython和PyQt6中实现更多的功能和控制。
花灯制作工序非常简单,不足30行代码,可以直接在Python IDLE中以交互方式逐行执行。
>>> import numpy as np
>>> from PIL import Image
>>> import wxgl
>>> image_file = '/home/xufive/MyCode/GitHub/wxgl/example/res/bull.png'
>>> w_im, h_im = Image.open(image_file).size
>>> w_im, h_im
(942, 400)
image_file是花灯纸的存储路径,请据实修改。使用Image.open()打开这个图像文件,得到图像分辨率为400像素高、942像素宽。
纸长942像素,卷成圆筒,半径就是149.9像素,如果把半径视为1个单位,则高度400像素相当于2.668个单位。
>>> r, h = 1, 2*np.pi*h_im/w_im
>>> r, h
(1, 2.6680192387174464)
绘制3D模型前,我们得先定义好空间坐标系。通常三维空间坐标系有两种不同的习惯,一是使用y轴作为高度轴,二是使用z轴作为高度轴。两种坐标系都遵从右手定则,因此本质上并没有差别。我们就选择使用y轴作为高度轴,那么花灯圆柱的上端面圆心坐标为(0, h/2, 0),下端面圆心坐标为(0, -h/2, 0),圆柱半径为1。wxgl提供了圆柱绘制函数cylinder,可以方便地绘制圆柱。
>>> app = wxgl.App()
>>> app.cylinder((0,h/2,0), (0,-h/2,0), r, texture=image_file)
>>> app.show()
这里将花灯纸的文件路径传给texture参数,就实现了3D纹理贴图。如果绘制单色的圆柱,则使用color参数传入颜色即可。
上面这个圆柱默认使用户外光照效果,看起来不够亮。我们可以自定义一个灯光,让花灯看起来足够亮。
>>> ight = wxgl.BaseLight()
>>> app = wxgl.App()
>>> app.cylinder((0,h/2,0), (0,-h/2,0), r, texture=image_file light=light)
>>> app.show()
WxGL的模型函数接收名为transform的参数以实现模型的旋转、平移、缩放。该参数是一个函数,以时间t(单位毫秒)为自变量,返回返回模型旋转、平移、缩放的任意组合序列,该序列元素可能的类型:
下面的代码,使用lambda函数让花灯以20°/s的角速度绕y逆时针轴旋转。
>>> tf = lambda t : ((0, 1, 0, (0.02*t)%360), )
>>> app = wxgl.App()
>>> app.cylinder((0,h/2,0), (0,-h/2,0), r, texture=image_file, transform=tf, light=light)
>>> app.show()
走马灯之所以能够转动,是因为里面有蜡烛加热形成上升气流,推动顶部的叶轮旋转,从而带动花灯旋转。这里的叶轮仅仅是个样子,叶轮旋转同样使用上面的lambda函数。
>>> theta = np.linspace(0, 2*np.pi, 18, endpoint=False)
>>> x = r*np.cos(theta)
>>> z = r*np.sin(theta)
>>> x[2::3] = x[1::3]
>>> x[1::3] = 0
>>> z[2::3] = z[1::3]
>>> z[1::3] = 0
>>> y = np.ones(18) * h/2 * 0.9
>>> vs = np.stack((x,y,z), axis=1)
>>> app = wxgl.App()
>>> app.cylinder((0,h/2,0), (0,-h/2,0), r, texture=image_file, transform=tf, light=light)
>>> app.surface(vs, color='#C03000', method='isolate', alpha=0.8, transform=tf)
>>> app.show()
叶轮设计有6片,用三角形模拟,颜色深红,透明度0.8,整体效果略显粗糙了一点。
照明灯用一个白色的圆球表示,提系则是黄色的一条直线,兼做照明灯的电源线。WxGL提供了圆球绘制函数isosphere和线段绘制函数line,使用风格与前面的圆柱函数保持一致。需要说明的,isosphere函数通过迭代正十二面体得到球体,参数iterate为迭代次数。这里选择1次迭代,故意让球体看起来不够光滑,是为了能够看清球的旋转。
>>> app = wxgl.App()
>>> app.cylinder((0,h/2,0), (0,-h/2,0), r, texture=image_file, transform=tf, light=light)
>>> app.surface(vs, color='#C03000', method='isolate', alpha=0.8, transform=tf)
>>> app.isosphere((0,0,0), 0.3, color='#FFFFFF', iterate=1, transform=tf)
>>> app.line([[0,0,0], [0,0.8*h,0]], color='#A0A000', width=3.0)
>>> app.show()
import numpy as np
from PIL import Image
import wxgl
image_file = 'res/bull.png' # 制作走马灯的图片
w_im, h_im = Image.open(image_file).size # 获取图片宽度和高度
r, h = 1, 2*np.pi*h_im/w_im # 计算圆柱形花灯的直径和高度
tf = lambda t : ((0, 1, 0, (0.02*t)%360), ) # 走马灯旋转函数,以20°/s的角速度绕y逆时针轴旋转
light = wxgl.BaseLight()
theta = np.linspace(0, 2*np.pi, 18, endpoint=False)
x = r*np.cos(theta)
z = r*np.sin(theta)
x[2::3] = x[1::3]
x[1::3] = 0
z[2::3] = z[1::3]
z[1::3] = 0
y = np.ones(18) * h/2 * 0.9
vs = np.stack((x,y,z), axis=1)
app = wxgl.App(elev=30) # 设置高度角30°
app.title('元宵节的走马灯')
app.cylinder((0,h/2,0), (0,-h/2,0), r, texture=image_file, transform=tf, light=light)
app.isosphere((0,0,0), 0.3, color='#FFFFFF', iterate=1, transform=tf)
app.surface(vs, color='#C03000', method='isolate', alpha=0.8, transform=tf)
app.line([[0,0,0], [0,0.8*h,0]], color='#A0A000', width=3.0)
app.show()
最终效果如下图。