游戏中的刚体碰撞分析
前言
本文提供了一种方法(Impulse Method)来处理游戏中几何体间的碰撞处理。主要思路为利用 SDF 与梯度求出反弹法线并计算动量。思路来自 GAMES103。本文针对其进行了更详细的讲解以及提供一个可编译的代码实现。
具体实现效果如下所示:
具体效果
前置知识
SDF(Signed distance function),有符号距离函数。是一个多元函数(具体几元取决于研究对象所处的维度),本文记作
偏导(Partial Derivative),偏导是多变量微积分中的一个基本概念。在单变量微积分中,我们经常用
梯度(Gradient),一般用拉普拉斯算子(
梯度示意图
Ⅰ、利用 SDF 判断物体碰撞
利用 SDF 有两个方法,一个是对物体边缘上的每个点都进行一次 SDF 计算。这并在性能上并不划算,这里我将提供一种更好的方法,尽管这种方法可能会失去一些普适性,但是带来的性能证明这是值得的。
我们先考虑两个圆的碰撞如下图所示:
两个圆还未相撞
当两个圆还没相撞时,我们能观察到两个圆心之间的距离
图 1.1:两个圆已经发生了碰撞并重叠
我们可以观察到两个圆心之间的距离
圆形搞定了,我们再来看圆和线之间的相撞,如图 1.2。
图 1.2:点与线之间的碰撞
其实点与线之间的碰撞更为简单。只要
有了上述基础上,我们就可以描述了圆形与任意几何体(圆弧除外)之间的碰撞了,而线与线之间的碰撞由于篇幅原因,本文不再介绍,读者可自行介绍,我也并没有在代码中实现线线碰撞的代码。
Ⅱ、碰撞后位置的修正
当两个物体碰撞以后,我们并不能确保物体的位置一定是在我们预期之中的,例如图 1.1 就是一个意外情况。由于速度过快导致两个物体出现了重叠。此时我们就需要修正物体的位置。现在我们关注如图 2.1 的情况。
图 2.1:一种重叠的情况
不难发现,我们所需要的位置修复就是将圆形沿着法线方向(
这里特地说一下 SDF 的梯度
auto GradientSDF(const Vec &Point) -> Vec override {
auto origin = SDF(Vec({Point.x[0], Point.x[1]}));
return Vec({SDF(Vec({Point.x[0] + 0.0000000001, Point.x[1]})) - origin,
SDF(Vec({Point.x[0], Point.x[1] + 0.0000000001})) - origin});
}
Ⅲ、碰撞后的速度修正
在进行速度的修正之前,我们应该确保一件事情:速度是需要修正的。什么意思呢,就是这个物体的速度可能已经在前几个 Tick 内被修正了,这种情况下我们就不需要在进行修正了。判断速度是否需要被修正的一个方法就是看其是否指向碰撞目标物体内,即比较
对于速度的修复,我们先将速度沿法相法相正交分解:
根据阿蒙顿-库仑摩擦定律(
有了以上理论推导就可以开始实现代码了。
Ⅳ、代码实现
本代码实现取消了摩擦因数,默认动量不衰减。读者若有需要可以根据本文内容自行修改。
/*
* Program Name : Rigid Body Collision Demo
* Last Modify Date : 2024/5/11
* Author : Margoo(1683691371@qq.com)
* CPP Version : >=C20
*/
#define _UNICODE
#define UNICODE
#include <array>
#include <cmath>
#include <ctime>
#include <format>
#include <graphics.h>
#include <vector>
typedef struct tagRECTF {
double left;
double top;
double right;
double bottom;
} RECTF, *PRECTF, NEAR *NPRECTF, FAR *LPRECTF;
// 2D Vector
class Vec final {
public:
Vec() : x({0, 0}) {
}
Vec(std::array<double, 2> Init) : x(Init) {
}
public:
auto Length() noexcept -> double {
return sqrt(pow(x[0], 2) + pow(x[1], 2));
}
auto Normalize() noexcept -> Vec {
return (*this) / Length();
}
auto DotProduct(const Vec &Vector) noexcept -> double {
return Vector.x[0] * x[0] + Vector.x[1] * x[1];
}
public:
auto operator*=(const Vec &Value) -> Vec & {
for (auto position = size_t(0); position < 2; ++position) {
x[position] *= Value.x[position];
}
return (*this);
}
auto operator/=(const Vec &Value) -> Vec & {
for (auto position = size_t(0); position < 2; ++position) {
x[position] /= Value.x[position];
}
return (*this);
}
auto operator+=(const Vec &Value) -> Vec & {
for (auto position = size_t(0); position < 2; ++position) {
x[position] += Value.x[position];
}
return (*this);
}
auto operator-=(const Vec &Value) -> Vec & {
for (auto position = size_t(0); position < 2; ++position) {
x[position] -= Value.x[position];
}
return (*this);
}
auto operator*=(const double &Value) -> Vec & {
for (auto position = size_t(0); position < 2; ++position) {
x[position] * Value;
}
return (*this);
}
auto operator/=(const double &Value) -> Vec & {
for (auto position = size_t(0); position < 2; ++position) {
x[position] / Value;
}
return (*this);
}
auto operator+=(const double &Value) -> Vec & {
for (auto position = size_t(0); position < 2; ++position) {
x[position] + Value;
}
return (*this);
}
auto operator-=(const double &Value) -> Vec & {
for (auto position = size_t(0); position < 2; ++position) {
x[position] - Value;
}
return (*this);
}
public:
friend auto operator/(const Vec &Left, const Vec &Right) -> Vec;
friend auto operator/(const Vec &Left, const double &Right) -> Vec;
friend auto operator*(const Vec &Left, const Vec &Right) -> Vec;
friend auto operator*(const Vec &Left, const double &Right) -> Vec;
friend auto operator+(const Vec &Left, const Vec &Right) -> Vec;
friend auto operator+(const Vec &Left, const double &Right) -> Vec;
friend auto operator-(const Vec &Left, const Vec &Right) -> Vec;
friend auto operator-(const Vec &Left, const double &Right) -> Vec;
public:
std::array<double, 2> x;
};
auto operator/(const Vec &Left, const Vec &Right) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] / Right.x[position];
}
return result;
}
auto operator/(const Vec &Left, const double &Right) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] / Right;
}
return result;
}
auto operator*(const Vec &Left, const Vec &Right) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] * Right.x[position];
}
return result;
}
auto operator*(const Vec &Left, const double &Right) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] * Right;
}
return result;
}
auto operator+(const Vec &Left, const Vec &Right) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] + Right.x[position];
}
return result;
}
auto operator+(const Vec &Left, const double &Right) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] + Right;
}
return result;
}
auto operator-(const Vec &Left, const Vec &Right) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] - Right.x[position];
}
return result;
}
auto operator-(const Vec &Left, const double &Right) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] - Right;
}
return result;
}
auto operator/(const double &Right, const Vec &Left) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Right / Left.x[position];
}
return result;
}
auto operator*(const double &Right, const Vec &Left) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] * Right;
}
return result;
}
auto operator+(const double &Right, const Vec &Left) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Left.x[position] + Right;
}
return result;
}
auto operator-(const double &Right, const Vec &Left) -> Vec {
Vec result;
for (auto position = size_t(0); position < 2; ++position) {
result.x[position] = Right - Left.x[position];
}
return result;
}
class Sprite {
public:
Sprite() = default;
public:
virtual auto Draw() -> void = 0;
// Use SDF for collision judgement
virtual auto SDF(const Vec &Point) -> double = 0;
virtual auto GradientSDF(const Vec &Point) -> Vec = 0;
virtual auto DealSDF(const double &SDF) -> double {
return SDF;
}
virtual auto Particle() -> Vec = 0;
virtual auto RelativeMove(const Vec &Position) -> void = 0;
public:
auto Move(const double &X, const double &Y) -> void {
auto width = boundingBox.right - boundingBox.left;
auto height = boundingBox.bottom - boundingBox.top;
boundingBox = {X, Y, X + width, Y + height};
}
public:
bool lock = true;
RECTF boundingBox{};
Vec velocity;
};
class RoundSprite : public Sprite {
public:
explicit RoundSprite(const double &Radius) : radius(Radius), Sprite() {
boundingBox = {0, 0, Radius * 2, Radius * 2};
}
public:
auto Draw() -> void override {
setfillcolor(WHITE);
solidcircle(boundingBox.left + radius, boundingBox.top + radius, radius);
}
auto SDF(const Vec &Point) -> double override {
Vec centre({boundingBox.left + radius, boundingBox.top + radius});
return (Point - centre).Length() - radius;
}
auto GradientSDF(const Vec &Point) -> Vec override {
auto base = SDF(Point) + radius;
Vec centre({boundingBox.left + radius, boundingBox.top + radius});
return Vec({(Point.x[0] - centre.x[0]) / base, (Point.x[1] - centre.x[1]) / base});
}
auto DealSDF(const double &SDF) -> double override {
return SDF - radius;
}
auto Particle() -> Vec override {
return Vec({boundingBox.left + radius, boundingBox.top + radius});
}
auto RelativeMove(const Vec &Position) -> void override {
Move(Position.x[0] - radius, Position.x[1] - radius);
}
public:
double radius;
};
class LineSprite : public Sprite {
public:
explicit LineSprite(const Vec &Point1, const Vec &Point2) : point1(Point1), point2(Point2) {
boundingBox = {Point1.x[0], Point1.x[1], Point2.x[0], Point2.x[1]};
}
public:
auto Draw() -> void override {
setlinecolor(WHITE);
setlinestyle(PS_SOLID, 1);
line(point1.x[0], point1.x[1], point2.x[0], point2.x[1]);
}
auto SDF(const Vec &Point) -> double override {
Vec ap = Point - point1;
Vec ab = point2 - point1;
double h = ap.DotProduct(ab) / ab.DotProduct(ab);
h = h >= 1.f ? 1.f : h;
h = h <= 0.f ? 0.f : h;
return (ap - h * ab).Length();
}
auto GradientSDF(const Vec &Point) -> Vec override {
auto origin = SDF(Vec({Point.x[0], Point.x[1]}));
return Vec({SDF(Vec({Point.x[0] + 0.0000000001, Point.x[1]})) - origin,
SDF(Vec({Point.x[0], Point.x[1] + 0.0000000001})) - origin});
}
auto Particle() -> Vec override {
return point1 + (point2 - point1) / 2;
}
auto RelativeMove(const Vec &Position) -> void override {
auto width = boundingBox.right - boundingBox.left;
auto height = boundingBox.bottom - boundingBox.top;
point1 = Position;
point2 = point1 + Vec({width, height});
Move(Position.x[0], Position.x[1]);
}
public:
Vec point1;
Vec point2;
};
class SpriteManager {
public:
SpriteManager() = default;
public:
auto UpdateSprite() -> void {
for (auto &sprite : spriteList) {
if (!sprite->lock) {
for (auto &other : spriteList) {
if (&other == &sprite) {
continue;
}
auto spritePoint = sprite->Particle();
auto sdf = sprite->DealSDF(other->SDF(spritePoint));
if (sdf <= 0.001) {
// Normal vector
auto normal = other->GradientSDF(spritePoint).Normalize();
auto newPosition = spritePoint + normal * abs(sdf);
// Fix the position
sprite->RelativeMove(newPosition);
// Fix the speed
if (sprite->velocity.DotProduct(normal) < 0) {
Vec NVelocity = sprite->velocity.DotProduct(normal) * normal;
Vec TVelocity = sprite->velocity - NVelocity;
Vec newNVelocity = (0.f - NVelocity);
Vec newTVelocity = max(1.f - (NVelocity.Length() / TVelocity.Length()), 1.f) * TVelocity;
sprite->velocity = newNVelocity + newTVelocity;
}
}
}
}
}
for (auto &sprite : spriteList) {
auto newPosition =
Vec({static_cast<double>(sprite->boundingBox.left), static_cast<double>(sprite->boundingBox.top)}) +
sprite->velocity * timingTick;
sprite->Move(newPosition.x[0], newPosition.x[1]);
}
cleardevice();
for (auto &sprite : spriteList) {
sprite->Draw();
}
}
public:
double timingTick = 1.f;
std::vector<Sprite *> spriteList;
};
int main() {
initgraph(640, 480);
SpriteManager manager;
auto roundSprite1 = new RoundSprite(20.f);
auto roundSprite2 = new RoundSprite(60.f);
auto roundSprite3 = new RoundSprite(70.f);
auto roundSprite4 = new RoundSprite(20.f);
auto roundSprite5 = new RoundSprite(20.f);
auto roundSprite6 = new RoundSprite(20.f);
auto border1 = new LineSprite(Vec({0, 0}), Vec({static_cast<double>(getwidth()), 0}));
auto border2 = new LineSprite(Vec({0, 0}), Vec({0, static_cast<double>(getheight())}));
auto border3 = new LineSprite(Vec({static_cast<double>(getwidth()), 0}),
Vec({static_cast<double>(getwidth()), static_cast<double>(getheight())}));
auto border4 = new LineSprite(Vec({0, static_cast<double>(getheight())}),
Vec({static_cast<double>(getwidth()), static_cast<double>(getheight())}));
auto lineSprite = new LineSprite(Vec({70, 390}), Vec({100, 190}));
manager.spriteList.push_back(roundSprite1);
manager.spriteList.push_back(lineSprite);
manager.spriteList.push_back(border1);
manager.spriteList.push_back(border2);
manager.spriteList.push_back(border3);
manager.spriteList.push_back(border4);
manager.spriteList.push_back(roundSprite2);
manager.spriteList.push_back(roundSprite3);
manager.spriteList.push_back(roundSprite4);
manager.spriteList.push_back(roundSprite5);
manager.spriteList.push_back(roundSprite6);
roundSprite2->Move(180.f, 80.f);
roundSprite3->Move(380.f, 180.f);
roundSprite4->Move(300.f, 120.f);
roundSprite5->Move(400.f, 320.f);
roundSprite1->Move(20.f, 20.f);
roundSprite1->velocity.x = {1.5f, 0.6f};
roundSprite4->velocity.x = {-1.7f, 1.3f};
roundSprite5->velocity.x = {-1.5f, -2.3f};
roundSprite6->velocity.x = {3.f, -2.3f};
roundSprite4->lock = false;
roundSprite1->lock = false;
roundSprite5->lock = false;
roundSprite6->lock = false;
BeginBatchDraw();
settextcolor(GREEN);
auto fpsCount = int(0);
auto fps = int(0);
auto time = clock();
while (true) {
manager.UpdateSprite();
outtextxy(0.f, 0.f, std::format(L"FPS : {}", fpsCount).c_str());
if (clock() - time >= 1000) {
fpsCount = fps;
fps = 0;
time = clock();
}
++fps;
FlushBatchDraw();
Sleep(2);
}
return 0;
}
添加评论
取消回复