iT邦幫忙

2021 iThome 鐵人賽

DAY 14
0
Modern Web

成為Canvas Ninja ! ~ 理解2D渲染的精髓系列 第 14

Day14 - 物理模擬篇 - 彈跳球世界IV(補完篇) - 成為Canvas Ninja ~ 理解2D渲染的精髓

沒錯~我就硬是不要給把標題打成『彈跳球世界V』,咬我啊~

這篇是斜面碰撞後篇~

今天要來補完我們在上一篇沒有解決掉的reposition反射速度運算的部分。

之前提到的我會把源碼更新在Github Repo上面,大家可以在這邊自行clone :D

然後這裡是repo的Github Page

我簡單錄了一段實際play的畫面:
Yes

雖然說前面有講解過原理,但是這邊我們是稍微講解一些源碼中的細節:

這邊把之前的圖重新貼一次,讓大家能更方便的對照程式中向量運算的規則。

img

斜面碰撞主要的核心基本上還是在animateBall這個在每圈RAF循環(我們把requestAnimationFrame的遞迴操作稱為RAF循環)中會被觸發的方法,而最重要的計算則是在checkBoundary(),也就是碰撞偵測反彈計算的部分。

整個checkBoundary的流程有4大重點:

  1. 球的碰撞偵測, 也就是前面提到的兩大條件(這部分就是我們在上一篇做的)
    1.球和反射面的距離低於球的半徑
    2.球和反射面兩端點的向量投影長度(投影在反射面上的長度)都低於反射面的長度
  2. 給越過牆壁的球做我們前面提到的reposition操作
    img
  3. 重新計算reposition後的速度,這邊是用我們在前面提到的用微積分求得的公式
    v'/v ≒ 1- (a加速度向量・h向量)/ v平方
  4. 計算反射後的速度向量,這邊也是一樣可以利用我們在前面講的向量關係去做計算。
    img
normalVelocity = dist.para(ball.velocity.projection(dist),-1);
// 也就是把d向量的單位向量,去以『球速在d向量上的投影長度』為倍數做擴增,接著再顛倒過來
tangentVelocity = ball.velocity.subtract(normalVelocity);
//像我們之前說用『扣除』的方式去取得沿反射面方向的速度分量
// 最後把這兩個分量加起來,就會得到反射後的速度向量

源碼部分

這邊我把checkBoundary()的源碼也貼上來,方便大家對照

 checkBoundary(dt) {
    // 這邊就是碰撞偵測(第一重點)
    this.boundary.walls.forEach((o, i) => {
      const vectorAB = new Vector2D(
        o[1].x - o[0].x,
        o[1].y - o[0].y
      )
      this.balls.forEach((ball, index) => {
        const vectorAToBall = new Vector2D(
          ball.x - o[0].x,
          ball.y - o[0].y
        );
        const vectorBToBall = new Vector2D(
          ball.x - o[1].x,
          ball.y - o[1].y
        );

        const vectorAToBallProj = vectorAToBall.project(vectorAB);
        const vectorBToBallProj = vectorBToBall.project(vectorAB);
        const distVector = vectorAToBall.subtract(vectorAToBallProj);
        const dist = distVector.length();
        const collisionDetection =
          dist < ball.radius &&
          vectorAToBallProj.length() < vectorAB.length() &&
          vectorBToBallProj.length() < vectorAB.length();
        if (collisionDetection) {
          // 這邊是要先做reposition的部分(第二重點)
          // 這邊的算法是利用『插進牆壁後的球到牆壁距離+球的半徑 = (球在正確的碰撞點到已經插進牆壁這一幀的實際距離, 也就是deltaS)*sin(90度 - 入射角)』

          // 這邊一連串的動作是要把deltaS從純量轉換成向量,以便用substract方法去把球的位置倒回去正確的碰撞點

          // perp 是牆壁的單位法向量
          const perp = vectorAB.perp(1);

          // 這邊因為單位法向量的n必須要跟球的來向大致相反(也就是向量要夾超過90度),而perp本身又沒有辦法確定到底是取到正或反的法向量,所以要補一個防呆
          if (perp.dotProduct(ball.velocity) > 0) {
            perp.scaleBy(-1)
          }

          // 球速向量和牆壁的夾角
          const angle = Vector2D.angleBetween(ball.velocity, vectorAB);

          // 我們可以藉由算 (球半徑+球到牆壁距離向量和牆壁法向量的內積)/sin(球速與牆壁夾角) 來取得deltaS

          const deltaS = (ball.radius + distVector.dotProduct(perp)) / Math.sin(angle);

          // 把球速轉化成單位向量,接著再擴張deltaS倍,這樣就能取的到底要倒回去多少距離才能來到正確的碰撞點
          let displ = ball.velocity.para(deltaS);
          ball.x -= displ.x * dt;
          ball.y -= displ.y * dt;
          //到這邊就reposition完畢~

          // 這邊的vcor是我們之前有提到過,加速度和幀間誤差的相互關係會導致球被額外加速一小段距離,而這邊可以藉由乘以vcor 這個參數來抵銷多餘的加速量(第三重點)
          var vcor = 1 - ball.gravity.dotProduct(displ.multiply(dt)) / ball.velocity.lengthSquared();
          // 原速度乘以vcor
          var Velo = ball.velocity.multiply(vcor);

          // 這邊就是取球速於牆壁法線方向的分量
          var normalVelo = distVector.para(Velo.projection(distVector));
          // 這邊則是取球速平行於牆壁的分量
          var tangentVelo = Velo.subtract(normalVelo);
          // 兩者合併就是反射後的速度
          ball.velocity = tangentVelo.addScaled(normalVelo, -ball.friction);

        }
      })

    })
  }

我發現好像球還是有低機率產生異常的穿牆行為?

穿牆的bug之所以會發生,是因為動畫是frame by frame的。

有時候球在接近牆壁時,在這一幀還沒碰到牆壁,但是在下一幀卻已經完全穿越過牆壁了,這就導致碰撞偵測完全沒有被觸發。

延伸閱讀:避免子彈穿越牆壁

其實這個現象在物理模擬的領域有一個正式的名稱: 穿隧效應(Tunneling Effect)

符合下列條件的情形,穿隧效應發生的機率會比較高:

  • 球速非常快
  • 球的半徑非常小
  • 牆板不夠厚

其實物理模擬的碰撞偵測是一種很深的學問,而我在這個案例裡面用的方法是所謂的『離散式碰撞偵測(Discrete collision detection)』,通常如果要實現高速物體的碰撞偵測,要使用『連續式碰撞偵測(Continuous collision detection (CCD) )』。

連續式碰撞偵測的原理是依靠預先演算物體未來的前進軌跡,偵測其前進軌跡上是否有障礙物,計算出可能發生的碰撞,還有碰撞的時間點碰撞位置,然後準確地在那個時間點去把物體移位到指定的座標,避免穿隧效應發生。

不過很遺憾,在這個系列我其實沒有打算把這個領域的知識擴展到這麼深..
(因為那樣可能就要把整個鐵人賽的內容給砸下去了...)
但是如果真的要在前端環境實現CCD ,其實可以依賴一些現有的物理引擎,例如

以上五篇,包含今天的內容~就是彈跳球動畫的實作 :D ~

如果對程式原理/過程中有任何的疑問,也希望可以在留言區提出~ 我會盡可能地做解答!


上一篇
Day13 - 物理模擬篇 - 彈跳球世界IV - 成為Canvas Ninja ~ 理解2D渲染的精髓
下一篇
Day15 - 中場休息時間 - 來看看htmlToCanvas的實作吧 - 成為Canvas Ninja ~ 理解2D渲染的精髓
系列文
成為Canvas Ninja ! ~ 理解2D渲染的精髓31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言