군집 시뮬레이션 Flock Simulation

이번에는 조그만 적 우주선 여러 대가 큰 모선을 공격하는 동작을 시뮬레이션하는 방법에 대해 알아보겠습니다. 군집 시뮬레이션(Flock Simulation)이라고 불리는 이 기법은 새의 무리나, 파리 혹은 물고기의 움직임을 시뮬레이션 하기 위해 만들어진 기법입니다.  이 기법은 1987년 에 Craig W. Reynolds라는 사람이 쓴 글이 Computer Graphics 학회지에 게시되면서 인지도를 얻게 되었는데요 그 글은 아래를 방문하시면 읽어보실 수 있습니다.

Craig W. Reynolds, "Flocks, Herds, and Schools:A Distributed Behavioral Model", 1987, Computer Graphics, 21(4), July 1987, pp. 25-34.[http://www.cs.toronto.edu/~dt/siggraph97-course/cwr87/]

프로그래밍에 들어가기 전에 아래의 그림처럼 Blender에서 장면을 세팅합니다. 모선으로 사용할 큰 함선 한 대와 작은 우주선 여러 대를 만듭니다.

그리고 .dae 파일로 익스포트 합니다. 익스포트에 대한 사항은 이전 문서인 "OpenGL3.2 모델링 프로그램에서 만든 오브젝트(장면) 로딩하기"를 참고해 주세요.
이제 모선과 적 함정을 나타낼 클래스가 필요한데요. CShip이라는 클래스를 만들었고 이것을 상속받은 CMyShip과 CEnemyShip이 각각 모선과 적 함정을 표현합니다.
[ship.h]
#pragma once
class CObj;
#include "Transform.h"
class CShip : public CTransform
{
public:
CShip();
~CShip(void);

void SetMaxHP(float maxhp) {m_maxHP = maxhp;}
void SetCurHP(float curhp) {m_curHP = curhp;}
void LoadProperty(const char* filepath);
void Damage(float amount);
BOOL IsAlive() { return m_bAlive; }

protected:
//virtual void Destroyed();
protected:
BOOL m_bAlive;
float m_curHP;
float m_maxHP;
};

[myship.h]
#pragma once
#include "Ship.h"

class CMyShip : public CShip
{
public:
CMyShip();
~CMyShip(void);
void Update(float fElapsed);
void Draw(const glm::mat4& view);

private:
//UINT m_numFirePoint;
//std::vector<CFirePoint*> m_vFirePoints;
};

extern CMyShip* g_pMyShip;


[enemyship.h]
#pragma once
#include "Ship.h"

class CEnemyShip : public CShip
{
public:
CEnemyShip();
~CEnemyShip(void);
protected:
//virtual void Destroyed();
};
typedef std::vector<CEnemyShip*> ENEMYSHIPVECTOR;
extern ENEMYSHIPVECTOR g_vEnemyShips;

각 클래스들이 하는 일이 그렇게 많지 않아서 복잡하진 않습니다. 하지만 CShip이 상속받는 CTransform클래스는 조금 복잡한데요 왜냐하면 이 클래스가 함선의 이동에 관련된 일들을 해주기 때문입니다.

[CTransform.h]
#pragma once
#include "Obj.h"

class CTransform : public CObj
{
public:
CTransform(void);
~CTransform(void);
void Update(float fElapsedTime);
virtual void BuildBuffer(const aiScene* scene, const aiNode* nd, CObj* parentObj);

void SetAccel(float fAccel) { m_fCurAccel = m_fDestAccel = fAccel; }
void SetDeaccel(float fDeaccel) { m_fDeaccel = fDeaccel; }
void SetRotationSpeed(float fRotSpeed) { m_fCurRotSpeed = m_fDestRotSpeed = fRotSpeed; }
void SetDestDir(const glm::vec3& vec) { m_DestDir = vec; } // vec must be normalized.
void SetMaxSpeed(const float fMaxSpeed) { m_fMaxSpeed = fMaxSpeed; }
void SetDestSpeed(const float fDestSpeed);
void SetCurSpeed(const float fDestSpeed);
void ClearTransform();
void RecalcInitialMat();

const glm::vec3& GetCurDir() { return m_CurDir; }
virtual const glm::vec3& GetDestDir() { return m_DestDir; }
float GetCurSpeed() { return m_fCurSpeed; }
virtual float GetRotSpeed() { return m_fCurRotSpeed; }
virtual const glm::quat& GetCurRot() { return m_CurRot; }
const glm::vec3& GetPos() const { return m_CurPos; }
const glm::vec3& GetCurScale() const { return m_CurScale; }
void SetCurRot(glm::quat& q) { m_CurRot = q; }

private:
glm::vec3 m_CurPos; // 0.0, 0.0, 0.0
glm::vec3 m_DestPos; // 0.0, 0.0, 0.0
glm::vec3 m_CurScale; // 1.0, 1.0, 1.0
glm::vec3 m_DestScale; // 1.0, 1.0, 1.0
glm::quat m_CurRot;
glm::quat m_DestRot;

glm::vec3 m_CurDir; // 0.0, 1.0, 0.0
glm::vec3 m_DestDir; // 0.0, 1.0, 0.0

float m_fCurSpeed; //0
float m_fDestSpeed; //0
float m_fMaxSpeed; //1
float m_fCurAccel; //1
float m_fDestAccel; //1
float m_fDeaccel;   //1
float m_fCurRotSpeed; //0
float m_fDestRotSpeed;//0 
float m_fCurRotAccel; //1
float m_fDestRotAccel; //1
};
매 프레임마다 CTransform::Update()를 호출해서 함선들의 포지션을 업데이트 해줍니다. 각 함선은 CFlockSystem에 의해 컨트롤 되는 방향과 스피드를 가지고있습니다. CFlockSystem 클래스가 이 문서의 중요한 부분입니다. 소스를 한번 살펴보겠습니다.

[FlockSystem.h]
#pragma once
#include <vector>
#include "Boid.h"

class CShip;
class CFlockSystem
{
public:
CFlockSystem(void);
~CFlockSystem(void);
void AddBoid(CShip* ship);
void AddTarget(CShip* ship);
void Update();
glm::vec3 rule1(const CBoid* b);
glm::vec3 rule2(const CBoid* b);
glm::vec3 rule3(const CBoid* b);
glm::vec3 rule4(CBoid* b);
void DeleteBoid(CShip* ship);

private:
typedef std::vector<CBoid*> BOIDVECTOR;
BOIDVECTOR m_boids;
BOIDVECTOR m_targets;
};
AddBoid()를 호출함으로써 적 함선들을 Boid로 추가 시킬 수 있습니다. AddTarget()을 호출함으로써 모선을 등록할 수 있습니다. 보이드들은 이 타겟을 중심으로 움직임을 보이게 됩니다. 함수 rule1 부터 4까지는 보이드들이 어떤 움직임을 보일지를 결정합니다.

[rule1]
glm::vec3 CFlockSystem::rule1(const CBoid* b) // keep together
{
glm::vec3 pc;
UINT count=0;
glm::vec3 result;
BOIDVECTOR::iterator it = m_boids.begin(), itend = m_boids.end();
for (; it!=itend; it++)
{
if (*it != b)
{
count++;
pc += (*it)->GetPos();
}
}
if (count > 0)
{
pc /= count;
result = pc - b->GetPos();
}
return result;
}
Rule1은 함선들을 너무 분산되지 않도록 해줍니다. 다시 말해서, 모든 보이드들의 위치를 고려해서 중앙지점을 찾아 그곳의 좌표를 리턴해 줍니다. 

[rule2]
glm::vec3 CFlockSystem::rule2(const CBoid* b) // preventing collision with each other
{
glm::vec3 c;
BOIDVECTOR::iterator it = m_boids.begin(), itend = m_boids.end();
for (; it!=itend; it++)
if (*it != b)
if (glm::distance((*it)->GetPos(), b->GetPos()) < 1)
c -= (*it)->GetPos() - b->GetPos();

return c;
}
Rule 2는 서로간의 충돌을 방지해 주는 역할을 합니다.

[rule3]
glm::vec3 CFlockSystem::rule3(const CBoid* b) // go to target
{
glm::vec3 result = b->GetTarget() - b->GetPos();
return  glm::normalize(result);
}
Rule 3는 함선을 타겟으로 이동하게 끔 해줍니다.

[rule4]
glm::vec3 CFlockSystem::rule4(CBoid* b) // prevent collision with the Ship
{
glm::vec3 c;
glm::vec3 away;
UINT count=0;;
float fDist;
BOIDVECTOR::iterator it = m_targets.begin(), itend = m_targets.end();
for (; it!=itend; it++)
{
fDist = glm::distance((*it)->GetPos(), b->GetPos());
if (fDist < 4.0f)
{
count++;
away = b->GetPos() - (*it)->GetPos();
away *= -fDist/4.0f+1.0f;
c += away;
if (fDist<3.5f)
{
b->SetAwayPhase(TRUE);
}
}
}

return c;
}
Rule 4는 적 함선과 모선이 충돌하는 것을 방지해 줍니다. 적 함선이 모선에 다다르면 모선으로부터 멀어지도록 Away Phase를 설정합니다. 이 Phase는 랜덤한 초 동안 유지되고 모선으로부터 멀어지게 됩니다.

이 룰들은 Update함수에서 결합됩니다.
void CFlockSystem::Update()
{
BOIDVECTOR::iterator it = m_boids.begin(), itend = m_boids.end();
for (; it!=itend; it++)
{
glm::vec3 keepTogether, KeepDistance, gotoTargetNormalized, PrevectCollisionWithTarget;
keepTogether = rule1(*it);
KeepDistance = rule2(*it);
gotoTargetNormalized = rule3(*it);
PrevectCollisionWithTarget = rule4(*it);
(*it)->AddDir(keepTogether*0.01f +
  KeepDistance * 0.1f +
  gotoTargetNormalized * 2.0f +
  PrevectCollisionWithTarget * 3.0f);
}
it = m_boids.begin(), itend = m_boids.end();
for (; it!=itend; it++)
{
(*it)->Apply();
}

}
각 룰은 가중치 값을 가지고 있는데요 예를 들어 rule 1의 가중치 값은 0.01입니다. 이것은 매우 작은 값인데요 왜냐하면 적 함선들이 너무 뭉쳐서 다니면 모선에 의해 파괴되기 쉽기 때문입니다. 그래서 이 값을 작은 값으로 설정해 그들이 너무 뭉치지 않게 해 주는 것입니다.

아래는 결과 동영상입니다.

완성된 군집 시뮬레이션.



거대 모선에 레이저를 장착한 후.



거대한 모선을 공격하고 있는 소형 함선들이 상상 되시나요?

댓글

이 블로그의 인기 게시물