[Robot Laboratory 4] 1. ODE Simulation
Updated:
로봇학실험4 (Robot Laboratory4) Project
해당 수업에서 배운 내용은 다음과 같다.
- ODE(Open dynamics Engine)를 통한 기구학 Simulation
- MFC UI 제작
- ATMega128과 Maxon 모터를 이용한 각종 모터 제어
해당 포스팅에서는 ODE에 대한 설명을 할 것이다. 코드는 깃 허브 Repository에 업로드를 해 놓았으니 아래 링크를 타고 들어가 참고하기 바란다.
→
Repository로 이동
ODE (Open Dynamics Engine)
물리 엔진이며, 사실 조금 오래된 프로그램이라고 한다. MFC에 추가하여 사용할 수 있으며, 자세한 설치법을 설명하기 보다는 코드에 대해 설명하므로, 설치법을 알고 싶다면 아래의 다른 이의 블로그 포스팅을 참고해주기 바란다. 아래 블로그 주인장이 설명을 잘 해 주신다.
→
ODE 설치 관련 글로 이동
이를 MFC에서 실행하면 다음과 같이 나온다.

또한, MFC 코드를 열어보면 다음과 같은 cpp 파일들이 있는 것을 볼 수 있다.

여기에서 “ODE.cpp”파일에서 시뮬레이션을 위한 코딩을 하게 된다.
시뮬레이션 초기 설정
시뮬레이션을 위한 기본 Parameters 설정 값은 다음 표와 같다.

구분 | 링크1(빨강) | 링크2(초록) | 링크3(파랑) |
---|---|---|---|
길이 | 0.5 | 1.0 | 0.5 |
무게 | 0.5 | 1.0 | 0.5 |
반경 | 0.125 | 0.125 | 0.125 |
질량중심 | 0.25 | 0 | -0.75 |
관절 위치 | 0 | 0.5 | -0.5 |
해당 프로젝트에서 모터를 돌릴 때 아래를 기준으로 돌리기 때문에 링크 3의 관절 위치가 지면을 뚫고 들어간 위치로 지정하게 되었다.

ODE 코드 설명
먼저 가상 물리 엔진에 대한 가상 환경을 구축한다.
1
2
3
4
5
6
7
8
9
10
11
12
// 가상 물리 엔진의 가상 환경을 구축
void InitODE() {
//TO DO
dInitODE(); // ODE에 대한 초기 설정 함수 선언
g_World = dWorldCreate(); // 가상 물리 엔진 월드 설정
g_Space = dHashSpaceCreate(0); // 월드의 공간 설정
g_Contactgroup = dJointGroupCreate(0);
dWorldSetGravity(g_World, 0, 0, -GRAVITY); // 각 축에 작용하는 중력 설정
g_Ground = dCreatePlane(g_Space, 0, 0, 1, 0); // 지면과의 충돌이 있을 시 사용
dWorldSetCFM(g_World, 1e-5); // 생성한 월드의 기본적인 한계력(제약 사항) 설정
}
가상 환경이 구축이 되었다면, 가상환경에서 물체를 그리는 코드를 작성한다.
1
2
3
4
5
6
7
8
9
void InitDrawStuff() {
g_Fn.version = DS_VERSION;
g_Fn.start = &StartDrawStuff;
g_Fn.step = &SimLoopDrawStuff;
g_Fn.command = &CommandDrawStuff;
g_Fn.stop = StopDrawStuff;
g_Fn.path_to_textures = DRAWSTUFF_TEXTURE_PATH;
}
가상환경에서의 지면과 접촉할 때 지면을 뚫고가지 않도록 하는 코드를 작성한다. 그런데 해당 프로젝트에서는 지면을 뚫고 가야 내가 원하는 자세를 취할 수 있으므로 해당 함수는 결과적으로는 사용하지 않았다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//지면과 접촉할시 출돌이 일어나게 코드를 작성
static void SpaceCollide(void* data, dGeomID o1, dGeomID o2) {
int i;
static const int N = 5; //접촉면의 최대개수
dContact contact[N];
bool isGround = (o1 == g_Ground) || (o2 == g_Ground); //지면과 접촉햇는지?
if (isGround) {
int n = dCollide(o1, o2, N, &contact[0].geom, sizeof(dContact));
for (int i = 0; i < n; i++) {
contact[i].surface.mu = 0.1; // 마찰계수
contact[i].surface.mode = dContactBounce;
contact[i].surface.bounce = 1.0;// 반발계수
contact[i].surface.bounce_vel = 0.0;// 튀어오르기 위한 최소속도
dJointID c = dJointCreateContact(g_World, g_Contactgroup, &contact[i]);
dJointAttach(c, dGeomGetBody(contact[i].geom.g1), dGeomGetBody(contact[i].geom.g2));
}
}
}
시뮬레이션을 실행할 때 물체를 바라보는 시점을 변경하는 코드이다. 이번 프로젝트에서는 지면을 뚫고 들어가야 하기에, 시점이 지평선에 위치해야 지면을 뚫고 들어가는 물체도 볼 수 있다.
1
2
3
4
5
6
7
void StartDrawStuff() {
// 시작 시 시점 설정
float dPos[3] = { 3.0, 0.0, -0.1 };
float dRot[3] = { 180.0, 0.0, 0.0 };
dsSetViewpoint(dPos, dRot);
}
그리고 시뮬레이션 상에서 오브젝트를 그리는 코드이다. 오브젝트의 스펙은 위 표로 설명한 것과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void InitRobot()
{
// Body 생성
dMass mass;
dMatrix3 R; // 4 X 3 행렬을 생성
// 질량 중심점( 평행이동 )
dReal x[MAX_JOINT_NUM + 1] = { 0.0, 0.0, 0.0 };
dReal y[MAX_JOINT_NUM + 1] = { 0.0, 0.0, 0.0 };
dReal z[MAX_JOINT_NUM + 1] = { 0.25, 0.0, -0.75 }; // Z축으로 서 있는 상태를 default로 두었기에 Z축에만 값을 넣는다.
// 링크 자세 설정
dReal ori_x[MAX_JOINT_NUM + 1] = { 0.0, 0.0, 0.0 };
dReal ori_y[MAX_JOINT_NUM + 1] = { 0.0, 0.0, 0.0 };
dReal ori_z[MAX_JOINT_NUM + 1] = { 1.0, 1.0, 1.0 };
dReal ori_q[MAX_JOINT_NUM + 1] = { 0.0, 0.0, 0.0 }; // 해당 방향으로 틀어진 정도를 설정
// 한 덩이의 링크 길이 ( 질량 중심점을 기준으로 양방향으로 줄어듦 )
dReal length[MAX_JOINT_NUM + 1] = { 0.5, 1.0, 0.5 };
// 한 덩이의 무게
dReal weight[MAX_JOINT_NUM + 1] = { 0.5, 1.0, 0.5};
// 캡슐의 반지름
dReal r[MAX_JOINT_NUM + 1] = { 0.125f, 0.125f, 0.125f };
//body 생성
for (int i = 0; i < MAX_JOINT_NUM + 1; i++) {
g_oObj[i].body = dBodyCreate(g_World);
dBodySetPosition(g_oObj[i].body, x[i], y[i], z[i]);
dMassSetZero(&mass);
dMassSetCapsuleTotal(&mass, weight[i], 1, r[i], length[i]);
dBodySetMass(g_oObj[i].body, &mass); // 물체의 위치 설정
g_oObj[i].geom = dCreateCapsule(g_Space, r[i], length[i]);
dGeomSetBody(g_oObj[i].geom, g_oObj[i].body);
dRFromAxisAndAngle(R, ori_x[i], ori_y[i], ori_z[i], ori_q[i]);
// 회전축 x, y, z를 중심으로 각 q만큼 회전하는 회전행렬 R를 생성함
dBodySetRotation(g_oObj[i].body, R); // 물체의 형태 방향 등등 설정
}
//joint 관련 설정 및 생성
// 관절의 좌표 원점 설정
dReal djointx[MAX_JOINT_NUM + 1] = { 0.0, 0.0, 0.0 };
dReal djointy[MAX_JOINT_NUM + 1] = { 0.0, 0.0, 0.0 };
dReal djointz[MAX_JOINT_NUM + 1] = { 0.0, 0.5, -0.5 };
// 회전축의 로테이션 방향
dReal axis_x[MAX_JOINT_NUM + 1] = { 0.0, 1.0, 1.0 };
dReal axis_y[MAX_JOINT_NUM + 1] = { 0.0, 0.0, 0.0 };
dReal axis_z[MAX_JOINT_NUM + 1] = { 0.0, 0.0, 0.0 };
//joint 결합
//먼저 fixed joint 부터 world와 연결
g_oJoint[0] = dJointCreateFixed(g_World, 0);
dJointAttach(g_oJoint[0], g_oObj[0].body, 0);
dJointSetFixed(g_oJoint[0]);
//Hinge Joint 생성 및 결합
for (int i = 1; i < MAX_JOINT_NUM + 1; i++) {
g_oJoint[i] = dJointCreateHinge(g_World, 0); // Hinge Joint 생성
dJointAttach(g_oJoint[i], g_oObj[i].body, g_oObj[i - 1].body); // Joint 결합
dJointSetHingeAnchor(g_oJoint[i], djointx[i], djointy[i], djointz[i]); // Hinge의 중심점을 설정
dJointSetHingeAxis(g_oJoint[i], axis_x[i], axis_y[i], axis_z[i]); // Hinge의 회전축 벡터 설정
}
}
또한, 가상환경에서 오브젝트가 움직일텐데, 움직일 때 스위치마냥 딱딱 떨어지게만 움직이면 수렴하는 과정을 볼 수 없으므로, 움직이는 것에 대한 제어기를 설정해준다. 단순하게 P 제어기로 코드를 작성했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void PControl()
{
//joint control을 위한 간단한 p control 만들기
dReal Kp = 1, fmax = 100.0;
dReal derror_q[MAX_JOINT_NUM + 1];
for (int i = 1; i < MAX_JOINT_NUM + 1; i++)
{
g_cur_q[i-1] = dJointGetHingeAngle(g_oJoint[i]);
if (g_tar_q[i - 1] - g_cur_q[i - 1] > 180.0*DEG2RAD)
{
g_cur_q[i - 1] += 359.9*DEG2RAD;
}
if (g_tar_q[i - 1] - g_cur_q[i - 1] < -180.0*DEG2RAD)
{
g_cur_q[i - 1] -= 359.9*DEG2RAD;
}
// g_cur_q[i] = dJointGetHingeAngle(g_oJoint[i]);
derror_q[i - 1] = g_tar_q[i - 1] - g_cur_q[i - 1];
dJointSetHingeParam(g_oJoint[i], dParamVel, Kp*derror_q[i - 1]);
dJointSetHingeParam(g_oJoint[i], dParamFMax, fmax);
}
}
그리고 MCU로 따지면 제어주기인 함수이다. 해당 함수에서는 앞서 작성된 오브젝트와 P제어기를 토대로 하여 목표 값을 입력받으면, 해당 각도로 오브젝트를 회전시키게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 여기가 MCU로 따지면 제어주기 코드. ODE의 움직임을 여기서 설정함
void SimLoopDrawStuff(int pause) {
DataType_t jointData;
ControlData_t graph;
bool connected;
GET_SYSTEM_MEMORY("JointData", jointData);
g_tar_q[0] = jointData.Q_tar[0];
g_tar_q[1] = jointData.Q_tar[1];
jointData.Q_cur[0] = g_cur_q[0];
jointData.Q_cur[1] = g_cur_q[1];
SET_SYSTEM_MEMORY("JointData", jointData);
GET_SYSTEM_MEMORY("connect", connected);
GET_SYSTEM_MEMORY("graph", graph);
if (connected)
g_tar_q[0] = graph.position;
// 첫 번째 관절에 대한 예외처리 코드
if (g_tar_q[0] >= 360.0 * DEG2RAD) g_tar_q[0] = fmod(g_tar_q[0], 360.0 * DEG2RAD);
if (g_tar_q[0] <= -360.0 * DEG2RAD) g_tar_q[0] = fmod(g_tar_q[0], 360.0 * DEG2RAD);
// 두 번째 관절에 대한 예외처리 코드
if (g_tar_q[1] >= 360.0 * DEG2RAD) g_tar_q[1] = fmod(g_tar_q[1], 360.0 * DEG2RAD);
if (g_tar_q[1] <= -360.0 * DEG2RAD) g_tar_q[1] = fmod(g_tar_q[1], 360.0 * DEG2RAD);
// 해당 관절 각도 값이 360도 보다 크면 360도 안에서 값이 변하도록 360을 빼 주고,
// -360도보다 값이 작으면 이 또한 마찬가지로 음수 영억 -360도 안에서 값이 변하도록 360을 더해준다.
PControl(); // 제어기 선언
//루프에 바디를 그리는 함수를 집어넣는 과정: 각 물체마다 설정해 주어야 함
dReal dR, dLength;
// dSpaceCollide(g_Space, 0, &SpaceCollide); // 지면과의 충돌 고려 ON
dsSetColor(1, 0, 0); // 컬러 설정 ( RGB 255를 100%(1)로 보고 설정 )
dGeomCapsuleGetParams(g_oObj[0].geom, &dR, &dLength); // 강체 생성: 캡슐 형태
// Parameter로 캡슐 정보, 반경, 길이 정보를 넘김
dsDrawCapsuleD(dBodyGetPosition(g_oObj[0].body), // 디자인된 강체를 작업하는 월드에 생성
dBodyGetRotation(g_oObj[0].body), (float)dLength, (float)dR); // ㄴ> 월드에 해당 강체를 그리기 위해
// 강체에 대한 정보들을 Parameter로 넘김
dsSetColor(0, 1, 0);
dGeomCapsuleGetParams(g_oObj[1].geom, &dR, &dLength);
dsDrawCapsuleD(dBodyGetPosition(g_oObj[1].body),
dBodyGetRotation(g_oObj[1].body), (float)dLength, (float)dR);
dsSetColor(0, 0, 1);
dGeomCapsuleGetParams(g_oObj[2].geom, &dR, &dLength);
dsDrawCapsuleD(dBodyGetPosition(g_oObj[2].body),
dBodyGetRotation(g_oObj[2].body), (float)dLength, (float)dR);
double dt = 0.01;
dWorldStep(g_World, dt);
// dJointGroupEmpty(g_Contactgroup);
}
최종적으로 ODE를 실행시키기 위한 함수이다.
1
2
3
4
5
6
7
8
9
10
void RunODE(size_t width, size_t height) {
//TO DO
InitDrawStuff();
InitODE();
InitRobot();
dsSimulationLoop(0, 0, width, height, &g_Fn);
}
그러면 다음과 같이 시뮬레이션이 실행이 됨을 알 수 있다.

Leave a comment