彈簧系統三維視覺化

隨遇丿而安發表於2022-03-30

彈簧系統三維視覺化

  • 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 公式,使用兩個小球進行驗證

simple實現1

從圖中可看出,小球受重力的影響,具有向下的加速度,又因為彈力的作用,會有彈簧拉力將其拉回。

  • 模擬:求出合併後的加速度,應用 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 類程式碼量比較多,但實際上僅做了初始化和更新兩個操作

  1. 初始化物體時,建立對應數目的彈簧和小球
  2. 通過 addMesh 方法將小球和彈簧加入虛擬三維場景中
  3. 通過 updateRope 方法更新小球和彈簧的位置
    1. 首先計算一個小球相鄰彈簧帶給其的彈力
    2. 再計算小球的重力
    3. 將小球的兩個彈力和重力加起來,注意是向量相加
    4. 通過牛頓第二定律求出小球的加速度 a
    5. 更新小球座標
    6. 根據小球座標更新彈簧座標

渲染結果

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 方法重複渲染頁面

程式碼倉庫

作業8 · XiaXiang/web games101 - 碼雲 - 開源中國 (gitee.com)

該倉庫還有利用 WebGL 實現 Games101 其它作業的程式碼,由於實驗使用,很多程式碼沒有經過美化,望理解

相關文章