중급 튜토리얼 1-2

Ogre3D 삽질란/Intermediate Tutorial 1 2008. 12. 9. 20:05

장면 설정하기

시작하기에 앞서 MoveDemoApplication에서 설정했던 3개의 변수를 살펴봅시다. mEntity 생성시킬 엔티티를 가집니다. mNode 생성시킬 노드, mWalkList 객체가 걷게 포인트 위치정보를 포함하게 것입니다.

MoveDemoApplication::createScene함수로 가서 다음 코드를 추가하세요. 우선 밝은 주변광을 넣어서 객체들이 보이도록 만듭시다.

        // Set the default lighting.

        mSceneMgr->setAmbientLight(ColourValue(1.0f, 1.0f, 1.0f));

이제 가지고 로봇을 생성해 봅시다. 로봇 엔티티를 생성하고 SceneNode 만든다음 로봇을 매달아 놓읍시다.

       // Create the entity

       mEntity = mSceneMgr->createEntity("Robot", "robot.mesh");

 

       // Create the scene node

       mNode = mSceneMgr->getRootSceneNode()->

           createChildSceneNode("RobotNode", Vector3(0.0f, 0.0f, 25.0f));

       mNode->attachObject(mEntity);

매우 쉬운수준의 코드이므로 자세한 설명은 생략합니다. 다음코드는 로봇이 어디로 이동해야 할지를 설정합니다. STL 모르는 분들을 위해서 간략하게 설명하자면 deque객체는 double ended queue 효과적으로 구성한 형태 입니다. 여기서는 deque 일부분만 사용할겁니다. push_front push_back함수는 각각 deque 앞과 뒤에서 item 삽입하는 역할을 합니다. Front back함수는 각각 deque 앞과 뒤의 값을 리턴하는 역할을 합니다. The pop_front pop_back함수는 각각 deque 앞과 뒤의 item 삭제하는 역할을 합니다. 마지막으로 empty함수는 deque 비었는지에 대한 여부를 리턴합니다. 코드는 로봇이 움직일 목표점인 2개의 벡터를 deque 삽입합니다.

       // Create the walking list

       mWalkList.push_back(Vector3(550.0f,  0.0f,  50.0f ));

       mWalkList.push_back(Vector3(-100.0f,  0.0f, -200.0f));

이제는 로봇이 이동하고자 하는 목표점에 뭔가의 객체를 놓도록 합니다. 그렇게 하면 로봇이 어떠한 객체를 향해서 움직이는것 처럼 보일겁니다. Y 좌표가 음수인 것을 감안하세요. , 로봇이 움직이는 목표점의 하단에 놓이게 됩니다. 로봇이 객체에 도달하면 객체를 밟고 서있는 모습이 것입니다.

       // Create objects so we can see movement

       Entity *ent;

       SceneNode *node;

 

       ent = mSceneMgr->createEntity("Knot1", "knot.mesh");

       node = mSceneMgr->getRootSceneNode()->createChildSceneNode("Knot1Node",

           Vector3(0.0f, -10.0f,  25.0f));

       node->attachObject(ent);

       node->setScale(0.1f, 0.1f, 0.1f);

 

       ent = mSceneMgr->createEntity("Knot2", "knot.mesh");

       node = mSceneMgr->getRootSceneNode()->createChildSceneNode("Knot2Node",

           Vector3(550.0f, -10.0f,  50.0f));

       node->attachObject(ent);

       node->setScale(0.1f, 0.1f, 0.1f);

 

       ent = mSceneMgr->createEntity("Knot3", "knot.mesh");

       node = mSceneMgr->getRootSceneNode()->createChildSceneNode("Knot3Node",

           Vector3(-100.0f, -10.0f,-200.0f));

       node->attachObject(ent);

       node->setScale(0.1f, 0.1f, 0.1f);

마지막으로 장면들이 보이게 하기 위해 카메라를 적당하게 설정합니다. 장면이 보이도록 카메라를 좋은 위치로 옮깁니다.

       // Set the camera to look at our handiwork

       mCamera->setPosition(90.0f, 280.0f, 535.0f);

       mCamera->pitch(Degree(-30.0f));

       mCamera->yaw(Degree(-15.0f));

컴파일 실행시키면 다음과 같은 화면이 보일겁니다 :


다음섹션으로 넘어가기전에 MoveDemoApplication::createFrameListener 에서 호출되는 MoveDemoListener 생성자를 확인하세요. BaseFrameListener 기본 매개변수와 함께 SceneNode, Entity, deque 전달됩니다.

 

애니메이션

이제 기본적인 애니메이션을 다뤄봅시다. 오우거에 있어서 애니메이션은 무척 단순한 부분입니다. 일단 엔티티객체로부터 AnimationState 구한다음 옵션을 설정하고 enable시키면 됩니다. 이렇게 애니메이션을 활성화 시킵니다. 그리고 애니메이션이 실행되기 위해서 프레임별 시간도 설정해 줘야 합니다. 과정들을 한방에 끝내봅시다. MoveDemoListener 생성자로 가서 다음 코드를 추가하세요 :

       // Set idle animation

       mAnimationState = ent->getAnimationState("Idle");

       mAnimationState->setLoop(true);

       mAnimationState->setEnabled(true);

두번째 라인에서 엔티티로부터 AnimationState 얻습니다. 세번째 라인에서 setLoop(true) 호출하는데 이건 애니메이션을 계속해서 반복시켜줍니다. 어떤 애니메이션(죽은상황같은) 경우는 true대신 false 설정해줘야 할지도 모릅니다. 네번째 라인에서는 애니메이션을 활성화 시켜줍니다. 그런데 여기서 잠깐.. “Idle”상태를 어디서 구한걸까요? 신기한 상태값은 어디서부터 걸까요? 모든 메쉬는 그것들만의 애니메이션상태정의값 셋트를 가지고 있습니다. 작업하려는 각각의 모든 메쉬들이 어떤 애니메이션들을 가지고 있는지 확인하려면 OgreMeshViewer 다운로드받고 직접 해당 메쉬를 확인해야 합니다.

이제 컴파일 실행시키면.. 아무것도 바뀐게 없습니다. 왜냐하면 시간마다 프레임을 갱신시켜줘야 합니다. MoveDemoListener::frameStarted함수의 시작부분에 다음코드를 추가하세요 :

        mAnimationState->addTime(evt.timeSinceLastFrame);

다시 빌드하고 실행시켜보세요. 로봇이 서서 idle 애니메이션을 수행하고 있음을 있습니다.

 

로봇 움직이기

이제 로봇이 멋지게 point to point 움직이게 해봅시다. 시작하기에 앞서 MoveDemoListener클래스에 저장된 몇가지 변수들을 설명하고자 합니다. 로봇이 움직이는데 4가지 변수가 사용될 것입니다. 로봇이 움직이는 방향을 mDirection 저장합니다. 현재의 로봇 도착지점은 mDestination 저장되며 도착지점까지 남은 거리는 mDistance 저장됩니다. 마지막으로 로봇의 이동속도는 mWalkSpeed 저장됩니다.

우선 MoveDemoListener 생성자 내용을 모두 지워야 합니다. 지금 작성된 코드와는 약간 다른 코드로 교체해야 합니다. 첫째로 해야 일은 클래스의 변수들을 셋업해야 합니다. 걷는속도는 35 units/sec입니다. 여기서 중요한 사항이 하나 있습니다. mDirection 영벡터로 설정하게 될텐데 이유는 나중에 변수로 로봇이 움직이고 있는지 아닌지를 판별하게 것이기 때문입니다.

        // Set default values for variables

        mWalkSpeed = 35.0f;

        mDirection = Vector3::ZERO;

일단 로봇이 움직이는데 필요한 작업은 끝입니다. 로봇을 움직이려면 단순히 로봇의 애니메이션만 바꿔주면 됩니다. 하지만 우리는 로봇이 이동할 도착지점이 있는 경우에만 움직이게 하고 싶습니다. 이런 이유로 nextLocation함수를 호출하게 됩니다. MoveDemoListener::frameStarted함수의 윗부분, AnimationState::addTime호출 이전에 다음 코드를 추가하세요 :

       if (mDirection == Vector3::ZERO)

       {

           if (nextLocation())

           {

               // Set walking animation

               mAnimationState = mEntity->getAnimationState("Walk");

               mAnimationState->setLoop(true);

               mAnimationState->setEnabled(true);

           }

       }

지금 실행시켜보면 로봇이 제자리에서 걷게 겁니다. 왜냐하면 영벡터인 상태에서 로봇이 시작되었고 MoveDemoListener::nextLocation함수는 항상 true값만 리턴하기 때문입니다. 나중 단계에서 MoveDemoListener::nextLocation함수에 지능적인 동작을 추가할 계획입니다.

이제 로봇을 장면에서 제대로 움직이게끔 만들어 봅시다. 그럴려면 프레임마다 조금씩 로봇을 이동시켜야 합니다. MoveDemoListener::frameStarted함수로 가서 AnimationState::addTime호출 이전에 다음 코드를 추가하세요. 코드가 로봇을 실제로 움직여 줄겁니다; mDirection != Vector3::ZERO. mWalkspeed evt.timeSinceLastFrame변수와 곱해주는 이유는 framerate 변하더라도 walkspeed 일정하게 유지시켜 주기 위함입니다. 만약 Real move = mWalkspeed 움직인다면 느린컴퓨터에서는 느리게 움직이고 빠른컴퓨터에서는 빠르게 움직일 겁니다.


        else

        {

            Real move = mWalkSpeed * evt.timeSinceLastFrame;

            mDistance -= move;

이제 로봇이 목표위치를 지나치는지감시할 필요가 있습니다. 말은 mDistance값이 현재 0보다 낮다면 이동할 위치를 건너뛰고이동할 위치를 다음지점으로 설정해야 합니다. mDirection값을 영벡터로 설정했습니다. 만약 nextLocation함수가 mDirection값을 바꾸지 않는다면(예를들면 갈곳이 없는경우) 이상 움직이지 않게 됩니다.

            if (mDistance <= 0.0f)

            {

                mNode->setPosition(mDestination);

                mDirection = Vector3::ZERO;

이제 다음지점으로 옮길 있게 되었습니다. 이제 다음지점으로 모션을 설정해야 합니다. 다음지점으로 이동해야 시점에서 적절한 애니메이션을 설정할 있습니다; 이동해야 또다른 다음지점이 있다면 걷고 더이상 도착지점이 없다면 idle입니다. 더이상 곳이 없을경우 idle 애니메이션을 설정하는것은 간단한 일입니다.

               // Set animation based on if the robot has another point to walk to.

               if (! nextLocation())

               {

                   // Set Idle animation                    

                   mAnimationState = mEntity->getAnimationState("Idle");

                   mAnimationState->setLoop(true);

                   mAnimationState->setEnabled(true);

               }

               else

               {

                   // Rotation Code will go here later

               }

           }

Queue 이동해야 지점이 있다고 해서 다시 걷기애니메이션을 설정할 필요는 없습니다. 이미 걷고 있는 상태일것이기 때문에 다시 하라고 알릴필요는 없습니다. 하지만 다른 포인트로 이동해야 한다면 지점을 바라볼 있도록 회전시켜야 합니다. else구문안에 주석표시를 달아놓읍시다; 나중에 여기로 다시 돌아와야 하거든요.

코드는 목표지점에 거의 도달했을때를 다룹니다. 지금은 한참 움직이고 있을 상태인 일반적인 경우를 다룰때 입니다. 이동방향으로 로봇을 이동변수값 만큼 이동시킵니다. 다음 코드를 추가함으로써 완성됩니다 :

            else

            {

                mNode->translate(mDirection * move);

            } // else

        } // if

거의 다했습니다. 이동에 필요한 변수값을 설정하는 부분을 제외하고는 모두 제대로 동작합니다. 만약 적절하게 이동변수값을 설정할 있게된다면 로봇은 스스로 움직일 것입니다. MoveDemoListener::nextLocation함수를 찾아가세요. 이상 움직일 지점이 없다면 false 리턴해야 합니다. 구현은 함수의 첫줄이 겁니다. (함수의 하단부분에 true 리턴하는 구문은 남겨둬야 합니다.)

        if (mWalkList.empty())

            return false;

이제 변수값을 설정해 봅시다. 먼저 deque에서 목표지점을 하나 건너뜁니다. 방향벡터는 목표지점으로부터 SceneNode 현재위치를 뺄셈연산으로 구해집니다. 여기서 작은 문제가 있습니다. 어떻게 mDirection값을 frameStarted에서 곱하는지 기억하십니까? 그렇게 하려면 방향벡터는 단위벡터(1 기준이 되는 길이) 되어야 합니다. Normalise함수가 문제를 해결해 주고 예전 벡터길이를 리턴합니다. 리턴된 값은 목표지점까지의 거리를 설정하는데 있어서도 유용합니다.

       mDestination = mWalkList.front();  // this gets the front of the deque

       mWalkList.pop_front();             // this removes the front of the deque

 

       mDirection = mDestination - mNode->getPosition();

       mDistance = mDirection.normalise();

컴파일하고 실행시켜보세요. 돌아갑니다! Sotra. 로봇이 모든포인트를 거치기는 하는데 항상 Vector3::UNIT_X방향(기본값) 바라보는군요. 로봇이 움직이는 방향쪽으로 바라보게 바꿔줘야 합니다.

로봇이 바라보고 있는 방향을 구하고 옳바른 방향으로 rotate함수를 이용해 회전시켜줘야 합니다. 이전과정에서 주석표시를 해둔 지점으로 가서 다음 코드를 추가하세요. 첫번째 줄은 로봇이 바라보는 방향을 구합니다. 두번째 라인은 현재방향에서 목표방향까지의 회전을 표현하는 사원수를 생성합니다. 세번째 라인은 실제로 로봇을 회전시킵니다.

        Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;

        Ogre::Quaternion quat = src.getRotationTo(mDirection);

        mNode->rotate(quat);

기초 튜토리얼 4에서 간단하게 사원수를 다루었습니다. 하지만 여기서 처음으로 사용되었습니다. 기본적으로 사원수는 3차원 공간에서의 회전을 표현합니다. 오우거에 있어서 객체가 공간의 어디에 위치되고 어떻게 회전되어야 할지에 대해서 쓰입니다. 함수의 첫번째 줄에서 호출된 getOrientation함수는 공간속의 로봇에 대한 사원수를 리턴합니다. 오우거가 어느면이 로봇의 정면인지 없으므로 위치정보를 UNIT_X 벡터(원래 바라보고 있던 로봇의 정면방향) 곱하여 로봇이 현재 바라보고 있는 방향을 구해야 합니다. 방향을 src변수에 저장하였습니다. 두번째 라인의 getRotationTo함수는 로봇이 바라보고 있는 방향으로부터 우리가 원하는 방향까지의 회전을 표현하는 사원수를 제공합니다. 세번째 라인에서는 새로운 방향으로 노드를 회전시킵니다.

지금까지 작성한 코드에는 한가지 문제점이 있습니다. SceneNode::rotate함수가 실패하는 특별한 경우입니다. 만약에 로봇을 정확하게 180도로 회전시키면 회전코드는 divide by zero 에러를 발생시킬 입니다. 경우 우리는 180 회전을 rotate함수를 안쓰고 yaw 처리할 입니다. 그러기 위해서 방금전의 3 라인들을 모두 지우고 다음코드로 교체합니다 :

       Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;

       if ((1.0f + src.dotProduct(mDirection)) < 0.0001f)

       {

           mNode->yaw(Degree(180));

       }

       else

       {

           Ogre::Quaternion quat = src.getRotationTo(mDirection);

           mNode->rotate(quat);

       } // else

if문으로 둘러쌓인 부분을 제외하고는 설명이 필요없는 부분들 입니다. 만약 2개의 벡터들이 서로 마주보고 있는 상황(2개의 각도가 180도인 경우)이라면 그들의 dot product값은 -1 됩니다. 그래서 만약 2개의 벡터를 서로 dotProduct함수로 처리 한다면결과값은 -1.0f 되고 180도로 yaw시켜야 합니다. 이외의 상황에서는 rotate 이용합니다. 1.0f 더한뒤 0.0001f보다 작은지를 검사할까요? floating point rounding error 잊지마세요. 2개의 부동소숫점 숫자를 바로 비교해서는 안됩니다. 마지막으로 알아두실점은 2개의 벡터들에 대한 dot product값은 [-1, 1] 사이로 떨어진다는점 입니다. 간결하게 설명되지는 않지만 그래픽 프로그래밍을 하려면 적어도 선형대수학에 대한 기본지식은 필요합니다! 최소한 Quaternion and Rotation Primer부분정도는 복습하시고 벡터와 행렬연산에 대한 기본서를 참고하시길 바랍니다.

코드가 모두 완성되었습니다! 컴파일후 데모를 실행하여 로봇이 주어진 점들 사이를 걷는것을 확인하세요.


: