diff --git a/src/characters/character.cpp b/src/characters/character.cpp new file mode 100644 index 0000000..0991132 --- /dev/null +++ b/src/characters/character.cpp @@ -0,0 +1,366 @@ +#include +#include +#include +#include "BulletCollision/CollisionDispatch/btGhostObject.h" +#include "LinearMath/btTransform.h" +#include "character.h" + +#define RUN_SPEED 17 // character running speed in units per second +#define TURN_SPEED 500.0f // character turning in degrees per second +#define ANIM_FADE_SPEED \ + 7.5f // animation crossfade speed in % of full weight per second + +Character::Character(Ogre::SceneManager *scnMgr, const Ogre::String &model, + Ogre::Bullet::DynamicsWorld *world) + : mModelName(model) + , mScnMgr(scnMgr) + , mPivotPitch(0) + , mVerticalVelocity(0) + , mAnimID(ANIM_NONE) + , mRunning(false) + , mCollisionShape(nullptr) + , mGhostObject(nullptr) + , mWorld(world) + , mGoalDirection(0, 0, 0) +{ + setupBody(); + setupAnimations(); +} + +Character::~Character() +{ +} + +bool Character::frameStarted(const Ogre::FrameEvent &evt) +{ + return true; +} + +bool Character::frameRenderingQueued(const Ogre::FrameEvent &evt) +{ + updateBody(evt.timeSinceLastFrame); + updateAnimations(evt.timeSinceLastFrame); + if (evt.timeSinceLastFrame > 0) + updateRootMotion(evt.timeSinceLastFrame); + return true; +} + +bool Character::frameEnded(const Ogre::FrameEvent &evt) +{ + return true; +} + +void Character::setupBody() +{ + mBodyEnt = mScnMgr->createEntity(mModelName); + mBodyNode = mScnMgr->getRootSceneNode()->createChildSceneNode(); + mBodyNode->attachObject(mBodyEnt); + mSkeleton = mBodyEnt->getSkeleton(); + // mRigidBody = world->addCharacter(mBodyEnt, 0); + // mCollisionShape = static_cast(mRigidBody->getCollisionShape()); + mGhostObject = new btPairCachingGhostObject(); + mCollisionShape = new btCompoundShape; + mGhostObject->setCollisionShape(mCollisionShape); + + { + btVector3 inertia(0, 0, 0); + // mCollisionShape = new btCompoundShape(); + btScalar height = 1.0f; + btScalar radius = 0.3f; + btCapsuleShape *shape = + new btCapsuleShape(radius, 2 * height - 2 * radius); + btTransform transform; + transform.setIdentity(); + transform.setOrigin(btVector3(0, 1, 0)); + static_cast(mCollisionShape) + ->addChildShape(transform, shape); + btScalar masses[1] = { 0 }; + btTransform principal; + static_cast(mCollisionShape) + ->calculatePrincipalAxisTransform(masses, principal, + inertia); + } + mGhostObject->setCollisionFlags( + btCollisionObject::CF_KINEMATIC_OBJECT | + btCollisionObject::CF_NO_CONTACT_RESPONSE); + mGhostObject->setActivationState(DISABLE_DEACTIVATION); + Ogre::Bullet::KinematicMotionSimple *controller = + new Ogre::Bullet::KinematicMotionSimple(mGhostObject, + mBodyNode); + mWorld->attachCollisionObject(mGhostObject, mBodyEnt, + btBroadphaseProxy::AllFilter, + btBroadphaseProxy::AllFilter); + mWorld->getBtWorld()->addAction(controller); + + assert(mCollisionShape); +#if 0 + if (mRigidBody->getMass() == 0) { +#if 0 + mRigidBody->setCollisionFlags(mRigidBody->getCollisionFlags() + | btCollisionObject::CF_KINEMATIC_OBJECT + | btCollisionObject::CF_NO_CONTACT_RESPONSE + ); +#endif +#if 0 + mGhostObject->setWorldTransform(mRigidBody->getWorldTransform()); + WorldData::get_singleton()->getBtWorld() + ->getBroadphase()->getOverlappingPairCache() + ->setInternalGhostPairCallback(new btGhostPairCallback()); +#endif + } +#endif +#if 0 + mRigidBody->setActivationState(DISABLE_DEACTIVATION); +#endif +#if 0 + { + Ogre::Entity *e2 = mScnMgr->createEntity("normal-male.glb"); + Ogre::SceneNode *e2node = mScnMgr->getRootSceneNode()->createChildSceneNode(); + e2node->attachObject(e2); + mGhostObject = WorldData::get_singleton()->addGhostObject(e2, mCollisionShape); + mController = new btKinematicCharacterController(mGhostObject, mCollisionShape, 0.5f); + WorldData::get_singleton()->getBtWorld()->addAction(mController); + } +#endif + assert(mSkeleton->hasBone("Root")); + mRootBone = mSkeleton->getBone("Root"); + assert(mRootBone); +} +void Character::setupAnimations() +{ + int i, j; + mSkeleton->setBlendMode(Ogre::ANIMBLEND_CUMULATIVE); + Ogre::String animNames[NUM_ANIMS] = { "idle", "walking", "running" }; + for (i = 0; i < NUM_ANIMS; i++) { + mAnims[i] = mBodyEnt->getAnimationState(animNames[i]); + mAnims[i]->setLoop(true); + mAnims[i]->setEnabled(true); + mAnims[i]->setWeight(0); + mFadingIn[i] = false; + mFadingOut[i] = false; + mSkelAnimations[i] = mSkeleton->getAnimation(animNames[i]); + for (const auto &it : mSkelAnimations[i]->_getNodeTrackList()) { + Ogre::NodeAnimationTrack *track = it.second; + Ogre::String trackName = + track->getAssociatedNode()->getName(); + if (trackName == "mixamorig:Hips") { + mHipsTracks[i] = track; + } else if (trackName == "Root") { + mRootTracks[i] = track; + // mRootTracks[i]->removeAllKeyFrames(); + } + } + Ogre::Vector3 delta = Ogre::Vector3::ZERO; + Ogre::Vector3 motion = Ogre::Vector3::ZERO; + for (j = 0; j < mRootTracks[i]->getNumKeyFrames(); j++) { + Ogre::Vector3 trans = mRootTracks[i] + ->getNodeKeyFrame(j) + ->getTranslate(); + if (j == 0) + delta = trans; + else + delta = trans - motion; + mRootTracks[i]->getNodeKeyFrame(j)->setTranslate(delta); + motion = trans; + } + } +#if 0 + for(i = 0; i < NUM_ANIMS - 1; i++) { + // need to cache + int j; + Ogre::String animName = mAnims[i]->getAnimationName(); + Ogre::Animation *anim = mSkeleton->getAnimation(animName); + Ogre::NodeAnimationTrack *hips_track = nullptr, *root_track = nullptr; + Ogre::Node *root_node = nullptr; + for (const auto& it : anim->_getNodeTrackList()) { + Ogre::NodeAnimationTrack* track = it.second; + Ogre::String trackName = track->getAssociatedNode()->getName(); + std::cout << animName << " track: " << trackName << "\n"; + if (trackName == "mixamorig:Hips") + hips_track = track; + else if (trackName == "Root") { + root_track = track; + root_node = track->getAssociatedNode(); + } + } + assert(false); + root_track->removeAllKeyFrames(); + std::cout << hips_track << " " << root_track << "\n"; + std::cout << hips_track->getNumKeyFrames() << " " << root_track->getNumKeyFrames() << "\n"; + assert(hips_track && root_track); + Ogre::Vector3 delta = Ogre::Vector3::ZERO; + for(j = 0; j < hips_track->getNumKeyFrames(); j++) { + float timePos = hips_track->getNodeKeyFrame(j)->getTime(); + Ogre::Vector3 trans = hips_track->getNodeKeyFrame(j)->getTranslate(); + Ogre::Vector3 hips_trans(0, 0, 0); + Ogre::Vector3 root_trans(0, 0, 0); + hips_track->getNodeKeyFrame(j)->setTranslate(hips_trans); + Ogre::TransformKeyFrame *nk = root_track->createNodeKeyFrame(timePos); + nk->setTranslate(root_trans - delta); + nk->setScale(Ogre::Vector3(1, 1, 1)); + nk->setRotation(Ogre::Quaternion()); + std::cout << animName << " delta: " << j << " " << timePos << " " << root_trans - delta << "\n"; + delta = root_trans; + } + for(j = 0; j < root_track->getNumKeyFrames(); j++) { + float timePos = hips_track->getNodeKeyFrame(j)->getTime(); + Ogre::Vector3 root_trans = hips_track->getNodeKeyFrame(j)->getTranslate(); + std::cout << animName << " delta: root: " << j << " " << timePos << " " << root_trans << "\n"; + } + } +// assert(false); +#endif + setAnimation(ANIM_IDLE); +} +void Character::updateBody(Ogre::Real delta) +{ + Ogre::Quaternion toGoal = + mBodyNode->getOrientation().zAxis().getRotationTo( + mGoalDirection); + // calculate how much the character has to turn to face goal direction + Ogre::Real yawToGoal = toGoal.getYaw().valueDegrees(); + // this is how much the character CAN turn this frame + Ogre::Real yawAtSpeed = + yawToGoal / Ogre::Math::Abs(yawToGoal) * delta * TURN_SPEED; + // reduce "turnability" if we're in midair + // if (mBaseAnimID == ANIM_JUMP_LOOP) yawAtSpeed *= 0.2f; + if (yawToGoal < 0) + yawToGoal = std::min( + 0, + std::max( + yawToGoal, + yawAtSpeed)); //yawToGoal = Math::Clamp(yawToGoal, yawAtSpeed, 0); + else if (yawToGoal > 0) + yawToGoal = std::max( + 0, + std::min( + yawToGoal, + yawAtSpeed)); //yawToGoal = Math::Clamp(yawToGoal, 0, yawAtSpeed); + mBodyNode->yaw(Ogre::Degree(yawToGoal)); +} +void Character::updateAnimations(Ogre::Real delta) +{ + int i, j, k; + Ogre::Real animSpeed = 1; + mTimer += delta; + { + Ogre::Quaternion rot = mBodyNode->getOrientation(); + OgreAssert(!Ogre::Math::isNaN(rot.x), "NaN"); + OgreAssert(!Ogre::Math::isNaN(rot.y), "NaN"); + OgreAssert(!Ogre::Math::isNaN(rot.z), "NaN"); + } + if (mAnimID != ANIM_NONE) { + if (mAnimID == ANIM_WALK) + mAnims[mAnimID]->addTime(delta * 1.0f); + else + mAnims[mAnimID]->addTime(delta * animSpeed); + } + fadeAnimations(delta); +} +void Character::updateRootMotion(Ogre::Real delta) +{ + Ogre::Vector3 boneMotion = mRootBone->getPosition(); + OgreAssert(delta > 0.0f, "Zero delta"); + Ogre::Vector3 motion = boneMotion - rootMotion; + if (motion.squaredLength() > 0.1f * 0.1f) + motion = Ogre::Vector3(); + rootMotion = boneMotion; +#if 0 + float mass = mRigidBody->getMass(); + std::cout << "Root bone position: " << boneMotion << "\n"; + std::cout << "body mass: " << mass << "\n"; +#endif + /* Kinematic motion */ + Ogre::Quaternion rot = mBodyNode->getOrientation(); + // Ogre::Vector3 gravity(0, -9.8, 0); + Ogre::Vector3 gravity(0, 0, 0); + Ogre::Vector3 velocity = rot * boneMotion / delta; + velocity += gravity * delta; + Ogre::Vector3 rotMotion = velocity * delta; + btTransform from(convert(mBodyNode->getOrientation()), + convert(mBodyNode->getPosition())); + mBodyNode->setPosition(mBodyNode->getPosition() + rotMotion); + // WorldData::get_singleton()->getWorld()->testBodyMotion(mRigidBody, from, Ogre::Bullet::convert(rotMotion), true, + // nullptr, false, std::set()); +} +void Character::fadeAnimations(Ogre::Real delta) +{ + int i; + for (i = 0; i < NUM_ANIMS; i++) { + if (mFadingIn[i]) { + // slowly fade this animation in until it has full weight + Ogre::Real newWeight = mAnims[i]->getWeight() + + delta * ANIM_FADE_SPEED; + mAnims[i]->setWeight( + Ogre::Math::Clamp(newWeight, 0, 1)); + if (newWeight >= 1) + mFadingIn[i] = false; + } else if (mFadingOut[i]) { + // slowly fade this animation out until it has no weight, and then disable it + Ogre::Real newWeight = mAnims[i]->getWeight() - + delta * ANIM_FADE_SPEED; + mAnims[i]->setWeight( + Ogre::Math::Clamp(newWeight, 0, 1)); + if (newWeight <= 0) { + mAnims[i]->setEnabled(false); + mFadingOut[i] = false; + } + } + } +} +void Character::setAnimation(AnimID id, bool reset) +{ + assert(id >= 0 && id < NUM_ANIMS); + if (mAnimID != ANIM_NONE) { + mFadingIn[mAnimID] = false; + mFadingOut[mAnimID] = true; + } + mAnimID = id; + if (id != ANIM_NONE) { + mAnims[id]->setEnabled(true); + mAnims[id]->setWeight(0); + mFadingOut[id] = false; + mFadingIn[id] = true; + if (reset) + mAnims[id]->setTimePosition(0); + } +} + +bool Character::act_run() +{ + if (mAnimID == ANIM_IDLE) + setAnimation(ANIM_RUN, true); + else if (mAnimID == ANIM_WALK) + setAnimation(ANIM_RUN); + return true; +} + +bool Character::act_walk() +{ + if (mAnimID == ANIM_IDLE) + setAnimation(ANIM_WALK, true); + else if (mAnimID == ANIM_RUN) + setAnimation(ANIM_WALK); + return true; +} + +bool Character::act_idle() +{ + setAnimation(ANIM_IDLE); + return true; +} + +bool Character::isRunning() +{ + return mAnimID == ANIM_RUN; +} + +bool Character::isWalking() +{ + return mAnimID == ANIM_WALK; +} + +Ogre::Vector3 Character::getPosition() +{ + return mBodyNode->_getDerivedPosition(); +} diff --git a/src/characters/character.h b/src/characters/character.h new file mode 100644 index 0000000..8c7cb01 --- /dev/null +++ b/src/characters/character.h @@ -0,0 +1,115 @@ +#include +#include +#include +class btCompoundShape; +class btPairCachingGhostObject; +class Character : public Ogre::FrameListener { + enum AnimID { + ANIM_IDLE = 0, + ANIM_WALK, + ANIM_RUN, + NUM_ANIMS, + ANIM_NONE = NUM_ANIMS + }; + Ogre::String mModelName; + Ogre::Node *mRootBone; + Ogre::SceneManager *mScnMgr; + + Ogre::SceneNode *mCameraPivot; + Ogre::SceneNode *mCameraGoal, *mBodyNode; + Ogre::Entity *mBodyEnt; + Ogre::Real mPivotPitch; + Ogre::Real mVerticalVelocity; + Ogre::Vector3 mGoalDirection; // actual intended direction in world-space + Ogre::AnimationState *mAnims[NUM_ANIMS]; // master animation list + Ogre::Animation *mSkelAnimations[NUM_ANIMS]; + Ogre::NodeAnimationTrack *mHipsTracks[NUM_ANIMS]; + Ogre::NodeAnimationTrack *mRootTracks[NUM_ANIMS]; + AnimID mAnimID; + bool mFadingIn[NUM_ANIMS]; // which animations are fading in + bool mFadingOut[NUM_ANIMS]; // which animations are fading out + Ogre::Real + mTimer; // general timer to see how long animations have been playing + Ogre::Skeleton *mSkeleton; + bool mRunning; + Ogre::Vector3 rootMotion; + Ogre::Quaternion rootRotation; + // btRigidBody *mRigidBody; + btCompoundShape *mCollisionShape; + btPairCachingGhostObject *mGhostObject; + Ogre::Bullet::DynamicsWorld *mWorld; + +public: + Character(Ogre::SceneManager *scnMgr, const Ogre::String &modelName, + Ogre::Bullet::DynamicsWorld *world); + ~Character(); + +private: + void setupBody(); + void setupAnimations(); + +public: + bool frameStarted(const Ogre::FrameEvent &evt) override; + bool frameEnded(const Ogre::FrameEvent &evt) override; + bool frameRenderingQueued(const Ogre::FrameEvent &evt) override; + +private: + void updateBody(Ogre::Real deltaTime); + void updateAnimations(Ogre::Real deltaTime); + void updateRootMotion(Ogre::Real deltaTime); + void fadeAnimations(Ogre::Real deltaTime); + void setAnimation(AnimID id, bool reset = false); + inline btQuaternion convert(const Ogre::Quaternion &q) + { + return btQuaternion(q.x, q.y, q.z, q.w); + } + inline btVector3 convert(const Ogre::Vector3 &v) + { + return btVector3(v.x, v.y, v.z); + } + inline btTransform convert(const Ogre::Quaternion &q, + const Ogre::Vector3 &v) + { + btQuaternion mq = convert(q); + btVector3 mv = convert(v); + return btTransform(mq, mv); + } + inline Ogre::Quaternion convert(const btQuaternion &q) + { + return Ogre::Quaternion(q.w(), q.x(), q.y(), q.z()); + } + inline Ogre::Vector3 convert(const btVector3 &v) + { + return Ogre::Vector3(v.x(), v.y(), v.z()); + } + inline void convert(const btTransform &from, Ogre::Quaternion &q, + Ogre::Vector3 &v) + { + q = convert(from.getRotation()); + v = convert(from.getOrigin()); + } + +public: + bool isIdle() + { + return mAnimID == ANIM_IDLE; + } + bool act_run(); + bool act_walk(); + bool act_idle(); + bool isRunning(); + bool isWalking(); + bool isMoving() + { + return isRunning() || isWalking(); + } + void setGoalDirection(const Ogre::Vector3 &goalDirection) + { + mGoalDirection = goalDirection; + } + Ogre::Vector3 getGoalDirection() + { + return mGoalDirection; + } + Ogre::Vector3 getPosition(); +}; \ No newline at end of file diff --git a/src/characters/controller.cpp b/src/characters/controller.cpp new file mode 100644 index 0000000..f76523a --- /dev/null +++ b/src/characters/controller.cpp @@ -0,0 +1,629 @@ +#include +#include "character.h" +#include "controller.h" +#if 0 +#define CAM_HEIGHT 1.6f // height of camera above character's center of mass +CharacterController::CharacterController(Ogre::SceneNode *camNode, + Ogre::Camera *cam, + Ogre::SceneManager *scnMgr, + Character *character) + : mCameraNode(camNode) + , mCamera(cam) + , mScnMgr(scnMgr) + , mPivotPitch(0) + , mVerticalVelocity(0) + , mRunning(false) + , mCharacter(character) + , mCameraPivot(nullptr) +{ + setupCamera(); +} +CharacterController::~CharacterController() +{ +} +void CharacterController::setupCamera() +{ + // create a pivot at roughly the character's shoulder + mCameraPivot = mScnMgr->getRootSceneNode()->createChildSceneNode(); + mCameraGoal = + mCameraPivot->createChildSceneNode(Ogre::Vector3(0, 2, 3)); + mCameraNode->setPosition(mCameraPivot->getPosition() + + mCameraGoal->getPosition()); + mCameraPivot->setFixedYawAxis(true); + mCameraGoal->setFixedYawAxis(true); + mCameraNode->setFixedYawAxis(true); + // our model is quite small, so reduce the clipping planes + mCamera->setNearClipDistance(0.1f); + mCamera->setFarClipDistance(700); + + mPivotPitch = 0; + mKeyDirection = Ogre::Vector3::ZERO; + mVerticalVelocity = 0; + std::cout << "ORIGINAL: " << mCameraNode->getPosition() << "\n"; +} +bool CharacterController::keyPressed(const OgreBites::KeyboardEvent &evt) +{ + OgreBites::Keycode key = evt.keysym.sym; + if (key == 'q') { + /* ... */ + } else if (key == 'e') { + } else if (key == 'w') + mKeyDirection.z = -1; + else if (key == 'a') + mKeyDirection.x = -1; + else if (key == 's') + mKeyDirection.z = 1; + else if (key == 'd') + mKeyDirection.x = 1; + if (key == OgreBites::SDLK_LSHIFT) + mRunning = true; + if (!mKeyDirection.isZeroLength() && mCharacter->isIdle()) { + if (mRunning) + mCharacter->act_run(); + else + mCharacter->act_walk(); + // std::cout << "Walking\n"; + } else if (!mKeyDirection.isZeroLength() && mCharacter->isWalking() && + mRunning) + mCharacter->act_run(); + return true; +} +bool CharacterController::keyReleased(const OgreBites::KeyboardEvent &evt) +{ + OgreBites::Keycode key = evt.keysym.sym; + if (key == 'w' && mKeyDirection.z == -1) + mKeyDirection.z = 0; + else if (key == 'a' && mKeyDirection.x == -1) + mKeyDirection.x = 0; + else if (key == 's' && mKeyDirection.z == 1) + mKeyDirection.z = 0; + else if (key == 'd' && mKeyDirection.x == 1) + mKeyDirection.x = 0; + if (key == OgreBites::SDLK_LSHIFT) + mRunning = false; + + if (mKeyDirection.isZeroLength() && mCharacter->isMoving()) + mCharacter->act_idle(); + else if (!mKeyDirection.isZeroLength() && mCharacter->isRunning() && + !mRunning) + mCharacter->act_walk(); + + return true; +} +bool CharacterController::mouseMoved(const OgreBites::MouseMotionEvent &evt) +{ + // update camera goal based on mouse movement + updateCameraGoal(-0.18f * evt.xrel, -0.12f * evt.yrel, 0); + return true; +} +bool CharacterController::mouseWheelRolled(const OgreBites::MouseWheelEvent &evt) +{ + // update camera goal based on mouse movement + updateCameraGoal(0, 0, -0.15f * evt.y); + return true; +} +bool CharacterController::mousePressed(const OgreBites::MouseButtonEvent &evt) +{ + std::cout << "Mouse press\n"; + return false; +} +void CharacterController::frameRendered(const Ogre::FrameEvent &evt) +{ + Ogre::Vector3 goalDirection = Ogre::Vector3::ZERO; + updateCamera(evt.timeSinceLastFrame); + if (mKeyDirection != Ogre::Vector3::ZERO) { + // calculate actually goal direction in world based on player's key directions + goalDirection += + mKeyDirection.z * mCameraNode->getOrientation().zAxis(); + goalDirection += + mKeyDirection.x * mCameraNode->getOrientation().xAxis(); + goalDirection.y = 0; + goalDirection.normalise(); + mCharacter->setGoalDirection(goalDirection); + } +} +bool CharacterController::frameStarted(const Ogre::FrameEvent &evt) +{ + return true; +} +void CharacterController::updateCameraGoal(Ogre::Real deltaYaw, + Ogre::Real deltaPitch, + Ogre::Real deltaZoom) +{ + mCameraPivot->yaw(Ogre::Degree(deltaYaw), Ogre::Node::TS_PARENT); + if (!(mPivotPitch + deltaPitch > 25 && deltaPitch > 0) && + !(mPivotPitch + deltaPitch < -60 && deltaPitch < 0)) { + mCameraPivot->pitch(Ogre::Degree(deltaPitch), + Ogre::Node::TS_LOCAL); + mPivotPitch += deltaPitch; + } + Ogre::Real dist = mCameraGoal->_getDerivedPosition().distance( + mCameraPivot->_getDerivedPosition()); + Ogre::Real distChange = deltaZoom * dist; + + // bound the zoom + if (!(dist + distChange < 8 && distChange < 0) && + !(dist + distChange > 25 && distChange > 0)) + mCameraGoal->translate(0, 0, distChange, Ogre::Node::TS_LOCAL); +} +void CharacterController::updateCamera(Ogre::Real delta) +{ + // static int count = 0; + // place the camera pivot roughly at the character's shoulder + Ogre::Vector3 pivotOffset = + mCharacter->getPosition() - mCameraPivot->_getDerivedPosition(); + Ogre::Vector3 pivotPos = mCameraPivot->_getDerivedPosition(); + mCameraPivot->setPosition(pivotPos + pivotOffset * 0.1f * delta + + Ogre::Vector3::UNIT_Y * CAM_HEIGHT); + // move the camera smoothly to the goal + Ogre::Vector3 goalOffset = + mCameraGoal->_getDerivedPosition() - mCameraNode->getPosition(); + mCameraNode->translate(goalOffset * delta * 9.0f); + // always look at the pivot + mCameraNode->lookAt(mCameraPivot->_getDerivedPosition(), + Ogre::Node::TS_PARENT); +} +#endif +#define CAM_HEIGHT 1.6f // height of camera above character's center of mass +#define RUN_SPEED 17 // character running speed in units per second +#define TURN_SPEED 500.0f // character turning in degrees per second +#define ANIM_FADE_SPEED \ + 7.5f // animation crossfade speed in % of full weight per second +CharacterController::CharacterController(Ogre::SceneNode *camNode, + Ogre::Camera *cam, + Ogre::SceneManager *scnMgr, + Ogre::Bullet::DynamicsWorld *world) + : mCameraNode(camNode) + , mCamera(cam) + , mScnMgr(scnMgr) + , mPivotPitch(0) + , mVerticalVelocity(0) + , mAnimID(ANIM_NONE) + , mRunning(false) + , mWorld(world) + , mCollisionShape(nullptr) + , mGhostObject(nullptr) +{ + setupBody(); + setupCamera(); + setupAnimations(); + Ogre::Root::getSingleton().addFrameListener(this); +} +CharacterController::~CharacterController() +{ +} +void CharacterController::setupBody() +{ + mBodyEnt = mScnMgr->createEntity("normal-male.glb"); + mBodyNode = mScnMgr->getRootSceneNode()->createChildSceneNode(); + mBodyNode->attachObject(mBodyEnt); + mSkeleton = mBodyEnt->getSkeleton(); + // mRigidBody = world->addCharacter(mBodyEnt, 0); + // mCollisionShape = static_cast(mRigidBody->getCollisionShape()); + mGhostObject = new btPairCachingGhostObject(); + mCollisionShape = new btCompoundShape; + mGhostObject->setCollisionShape(mCollisionShape); + + { + btVector3 inertia(0, 0, 0); + // mCollisionShape = new btCompoundShape(); + btScalar height = 1.0f; + btScalar radius = 0.3f; + btCapsuleShape *shape = + new btCapsuleShape(radius, 2 * height - 2 * radius); + btTransform transform; + transform.setIdentity(); + transform.setOrigin(btVector3(0, 1, 0)); + static_cast(mCollisionShape) + ->addChildShape(transform, shape); + btScalar masses[1] = { 0 }; + btTransform principal; + static_cast(mCollisionShape) + ->calculatePrincipalAxisTransform(masses, principal, + inertia); + } + mGhostObject->setCollisionFlags( + btCollisionObject::CF_KINEMATIC_OBJECT | + btCollisionObject::CF_NO_CONTACT_RESPONSE); + mGhostObject->setActivationState(DISABLE_DEACTIVATION); + Ogre::Bullet::KinematicMotionSimple *controller = + new Ogre::Bullet::KinematicMotionSimple(mGhostObject, + mBodyNode); + mWorld->attachCollisionObject(mGhostObject, mBodyEnt, + btBroadphaseProxy::AllFilter, + btBroadphaseProxy::AllFilter); + mWorld->getBtWorld()->addAction(controller); + + assert(mCollisionShape); +#if 0 + if (mRigidBody->getMass() == 0) { +#if 0 + mRigidBody->setCollisionFlags(mRigidBody->getCollisionFlags() + | btCollisionObject::CF_KINEMATIC_OBJECT + | btCollisionObject::CF_NO_CONTACT_RESPONSE + ); +#endif +#if 0 + mGhostObject->setWorldTransform(mRigidBody->getWorldTransform()); + WorldData::get_singleton()->getBtWorld() + ->getBroadphase()->getOverlappingPairCache() + ->setInternalGhostPairCallback(new btGhostPairCallback()); +#endif + } +#endif +#if 0 + mRigidBody->setActivationState(DISABLE_DEACTIVATION); +#endif +#if 0 + { + Ogre::Entity *e2 = mScnMgr->createEntity("normal-male.glb"); + Ogre::SceneNode *e2node = mScnMgr->getRootSceneNode()->createChildSceneNode(); + e2node->attachObject(e2); + mGhostObject = WorldData::get_singleton()->addGhostObject(e2, mCollisionShape); + mController = new btKinematicCharacterController(mGhostObject, mCollisionShape, 0.5f); + WorldData::get_singleton()->getBtWorld()->addAction(mController); + } +#endif + assert(mSkeleton->hasBone("Root")); + mRootBone = mSkeleton->getBone("Root"); + assert(mRootBone); +} +void CharacterController::setupCamera() +{ + // create a pivot at roughly the character's shoulder + mCameraPivot = mScnMgr->getRootSceneNode()->createChildSceneNode(); + mCameraGoal = + mCameraPivot->createChildSceneNode(Ogre::Vector3(0, 2, 3)); + mCameraNode->setPosition(mCameraPivot->getPosition() + + mCameraGoal->getPosition()); + mCameraPivot->setFixedYawAxis(true); + mCameraGoal->setFixedYawAxis(true); + mCameraNode->setFixedYawAxis(true); + // our model is quite small, so reduce the clipping planes + mCamera->setNearClipDistance(0.1f); + mCamera->setFarClipDistance(700); + + mPivotPitch = 0; + mKeyDirection = Ogre::Vector3::ZERO; + mVerticalVelocity = 0; +} +void CharacterController::setupAnimations() +{ + int i, j; + mSkeleton->setBlendMode(Ogre::ANIMBLEND_CUMULATIVE); + Ogre::String animNames[NUM_ANIMS] = { "idle", "walking", "running" }; + for (i = 0; i < NUM_ANIMS; i++) { + mAnims[i] = mBodyEnt->getAnimationState(animNames[i]); + mAnims[i]->setLoop(true); + mAnims[i]->setEnabled(true); + mAnims[i]->setWeight(0); + mFadingIn[i] = false; + mFadingOut[i] = false; + mSkelAnimations[i] = mSkeleton->getAnimation(animNames[i]); + for (const auto &it : mSkelAnimations[i]->_getNodeTrackList()) { + Ogre::NodeAnimationTrack *track = it.second; + Ogre::String trackName = + track->getAssociatedNode()->getName(); + if (trackName == "mixamorig:Hips") { + mHipsTracks[i] = track; + } else if (trackName == "Root") { + mRootTracks[i] = track; + // mRootTracks[i]->removeAllKeyFrames(); + } + } + Ogre::Vector3 delta = Ogre::Vector3::ZERO; + Ogre::Vector3 motion = Ogre::Vector3::ZERO; + for (j = 0; j < mRootTracks[i]->getNumKeyFrames(); j++) { + Ogre::Vector3 trans = mRootTracks[i] + ->getNodeKeyFrame(j) + ->getTranslate(); + if (j == 0) + delta = trans; + else + delta = trans - motion; + mRootTracks[i]->getNodeKeyFrame(j)->setTranslate(delta); + motion = trans; + } + } +#if 0 + for(i = 0; i < NUM_ANIMS - 1; i++) { + // need to cache + int j; + Ogre::String animName = mAnims[i]->getAnimationName(); + Ogre::Animation *anim = mSkeleton->getAnimation(animName); + Ogre::NodeAnimationTrack *hips_track = nullptr, *root_track = nullptr; + Ogre::Node *root_node = nullptr; + for (const auto& it : anim->_getNodeTrackList()) { + Ogre::NodeAnimationTrack* track = it.second; + Ogre::String trackName = track->getAssociatedNode()->getName(); + std::cout << animName << " track: " << trackName << "\n"; + if (trackName == "mixamorig:Hips") + hips_track = track; + else if (trackName == "Root") { + root_track = track; + root_node = track->getAssociatedNode(); + } + } + assert(false); + root_track->removeAllKeyFrames(); + std::cout << hips_track << " " << root_track << "\n"; + std::cout << hips_track->getNumKeyFrames() << " " << root_track->getNumKeyFrames() << "\n"; + assert(hips_track && root_track); + Ogre::Vector3 delta = Ogre::Vector3::ZERO; + for(j = 0; j < hips_track->getNumKeyFrames(); j++) { + float timePos = hips_track->getNodeKeyFrame(j)->getTime(); + Ogre::Vector3 trans = hips_track->getNodeKeyFrame(j)->getTranslate(); + Ogre::Vector3 hips_trans(0, 0, 0); + Ogre::Vector3 root_trans(0, 0, 0); + hips_track->getNodeKeyFrame(j)->setTranslate(hips_trans); + Ogre::TransformKeyFrame *nk = root_track->createNodeKeyFrame(timePos); + nk->setTranslate(root_trans - delta); + nk->setScale(Ogre::Vector3(1, 1, 1)); + nk->setRotation(Ogre::Quaternion()); + std::cout << animName << " delta: " << j << " " << timePos << " " << root_trans - delta << "\n"; + delta = root_trans; + } + for(j = 0; j < root_track->getNumKeyFrames(); j++) { + float timePos = hips_track->getNodeKeyFrame(j)->getTime(); + Ogre::Vector3 root_trans = hips_track->getNodeKeyFrame(j)->getTranslate(); + std::cout << animName << " delta: root: " << j << " " << timePos << " " << root_trans << "\n"; + } + } +// assert(false); +#endif + setAnimation(ANIM_IDLE); +} +bool CharacterController::keyPressed(const OgreBites::KeyboardEvent &evt) +{ + OgreBites::Keycode key = evt.keysym.sym; + if (key == 'q' && (mAnimID == ANIM_IDLE)) { + /* ... */ + mTimer = 0; + } else if (key == 'e') { + } else if (key == 'w') + mKeyDirection.z = -1; + else if (key == 'a') + mKeyDirection.x = -1; + else if (key == 's') + mKeyDirection.z = 1; + else if (key == 'd') + mKeyDirection.x = 1; + if (key == OgreBites::SDLK_LSHIFT) + mRunning = true; + if (!mKeyDirection.isZeroLength() && mAnimID == ANIM_IDLE) { + if (mRunning) + setAnimation(ANIM_RUN, true); + else + setAnimation(ANIM_WALK, true); + // std::cout << "Walking\n"; + } else if (!mKeyDirection.isZeroLength() && mAnimID == ANIM_WALK && + mRunning) + setAnimation(ANIM_RUN); + return true; +} +bool CharacterController::keyReleased(const OgreBites::KeyboardEvent &evt) +{ + OgreBites::Keycode key = evt.keysym.sym; + if (key == 'w' && mKeyDirection.z == -1) + mKeyDirection.z = 0; + else if (key == 'a' && mKeyDirection.x == -1) + mKeyDirection.x = 0; + else if (key == 's' && mKeyDirection.z == 1) + mKeyDirection.z = 0; + else if (key == 'd' && mKeyDirection.x == 1) + mKeyDirection.x = 0; + if (key == OgreBites::SDLK_LSHIFT) + mRunning = false; + + if (mKeyDirection.isZeroLength() && + (mAnimID == ANIM_WALK || mAnimID == ANIM_RUN)) + setAnimation(ANIM_IDLE); + else if (!mKeyDirection.isZeroLength() && mAnimID == ANIM_RUN && + !mRunning) + setAnimation(ANIM_WALK); + + return true; +} +bool CharacterController::mouseMoved(const OgreBites::MouseMotionEvent &evt) +{ + // update camera goal based on mouse movement + updateCameraGoal(-0.18f * evt.xrel, -0.12f * evt.yrel, 0); + return true; +} +bool CharacterController::mouseWheelRolled(const OgreBites::MouseWheelEvent &evt) +{ + // update camera goal based on mouse movement + updateCameraGoal(0, 0, -0.15f * evt.y); + return true; +} +bool CharacterController::mousePressed(const OgreBites::MouseButtonEvent &evt) +{ + std::cout << "Mouse press\n"; + return false; +} +void CharacterController::frameRendered(const Ogre::FrameEvent &evt) +{ + updateBody(evt.timeSinceLastFrame); + updateAnimations(evt.timeSinceLastFrame); + updateCamera(evt.timeSinceLastFrame); + if (evt.timeSinceLastFrame > 0) + updateRootMotion(evt.timeSinceLastFrame); +} +bool CharacterController::frameStarted(const Ogre::FrameEvent &evt) +{ + return true; +} +void CharacterController::updateCameraGoal(Ogre::Real deltaYaw, + Ogre::Real deltaPitch, + Ogre::Real deltaZoom) +{ + mCameraPivot->yaw(Ogre::Degree(deltaYaw), Ogre::Node::TS_PARENT); + if (!(mPivotPitch + deltaPitch > 25 && deltaPitch > 0) && + !(mPivotPitch + deltaPitch < -60 && deltaPitch < 0)) { + mCameraPivot->pitch(Ogre::Degree(deltaPitch), + Ogre::Node::TS_LOCAL); + mPivotPitch += deltaPitch; + } + Ogre::Real dist = mCameraGoal->_getDerivedPosition().distance( + mCameraPivot->_getDerivedPosition()); + Ogre::Real distChange = deltaZoom * dist; + + // bound the zoom + if (!(dist + distChange < 8 && distChange < 0) && + !(dist + distChange > 25 && distChange > 0)) + mCameraGoal->translate(0, 0, distChange, Ogre::Node::TS_LOCAL); +} +void CharacterController::updateBody(Ogre::Real delta) +{ + mGoalDirection = Ogre::Vector3::ZERO; + if (mKeyDirection != Ogre::Vector3::ZERO) { + // calculate actually goal direction in world based on player's key directions + mGoalDirection += + mKeyDirection.z * mCameraNode->getOrientation().zAxis(); + mGoalDirection += + mKeyDirection.x * mCameraNode->getOrientation().xAxis(); + mGoalDirection.y = 0; + mGoalDirection.normalise(); + + Ogre::Quaternion toGoal = + mBodyNode->getOrientation().zAxis().getRotationTo( + mGoalDirection); + // calculate how much the character has to turn to face goal direction + Ogre::Real yawToGoal = toGoal.getYaw().valueDegrees(); + // this is how much the character CAN turn this frame + Ogre::Real yawAtSpeed = yawToGoal / Ogre::Math::Abs(yawToGoal) * + delta * TURN_SPEED; + // reduce "turnability" if we're in midair + // if (mBaseAnimID == ANIM_JUMP_LOOP) yawAtSpeed *= 0.2f; + if (yawToGoal < 0) + yawToGoal = std::min( + 0, + std::max( + yawToGoal, + yawAtSpeed)); //yawToGoal = Math::Clamp(yawToGoal, yawAtSpeed, 0); + else if (yawToGoal > 0) + yawToGoal = std::max( + 0, + std::min( + yawToGoal, + yawAtSpeed)); //yawToGoal = Math::Clamp(yawToGoal, 0, yawAtSpeed); + mBodyNode->yaw(Ogre::Degree(yawToGoal)); + // move in current body direction (not the goal direction) +// mBodyNode->translate(0, 0, delta * RUN_SPEED * mAnims[mAnimID]->getWeight(), +// Ogre::Node::TS_LOCAL); +#if 0 + if (mBaseAnimID == ANIM_JUMP_LOOP) + { + // if we're jumping, add a vertical offset too, and apply gravity + mBodyNode->translate(0, mVerticalVelocity * deltaTime, 0, Node::TS_LOCAL); + mVerticalVelocity -= GRAVITY * deltaTime; + + Vector3 pos = mBodyNode->getPosition(); + if (pos.y <= CHAR_HEIGHT) + { + // if we've hit the ground, change to landing state + pos.y = CHAR_HEIGHT; + mBodyNode->setPosition(pos); + setBaseAnimation(ANIM_JUMP_END, true); + mTimer = 0; + } + } +#endif + } +} +void CharacterController::updateAnimations(Ogre::Real delta) +{ + int i, j, k; + Ogre::Real animSpeed = 1; + mTimer += delta; + if (mAnimID != ANIM_NONE) { + if (mAnimID == ANIM_WALK) + mAnims[mAnimID]->addTime(delta * 1.0f); + else + mAnims[mAnimID]->addTime(delta * animSpeed); + } + fadeAnimations(delta); +} +void CharacterController::updateRootMotion(Ogre::Real delta) +{ + Ogre::Vector3 boneMotion = mRootBone->getPosition(); + OgreAssert(delta > 0.0f, "Zero delta"); +#if 0 + Ogre::Vector3 motion = boneMotion - rootMotion; + if (motion.squaredLength() > 0.1f * 0.1f) + motion = Ogre::Vector3(); + rootMotion = boneMotion; +#endif +#if 0 + float mass = mRigidBody->getMass(); + std::cout << "Root bone position: " << boneMotion << "\n"; + std::cout << "body mass: " << mass << "\n"; +#endif + /* Kinematic motion */ + Ogre::Quaternion rot = mBodyNode->getOrientation(); + Ogre::Vector3 gravity(0, -9.8, 0); + gravity.y = 0.5f; + Ogre::Vector3 velocity = rot * boneMotion / delta; + velocity += gravity * delta; + Ogre::Vector3 rotMotion = velocity * delta; + btTransform from(convert(mBodyNode->getOrientation()), + convert(mBodyNode->getPosition())); + mBodyNode->setPosition(mBodyNode->getPosition() + rotMotion); + // WorldData::get_singleton()->getWorld()->testBodyMotion(mRigidBody, from, Ogre::Bullet::convert(rotMotion), true, + // nullptr, false, std::set()); +} +void CharacterController::fadeAnimations(Ogre::Real delta) +{ + int i; + for (i = 0; i < NUM_ANIMS; i++) { + if (mFadingIn[i]) { + // slowly fade this animation in until it has full weight + Ogre::Real newWeight = mAnims[i]->getWeight() + + delta * ANIM_FADE_SPEED; + mAnims[i]->setWeight( + Ogre::Math::Clamp(newWeight, 0, 1)); + if (newWeight >= 1) + mFadingIn[i] = false; + } else if (mFadingOut[i]) { + // slowly fade this animation out until it has no weight, and then disable it + Ogre::Real newWeight = mAnims[i]->getWeight() - + delta * ANIM_FADE_SPEED; + mAnims[i]->setWeight( + Ogre::Math::Clamp(newWeight, 0, 1)); + if (newWeight <= 0) { + mAnims[i]->setEnabled(false); + mFadingOut[i] = false; + } + } + } +} +void CharacterController::updateCamera(Ogre::Real delta) +{ + // place the camera pivot roughly at the character's shoulder + mCameraPivot->setPosition(mBodyNode->getPosition() + + Ogre::Vector3::UNIT_Y * CAM_HEIGHT); + // move the camera smoothly to the goal + Ogre::Vector3 goalOffset = + mCameraGoal->_getDerivedPosition() - mCameraNode->getPosition(); + mCameraNode->translate(goalOffset * delta * 9.0f); + // always look at the pivot + mCameraNode->lookAt(mCameraPivot->_getDerivedPosition(), + Ogre::Node::TS_PARENT); +} +void CharacterController::setAnimation(AnimID id, bool reset) +{ + assert(id >= 0 && id < NUM_ANIMS); + if (mAnimID != ANIM_NONE) { + mFadingIn[mAnimID] = false; + mFadingOut[mAnimID] = true; + } + mAnimID = id; + if (id != ANIM_NONE) { + mAnims[id]->setEnabled(true); + mAnims[id]->setWeight(0); + mFadingOut[id] = false; + mFadingIn[id] = true; + if (reset) + mAnims[id]->setTimePosition(0); + } +} diff --git a/src/characters/controller.h b/src/characters/controller.h new file mode 100644 index 0000000..f0852a3 --- /dev/null +++ b/src/characters/controller.h @@ -0,0 +1,162 @@ +#include +#include +#include +#if 0 +class Character; +class CharacterController : public OgreBites::InputListener, + Ogre::FrameListener { + Ogre::SceneNode *mCameraNode; + Ogre::Camera *mCamera; + Ogre::SceneManager *mScnMgr; + + Ogre::SceneNode *mCameraPivot; + Ogre::SceneNode *mCameraGoal; + Ogre::Real mPivotPitch; + Ogre::Real mVerticalVelocity; + Ogre::Vector3 + mKeyDirection; // player's local intended direction based on WASD keys + bool mRunning; + Character *mCharacter; + +public: + CharacterController(Ogre::SceneNode *camNode, Ogre::Camera *cam, + Ogre::SceneManager *scnMgr, Character *character); + ~CharacterController(); + +private: + void setupCamera(); + +public: + bool keyPressed(const OgreBites::KeyboardEvent &evt) override; + bool keyReleased(const OgreBites::KeyboardEvent &evt) override; + bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override; + bool mouseWheelRolled(const OgreBites::MouseWheelEvent &evt) override; + bool mousePressed(const OgreBites::MouseButtonEvent &evt) override; + bool frameStarted(const Ogre::FrameEvent &evt) override; + void frameRendered(const Ogre::FrameEvent &evt) override; + +private: + void updateCamera(Ogre::Real deltaTime); + void updateCameraGoal(Ogre::Real deltaYaw, Ogre::Real deltaPitch, + Ogre::Real deltaZoom); +}; +#endif +class CharacterController : public OgreBites::InputListener, + Ogre::FrameListener { + enum AnimID { + ANIM_IDLE = 0, + ANIM_WALK, + ANIM_RUN, + NUM_ANIMS, + ANIM_NONE = NUM_ANIMS + }; + Ogre::Node *mRootBone; + Ogre::SceneNode *mCameraNode; + Ogre::Camera *mCamera; + Ogre::SceneManager *mScnMgr; + + Ogre::SceneNode *mCameraPivot; + Ogre::SceneNode *mCameraGoal, *mBodyNode; + Ogre::Entity *mBodyEnt; + Ogre::Real mPivotPitch; + Ogre::Real mVerticalVelocity; + Ogre::Vector3 + mKeyDirection; // player's local intended direction based on WASD keys + Ogre::Vector3 mGoalDirection; // actual intended direction in world-space + Ogre::AnimationState *mAnims[NUM_ANIMS]; // master animation list + Ogre::Animation *mSkelAnimations[NUM_ANIMS]; + Ogre::NodeAnimationTrack *mHipsTracks[NUM_ANIMS]; + Ogre::NodeAnimationTrack *mRootTracks[NUM_ANIMS]; + AnimID mAnimID; + bool mFadingIn[NUM_ANIMS]; // which animations are fading in + bool mFadingOut[NUM_ANIMS]; // which animations are fading out + Ogre::Real + mTimer; // general timer to see how long animations have been playing + Ogre::Skeleton *mSkeleton; + bool mRunning; + Ogre::Bullet::DynamicsWorld *mWorld; + Ogre::Vector3 rootMotion; + Ogre::Quaternion rootRotation; + // btRigidBody *mRigidBody; + btCompoundShape *mCollisionShape; + btPairCachingGhostObject *mGhostObject; + +public: + CharacterController(Ogre::SceneNode *camNode, Ogre::Camera *cam, + Ogre::SceneManager *scnMgr, + Ogre::Bullet::DynamicsWorld *world); + ~CharacterController(); + +private: + void setupBody(); + void setupCamera(); + void setupAnimations(); + +public: + bool keyPressed(const OgreBites::KeyboardEvent &evt) override; + bool keyReleased(const OgreBites::KeyboardEvent &evt) override; + bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override; + bool mouseWheelRolled(const OgreBites::MouseWheelEvent &evt) override; + bool mousePressed(const OgreBites::MouseButtonEvent &evt) override; + bool frameStarted(const Ogre::FrameEvent &evt) override; + void frameRendered(const Ogre::FrameEvent &evt) override; + +private: + void updateBody(Ogre::Real deltaTime); + void updateAnimations(Ogre::Real deltaTime); + void updateRootMotion(Ogre::Real deltaTime); + void fadeAnimations(Ogre::Real deltaTime); + void updateCamera(Ogre::Real deltaTime); + void updateCameraGoal(Ogre::Real deltaYaw, Ogre::Real deltaPitch, + Ogre::Real deltaZoom); + void setAnimation(AnimID id, bool reset = false); +#if 0 + struct testMotionResult { + }; + struct recoverResult { + }; + + bool bodyTestMotion(btRigidBody *body, + const btTransform &from, + const btVector3 &motion, bool infinite_inertia, + textMotionResult *result, + bool excludeRaycastShapes, + const std::set &exclude); + bool recoverFromPenetration(btRigidBody *body, + const btTransform &body_position, + btScalar recover_movement_scale, + bool infinite_inertia, + btVector3 &delta_recover_movement, + recoverResult *recover_result, + const std::set &exclude); +#endif + inline btQuaternion convert(const Ogre::Quaternion &q) + { + return btQuaternion(q.x, q.y, q.z, q.w); + } + inline btVector3 convert(const Ogre::Vector3 &v) + { + return btVector3(v.x, v.y, v.z); + } + inline btTransform convert(const Ogre::Quaternion &q, + const Ogre::Vector3 &v) + { + btQuaternion mq = convert(q); + btVector3 mv = convert(v); + return btTransform(mq, mv); + } + inline Ogre::Quaternion convert(const btQuaternion &q) + { + return Ogre::Quaternion(q.w(), q.x(), q.y(), q.z()); + } + inline Ogre::Vector3 convert(const btVector3 &v) + { + return Ogre::Vector3(v.x(), v.y(), v.z()); + } + inline void convert(const btTransform &from, Ogre::Quaternion &q, + Ogre::Vector3 &v) + { + q = convert(from.getRotation()); + v = convert(from.getOrigin()); + } +}; \ No newline at end of file