# Vpython 物理模擬 : 撞球桌上的彈性碰撞
> 作者 : 黃柏豪
> 第一版 : 2022/11/16
> 第二版 : 2023/5/18
## 前言
此次專案是由中和高中112應屆畢業生黃柏豪所製作,目的在於使用物理課本中的彈性碰撞公式,去模擬現實情況下的撞球桌的第一桿衝球情況。
## 程式碼一覽
:::info
親愛的讀者:
這段落的程式碼可直接複製並運行,歡迎於自己的電腦執行此專案。
專案中有多處註解,其意義可參閱程式碼解說區的講解。
感謝您的觀看。
:::
```python=
"""
物理期末作業: 撞球桌模擬
ver.1 2022/11/16 ( 參數設定, 畫面設定, 一維碰撞測試)
ver.2 2022/11/17 ( 設定三個球體, 三維碰撞測試, 牆壁碰撞測試, 完美狀態下 )
ver.3 2022/11/24 ( 參閱行星運動的程式,聽取老師的教誨修程式 )
ver.4 2022/12/8 ( 修改位置的設定,設定顏色變數 )
ver.5 2022/12/11 ( 由老師建議下,新增類似摩擦力的程式碼 )
ver.6 2022/12/15 ( 查詢撞球最快球速 120 km/hr , 新增撞牆損耗 , 測試圓柱體 )
ver.7 2022/12/17 ( 排版刪減, 新增撞球洞口以及進球消失 )
作者:黃柏豪
"""
from vpython import *
"""
1. 參數設定, 設定變數及初始值
"""
# 場地的參數
length = 2540 # 單位為mm,內框尺寸
width = 1270 # 單位為mm,內框尺寸
height = 10 # 因不影響物理模擬,所以假設為10mm
# 球的參數
r = 57.15 # 單位為mm,球的直徑
weight = 170 # 單位為g , 球的重量
v1 = vec(120000000/3600, 0, 0) # 母球的衝球速度, 參考史上最快的120km/hr
v2 = vec(0,0,0) # 球堆的靜止速度
x0 = 0.2*length # 球堆的初始位置
z0 = 0 # 球堆的初始位置
# 洞口的參數
r2 = 114.3 # 單位為mm, 外袋的直徑
r3 = 127 # 單位為mm, 內袋的直徑, 內外應相差12.7mm
# 時間參數
t, dt = 0, 0.001 # 時間, 時間間隔
# 位置的參數
x = x0 # 創建位置, 此為球堆的, 之後會與洞口共用
z = z0 # 創建位置, 此為球堆的, 之後會與洞口共用
num = 0 # 位置的判斷值
r0 = 0 # 洞口直徑的參數調整
"""
2. 畫面設定
"""
# 產生動畫視窗、桌面
scene = canvas(title="撞球桌模擬", width=600, height=600, x=0, y=0, background=color.black)
scene.camera.pos = vec(0, width*2, length*0.4)
scene.camera.axis = vec(0, -width*4, -length)
table = box(pos=vec(0, -(height/2), 0), size=vec(length, height, width), color=color.green)
# 產生牆面
wall1 = box(pos=vec(0, r, -(width/2)-10), size=vec(length, 2*r, 20), color=color.cyan)
wall2 = box(pos=vec(0, r, (width/2)+10), size=vec(length, 2*r, 20), color=color.cyan)
wall3 = box(pos=vec((length/2)+10, r, 0), size=vec(20, 2*r, width), color=color.cyan)
wall4 = box(pos=vec(-(length/2)-10, r, 0), size=vec(20, 2*r, width), color=color.cyan)
# 計算撞後速度的函式
def af_col_v(v1, v2, x1, x2):
v1_prime = v1 + dot((v2 - v1), (x1 - x2)) / mag2(x1 - x2) * (x1 - x2)
v2_prime = v2 + dot((v1 - v2), (x2 - x1)) / mag2(x2 - x1) * (x2 - x1)
return (v1_prime, v2_prime)
"""
3. 物體創建部分
"""
# 產生母球
b1 = sphere(pos=vec(-0.2*length, r/2, 0), m=weight, v=v1, radius=r/2, color=color.white, make_trail=False)
# 創建球的串列
names_balls = ["b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "b10" ,"b11"]
balls = []
balls.append(b1)
# 設定顏色
colors = [[1,0,0], [0,1,0], [0,0,1], [0.4,0.2,0.6], [1,1,0], [1,0.6,0], [0,1,1], [1,0,1], [0,0,0], [0.5,0.5,0.5]]
cnt = 0
# 產生靜止的球堆
for name in names_balls:
balls.append(sphere(pos=vec(x, r/2, z), radius = r/2, m = weight, color=vec(colors[cnt][0],colors[cnt][1],colors[cnt][2]), v=vec(0, 0, 0), a=vec(0, 0, 0)))
x += r
z += r/2
cnt +=1
if( x > 0.2*length+3*r ):
num += 1
x = x0 + (num*r)
z = z0 - (num*r/2)
# 創建洞口
names_holes = ['R', "C1", 'L', 'M', 'R', "C2", 'L']
holes = []
x = wall3.pos.x
z = wall1.pos.z
num = 0
for name in names_holes:
# 略過一列
if( name == 'M' ):
x = wall3.pos.x
z = wall2.pos.z
continue
else:
if( name == "C1" or name == "C2" ): # 因為中間的洞有點太大,所以我將其移動r3/2
r0 = r3
if ( name == "C1" ):
holes.append(cylinder(pos=vec(x, -(height/2), z - r3/2), axis=vec(0, 1, 0), radius=r0, length = 6, color=color.black, opacity=1, v=vec(0,0,0)))
else:
holes.append(cylinder(pos=vec(x, -(height/2), z + r3/2), axis=vec(0, 1, 0), radius=r0, length = 6, color=color.black, opacity=1, v=vec(0,0,0)))
else:
r0 = r2
holes.append(cylinder(pos=vec(x, -(height/2), z), axis=vec(0, 1, 0), radius=r0, length = 6, color=color.black, opacity=1, v=vec(0,0,0)))
num += 1
if( num == 3 ):
num = 0
x = wall3.pos.x
z += (width/2)+10
else:
x -= (length/2)+10
"""
4. 球體運動部分
"""
# 由於母球初速度極快,容易在網頁跑出來前就已經執行完動作,因此設定延遲方便觀察撞擊前的狀態
sleep(5)
while True:
rate(1000)
# 用 for 迴圈自動跑完所有球體的資料
for ball in balls:
if ( ball.v.mag >= 0.000001 ):
ball.a = -100*ball.v.norm()
ball.v += ball.a*dt
ball.pos += ball.v*dt
# 偵測球與球之間的碰撞
for i in balls:
for j in balls:
if( i.pos == j.pos ):
continue
if( i.opacity == 0 or j.opacity == 0 ):
continue
if( mag(i.pos - j.pos) <= r and dot((i.pos - j.pos), (i.v - j.v)) <=0 ):
i.v, j.v = af_col_v(i.v, j.v, i.pos, j.pos)
# 偵測球與洞口之間的距離
for i in balls:
for j in holes:
if( mag(i.pos - j.pos) <= (j.radius/2 + r + 10* sqrt(2)) and dot((i.pos - j.pos), (i.v - j.v)) <=0 ):
i.v = vec(0,0,0)
i.opacity = 0
# 偵測球與牆之間的碰撞
for i in balls:
if( i.pos.z - r/2 <= wall1.pos.z+10 and i.v.z < 0 ):
i.v.z = -i.v.z * 0.8
elif( i.pos.z + r/2 >= wall2.pos.z-10 and i.v.z > 0 ):
i.v.z = -i.v.z * 0.8
if( i.pos.x + r/2 >= wall3.pos.x-10 and i.v.x > 0 ):
i.v.x = -i.v.x * 0.8
elif( i.pos.x - r/2 <= wall4.pos.x+10 and i.v.x < 0 ):
i.v.x = -i.v.x * 0.8
# 更新時間
t += dt
```
## 程式碼解說
:::warning
親愛的讀者:
以下程式碼解說中的片段,會有筆者自行的分段處理以及裁切排版。
**!若直接引用以下的程式碼將會報錯!**
**若想看到運行的整體狀態,請直接複製上文提供的完整程式碼來執行。**
謝謝您的諒解。
:::
### 撞球檯架構
```python=
# 場地的參數
length = 2540 # 單位為mm,內框尺寸
width = 1270 # 單位為mm,內框尺寸
height = 10 # 因不影響物理模擬,所以假設為10mm
# 球的參數
r = 57.15 # 單位為mm,球的直徑
weight = 170 # 單位為g , 球的重量
v1 = vec(120000000/3600, 0, 0) # 母球的衝球速度, 參考史上最快的120km/hr
v2 = vec(0,0,0) # 球堆的靜止速度
x0 = 0.2*length # 球堆的初始位置
z0 = 0 # 球堆的初始位置
# 洞口的參數
r2 = 114.3 # 單位為mm, 外袋的直徑
r3 = 127 # 單位為mm, 內袋的直徑, 內外應相差12.7mm
# 時間參數
t, dt = 0, 0.001 # 時間, 時間間隔
# 位置的參數
x = x0 # 創建位置, 此為球堆的, 之後會與洞口共用
z = z0 # 創建位置, 此為球堆的, 之後會與洞口共用
num = 0 # 位置的判斷值
r0 = 0 # 洞口直徑的參數調整
```
撞球檯的尺寸設定均參考為美國司諾克玩法。
使用的比例尺基準為 「毫米 (mm)」這能最大限度的讓我們觀察到視野下的運動狀態。
### 彈性碰撞公式設定
```python=
# 計算撞後速度的函式
def af_col_v(v1, v2, x1, x2):
v1_prime = v1 + dot((v2 - v1), (x1 - x2)) / mag2(x1 - x2) * (x1 - x2)
v2_prime = v2 + dot((v1 - v2), (x2 - x1)) / mag2(x2 - x1) * (x2 - x1)
return (v1_prime, v2_prime)
```
原彈性碰撞公式參考於物理課本上的公式。
程式碼則參考 王一哲教學網站上「三維碰撞」筆記,網址連結在本文文末。
### 畫面與場地生成
```python=
# 產生動畫視窗、桌面
scene = canvas(title="撞球桌模擬", width=600, height=600, x=0, y=0, background=color.black)
scene.camera.pos = vec(0, width*2, length*0.4)
scene.camera.axis = vec(0, -width*4, -length)
table = box(pos=vec(0, -(height/2), 0), size=vec(length, height, width), color=color.green)
# 產生牆面
wall1 = box(pos=vec(0, r, -(width/2)-10), size=vec(length, 2*r, 20), color=color.cyan)
wall2 = box(pos=vec(0, r, (width/2)+10), size=vec(length, 2*r, 20), color=color.cyan)
wall3 = box(pos=vec((length/2)+10, r, 0), size=vec(20, 2*r, width), color=color.cyan)
wall4 = box(pos=vec(-(length/2)-10, r, 0), size=vec(20, 2*r, width), color=color.cyan)
```
利用此前設定過的參數,來生成場地。
鏡頭則位於斜上方,面朝撞球桌的中心點。
### 母球 - 子球生成
```python=
# 產生母球
b1 = sphere(pos=vec(-0.2*length, r/2, 0), m=weight, v=v1, radius=r/2, color=color.white, make_trail=False)
# 創建球的串列
names_balls = ["b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "b10" ,"b11"]
balls = []
balls.append(b1)
# 設定顏色
colors = [[1,0,0], [0,1,0], [0,0,1], [0.4,0.2,0.6], [1,1,0], [1,0.6,0], [0,1,1], [1,0,1], [0,0,0], [0.5,0.5,0.5]]
cnt = 0
# 產生靜止的球堆
for name in names_balls:
balls.append(sphere(pos=vec(x, r/2, z), radius = r/2, m = weight, color=vec(colors[cnt][0],colors[cnt][1],colors[cnt][2]), v=vec(0, 0, 0), a=vec(0, 0, 0)))
x += r
z += r/2
cnt +=1
if( x > 0.2*length+3*r ):
num += 1
x = x0 + (num*r)
z = z0 - (num*r/2)
```
母球的部分按照參數設定並加入球的陣列,比較簡單沒什麼好說。
子球的部分因為必須讓每顆子球變成不同的顏色以方便觀察運動的情形,所以我額外創建一個 color 的參數陣列。使子球生成時能按照 color 陣列參數的順序來展示不同的顏色。
而子球的生成是先訂定原座標並以斜左後方為方向生成,當生成到該序列最後一顆時,將原作標向斜右後方改變,並繼續生成。
### 洞口生成
```python=
# 創建洞口
names_holes = ['R', "C1", 'L', 'M', 'R', "C2", 'L']
holes = []
x = wall3.pos.x
z = wall1.pos.z
num = 0
for name in names_holes:
# 略過一列
if( name == 'M' ):
x = wall3.pos.x
z = wall2.pos.z
continue
else:
if( name == "C1" or name == "C2" ): # 因為中間的洞有點太大,所以我將其移動r3/2
r0 = r3
if ( name == "C1" ):
holes.append(cylinder(pos=vec(x, -(height/2), z - r3/2), axis=vec(0, 1, 0), radius=r0, length = 6, color=color.black, opacity=1, v=vec(0,0,0)))
else:
holes.append(cylinder(pos=vec(x, -(height/2), z + r3/2), axis=vec(0, 1, 0), radius=r0, length = 6, color=color.black, opacity=1, v=vec(0,0,0)))
else:
r0 = r2
holes.append(cylinder(pos=vec(x, -(height/2), z), axis=vec(0, 1, 0), radius=r0, length = 6, color=color.black, opacity=1, v=vec(0,0,0)))
num += 1
if( num == 3 ):
num = 0
x = wall3.pos.x
z += (width/2)+10
else:
x -= (length/2)+10
```
和生成球體一樣,我使用陣列來生成,在之後的碰撞判斷上會比較省時。
我將洞口編號為 R、L、C1、C2、M 分別代表洞口的 XZ 軸位置,R、L、C 分別代表同側牆面上洞口的位置位於右、左、中間,而 M 則代表位於撞球檯的正中間故忽略其生成。
此程式碼作動時,會先從右上角向左依序生成。
### 球運動狀態的更新
```python=
# 由於母球初速度極快,容易在網頁跑出來前就已經執行完動作,因此設定延遲方便觀察撞擊前的狀態
sleep(5)
while True:
rate(1000)
# 用 for 迴圈自動跑完所有球體的資料
for ball in balls:
if ( ball.v.mag >= 0.000001 ):
ball.a = -100*ball.v.norm()
ball.v += ball.a*dt
ball.pos += ball.v*dt
# 更新時間
t += dt
```
在這段程式碼中我利用加速度公式與自訂摩擦力來改變球的速度。
### 球 - 球碰撞
```python=
# 偵測球與球之間的碰撞
for i in balls:
for j in balls:
if( i.pos == j.pos ):
continue
if( i.opacity == 0 or j.opacity == 0 ):
continue
if( mag(i.pos - j.pos) <= r and dot((i.pos - j.pos), (i.v - j.v)) <=0 ):
i.v, j.v = af_col_v(i.v, j.v, i.pos, j.pos)
```
此前我們將所有的球創建成一個陣列,這段程式碼用於一個一個檢測陣列裡球的狀態。
第一個 if 利用 i 與 j 的位置來判斷是不是同一顆球,若是則無需判斷直接跳下一個。
第二個 if 利用 i 與 j 的透明度來判斷其一是否已進洞口,若是則無須判斷直接跳入下一個。
第三個 if 利用 i 與 j 圓心距離與速度來判斷是否發生碰撞,若是則用彈性碰撞公式改變速度與方向。
### 球 - 牆面碰撞
```python=
# 偵測球與牆之間的碰撞
for i in balls:
if( i.pos.z - r/2 <= wall1.pos.z+10 and i.v.z < 0 ):
i.v.z = -i.v.z * 0.8
elif( i.pos.z + r/2 >= wall2.pos.z-10 and i.v.z > 0 ):
i.v.z = -i.v.z * 0.8
if( i.pos.x + r/2 >= wall3.pos.x-10 and i.v.x > 0 ):
i.v.x = -i.v.x * 0.8
elif( i.pos.x - r/2 <= wall4.pos.x+10 and i.v.x < 0 ):
i.v.x = -i.v.x * 0.8
```
此前我們將所有的球創建成一個陣列,這段程式碼用於一個一個檢測陣列裡球的狀態。
牆面與球的碰撞比較簡單,只要利用四面牆的 pos 劃出一道邊界線,並透過判斷球的 pos 是否超出此邊界線就可以完成球對牆面的碰撞判定。
而這裡我採用的是將速度方向設為相反,並且基於前刻的速度量值 * 0.8 造成減速的效果。
### 球 - 洞口碰撞
```python=
# 偵測球與洞口之間的距離
for i in balls:
for j in holes:
if( mag(i.pos - j.pos) <= (j.radius/2 + r + 10* sqrt(2)) and dot((i.pos - j.pos), (i.v - j.v)) <=0 ):
i.v = vec(0,0,0)
i.opacity = 0
```
此前我們將所有的球和洞口個別創建陣列,這段程式碼用於一個一個檢測陣列裡球與洞口的狀態。
洞口與球的判定比較複雜一點,因為洞口的中心點並不在牆面上 ( 當初為了避免球洞太大而特意調整了位置 ) 所以在直線距離的判斷上加上了一點補償值進去以彌補判斷上的誤差。
而當球與洞口接觸時,即為進球的狀態,所以這裡我將此球的速度量值直接設為0,透明度設為不可見以在產生畫面上球進洞口的錯覺。
## 簡報簡易說明連結
[Google 簡報連結](https://docs.google.com/presentation/d/1RjWbNKbcuzOFTc2lcMZ6Kapq0hWv5gNuSDi7XwjhGoE/edit?usp=sharing)
這是我上台報告的簡報原檔,希望能讓你對這專案有更好的理解。
## 結語
透過這項物理模擬,我們大致上可以模擬出撞球桌上的第一次出桿的狀態。
**此專案仍有許多可以改進的地方,像是加入非彈性碰撞、球的自體旋轉對速度以及方向的影...等**。
**所以歡迎讀者可以引用此篇文章的程式碼來加以改進,並加註此篇文章在參考文獻中。**
感謝您的閱讀與耐心,筆者在此感謝。
## 參考文獻
1. [花式撞球 - 維基百科](https://zh.m.wikipedia.org/zh-tw/%E8%8A%B1%E5%BC%8F%E6%92%9E%E7%90%83)
2. [王一哲的教學網站](https://sites.google.com/view/yizhe/%E8%AA%B2%E7%A8%8B/python%E7%89%A9%E7%90%86%E6%A8%A1%E6%93%AC)