1. 总览
1.1 连接绳子的约束
在 rope.cpp 中, 实现 Rope 类的构造函数。这个构造函数应该可以创建一个新的绳子 (Rope) 对象,该对象从 start 开始, end 结束,包含 num_nodes 个节点。也就是如下图所示:
(这里画了一个三节点的rope)
每个结点都有质量,称为质点;质点之间的线段是一个弹簧。通过创建一系列的
质点和弹簧,你就可以创建一个像弹簧一样运动的物体。
pinned_nodes 设置结点的索引。这些索引对应结点的固定属性 (pinned at-
tribute) 应该设置为真(他们是静止的)。对于每一个结点,你应该构造一个 Mass
对象,并在 Mass 对象的构造函数里设置质量和固定属性。
(请仔细阅读代码,确定传递给构造函数的参数)。你应该在连续的两个结点之间创建一个弹簧,设置弹
簧两端的结点索引和弹簧系数 k,请检查构造函数的签名以确定传入的参数。
运行./ropesim。你应该可以看到屏幕上画出绳子,但它不发生运动。
1.2 显式/半隐式欧拉法
胡克定律表示弹簧连接的两个质点之间的力和他们之间的距离成比例。也就是:
在 Rope::simulateEuler 中, 首先实现胡克定律。遍历所有的弹簧,对弹簧两端的质点施加正确的弹簧力。保证力的方向是正确的!对每个质点,累加所有的弹簧力。
一旦计算出所有的弹簧力,对每个质点应用物理定律:
运行./ropesim。仿真应该就开始运行了,但是只有 3 个结点,看起来不够多。在application.cpp 文件的最上方,你应该可以看到欧拉绳子和 Verlet 绳子的定义。改变两个绳子结点个数(默认为 3 个),比如 16 或者更多。
运行 ./ropesim -s 32 来设置仿真中每帧不同的仿真步数。尝试设置较小的值和较大的值(默认值为 64)。
1.3 显式 Verlet
Verlet 是另一种精确求解所有约束的方法。这种方法的优点是只处理仿真中顶点的位置并且保证四阶精度。和欧拉法不同,Verlet 积分按如下的方式来更新下一步位置:
除此之外,我们可以仿真弹簧系数无限大的弹簧。不用再考虑弹簧力,而是用解约束的方法来更新质点位置:只要简单的移动每个质点的位置使得弹簧的长度保持原长。修正向量应该和两个质点之间的位移成比例,方向为一个质点指向另一质点。每个质点应该移动位移的一半。只要对每个弹簧执行这样的操作,我们就可以得到稳定的仿真。为了使运动更加平滑,每一帧可能需要更多的仿真次数。
1.4 阻尼
向显式 Verlet 方法积分的胡克定律中加入阻尼。现实中的弹簧不会永远跳
动-因为动能会因摩擦而减小。阻尼系数设置为 0.00005, 加入阻尼之后质点位置更
新如下
1.5 任务
你应该修改的函数是:
• rope.cpp 中的 Rope::rope(...)
• rope.cpp 中的 void Rope::simulateEuler(...)
• rope.cpp 中的 void Rope::simulateVerlet(...)
2. 编写:
首先先安装 OpenGL, Freetype 还有 RandR 这三个库
sudo apt install libglu1-mesa-dev freeglut3-dev mesa-common-dev
sudo apt install xorg-dev
2.1 Rope::rope()的编写
Rope::Rope(Vector2D start, Vector2D end, int num_nodes, float node_mass, float k, vector<int> pinned_nodes)
{
// TODO (Part 1): Create a rope starting at `start`, ending at `end`, and containing `num_nodes` nodes.
for (int i = 0; i < num_nodes; ++i)
{
// 转成double再做除法,否则就会截断成int
Vector2D p = start + (end - start) * (double)i / ((double)num_nodes - 1.0) ;
masses.emplace_back(new Mass(p, node_mass, false));
}
for (int i = 0; i < num_nodes - 1; ++i)
{
springs.emplace_back(new Spring(masses[i], masses[i + 1], k));
}
for (auto &i : pinned_nodes) {
masses[i]->pinned = true;
}
}
先创建质量,再创建弹簧,num_nodes个节点,均匀排布,使用线性插值的方法,植树问题两个相邻节点之间的距离应该是start和end的插值除以(num_nodes - 1)。pinned表示固定,查看pinned_nodes里只有一个{0},所以是只有start是固定的,而其他是可动的。
运行
mkdir build
cd build
cmake ..
make
./ropesim
应该看到一段横着的绳子,静止不动,因为还没有写运动的逻辑。
2.2 void Rope::simulateEuler()的编写
- 显式欧拉
F = m a v ( t + 1 ) = v ( t ) + a ⋅ d t x ( t + 1 ) = x ( t ) + v ( t ) ⋅ d t \begin{aligned}&F=ma\\ &v(t+1)=v(t)+a\cdot dt\\ &x(t+1)=x(t)+v(t)\cdot dt\end{aligned} F=mav(t+1)=v(t)+a⋅dtx(t+1)=x(t)+v(t)⋅dt
void Rope::simulateEuler(float delta_t, Vector2D gravity)
{
for (auto &s : springs)
{
// TODO (Part 2): Use Hooke's law to calculate the force on a node
double dist = (s->m2->position - s->m1->position).norm();
s->m1->forces += -s->k * (s->m1->position - s->m2->position) / dist * (dist - s->rest_length);
s->m2->forces += -s->k * (s->m2->position - s->m1->position) / dist * (dist - s->rest_length);
}
double damping_factor = 0.005;
for (auto &m : masses)
{
if (!m->pinned)
{
// TODO (Part 2): Add the force due to gravity, then compute the new velocity and position
Vector2D acc = m->forces/ m->mass + gravity;
// 显式方法
m->position += m->velocity * delta_t;
m->velocity += acc * delta_t;
// TODO (Part 2): Add global damping
}
// Reset all forces on each mass
m->forces = Vector2D(0, 0);
}
}
显式欧拉方法不稳定,仿真绳子直接就飞了。
- 半隐式欧拉
F = m a v ( t + 1 ) = v ( t ) + a ⋅ d t x ( t + 1 ) = x ( t ) + v ( t + 1 ) ⋅ d t \begin{aligned}&F=ma\\ &v(t+1)=v(t)+a\cdot dt\\ &x(t+1)=x(t)+v(t+1)\cdot dt\end{aligned} F=mav(t+1)=v(t)+a⋅dtx(t+1)=x(t)+v(t+1)⋅dt
把速度和位置两个表达式互换位置即可。
void Rope::simulateEuler(float delta_t, Vector2D gravity)
{
for (auto &s : springs)
{
// TODO (Part 2): Use Hooke's law to calculate the force on a node
double dist = (s->m2->position - s->m1->position).norm();
s->m1->forces += -s->k * (s->m1->position - s->m2->position) / dist * (dist - s->rest_length);
s->m2->forces += -s->k * (s->m2->position - s->m1->position) / dist * (dist - s->rest_length);
}
double damping_factor = 0.005;
for (auto &m : masses)
{
if (!m->pinned)
{
// TODO (Part 2): Add the force due to gravity, then compute the new velocity and position
Vector2D acc = m->forces/ m->mass + gravity;
// 隐式方法
m->velocity += acc * delta_t;
m->position += m->velocity * delta_t;
// TODO (Part 2): Add global damping
}
// Reset all forces on each mass
m->forces = Vector2D(0, 0);
}
}
可以加到16个节点
可以使用 ./ropesim -s 32 来设置仿真中每帧的仿真步数为 32,默认是 64,小的步数会不容易趋于稳定(在后面加入摩擦力之后可以更容易看出),使用更大的步数,会更容易趋于稳定。
- 添加阻尼
每个质量点的力多加一个 − k v -kv −kv, k k k这里设置了0.005
void Rope::simulateEuler(float delta_t, Vector2D gravity)
{
for (auto &s : springs)
{
// TODO (Part 2): Use Hooke's law to calculate the force on a node
double dist = (s->m2->position - s->m1->position).norm();
s->m1->forces += -s->k * (s->m1->position - s->m2->position) / dist * (dist - s->rest_length);
s->m2->forces += -s->k * (s->m2->position - s->m1->position) / dist * (dist - s->rest_length);
}
double damping_factor = 0.005;
for (auto &m : masses)
{
if (!m->pinned)
{
// TODO (Part 2): Add the force due to gravity, then compute the new velocity and position
Vector2D acc = (m->forces - damping_factor * m->velocity)/ m->mass + gravity;
// 显式方法
/* m->position += m->velocity * delta_t;
m->velocity += acc * delta_t; */
// 隐式方法
m->velocity += acc * delta_t;
m->position += m->velocity * delta_t;
// TODO (Part 2): Add global damping
}
// Reset all forces on each mass
m->forces = Vector2D(0, 0);
}
}
2.3 void Rope::simulateVerlet()的编写
公式是,我们还是用牛二求出加速度再使用公式:
F = m a x ( t + 1 ) = x ( t ) + [ x ( t ) − x ( t − 1 ) ] + a ( t ) ⋅ d t ⋅ d t \begin{aligned}&F=ma\\ &x(t+1)=x(t)+[x(t)-x(t-1)]+a(t)\cdot dt\cdot dt\end{aligned} F=max(t+1)=x(t)+[x(t)−x(t−1)]+a(t)⋅dt⋅dt
void Rope::simulateVerlet(float delta_t, Vector2D gravity)
{
for (auto &s : springs)
{
// TODO (Part 3): Simulate one timestep of the rope using explicit Verlet (solving constraints)
double dist = (s->m2->position - s->m1->position).norm();
s->m1->forces += -s->k * (s->m1->position - s->m2->position) / dist * (dist - s->rest_length);
s->m2->forces += -s->k * (s->m2->position - s->m1->position) / dist * (dist - s->rest_length);
}
for (auto &m : masses)
{
if (!m->pinned)
{
Vector2D temp_position = m->position;
// TODO (Part 3.1): Set the new position of the rope mass
Vector2D acc = m->forces / m->mass + gravity;
m->position = m->position + (m->position - m->last_position) + acc * delta_t * delta_t;
m->last_position = temp_position;
// TODO (Part 4): Add global Verlet damping
}
m->forces = Vector2D(0, 0);
}
}
加上阻尼:
F
=
m
a
x
(
t
+
1
)
=
x
(
t
)
+
(
1
−
d
a
m
p
f
a
c
t
o
r
)
⋅
[
x
(
t
)
−
x
(
t
−
1
)
]
+
a
(
t
)
⋅
d
t
⋅
d
t
\begin{aligned}&F=ma\\ &x(t+1)=x(t)+(1-damp_factor)\cdot[x(t)-x(t-1)]+a(t)\cdot dt\cdot dt\end{aligned}
F=max(t+1)=x(t)+(1−dampfactor)⋅[x(t)−x(t−1)]+a(t)⋅dt⋅dt
void Rope::simulateVerlet(float delta_t, Vector2D gravity)
{
for (auto &s : springs)
{
// TODO (Part 3): Simulate one timestep of the rope using explicit Verlet (solving constraints)
double dist = (s->m2->position - s->m1->position).norm();
s->m1->forces += -s->k * (s->m1->position - s->m2->position) / dist * (dist - s->rest_length);
s->m2->forces += -s->k * (s->m2->position - s->m1->position) / dist * (dist - s->rest_length);
}
double damping_factor = 0.00005;
for (auto &m : masses)
{
if (!m->pinned)
{
Vector2D temp_position = m->position;
// TODO (Part 3.1): Set the new position of the rope mass
Vector2D acc = m->forces / m->mass + gravity;
m->position = m->position + (1 - damping_factor) * (m->position - m->last_position) + acc * delta_t * delta_t;
m->last_position = temp_position;
// TODO (Part 4): Add global Verlet damping
}
m->forces = Vector2D(0, 0);
}
}
最后看一个两个都加阻尼的动图吧
在我的电脑跑不知道怎么卡卡的,但是运行是可以的。
后记
在编写构造函数的时候在数组插入指针下面这样写出现了段错误
Spring spring(masses[i], masses[i + 1], k);
springs.emplace_back(&spring);
而这样写不会
springs.emplace_back(new Spring(masses[i], masses[i + 1], k));
则不会报错。
这是因为在C++中,emplace_back函数用于在容器的末尾构造一个新元素。springs.emplace_back(&spring)尝试将一个指向局部变量spring的指针添加到springs容器中。这是不安全的,因为当spring超出其作用域时,指向它的指针将变为悬空指针(野指针)。
悬空指针是指指向已释放或超出作用域的对象的指针。当尝试使用悬空指针时,会导致未定义的行为,其中包括段错误(Segmentation Fault)。相比之下,springs.emplace_back(new Spring(masses[i], masses[i + 1], k))使用new运算符在堆上动态分配了一个新的Spring对象,并将指向该对象的指针添加到springs容器中。由于该对象在堆上分配,它的生命周期不会受限于局部作用域,直到显式调用delete释放内存。因此,这种方式不会导致段错误。
为了避免段错误,可以使用智能指针(如std::shared_ptr或std::unique_ptr)来管理动态分配的对象,或者确保在使用指针之前,被指向的对象的生命周期仍然有效。