没错~我就硬是不要给把标题打成『弹跳球世界V』,咬我啊~
这篇是斜面碰撞
的後篇
~
今天要来补完我们在上一篇没有解决掉的reposition
和反射
速度运算的部分。
之前提到的我会把源码更新在Github Repo上面,大家可以在这边自行clone :D
然後这里是repo的Github Page
我简单录了一段实际play的画面:
虽然说前面有讲解过原理,但是这边我们是稍微讲解一些源码中的细节:
这边把之前的图重新贴一次,让大家能更方便的对照程序中向量运算的规则。
斜面碰撞
主要的核心基本上还是在animateBall这个在每圈RAF循环
(我们把requestAnimationFrame的递回操作称为RAF循环)中会被触发的方法,而最重要的计算则是在checkBoundary(),也就是碰撞侦测
和反弹计算
的部分。
整个checkBoundary的流程有4
大重点:
v'/v ≒ 1- (a加速度向量・h向量)/ v平方
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 ~
如果对程序原理/过程中有任何的疑问,也希望可以在留言区提出~ 我会尽可能地做解答!
<<: Day16_ISO 27701 - 个人资料隐私资讯管理系统(隐私资讯管理体系(PIMS))
>>: [ Day 14 ] - 存取网页元素之变更指定内容
watch监听器 监听data里面的值,当值有变化时,就会触发事件。 watch监听一个变数: &l...
Traces - 观察应用程序的效能瓶颈 系列文章 (1/6) - Elastic APM 基本介绍...
上一篇我们写了一些 EtaViewModel 的测试,这一篇会集中写跟时间相关的测试。 之前在 Et...
前言 昨日我们学会了Props,而Props是由外传进来的资料,进而改变组件。而组件本身的状态我们称...
大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 ...