笔者学习VPython的时候曾遇到了不少困难,所以在此撰写系列教程与案例,希望能对学习VPython提供相关借鉴。如有任何错误或疑问,欢迎评论指出!
本案例将实现盒子中反弹的球,以下为详细教程:
1.导入VPython库
from vpython import *
2.相机的位置与方位
在场景窗口中,同时按住鼠标左右按键,上下移动鼠标(或者滚动鼠标中键)可以进行缩放场景;按住鼠标右键移动鼠标可以旋转场景。进行这些操作时,场景中的物体并没有改变,只是我们观察物体的方位改变了。我们观察3D场景的工具就是摄像机,我们可以定义摄像机的相关参数:
scene.center
摄像机持续注视的位置。默认为< 0,0,0 >。
scene.forward
指向相机所指向方向的矢量。默认为< 0,0,1 >,向量的大小被忽略。如果scene.forward
改变, 摄像机的位置会移动到使scene.forward
指向scene.center
的位置。
scene.up
垂直于scene.forward
的矢量,默认为<0,1,0>。
scene.camera.pos
摄像机的位置。当 scene.autoscale为True时,摄像机的位置会自动改变。
scene.camera.axis
摄像机指向的当前方向。并且满足
scene.camera.axis = scene.center - scene.camera.pos.
为了明白照相机的位置和坐标轴之间的关系,可以运行以下程序:
from vpython import *
display = canvas(width=300, height=300)
# 建立一个空间直角坐标系 —— 右手系(RGB对应XYZ)
arrow_x = arrow(pos=vector(1,0,0), axis=vector(1,0,0), color=color.red)
arrow_y = arrow(pos=vector(0,1,0), axis=vector(0,1,0), color=color.green)
arrow_z = arrow(pos=vector(0,0,1), axis=vector(0,0,1), color=color.blue)
通过该图我们可以知道,窗口的中心为坐标原点,x轴为从左到右,y轴为从下到上,z轴为从屏幕里到屏幕外,此时的照相机位于z轴正方向上的某点,方向沿着z轴负方向俯视。为方便显示,固下文的重力加速度g设为vector(0, 9.8, 0)。(笔者对摄像头的方向进行了大量调整,未能使初始坐标系变成习惯的角度,故出此下策,将绿色的轴视为z轴)
3.初始变量与画布设置
# 一些初始值的设定
t, dt, r, d = 0, 0.005, 0.1, 6
g = vector(0, 9.8, 0) # 重力加速度
f = 0.9 # 反弹能量系数,1.0表示完全反弹
display = canvas(width=500, height=500, background=color.white)
4.墙体与墙面的构建
这里墙体的构建使用了vpython中的box类型,由于其厚度不能取零,固取一较小值,在实际模拟的物理过程中并未考虑该厚度。
# 创建球体和6个墙面,墙面设置为半透明,以观察球体的运动轨迹
ball = sphere(pos=vector(-5, 0, 0), radius=0.5, color=color.cyan)
wall_right = box(pos=vector(6, 0, 0), size=vector(0.1, 2*d, 2*d), color=color.cyan, opacity=0.02)
wall_left = box(pos=vector(-6, 0, 0), size=vector(0.1, 2*d, 2*d), color=color.cyan, opacity=0.02)
wall_front = box(pos=vector(0, -6, 0), size=vector(2*d, 0.1, 12), color=color.cyan, opacity=0.02)
wall_back = box(pos=vector(0, 6, 0), size=vector(2*d, 0.1, 2*d), color=color.cyan, opacity=0.02)
wall_bottom = box(pos=vector(0, 0, -6), size=vector(2*d, 2*d, 0.1), color=color.cyan, opacity=0.02)
wall_top = box(pos=vector(0, 0, 6), size=vector(2*d, 2*d, 0.1), color=color.cyan, opacity=0.02)
为使可视化更加逼真,还可增加墙面的棱线,增强容器的立体效果。
# 墙面的棱线
bottom = curve(color=color.cyan, radius=r, opacity=0.03)
bottom.append([vector(-d, -d, -d), vector(-d, -d, d), vector(d, -d, d), vector(d, -d, -d), vector(-d, -d, -d)],
opacity=0.03)
top = curve(color=color.cyan, radius=r)
top.append([vector(-d, d, -d), vector(-d, d, d), vector(d, d, d), vector(d, d, -d), vector(-d, d, -d)],
opacity=0.03)
vert1 = curve(color=color.cyan, radius=r, opacity=0.03)
vert2 = curve(color=color.cyan, radius=r, opacity=0.03)
vert3 = curve(color=color.cyan, radius=r, opacity=0.03)
vert4 = curve(color=color.cyan, radius=r, opacity=0.03)
vert1.append([vector(-d, -d, -d), vector(-d, d, -d)], opacity=0.03)
vert2.append([vector(-d, -d, d), vector(-d, d, d)], opacity=0.03)
vert3.append([vector(d, -d, d), vector(d, d, d)], opacity=0.03)
vert4.append([vector(d, -d, -d), vector(d, d, -d)], opacity=0.03)
这里构建棱线的思路是使用curve类型:先将顶部和底部的两个正方形画出,然后画出四条垂直的棱线,则可组成正方体的12条棱线。
5.速度与轨迹的处理
为了使可视化更明显,我们添加一个箭头显示小球的速度方向,箭头的长短表示小球的速度大小。其中第二段代码是写在while循环之中,在循环中不断更新箭头的方向,长短。
ball.velocity = vector(8, 6, 12)
bv = arrow(pos=ball.pos, axis=ball.velocity*0.2, color=color.yellow)
ball.trail = curve(color=vector(1, 0, 0))
# 更新速度箭头的位置和方向
bv.pos = ball.pos
bv.axis = ball.velocity*0.2
ball.trail.append(pos=ball.pos, color=vector(1, 0, 0))
6.更新小球位置与速度
while t < 30:
rate(1/dt)
t += dt
# 重力加速度改变z轴方向的速度,不存在反弹时修改速度
ball.velocity -= g * dt
# 根据速度修改球体的位置
ball.pos += ball.velocity * dt
7.墙面碰撞的处理
处理球体和墙壁的碰撞,x, y, z三个方向的碰撞处理方式相同。这里以x方向为例简要说明一下碰撞处理。当球体的x轴方向的速度为正时,判断球体是否和正方向的墙壁(右墙)相撞,如果相撞的话则将其x轴方向的速度反向,并且乘以碰撞系数模拟能量损失。当球体的x轴方向速度为负时,和左墙进行碰撞检测,重复上述类似操作,对于剩下的y轴和z轴也是如此。
# 速度为正时判断正方向的墙,速度为负时判断负方向的墙
# 处理反弹时需要修正球的位置,使它正好和墙面接触
# 处理左右墙的反弹
if ball.velocity.x * ball.pos.x > 0 and ball.pos.x >= wall_right.pos.x - ball.radius:
ball.velocity.x *= -f
if ball.velocity.x * ball.pos.x > 0 and ball.pos.x <= wall_left.pos.x + ball.radius:
ball.velocity.x *= -f
# 处理前后墙的反弹
if ball.velocity.y * ball.pos.y > 0 and ball.pos.y >= wall_back.pos.y - ball.radius:
ball.velocity.y *= -f
if ball.velocity.y * ball.pos.y > 0 and ball.pos.y <= wall_front.pos.y + ball.radius:
ball.velocity.y *= -f
# 处理上下墙的反弹
if ball.velocity.z * ball.pos.z > 0 and ball.pos.z >= wall_top.pos.z - ball.radius:
ball.velocity.z *= -f
elif ball.velocity.z * ball.pos.z > 0 and ball.pos.z <= wall_bottom.pos.z + ball.radius:
ball.velocity.z *= -f
按照以上思路,即可实现盒子中反弹的球的案例,以下为源代码:
from vpython import *
# 一些初始值的设定
t, dt, r, d = 0, 0.005, 0.1, 6
g = vector(0, 9.8, 0) # 重力加速度
f = 0.9 # 反弹能量系数,1.0表示完全反弹
display = canvas(width=500, height=500, background=color.white)
# 创建球体和6个墙面,墙面设置为半透明,以观察球体的运动轨迹
ball = sphere(pos=vector(-5, 0, 0), radius=0.5, color=color.cyan)
wall_right = box(pos=vector(d, 0, 0), size=vector(0.1, 2*d, 2*d), color=color.cyan, opacity=0.02)
wall_left = box(pos=vector(-6, 0, 0), size=vector(0.1, 2*d, 2*d), color=color.cyan, opacity=0.02)
wall_front = box(pos=vector(0, -6, 0), size=vector(2*d, 0.1, 12), color=color.cyan, opacity=0.02)
wall_back = box(pos=vector(0, 6, 0), size=vector(2*d, 0.1, 2*d), color=color.cyan, opacity=0.02)
wall_bottom = box(pos=vector(0, 0, -6), size=vector(2*d, 2*d, 0.1), color=color.cyan, opacity=0.02)
wall_top = box(pos=vector(0, 0, 6), size=vector(2*d, 2*d, 0.1), color=color.cyan, opacity=0.02)
# 墙面的棱线
bottom = curve(color=color.cyan, radius=r, opacity=0.03)
bottom.append([vector(-d, -d, -d), vector(-d, -d, d), vector(d, -d, d), vector(d, -d, -d), vector(-d, -d, -d)],
opacity=0.03)
top = curve(color=color.cyan, radius=r)
top.append([vector(-d, d, -d), vector(-d, d, d), vector(d, d, d), vector(d, d, -d), vector(-d, d, -d)],
opacity=0.03)
vert1 = curve(color=color.cyan, radius=r, opacity=0.03)
vert2 = curve(color=color.cyan, radius=r, opacity=0.03)
vert3 = curve(color=color.cyan, radius=r, opacity=0.03)
vert4 = curve(color=color.cyan, radius=r, opacity=0.03)
vert1.append([vector(-d, -d, -d), vector(-d, d, -d)], opacity=0.03)
vert2.append([vector(-d, -d, d), vector(-d, d, d)], opacity=0.03)
vert3.append([vector(d, -d, d), vector(d, d, d)], opacity=0.03)
vert4.append([vector(d, -d, -d), vector(d, d, -d)], opacity=0.03)
ball.velocity = vector(8, 6, 12)
bv = arrow(pos=ball.pos, axis=ball.velocity*0.2, color=color.yellow)
ball.trail = curve(color=vector(1, 0, 0))
while t < 30:
rate(1/dt)
t += dt
# 重力加速度改变z轴方向的速度,不存在反弹时修改速度
ball.velocity -= g * dt
# 根据速度修改球体的位置
ball.pos += ball.velocity * dt
# 速度为正时判断正方向的墙,速度为负时判断负方向的墙
# 处理反弹时需要修正球的位置,使它正好和墙面接触
# 处理左右墙的反弹
if ball.velocity.x * ball.pos.x > 0 and ball.pos.x >= wall_right.pos.x - ball.radius:
ball.velocity.x *= -f
if ball.velocity.x * ball.pos.x > 0 and ball.pos.x <= wall_left.pos.x + ball.radius:
ball.velocity.x *= -f
# 处理前后墙的反弹
if ball.velocity.y * ball.pos.y > 0 and ball.pos.y >= wall_back.pos.y - ball.radius:
ball.velocity.y *= -f
if ball.velocity.y * ball.pos.y > 0 and ball.pos.y <= wall_front.pos.y + ball.radius:
ball.velocity.y *= -f
# 处理上下墙的反弹
if ball.velocity.z * ball.pos.z > 0 and ball.pos.z >= wall_top.pos.z - ball.radius:
ball.velocity.z *= -f
elif ball.velocity.z * ball.pos.z > 0 and ball.pos.z <= wall_bottom.pos.z + ball.radius:
ball.velocity.z *= -f
# 更新速度箭头的位置和方向
bv.pos = ball.pos
bv.axis = ball.velocity*0.2
ball.trail.append(pos=ball.pos, color=vector(1, 0, 0))