彈簧系統三維視覺化
- games 101 最後一次作業,彈簧系統三維視覺化
- 主要使用顯式 Verlet 方法,並加入阻尼,下面展示視覺化圖
實現歷程
實現彈簧系統視覺化需要經歷模擬和渲染,模擬和渲染實際上是兩個不同步驟。
- 模擬:輸入物體質量和位置以及收到的力,輸出該物體下一時刻的位置
- 渲染:根據物理質量,座標,外觀實時展示物體當前狀態
模擬使用到顯式 Verlet,根據加速度、前一刻座標和當前座標計算下一時刻的位置,數學公式為
\[\begin{equation}
x_{t+dt} = x(t) + [x(t)-x(t-dt)] + a(t) \times dt \times dt
\end{equation}
\]
如果不存在阻力,任何小球將一直執行下去,因此需要向其中新增阻尼,加入後公式為
\[\begin{equation}
x_{t+dt} = x(t) + (1 - dampingFactor) \times [x(t)-x(t-dt)] + a(t) \times dt \times dt
\end{equation}
\]
為了驗證加入阻尼的 Verlet 公式,使用兩個小球進行驗證
從圖中可看出,小球受重力的影響,具有向下的加速度,又因為彈力的作用,會有彈簧拉力將其拉回。
- 模擬:求出合併後的加速度,應用 Verlet 公式,求出小球下一刻的座標
- 渲染:通過 Threejs 渲染兩個小球和彈簧
小球運動的加速度由牛頓第二定律所得
\[F=ma
\]
因此小球運動速度與自身質量息息相關,若將小球質量增加,彈簧將被拉得更長
將一個彈簧系統完成後,即可開發更多小球和彈簧的模擬模擬。
程式碼設計
程式主要分為兩個部分,一個部分為模擬和渲染,另一部分為類設計。程式設計到三個類(class):整個彈簧系統(Rope)、小球(Mass)和彈簧(Spring)。詳情如下所示:
彈簧(Spring)類
class Spring {
// 彈簧長度
length;
// 彈簧係數
k=1;
// 彈簧相鄰小球集合
points;
// 渲染的線物件
line;
constructor(length=2,k=1, points) {
this.length = length;
this.k = k;
this.line=new THREE.Line(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({color: 0xf0f000}))
this.points = points;
this.updateLine();
}
// 更新彈簧位置
updateLine(){
this.line.geometry.setFromPoints(this.points);
}
}
彈簧類中屬性的作用
- length 和 k 用來計算彈簧產生的力,該力為一個向量
- points 和 line 用來進行渲染,將彈簧通過 three 展示在螢幕上
小球(Mass)類
class Mass{
// 質量
mass=1;
// 前一時刻的座標
positionPre;
// 當前時刻的座標
positionCurr;
// 下一時刻的座標
positionFuture;
// 渲染的小球物件
object=new THREE.Mesh(new THREE.SphereBufferGeometry(0.5), new THREE.MeshNormalMaterial());
constructor(mass, position) {
this.mass = mass;
this.positionPre = position;
this.positionCurr = position;
this.object.position.set(position.x, position.y, position.z);
}
// 根據阻尼係數,時間 delta 和加速度 a 計算下一時刻的座標
setPosition(dampingFactor, delta, a){
let {positionPre, positionCurr, positionFuture, object}=this
positionFuture = positionCurr.clone().add(positionCurr.clone().sub(positionPre).multiplyScalar(1-dampingFactor))
.add(a.multiplyScalar(delta^2));
object.position.set(positionFuture.x, positionFuture.y, positionFuture.z)
this.positionPre = positionCurr;
this.positionCurr = positionFuture;
}
}
小球類中屬性的作用
- mass 質量,計算加速度所需條件
- positionPre、positionCurr、positionFuture 不同時刻的座標
- object 渲染出的小球物件
- setPosition 復現 Verlet 公式
最複雜的彈簧系統類
class Rope{
// 節點數目
num_nodes=0;
// 包含的小球集合
massArray=[];
// 包含的彈簧集合
springArray=[];
constructor(num_nodes) {
this.num_nodes = num_nodes;
this.initRope(num_nodes);
}
// 根據節點數初始化彈簧
initRope(num){
if(num<2){
alert("節點數量不夠");
}
const {massArray, springArray} = this;
for (let i = 0; i < num; i++) {
const position = new THREE.Vector3(i*3, 1, 0);
massArray[i] = new Mass(500, position);
}
for (let i = 0; i < num - 1; i++) {
springArray[i] = new Spring(2, 1,[massArray[i].object.position, massArray[i+1].object.position]);
}
}
// 將所有加入虛擬場景中
addMesh(scene){
for (let i = 0; i < this.num_nodes; i++) {
scene.add(this.massArray[i].object);
}
for (let i = 0; i < this.num_nodes-1; i++) {
scene.add(this.springArray[i].line);
}
}
// 首先通過模擬計算彈簧系統各個成分的座標,然後通過各個類的更新方法更新座標
updateRope(delta){
let i=0;
for (i = 1; i < this.num_nodes-1; i++) {
const sphere = this.massArray[i];
const positionPre = this.massArray[i-1].object.position;
const positionCurr = this.massArray[i].object.position;
const positionFuture = this.massArray[i+1].object.position;
// 計算彈力
const vector1 = positionCurr.clone().sub(positionPre);
const springForce1 = vector1.clone().normalize().multiplyScalar(vector1.clone().length()-2).multiplyScalar(-1);
const vector2 = positionFuture.clone().sub(positionCurr);
const springForce2 = vector2.clone().normalize().multiplyScalar(vector2.clone().length()-2).multiplyScalar(-1*(-1));
// 計算重力
const gravity = new THREE.Vector3(0, -1, 0);
// 計算合力
const resultForce = springForce1.clone().add(gravity).add(springForce2);
const a = resultForce.multiplyScalar(1/sphere.mass);
const dampingFactor = 0.005;
sphere.setPosition(dampingFactor, delta, a);
}
for (let i = 0; i < this.num_nodes-1; i++) {
this.springArray[i].updateLine();
}
}
}
Rope 類程式碼量比較多,但實際上僅做了初始化和更新兩個操作
- 初始化物體時,建立對應數目的彈簧和小球
- 通過 addMesh 方法將小球和彈簧加入虛擬三維場景中
- 通過 updateRope 方法更新小球和彈簧的位置
- 首先計算一個小球相鄰彈簧帶給其的彈力
- 再計算小球的重力
- 將小球的兩個彈力和重力加起來,注意是向量相加
- 通過牛頓第二定律求出小球的加速度 a
- 更新小球座標
- 根據小球座標更新彈簧座標
渲染結果
import * as THREE from "/lib/three/build/three.module.js"
import {OrbitControls} from "/lib/three/examples/jsm/controls/OrbitControls.js"
import {DragControls} from "/lib/three/examples/jsm/controls/DragControls.js"
import {Rope} from "./springSystemClass.js";
// 建立 Canvas 元素
const canvas = document.createElement("canvas");
const width = canvas.width = 800;
const height = canvas.height = 500;
document.body.appendChild(canvas);
const clock = new THREE.Clock();
// init variable
const scene = new THREE.Scene();
const camera= new THREE.PerspectiveCamera(45, width/height);
const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: 1});
renderer.setSize(width, height);
// scene.add(new THREE.AxesHelper(10));
camera.position.set(10, 10, 10);
camera.lookAt(0, 0, 0);
const orbitControl = new OrbitControls(camera, canvas);
// 建立彈簧系統
const rope = new Rope(5);
rope.addMesh(scene);
let enableSelection = false;
const objects = rope.massArray.map(value => value.object);
const dragControls = new DragControls(objects, camera, canvas);
// 設定拖動事件
dragControls.addEventListener( 'dragstart', function ( event ) {
orbitControl.enabled = false;
enableSelection = true;
console.log(event)
} );
dragControls.addEventListener( 'dragend', function ( event ) {
orbitControl.enabled = true;
enableSelection = false;
console.log(event)
} );
animation();
function animation(){
renderer.render(scene, camera);
const delta = clock.getDelta();
if(!enableSelection) rope.updateRope(delta);
requestAnimationFrame(animation)
}
該程式碼是比較常見的 Threejs 渲染程式碼
其中需要注意的是
- 通過建立 Rope 的例項動態建立彈簧系統
- 通過 dragControls 控制小球
- 使用 animation 方法重複渲染頁面
程式碼倉庫
該倉庫還有利用 WebGL 實現 Games101 其它作業的程式碼,由於實驗使用,很多程式碼沒有經過美化,望理解