CG大作业

内容

成员:马梓培 李涛 熊蔚然

我们选取交互这个选题,想复现出⼀款游戏,“火柴人打羽毛球”。本地双人对打。

我们最终实现的效果:

  1. 控制火柴人的移动,包括左移右移,向上跳跃。以及击打羽毛球
  2. 根据球拍的角度,去实现对击打羽毛球的模拟,包括球速以及角度
  3. 背后场馆观众,可以通过纹理贴图实现

以下是网络上一些游戏的截图:

Alt text

1 火柴人控制原理

我是基于“GAMES105-计算机角色动画基础”课程,来建模以及控制火柴人动作的。

关节的种类

因为实现的是二维平面的运动,所以关节的自由度为1,只会在一个平面上进行顺时针or逆时针的旋转。

前向运动学

从根节点往叶结点乘变换矩阵

  • 朝向:关节的局部坐标系相对于世界坐标系的旋转
  • 旋转矩阵的逆就是旋转矩阵的转置: 正交矩阵的性质

某一个关节处的全局旋转矩阵 = 父关节旋转矩阵 * 该关节的局部旋转矩阵

2 火柴人建模(蓝色为关节):

举例来说,左手臂小臂的全局旋转矩阵 = 左手臂大臂的全局旋转矩阵 左手臂小臂的局部旋转矩阵;右小腿的全局旋转矩阵 = 右大腿的全局旋转矩阵 右小腿的局部旋转矩阵……

在具体实现时,通过glPushMatrix(),先把当前的矩阵压入栈中,然后进行旋转,最后通过glPopMatrix(),把当前矩阵弹出栈,恢复到之前的状态。

部分代码实现:

1
2
3
4
5
6
7
8
9
glPushMatrix();     // ArmA
glRotatef(armA, 0.f, 0.f, 1.f);
glPushMatrix(); // ArmB
glTranslatef(24.f, -2.f, 0.f);
glRotatef(armB, 0.f, 0.f, 1.f);
drawArmB();
glPopMatrix(); // ArmB
drawArmA();
glPopMatrix(); // ArmA

这里的实现中,ArmA表示大臂,ArmB表示小臂。

例如,我们想要小臂进行旋转,需要先将大臂的旋转矩阵压入栈中。在此基础之上,才能进行小臂的局部旋转。小臂是依托于大臂的,两者之间存在一定耦合性。

2.1 移动

由建模直接画出,但是关节间是存在依赖关系的。

左移、右移,我模拟了人走路的姿势:

Alt text

可以看到,正是小腿基于大腿的旋转,最后的移动是自然的,符合人类走路的方式。

1
2
3
4
right_legA = -legA_angle_vec[index];
right_legB = -legB_angle_vec[index];
left_legA = legA_angle_vec[index];
index = (++index) % 5;

具体实现时,因为人的走路姿势是循环的,我构造了两个数组,分别表示大腿和小腿的旋转角度。通过 index = (++index) % 5来更新下标,达到循环遍历数组的效果。这里我为了游戏的流畅性,将循环的频率设置为5,因此最后的人物移动可能看起来是会有点间断,不连续。

2.2 跳跃

火柴人的跳跃,我结合了物理学建模。

实时垂直方向速度:

实时垂直方向位置:

这里时间 t的获取我是通过调用 <chrono>这个库来实现的。首先,先定义一个 Common.cpp源文件,定义startTime变量

std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();

因此,startTime变量在程序开始运行时就被定义了。在其他文件中,我们可以一样使用 <chrono>库获得当前时间,再用extern引用startTime,两者相减,便可以知道程序运行的时间。出于方便,我又定义了一个 Common.h头文件,用 extern引用startTime。其他文件直接 include 'Common.h'即可,不需要重复extern。

因此,当我们想实现跳跃时。我们需要知道两个关于时间的变量,分别是跳跃起始时间,当前时间。当前时间-跳跃起始时间,便是跳跃时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// myglWideget.cpp
doubleGetCurrentTimeInSeconds() {
usingnamespacestd::chrono;

returnduration_cast<duration<double>>(steady_clock::now() - startTime).count();
}

player1->jumpStartTime = GetCurrentTimeInSeconds();

-----------------------------------------------------------------------------------

//player.cppp
float currentTime = playerGetCurrentTimeInSeconds();
float timeSinceJump = currentTime - jumpStartTime;

jumpHeight = jumpVelocity * timeSinceJump - 0.5f * GRAVITY * timeSinceJump * timeSinceJump;
right_legB = -20.f;
if (jumpHeight <= 0) {
isJumping = false;
jumpHeight = 0;
right_legB = 0.f;
}

Alt text

2.3 挥拍

挥拍的实现,和上面的思路基本一致。唯一不同的是,手臂旋转的角度,通过$sin(\theta)$实现,其中$\theta$属于[0,180]度。

1
2
3
4
5
6
if (timeSinceWave > 0.5) {
isWaving = false;
return;
}
arm = -135 * sin(2 * PI * timeSinceWave);
armA = -100 * sin(2 * PI * timeSinceWave);

-135和-100度,分别是持拍的手臂和大臂的旋转角度。

另外,我还建模了发球时人物的挥拍,具体实现原理类似。

Alt text

3 羽毛球运动建模

3.1 球速

Alt text

建模击打羽毛球后,羽毛球的旋转角度,以及速度大小。

$|v_{球}| = |v_{球}|+|v_{拍}|+k\theta-C$

击打回羽毛球后,羽毛球的球速等于当前羽毛球的球速+固定拍子动能+拍子旋转角度-羽毛球球拍吸收的动能。

这是对羽毛球运动的一个简单的建模,并且符合现实。随着拍子旋转角度越大,对羽毛球速度的提升越大。对应于$k\theta$。并且羽毛球拍会吸收固定的动能,对应于$-C$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
float cur_bmt_speed = sqrt(curspeed_x * curspeed_x + curspeed_y * curspeed_y);
cur_bmt_speed += 2.f * theta - 100.f + 110.f;

if (whohit == 1) {
curspeed_x = cur_bmt_speed * cos((60 - theta) * PI / 180.f);
curspeed_y = cur_bmt_speed * sin((60 - theta) * PI / 180.f);
prespeed_x = curspeed_x;
prespeed_y = curspeed_y;
}
elseif (whohit == 2) {
curspeed_x = -cur_bmt_speed * cos((60 - theta) * PI / 180.f);
curspeed_y = cur_bmt_speed * sin((60 - theta) * PI / 180.f);
prespeed_x = curspeed_x;
prespeed_y = curspeed_y;
}

3.2 球速更新

飞行时,通过这两个公式更新球速:

对应羽毛球的角度,通过反正切函数来获得。

实现代码

1
2
3
4
5
6
7
8
9
10
curposition_x = preposition_x + timeSinceHit * curspeed_x;
curposition_y = preposition_y + prespeed_y * timeSinceHit - 0.5 * GRAVITY * timeSinceHit * timeSinceHit;
curspeed_y = prespeed_y - GRAVITY * timeSinceHit;

if (prespeed_x - FRICTION * timeSinceHit > 0)
curspeed_x = prespeed_x - FRICTION * timeSinceHit;
if (whohit == 1)
angle = 90 + atan(curspeed_y / curspeed_x) * rad_to_deg;
else
angle = -90 + atan(curspeed_y / curspeed_x) * rad_to_deg;

羽毛球检测击中

根据球拍的全局旋转矩阵获得当前时刻的球拍的中心坐标,判断羽毛球当前时刻的位置是否与球拍中心位置接近。只要两者的距离小于设定的阈值,便视为击中。

1
2
3
4
5
6
7
8
9
10
float racket_global_angle = player1->arm + 135;
float d_x = (30.f + 40.f * sqrt(2.)) * cos(racket_global_angle / 180 * PI);
float d_y = (30.f + 40.f * sqrt(2.)) * sin(racket_global_angle / 180 * PI);

float cur_center_x = player1->position_x + (float)(95.0 / 120 * 10) + d_x;
float cur_center_y = 95.f + d_y;

if (badminton->curposition_x >= cur_center_x - bounding_box_size && badminton->curposition_x <= cur_center_x + bounding_box_size && badminton->curposition_y >= cur_center_y - bounding_box_size && cur_center_y + bounding_box_size >= badminton->curposition_y) {
return1;
}

最终的效果:

Alt text

实验总结

这个大作业,还是蛮有意思的。它设计到了方方面面。包括物理,数学的建模,以及人物运动时,身体各个关节处的耦合性。我是学习了Games105后才对其有了浅薄的理解。从头到尾,我们的大作业都是100%原创的(至少我的部分是),这还是蛮有挑战的。所幸,在老师的建议和同学的帮助下(深夜一起debug,感谢我的好homie 李涛),我们最终完成了这个大作业。我的收获也是颇丰,虽然火柴人的建模很简单,但是我对前向运动学知识的理解加深了很多。父关节和子关节之间的联系,是很关键的,这是让火柴人动起来的重要因素!!