Readded collision objects and movement physics

c++11
scrawl 10 years ago
parent 54c1f19c18
commit 47758c11cd

@ -151,7 +151,7 @@ if(WIN32)
set(QT_USE_QTMAIN TRUE)
endif(WIN32)
set(BOOST_COMPONENTS system filesystem program_options thread wave)
set(BOOST_COMPONENTS system filesystem program_options thread)
if(WIN32)
set(BOOST_COMPONENTS ${BOOST_COMPONENTS} locale)
endif(WIN32)
@ -202,7 +202,6 @@ target_link_libraries(openmw-cs
${OGRE_LIBRARIES}
${OPENSCENEGRAPH_LIBRARIES}
${Boost_LIBRARIES}
${BULLET_LIBRARIES}
${QT_LIBRARIES}
components
)

@ -74,7 +74,7 @@ add_openmw_dir (mwworld
)
add_openmw_dir (mwphysics
physicssystem
physicssystem trace collisiontype actor convert
)
add_openmw_dir (mwclass
@ -101,9 +101,9 @@ add_openmw_dir (mwbase
# Main executable
if (ANDROID)
set(BOOST_COMPONENTS system filesystem program_options thread wave atomic)
set(BOOST_COMPONENTS system filesystem program_options thread atomic)
else ()
set(BOOST_COMPONENTS system filesystem program_options thread wave)
set(BOOST_COMPONENTS system filesystem program_options thread)
endif ()
if(WIN32)

@ -302,7 +302,7 @@ namespace MWBase
virtual void positionToIndex (float x, float y, int &cellX, int &cellY) const = 0;
///< Convert position to cell numbers
virtual void queueMovement(const MWWorld::Ptr &ptr, const Ogre::Vector3 &velocity) = 0;
virtual void queueMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity) = 0;
///< Queues movement for \a ptr (in local space), to be applied in the next call to
/// doPhysics.
@ -555,7 +555,7 @@ namespace MWBase
virtual bool isInStorm() const = 0;
/// @see MWWorld::WeatherManager::getStormDirection
virtual Ogre::Vector3 getStormDirection() const = 0;
virtual osg::Vec3f getStormDirection() const = 0;
/// Resets all actors in the current active cells to their original location within that cell.
virtual void resetActors() = 0;

@ -35,8 +35,7 @@ namespace MWClass
void Apparatus::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Apparatus::getModel(const MWWorld::Ptr &ptr) const

@ -40,8 +40,7 @@ namespace MWClass
void Armor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Armor::getModel(const MWWorld::Ptr &ptr) const

@ -37,8 +37,7 @@ namespace MWClass
void Book::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Book::getModel(const MWWorld::Ptr &ptr) const

@ -37,8 +37,7 @@ namespace MWClass
void Clothing::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Clothing::getModel(const MWWorld::Ptr &ptr) const

@ -41,8 +41,7 @@ namespace MWClass
void Ingredient::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Ingredient::getModel(const MWWorld::Ptr &ptr) const

@ -47,8 +47,9 @@ namespace MWClass
ptr.get<ESM::Light>();
assert (ref->mBase != NULL);
if(!model.empty())
physics.addObject(ptr, model, (ref->mBase->mData.mFlags & ESM::Light::Carry) != 0);
// TODO: add option somewhere to enable collision for placeable objects
if ((ref->mBase->mData.mFlags & ESM::Light::Carry) == 0)
physics.addObject(ptr, model);
if (!ref->mBase->mSound.empty() && !(ref->mBase->mData.mFlags & ESM::Light::OffDefault))
MWBase::Environment::get().getSoundManager()->playSound3D(ptr, ref->mBase->mSound, 1.0, 1.0,

@ -36,8 +36,7 @@ namespace MWClass
void Lockpick::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Lockpick::getModel(const MWWorld::Ptr &ptr) const

@ -53,8 +53,7 @@ namespace MWClass
void Miscellaneous::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Miscellaneous::getModel(const MWWorld::Ptr &ptr) const

@ -39,8 +39,7 @@ namespace MWClass
void Potion::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Potion::getModel(const MWWorld::Ptr &ptr) const

@ -36,8 +36,7 @@ namespace MWClass
void Probe::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Probe::getModel(const MWWorld::Ptr &ptr) const

@ -35,8 +35,7 @@ namespace MWClass
void Repair::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Repair::getModel(const MWWorld::Ptr &ptr) const

@ -39,8 +39,7 @@ namespace MWClass
void Weapon::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const
{
if(!model.empty())
physics.addObject(ptr, model, true);
// TODO: add option somewhere to enable collision for placeable objects
}
std::string Weapon::getModel(const MWWorld::Ptr &ptr) const

@ -1346,7 +1346,7 @@ void CharacterController::update(float duration)
{
MWBase::World *world = MWBase::Environment::get().getWorld();
const MWWorld::Class &cls = mPtr.getClass();
Ogre::Vector3 movement(0.0f);
osg::Vec3f movement(0.f, 0.f, 0.f);
updateMagicEffects();
@ -1403,23 +1403,23 @@ void CharacterController::update(float duration)
}
}
Ogre::Vector3 vec(cls.getMovementSettings(mPtr).mPosition);
vec.normalise();
osg::Vec3f vec(cls.getMovementSettings(mPtr).asVec3());
vec.normalize();
if(mHitState != CharState_None && mJumpState == JumpState_None)
vec = Ogre::Vector3(0.0f);
vec = osg::Vec3f(0.f, 0.f, 0.f);
Ogre::Vector3 rot = cls.getRotationVector(mPtr);
mMovementSpeed = cls.getSpeed(mPtr);
vec.x *= mMovementSpeed;
vec.y *= mMovementSpeed;
vec.x() *= mMovementSpeed;
vec.y() *= mMovementSpeed;
CharacterState movestate = CharState_None;
CharacterState idlestate = CharState_SpecialIdle;
bool forcestateupdate = false;
mHasMovedInXY = std::abs(vec[0])+std::abs(vec[1]) > 0.0f;
mHasMovedInXY = std::abs(vec.x())+std::abs(vec.y()) > 0.0f;
isrunning = isrunning && mHasMovedInXY;
@ -1482,7 +1482,7 @@ void CharacterController::update(float duration)
cls.getCreatureStats(mPtr).setFatigue(fatigue);
if(sneak || inwater || flying)
vec.z = 0.0f;
vec.z() = 0.0f;
if (inwater || flying)
cls.getCreatureStats(mPtr).land();
@ -1505,22 +1505,23 @@ void CharacterController::update(float duration)
static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->getFloat();
float factor = fJumpMoveBase + fJumpMoveMult * mPtr.getClass().getSkill(mPtr, ESM::Skill::Acrobatics)/100.f;
factor = std::min(1.f, factor);
vec.x *= factor;
vec.y *= factor;
vec.z = 0.0f;
vec.x() *= factor;
vec.y() *= factor;
vec.z() = 0.0f;
}
else if(vec.z > 0.0f && mJumpState == JumpState_None)
else if(vec.z() > 0.0f && mJumpState == JumpState_None)
{
// Started a jump.
float z = cls.getJump(mPtr);
if (z > 0)
{
if(vec.x == 0 && vec.y == 0)
vec = Ogre::Vector3(0.0f, 0.0f, z);
if(vec.x() == 0 && vec.y() == 0)
vec = osg::Vec3f(0.0f, 0.0f, z);
else
{
Ogre::Vector3 lat = Ogre::Vector3(vec.x, vec.y, 0.0f).normalisedCopy();
vec = Ogre::Vector3(lat.x, lat.y, 1.0f) * z * 0.707f;
osg::Vec3f lat (vec.x(), vec.y(), 0.0f);
lat.normalize();
vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * z * 0.707f;
}
// advance acrobatics
@ -1544,7 +1545,7 @@ void CharacterController::update(float duration)
{
forcestateupdate = true;
mJumpState = JumpState_Landing;
vec.z = 0.0f;
vec.z() = 0.0f;
float height = cls.getCreatureStats(mPtr).land();
float healthLost = getFallDamage(mPtr, height);
@ -1575,28 +1576,28 @@ void CharacterController::update(float duration)
else
{
mJumpState = JumpState_None;
vec.z = 0.0f;
vec.z() = 0.0f;
inJump = false;
if(std::abs(vec.x/2.0f) > std::abs(vec.y))
if(std::abs(vec.x()/2.0f) > std::abs(vec.y()))
{
if(vec.x > 0.0f)
if(vec.x() > 0.0f)
movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight)
: (sneak ? CharState_SneakRight
: (isrunning ? CharState_RunRight : CharState_WalkRight)));
else if(vec.x < 0.0f)
else if(vec.x() < 0.0f)
movestate = (inwater ? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft)
: (sneak ? CharState_SneakLeft
: (isrunning ? CharState_RunLeft : CharState_WalkLeft)));
}
else if(vec.y != 0.0f)
else if(vec.y() != 0.0f)
{
if(vec.y > 0.0f)
if(vec.y() > 0.0f)
movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward)
: (sneak ? CharState_SneakForward
: (isrunning ? CharState_RunForward : CharState_WalkForward)));
else if(vec.y < 0.0f)
else if(vec.y() < 0.0f)
movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack)
: (sneak ? CharState_SneakBack
: (isrunning ? CharState_RunBack : CharState_WalkBack)));
@ -1676,7 +1677,7 @@ void CharacterController::update(float duration)
}
else
// We must always queue movement, even if there is none, to apply gravity.
world->queueMovement(mPtr, Ogre::Vector3(0.0f));
world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
movement = vec;
cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0;
@ -1688,7 +1689,7 @@ void CharacterController::update(float duration)
}
else if(cls.getCreatureStats(mPtr).isDead())
{
world->queueMovement(mPtr, Ogre::Vector3(0.0f));
world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
}
osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim ? 0.f : duration);
@ -1702,15 +1703,15 @@ void CharacterController::update(float duration)
{
float l = moved.length();
if((movement.x < 0.0f && movement.x < moved.x()*2.0f) ||
(movement.x > 0.0f && movement.x > moved.x()*2.0f))
moved.x() = movement.x;
if((movement.y < 0.0f && movement.y < moved.y()*2.0f) ||
(movement.y > 0.0f && movement.y > moved.y()*2.0f))
moved.y() = movement.y;
if((movement.z < 0.0f && movement.z < moved.z()*2.0f) ||
(movement.z > 0.0f && movement.z > moved.z()*2.0f))
moved.z() = movement.z;
if((movement.x() < 0.0f && movement.x() < moved.x()*2.0f) ||
(movement.x() > 0.0f && movement.x() > moved.x()*2.0f))
moved.x() = movement.x();
if((movement.y() < 0.0f && movement.y() < moved.y()*2.0f) ||
(movement.y() > 0.0f && movement.y() > moved.y()*2.0f))
moved.y() = movement.y();
if((movement.z() < 0.0f && movement.z() < moved.z()*2.0f) ||
(movement.z() > 0.0f && movement.z() > moved.z()*2.0f))
moved.z() = movement.z();
// but keep the original speed
float newLength = moved.length();
if (newLength > 0)
@ -1722,7 +1723,7 @@ void CharacterController::update(float duration)
// Update movement
if(mMovementAnimationControlled && mPtr.getClass().isActor())
world->queueMovement(mPtr, Ogre::Vector3(moved.x(), moved.y(), moved.z()));
world->queueMovement(mPtr, moved);
mSkipAnim = false;

@ -1,6 +1,8 @@
#ifndef GAME_MWMECHANICS_MOVEMENT_H
#define GAME_MWMECHANICS_MOVEMENT_H
#include <osg/Vec3f>
namespace MWMechanics
{
/// Desired movement for an actor
@ -14,6 +16,11 @@ namespace MWMechanics
mPosition[0] = mPosition[1] = mPosition[2] = 0.0f;
mRotation[0] = mRotation[1] = mRotation[2] = 0.0f;
}
osg::Vec3f asVec3()
{
return osg::Vec3f(mPosition[0], mPosition[1], mPosition[2]);
}
};
}

@ -0,0 +1,152 @@
#include "actor.hpp"
#include <osg/io_utils>
#include <osg/PositionAttitudeTransform>
#include <btBulletCollisionCommon.h>
#include "../mwworld/class.hpp"
#include <components/nifbullet/bulletnifloader.hpp>
#include "convert.hpp"
#include "collisiontype.hpp"
namespace MWPhysics
{
Actor::Actor(const MWWorld::Ptr& ptr, osg::ref_ptr<NifBullet::BulletShapeInstance> shape, btDynamicsWorld* world)
: mCanWaterWalk(false), mWalkingOnWater(false)
, mCollisionObject(0), mForce(0.f, 0.f, 0.f), mOnGround(false)
, mInternalCollisionMode(true)
, mExternalCollisionMode(true)
, mDynamicsWorld(world)
{
mPtr = ptr;
mHalfExtents = shape->mCollisionBoxHalfExtents;
mMeshTranslation = shape->mCollisionBoxTranslate;
// Use capsule shape only if base is square (nonuniform scaling apparently doesn't work on it)
if (std::abs(mHalfExtents.x()-mHalfExtents.y())<mHalfExtents.x()*0.05 && mHalfExtents.z() >= mHalfExtents.x())
{
// Could also be btCapsuleShapeZ, but the movement solver seems to have issues with it (jumping on slopes doesn't work)
mShape.reset(new btCylinderShapeZ(toBullet(mHalfExtents)));
}
else
mShape.reset(new btBoxShape(toBullet(mHalfExtents)));
mCollisionObject.reset(new btCollisionObject);
mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT);
mCollisionObject->setActivationState(DISABLE_DEACTIVATION);
mCollisionObject->setCollisionShape(mShape.get());
mCollisionObject->setUserPointer(static_cast<PtrHolder*>(this));
updateRotation();
updateScale();
// already called by updateScale()
//updatePosition();
updateCollisionMask();
}
Actor::~Actor()
{
if (mCollisionObject.get())
mDynamicsWorld->removeCollisionObject(mCollisionObject.get());
}
void Actor::enableCollisionMode(bool collision)
{
mInternalCollisionMode = collision;
}
void Actor::enableCollisionBody(bool collision)
{
if (mExternalCollisionMode != collision)
{
mExternalCollisionMode = collision;
updateCollisionMask();
}
}
void Actor::updateCollisionMask()
{
mDynamicsWorld->removeCollisionObject(mCollisionObject.get());
int collisionMask = CollisionType_World | CollisionType_HeightMap;
if (mExternalCollisionMode)
collisionMask |= CollisionType_Actor | CollisionType_Projectile;
if (mCanWaterWalk)
collisionMask |= CollisionType_Water;
mDynamicsWorld->addCollisionObject(mCollisionObject.get(), CollisionType_Actor, collisionMask);
}
void Actor::updatePosition()
{
osg::Vec3f position = mPtr.getRefData().getPosition().asVec3();
btTransform tr = mCollisionObject->getWorldTransform();
osg::Vec3f scaledTranslation = osg::componentMultiply(mMeshTranslation, mScale);
osg::Vec3f newPosition = scaledTranslation + position;
tr.setOrigin(toBullet(newPosition));
mCollisionObject->setWorldTransform(tr);
}
void Actor::updateRotation ()
{
btTransform tr = mCollisionObject->getWorldTransform();
tr.setRotation(toBullet(mPtr.getRefData().getBaseNode()->getAttitude()));
mCollisionObject->setWorldTransform(tr);
}
void Actor::updateScale()
{
float scale = mPtr.getCellRef().getScale();
osg::Vec3f scaleVec(scale,scale,scale);
if (!mPtr.getClass().isNpc())
mPtr.getClass().adjustScale(mPtr, scaleVec);
mScale = scaleVec;
mShape->setLocalScaling(toBullet(mScale));
updatePosition();
}
osg::Vec3f Actor::getHalfExtents() const
{
return osg::componentMultiply(mHalfExtents, mScale);
}
void Actor::setInertialForce(const osg::Vec3f &force)
{
mForce = force;
}
void Actor::setOnGround(bool grounded)
{
mOnGround = grounded;
}
bool Actor::isWalkingOnWater() const
{
return mWalkingOnWater;
}
void Actor::setWalkingOnWater(bool walkingOnWater)
{
mWalkingOnWater = walkingOnWater;
}
void Actor::setCanWaterWalk(bool waterWalk)
{
if (waterWalk != mCanWaterWalk)
{
mCanWaterWalk = waterWalk;
updateCollisionMask();
}
}
}

@ -0,0 +1,125 @@
#ifndef OPENMW_MWPHYSICS_ACTOR_H
#define OPENMW_MWPHYSICS_ACTOR_H
#include <memory>
#include "../mwworld/ptr.hpp"
#include <osg/Vec3f>
#include <osg/ref_ptr>
class btDynamicsWorld;
class btCollisionShape;
class btCollisionObject;
namespace NifBullet
{
class BulletShapeInstance;
}
namespace MWPhysics
{
class PtrHolder
{
public:
virtual ~PtrHolder() {}
protected:
MWWorld::Ptr mPtr;
};
class Actor : public PtrHolder
{
public:
Actor(const MWWorld::Ptr& ptr, osg::ref_ptr<NifBullet::BulletShapeInstance> shape, btDynamicsWorld* world);
~Actor();
/**
* Sets the collisionMode for this actor. If disabled, the actor can fly and clip geometry.
*/
void enableCollisionMode(bool collision);
bool getCollisionMode() const
{
return mInternalCollisionMode;
}
/**
* Enables or disables the *external* collision body. If disabled, other actors will not collide with this actor.
*/
void enableCollisionBody(bool collision);
void updateScale();
void updateRotation();
void updatePosition();
/**
* Returns the (scaled) half extents
*/
osg::Vec3f getHalfExtents() const;
/**
* Sets the current amount of inertial force (incl. gravity) affecting this physic actor
*/
void setInertialForce(const osg::Vec3f &force);
/**
* Gets the current amount of inertial force (incl. gravity) affecting this physic actor
*/
const osg::Vec3f &getInertialForce() const
{
return mForce;
}
void setOnGround(bool grounded);
bool getOnGround() const
{
return mInternalCollisionMode && mOnGround;
}
btCollisionObject* getCollisionObject() const
{
return mCollisionObject.get();
}
/// Sets whether this actor should be able to collide with the water surface
void setCanWaterWalk(bool waterWalk);
/// Sets whether this actor has been walking on the water surface in the last frame
void setWalkingOnWater(bool walkingOnWater);
bool isWalkingOnWater() const;
private:
/// Removes then re-adds the collision object to the dynamics world
void updateCollisionMask();
bool mCanWaterWalk;
bool mWalkingOnWater;
std::auto_ptr<btCollisionShape> mShape;
std::auto_ptr<btCollisionObject> mCollisionObject;
osg::Vec3f mMeshTranslation;
osg::Vec3f mHalfExtents;
osg::Vec3f mScale;
osg::Vec3f mPosition;
osg::Vec3f mForce;
bool mOnGround;
bool mInternalCollisionMode;
bool mExternalCollisionMode;
btDynamicsWorld* mDynamicsWorld;
Actor(const Actor&);
Actor& operator=(const Actor&);
};
}
#endif

@ -0,0 +1,17 @@
#ifndef OPENMW_MWPHYSICS_COLLISIONTYPE_H
#define OPENMW_MWPHYSICS_COLLISIONTYPE_H
namespace MWPhysics
{
enum CollisionType {
CollisionType_World = 1<<0,
CollisionType_Actor = 1<<1,
CollisionType_HeightMap = 1<<2,
CollisionType_Projectile = 1<<4,
CollisionType_Water = 1<<5
};
}
#endif

@ -0,0 +1,35 @@
#ifndef OPENMW_MWPHYSICS_CONVERT_H
#define OPENMW_MWPHYSICS_CONVERT_H
#include <LinearMath/btVector3.h>
#include <LinearMath/btQuaternion.h>
#include <osg/Vec3f>
#include <osg/Quat>
namespace MWPhysics
{
inline btVector3 toBullet(const osg::Vec3f& vec)
{
return btVector3(vec.x(), vec.y(), vec.z());
}
inline btQuaternion toBullet(const osg::Quat& quat)
{
return btQuaternion(quat.x(), quat.y(), quat.z(), quat.w());
}
inline osg::Vec3f toOsg(const btVector3& vec)
{
return osg::Vec3f(vec.x(), vec.y(), vec.z());
}
inline osg::Quat toOsg(const btQuaternion& quat)
{
return osg::Quat(quat.x(), quat.y(), quat.z(), quat.w());
}
}
#endif

@ -3,15 +3,17 @@
#include <stdexcept>
#include <osg/Group>
#include <osg/PositionAttitudeTransform>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <components/nifbullet/bulletshapemanager.hpp>
#include <components/nifbullet/bulletnifloader.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/esm/loadgmst.hpp>
#include "../mwbase/world.hpp" // FIXME
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwmechanics/creaturestats.hpp"
@ -28,6 +30,11 @@
#include "../mwworld/class.hpp"
#include "collisiontype.hpp"
#include "actor.hpp"
#include "convert.hpp"
#include "trace.h"
namespace
{
@ -89,16 +96,17 @@ namespace MWPhysics
// Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared.
static const int sMaxIterations = 8;
// FIXME: move to a separate file
class MovementSolver
{
private:
static float getSlope(const Ogre::Vector3 &normal)
static float getSlope(const osg::Vec3f &normal)
{
return normal.angleBetween(Ogre::Vector3(0.0f,0.0f,1.0f)).valueDegrees();
return osg::RadiansToDegrees(std::acos(normal * osg::Vec3f(0.f, 0.f, 1.f)));
}
static bool stepMove(btCollisionObject *colobj, Ogre::Vector3 &position,
const Ogre::Vector3 &toMove, float &remainingTime /*, OEngine::Physic::PhysicEngine *engine*/)
static bool stepMove(btCollisionObject *colobj, osg::Vec3f &position,
const osg::Vec3f &toMove, float &remainingTime, btDynamicsWorld* dynamicsWorld)
{
/*
* Slide up an incline or set of stairs. Should be called only after a
@ -144,11 +152,9 @@ namespace MWPhysics
* +--+ +--------
* ==============================================
*/
return false;
#if 0
OEngine::Physic::ActorTracer tracer, stepper;
ActorTracer tracer, stepper;
stepper.doTrace(colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSizeUp), engine);
stepper.doTrace(colobj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), dynamicsWorld);
if(stepper.mFraction < std::numeric_limits<float>::epsilon())
return false; // didn't even move the smallest representable amount
// (TODO: shouldn't this be larger? Why bother with such a small amount?)
@ -166,7 +172,7 @@ namespace MWPhysics
* +--+
* ==============================================
*/
tracer.doTrace(colobj, stepper.mEndPos, stepper.mEndPos + toMove, engine);
tracer.doTrace(colobj, stepper.mEndPos, stepper.mEndPos + toMove, dynamicsWorld);
if(tracer.mFraction < std::numeric_limits<float>::epsilon())
return false; // didn't even move the smallest representable amount
@ -185,7 +191,7 @@ namespace MWPhysics
* +--+ +--+
* ==============================================
*/
stepper.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-Ogre::Vector3(0.0f,0.0f,sStepSizeDown), engine);
stepper.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-osg::Vec3f(0.0f,0.0f,sStepSizeDown), dynamicsWorld);
if(stepper.mFraction < 1.0f && getSlope(stepper.mPlaneNormal) <= sMaxSlope)
{
// don't allow stepping up other actors
@ -201,41 +207,39 @@ namespace MWPhysics
// moved between 0 and just under sStepSize distance but slope was too great,
// or moved full sStepSize distance (FIXME: is this a bug?)
#endif
return false;
}
///Project a vector u on another vector v
static inline Ogre::Vector3 project(const Ogre::Vector3 u, const Ogre::Vector3 &v)
static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v)
{
return v * u.dotProduct(v);
return v * (u * v);
// ^ dot product
}
///Helper for computing the character sliding
static inline Ogre::Vector3 slide(Ogre::Vector3 direction, const Ogre::Vector3 &planeNormal)
static inline osg::Vec3f slide(const osg::Vec3f& direction, const osg::Vec3f &planeNormal)
{
return direction - project(direction, planeNormal);
}
static inline osg::Vec3f reflect(const osg::Vec3& velocity, const osg::Vec3f& normal)
{
return (normal * (normal * velocity)) * 2 - velocity;
}
public:
static Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr, /*OEngine::Physic::PhysicEngine *engine,*/ float maxHeight)
static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, Actor* actor, btDynamicsWorld* dynamicsWorld, float maxHeight)
{
const ESM::Position &refpos = ptr.getRefData().getPosition();
Ogre::Vector3 position(refpos.pos);
return position;
#if 0
OEngine::Physic::PhysicActor *physicActor = engine->getCharacter(ptr.getRefData().getHandle());
if (!physicActor)
return position;
osg::Vec3f position(ptr.getRefData().getPosition().asVec3());
OEngine::Physic::ActorTracer tracer;
tracer.findGround(physicActor, position, position-Ogre::Vector3(0,0,maxHeight), engine);
ActorTracer tracer;
tracer.findGround(actor, position, position-osg::Vec3f(0,0,maxHeight), dynamicsWorld);
if(tracer.mFraction >= 1.0f)
{
physicActor->setOnGround(false);
actor->setOnGround(false);
return position;
}
else
@ -243,79 +247,73 @@ namespace MWPhysics
// Check if we actually found a valid spawn point (use an infinitely thin ray this time).
// Required for some broken door destinations in Morrowind.esm, where the spawn point
// intersects with other geometry if the actor's base is taken into account
btVector3 from = BtOgre::Convert::toBullet(position);
btVector3 from = toBullet(position);
btVector3 to = from - btVector3(0,0,maxHeight);
btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to);
resultCallback1.m_collisionFilterGroup = 0xff;
resultCallback1.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap;
engine->mDynamicsWorld->rayTest(from, to, resultCallback1);
dynamicsWorld->rayTest(from, to, resultCallback1);
if (resultCallback1.hasHit() &&
(BtOgre::Convert::toOgre(resultCallback1.m_hitPointWorld).distance(tracer.mEndPos) > 30
( (toOsg(resultCallback1.m_hitPointWorld) - tracer.mEndPos).length() > 30
|| getSlope(tracer.mPlaneNormal) > sMaxSlope))
{
physicActor->setOnGround(getSlope(BtOgre::Convert::toOgre(resultCallback1.m_hitNormalWorld)) <= sMaxSlope);
return BtOgre::Convert::toOgre(resultCallback1.m_hitPointWorld) + Ogre::Vector3(0,0,1.f);
actor->setOnGround(getSlope(toOsg(resultCallback1.m_hitNormalWorld)) <= sMaxSlope);
return toOsg(resultCallback1.m_hitPointWorld) + osg::Vec3f(0.f, 0.f, 1.f);
}
physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope);
actor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope);
return tracer.mEndPos;
}
#endif
}
static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time,
bool isFlying, float waterlevel, float slowFall /*, OEngine::Physic::PhysicEngine *engine*/
static osg::Vec3f move(const MWWorld::Ptr &ptr, Actor* physicActor, const osg::Vec3f &movement, float time,
bool isFlying, float waterlevel, float slowFall, btDynamicsWorld* dynamicsWorld
, std::map<std::string, std::string>& collisionTracker
, std::map<std::string, std::string>& standingCollisionTracker)
{
const ESM::Position &refpos = ptr.getRefData().getPosition();
Ogre::Vector3 position(refpos.pos);
return position;
#if 0
const ESM::Position& refpos = ptr.getRefData().getPosition();
osg::Vec3f position(refpos.asVec3());
// Early-out for totally static creatures
// (Not sure if gravity should still apply?)
if (!ptr.getClass().isMobile(ptr))
return position;
OEngine::Physic::PhysicActor *physicActor = engine->getCharacter(ptr.getRefData().getHandle());
if (!physicActor)
return position;
// Reset per-frame data
physicActor->setWalkingOnWater(false);
// Anything to collide with?
if(!physicActor->getCollisionMode())
{
return position + (Ogre::Quaternion(Ogre::Radian(refpos.rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) *
Ogre::Quaternion(Ogre::Radian(refpos.rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X))
* movement * time;
return position + (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) *
osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))
) * movement * time;
}
btCollisionObject *colobj = physicActor->getCollisionBody();
Ogre::Vector3 halfExtents = physicActor->getHalfExtents();
position.z += halfExtents.z;
btCollisionObject *colobj = physicActor->getCollisionObject();
osg::Vec3f halfExtents = physicActor->getHalfExtents();
position.z() += halfExtents.z();
static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()
.find("fSwimHeightScale")->getFloat();
float swimlevel = waterlevel + halfExtents.z - (halfExtents.z * 2 * fSwimHeightScale);
float swimlevel = waterlevel + halfExtents.z() - (halfExtents.z() * 2 * fSwimHeightScale);
OEngine::Physic::ActorTracer tracer;
Ogre::Vector3 inertia = physicActor->getInertialForce();
Ogre::Vector3 velocity;
ActorTracer tracer;
osg::Vec3f inertia = physicActor->getInertialForce();
osg::Vec3f velocity;
if(position.z < swimlevel || isFlying)
if(position.z() < swimlevel || isFlying)
{
velocity = (Ogre::Quaternion(Ogre::Radian(refpos.rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z)*
Ogre::Quaternion(Ogre::Radian(refpos.rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X)) * movement;
velocity = (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) *
osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * movement;
}
else
{
velocity = Ogre::Quaternion(Ogre::Radian(refpos.rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * movement;
velocity = (osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * movement;
if (velocity.z > 0.f)
if (velocity.z() > 0.f)
inertia = velocity;
if(!physicActor->getOnGround())
{
@ -327,16 +325,16 @@ namespace MWPhysics
// Now that we have the effective movement vector, apply wind forces to it
if (MWBase::Environment::get().getWorld()->isInStorm())
{
Ogre::Vector3 stormDirection = MWBase::Environment::get().getWorld()->getStormDirection();
Ogre::Degree angle = stormDirection.angleBetween(velocity);
osg::Vec3f stormDirection = MWBase::Environment::get().getWorld()->getStormDirection();
float angleDegrees = osg::RadiansToDegrees(std::acos(stormDirection * velocity));
static const float fStromWalkMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()
.find("fStromWalkMult")->getFloat();
velocity *= 1.f-(fStromWalkMult * (angle.valueDegrees()/180.f));
velocity *= 1.f-(fStromWalkMult * (angleDegrees/180.f));
}
Ogre::Vector3 origVelocity = velocity;
osg::Vec3f origVelocity = velocity;
Ogre::Vector3 newPosition = position;
osg::Vec3f newPosition = position;
/*
* A loop to find newPosition using tracer, if successful different from the starting position.
* nextpos is the local variable used to find potential newPosition, using velocity and remainingTime
@ -345,27 +343,27 @@ namespace MWPhysics
float remainingTime = time;
for(int iterations = 0; iterations < sMaxIterations && remainingTime > 0.01f; ++iterations)
{
Ogre::Vector3 nextpos = newPosition + velocity * remainingTime;
osg::Vec3f nextpos = newPosition + velocity * remainingTime;
// If not able to fly, don't allow to swim up into the air
if(newPosition.z < swimlevel &&
if(newPosition.z() < swimlevel &&
!isFlying && // can't fly
nextpos.z > swimlevel && // but about to go above water
newPosition.z <= swimlevel)
nextpos.z() > swimlevel && // but about to go above water
newPosition.z() <= swimlevel)
{
const Ogre::Vector3 down(0,0,-1);
Ogre::Real movelen = velocity.normalise();
Ogre::Vector3 reflectdir = velocity.reflect(down);
reflectdir.normalise();
const osg::Vec3f down(0,0,-1);
float movelen = velocity.normalize();
osg::Vec3f reflectdir = reflect(velocity, down);
reflectdir.normalize();
velocity = slide(reflectdir, down)*movelen;
// NOTE: remainingTime is unchanged before the loop continues
continue; // velocity updated, calculate nextpos again
}
if(newPosition.squaredDistance(nextpos) > 0.0001)
if((newPosition - nextpos).length2() > 0.0001)
{
// trace to where character would go if there were no obstructions
tracer.doTrace(colobj, newPosition, nextpos, engine);
tracer.doTrace(colobj, newPosition, nextpos, dynamicsWorld);
// check for obstructions
if(tracer.mFraction >= 1.0f)
@ -375,11 +373,13 @@ namespace MWPhysics
}
else
{
/*
const btCollisionObject* standingOn = tracer.mHitObject;
if (const OEngine::Physic::RigidBody* body = dynamic_cast<const OEngine::Physic::RigidBody*>(standingOn))
{
collisionTracker[ptr.getRefData().getHandle()] = body->mName;
}
*/
}
}
else
@ -395,62 +395,68 @@ namespace MWPhysics
}
Ogre::Vector3 oldPosition = newPosition;
osg::Vec3f oldPosition = newPosition;
// We hit something. Try to step up onto it. (NOTE: stepMove does not allow stepping over)
// NOTE: stepMove modifies newPosition if successful
bool result = stepMove(colobj, newPosition, velocity*remainingTime, remainingTime, engine);
bool result = stepMove(colobj, newPosition, velocity*remainingTime, remainingTime, dynamicsWorld);
if (!result) // to make sure the maximum stepping distance isn't framerate-dependent or movement-speed dependent
result = stepMove(colobj, newPosition, velocity.normalisedCopy()*10.f, remainingTime, engine);
{
osg::Vec3f normalizedVelocity = velocity;
normalizedVelocity.normalize();
result = stepMove(colobj, newPosition, normalizedVelocity*10.f, remainingTime, dynamicsWorld);
}
if(result)
{
// don't let pure water creatures move out of water after stepMove
if (ptr.getClass().isPureWaterCreature(ptr)
&& newPosition.z + halfExtents.z > waterlevel)
&& newPosition.z() + halfExtents.z() > waterlevel)
newPosition = oldPosition;
}
else
{
// Can't move this way, try to find another spot along the plane
Ogre::Vector3 direction = velocity;
Ogre::Real movelen = direction.normalise();
Ogre::Vector3 reflectdir = velocity.reflect(tracer.mPlaneNormal);
reflectdir.normalise();
osg::Vec3f direction = velocity;
float movelen = direction.normalize();
osg::Vec3f reflectdir = reflect(velocity, tracer.mPlaneNormal);
reflectdir.normalize();
Ogre::Vector3 newVelocity = slide(reflectdir, tracer.mPlaneNormal)*movelen;
if ((newVelocity-velocity).squaredLength() < 0.01)
break;
if (velocity.dotProduct(origVelocity) <= 0.f)
osg::Vec3f newVelocity = slide(reflectdir, tracer.mPlaneNormal)*movelen;
if ((newVelocity-velocity).length2() < 0.01)
break;
if ((velocity * origVelocity) <= 0.f)
break; // ^ dot product
velocity = newVelocity;
// Do not allow sliding upward if there is gravity. Stepping will have taken
// care of that.
if(!(newPosition.z < swimlevel || isFlying))
velocity.z = std::min(velocity.z, 0.0f);
if(!(newPosition.z() < swimlevel || isFlying))
velocity.z() = std::min(velocity.z(), 0.0f);
}
}
bool isOnGround = false;
if (!(inertia.z > 0.f) && !(newPosition.z < swimlevel))
if (!(inertia.z() > 0.f) && !(newPosition.z() < swimlevel))
{
Ogre::Vector3 from = newPosition;
Ogre::Vector3 to = newPosition - (physicActor->getOnGround() ?
Ogre::Vector3(0,0,sStepSizeDown+2.f) : Ogre::Vector3(0,0,2.f));
tracer.doTrace(colobj, from, to, engine);
osg::Vec3f from = newPosition;
osg::Vec3f to = newPosition - (physicActor->getOnGround() ?
osg::Vec3f(0,0,sStepSizeDown+2.f) : osg::Vec3f(0,0,2.f));
tracer.doTrace(colobj, from, to, dynamicsWorld);
if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope
&& tracer.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup != CollisionType_Actor)
{
/*
const btCollisionObject* standingOn = tracer.mHitObject;
if (const OEngine::Physic::RigidBody* body = dynamic_cast<const OEngine::Physic::RigidBody*>(standingOn))
{
standingCollisionTracker[ptr.getRefData().getHandle()] = body->mName;
}
if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water)
physicActor->setWalkingOnWater(true);
*/
if (!isFlying)
newPosition.z = tracer.mEndPos.z + 1.0f;
newPosition.z() = tracer.mEndPos.z() + 1.0f;
isOnGround = true;
}
@ -461,13 +467,13 @@ namespace MWPhysics
// so that we do not stay suspended in air indefinitely.
if (tracer.mFraction < 1.0f && tracer.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor)
{
if (Ogre::Vector3(velocity.x, velocity.y, 0).squaredLength() < 100.f*100.f)
if (osg::Vec3f(velocity.x(), velocity.y(), 0).length2() < 100.f*100.f)
{
btVector3 aabbMin, aabbMax;
tracer.mHitObject->getCollisionShape()->getAabb(tracer.mHitObject->getWorldTransform(), aabbMin, aabbMax);
btVector3 center = (aabbMin + aabbMax) / 2.f;
inertia = Ogre::Vector3(position.x - center.x(), position.y - center.y(), 0);
inertia.normalise();
inertia = osg::Vec3f(position.x() - center.x(), position.y() - center.y(), 0);
inertia.normalize();
inertia *= 100;
}
}
@ -476,20 +482,19 @@ namespace MWPhysics
}
}
if(isOnGround || newPosition.z < swimlevel || isFlying)
physicActor->setInertialForce(Ogre::Vector3(0.0f));
if(isOnGround || newPosition.z() < swimlevel || isFlying)
physicActor->setInertialForce(osg::Vec3f(0.f, 0.f, 0.f));
else
{
inertia.z += time * -627.2f;
if (inertia.z < 0)
inertia.z *= slowFall;
inertia.z() += time * -627.2f;
if (inertia.z() < 0)
inertia.z() *= slowFall;
physicActor->setInertialForce(inertia);
}
physicActor->setOnGround(isOnGround);
newPosition.z -= halfExtents.z; // remove what was added at the beginning
newPosition.z() -= halfExtents.z(); // remove what was added at the beginning
return newPosition;
#endif
}
};
@ -543,11 +548,62 @@ namespace MWPhysics
btCollisionObject* mCollisionObject;
};
// ---------------------------------------------------------------
// --------------------------------------------------------------
class Object : public PtrHolder
{
public:
Object(const MWWorld::Ptr& ptr, osg::ref_ptr<NifBullet::BulletShapeInstance> shapeInstance)
: mShapeInstance(shapeInstance)
{
mPtr = ptr;
mCollisionObject.reset(new btCollisionObject);
mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape());
mCollisionObject->setUserPointer(static_cast<PtrHolder*>(this));
setScale(ptr.getCellRef().getScale());
setRotation(toBullet(ptr.getRefData().getBaseNode()->getAttitude()));
const float* pos = ptr.getRefData().getPosition().pos;
setOrigin(btVector3(pos[0], pos[1], pos[2]));
}
void updatePtr(const MWWorld::Ptr& updated)
{
mPtr = updated;
}
PhysicsSystem::PhysicsSystem(osg::ref_ptr<osg::Group> parentNode)
: mTimeAccum(0.0f)
void setScale(float scale)
{
mShapeInstance->getCollisionShape()->setLocalScaling(btVector3(scale,scale,scale));
}
void setRotation(const btQuaternion& quat)
{
mCollisionObject->getWorldTransform().setRotation(quat);
}
void setOrigin(const btVector3& vec)
{
mCollisionObject->getWorldTransform().setOrigin(vec);
}
btCollisionObject* getCollisionObject()
{
return mCollisionObject.get();
}
private:
std::auto_ptr<btCollisionObject> mCollisionObject;
osg::ref_ptr<NifBullet::BulletShapeInstance> mShapeInstance;
};
// ---------------------------------------------------------------
PhysicsSystem::PhysicsSystem(const VFS::Manager* vfs, osg::ref_ptr<osg::Group> parentNode)
: mShapeManager(new NifBullet::BulletShapeManager(vfs))
, mTimeAccum(0.0f)
, mWaterEnabled(false)
, mWaterHeight(0)
, mDebugDrawEnabled(false)
@ -577,6 +633,17 @@ namespace MWPhysics
delete it->second;
}
for (ObjectMap::iterator it = mObjects.begin(); it != mObjects.end(); ++it)
{
mDynamicsWorld->removeCollisionObject(it->second->getCollisionObject());
delete it->second;
}
for (ActorMap::iterator it = mActors.begin(); it != mActors.end(); ++it)
{
delete it->second;
}
delete mDynamicsWorld;
delete mSolver;
delete mCollisionConfiguration;
@ -668,9 +735,13 @@ namespace MWPhysics
return std::vector<std::string>();//mEngine->getCollisions(ptr.getRefData().getBaseNodeOld()->getName(), collisionGroup, collisionMask);
}
Ogre::Vector3 PhysicsSystem::traceDown(const MWWorld::Ptr &ptr, float maxHeight)
osg::Vec3f PhysicsSystem::traceDown(const MWWorld::Ptr &ptr, float maxHeight)
{
return Ogre::Vector3();//MovementSolver::traceDown(ptr, mEngine, maxHeight);
ActorMap::iterator found = mActors.find(ptr);
if (found == mActors.end())
return ptr.getRefData().getPosition().asVec3();
else
return MovementSolver::traceDown(ptr, found->second, mDynamicsWorld, maxHeight);
}
void PhysicsSystem::addHeightField (float* heights, int x, int y, float triSize, float sqrtVerts)
@ -693,45 +764,115 @@ namespace MWPhysics
}
}
void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, bool placeable)
void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh)
{
osg::ref_ptr<NifBullet::BulletShapeInstance> shapeInstance = mShapeManager->createInstance(mesh);
if (!shapeInstance->getCollisionShape())
return;
Object *obj = new Object(ptr, shapeInstance);
mObjects.insert(std::make_pair(ptr, obj));
mDynamicsWorld->addCollisionObject(obj->getCollisionObject(), CollisionType_World,
CollisionType_Actor|CollisionType_HeightMap|CollisionType_Projectile);
}
void PhysicsSystem::remove(const MWWorld::Ptr &ptr)
{
ObjectMap::iterator found = mObjects.find(ptr);
if (found != mObjects.end())
{
mDynamicsWorld->removeCollisionObject(found->second->getCollisionObject());
delete found->second;
mObjects.erase(found);
}
ActorMap::iterator foundActor = mActors.find(ptr);
if (foundActor != mActors.end())
{
delete foundActor->second;
mActors.erase(foundActor);
}
}
void PhysicsSystem::updateScale(const MWWorld::Ptr &ptr)
{
ObjectMap::iterator found = mObjects.find(ptr);
float scale = ptr.getCellRef().getScale();
if (found != mObjects.end())
{
found->second->setScale(scale);
mDynamicsWorld->updateSingleAabb(found->second->getCollisionObject());
return;
}
ActorMap::iterator foundActor = mActors.find(ptr);
if (foundActor != mActors.end())
{
foundActor->second->updateScale();
// no aabb update needed (DISABLE_DEACTIVATION)
return;
}
}
void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr)
{
ObjectMap::iterator found = mObjects.find(ptr);
if (found != mObjects.end())
{
found->second->setRotation(toBullet(ptr.getRefData().getBaseNode()->getAttitude()));
mDynamicsWorld->updateSingleAabb(found->second->getCollisionObject());
return;
}
ActorMap::iterator foundActor = mActors.find(ptr);
if (foundActor != mActors.end())
{
foundActor->second->updateRotation();
// no aabb update needed (DISABLE_DEACTIVATION)
return;
}
}
void PhysicsSystem::updatePosition(const MWWorld::Ptr &ptr)
{
ObjectMap::iterator found = mObjects.find(ptr);
if (found != mObjects.end())
{
found->second->setOrigin(toBullet(ptr.getRefData().getPosition().asVec3()));
mDynamicsWorld->updateSingleAabb(found->second->getCollisionObject());
return;
}
ActorMap::iterator foundActor = mActors.find(ptr);
if (foundActor != mActors.end())
{
foundActor->second->updatePosition();
// no aabb update needed (DISABLE_DEACTIVATION)
return;
}
}
void PhysicsSystem::addActor (const MWWorld::Ptr& ptr, const std::string& mesh)
{
osg::ref_ptr<NifBullet::BulletShapeInstance> shapeInstance = mShapeManager->createInstance(mesh);
Actor* actor = new Actor(ptr, shapeInstance, mDynamicsWorld);
mActors.insert(std::make_pair(ptr, actor));
}
bool PhysicsSystem::toggleCollisionMode()
{
/*
for(std::map<std::string,OEngine::Physic::PhysicActor*>::iterator it = mEngine->mActorMap.begin(); it != mEngine->mActorMap.end();++it)
ActorMap::iterator found = mActors.find(MWBase::Environment::get().getWorld()->getPlayerPtr());
if (found != mActors.end())
{
if (it->first=="player")
{
OEngine::Physic::PhysicActor* act = it->second;
bool cmode = act->getCollisionMode();
if(cmode)
{
act->enableCollisionMode(false);
return false;
}
else
{
act->enableCollisionMode(true);
return true;
}
}
bool cmode = found->second->getCollisionMode();
cmode = !cmode;
found->second->enableCollisionMode(cmode);
return cmode;
}
throw std::logic_error ("can't find player");
*/
return false;
}
void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement)
void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &movement)
{
PtrVelocityList::iterator iter = mMovementQueue.begin();
for(;iter != mMovementQueue.end();++iter)
@ -764,8 +905,6 @@ namespace MWPhysics
mCollisions.clear();
mStandingCollisions.clear();
/*
const MWBase::World *world = MWBase::Environment::get().getWorld();
PtrVelocityList::iterator iter = mMovementQueue.begin();
for(;iter != mMovementQueue.end();++iter)
@ -786,19 +925,20 @@ namespace MWPhysics
Ogre::Vector3(iter->first.getRefData().getPosition().pos)))
waterCollision = true;
OEngine::Physic::PhysicActor *physicActor = mEngine->getCharacter(iter->first.getRefData().getHandle());
if (!physicActor) // actor was already removed from the scene
ActorMap::iterator foundActor = mActors.find(iter->first);
if (foundActor == mActors.end()) // actor was already removed from the scene
continue;
Actor* physicActor = foundActor->second;
physicActor->setCanWaterWalk(waterCollision);
// Slow fall reduces fall speed by a factor of (effect magnitude / 200)
float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f));
Ogre::Vector3 newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum,
osg::Vec3f newpos = MovementSolver::move(iter->first, physicActor, iter->second, mTimeAccum,
world->isFlying(iter->first),
waterlevel, slowFall, mEngine, mCollisions, mStandingCollisions);
waterlevel, slowFall, mDynamicsWorld, mCollisions, mStandingCollisions);
float heightDiff = newpos.z - oldHeight;
float heightDiff = newpos.z() - oldHeight;
if (heightDiff < 0)
iter->first.getClass().getCreatureStats(iter->first).addToFallHeight(-heightDiff);
@ -806,8 +946,6 @@ namespace MWPhysics
mMovementResults.push_back(std::make_pair(iter->first, newpos));
}
*/
mTimeAccum = 0.0f;
}
mMovementQueue.clear();

@ -1,5 +1,5 @@
#ifndef GAME_MWWORLD_PHYSICSSYSTEM_H
#define GAME_MWWORLD_PHYSICSSYSTEM_H
#ifndef OPENMW_MWPHYSICS_PHYSICSSYSTEM_H
#define OPENMW_MWPHYSICS_PHYSICSSYSTEM_H
#include <memory>
@ -21,34 +21,48 @@ namespace MWRender
class DebugDrawer;
}
namespace NifBullet
{
class BulletShapeManager;
}
namespace VFS
{
class Manager;
}
class btSequentialImpulseConstraintSolver;
class btDiscreteDynamicsWorld;
namespace MWPhysics
{
typedef std::vector<std::pair<MWWorld::Ptr,Ogre::Vector3> > PtrVelocityList;
enum CollisionType {
CollisionType_World = 1<<0,
CollisionType_Actor = 1<<1,
CollisionType_HeightMap = 1<<2,
CollisionType_Projectile = 1<<4,
CollisionType_Water = 1<<5
};
typedef std::vector<std::pair<MWWorld::Ptr,osg::Vec3f> > PtrVelocityList;
class HeightField;
class Object;
class Actor;
class PhysicsSystem
{
public:
PhysicsSystem (osg::ref_ptr<osg::Group> parentNode);
PhysicsSystem (const VFS::Manager* vfs, osg::ref_ptr<osg::Group> parentNode);
~PhysicsSystem ();
void enableWater(float height);
void setWaterHeight(float height);
void disableWater();
void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, bool placeable=false);
void addObject (const MWWorld::Ptr& ptr, const std::string& mesh);
// Object or Actor
void remove (const MWWorld::Ptr& ptr);
void updateScale (const MWWorld::Ptr& ptr);
void updateRotation (const MWWorld::Ptr& ptr);
void updatePosition (const MWWorld::Ptr& ptr);
// TODO
//void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated);
void addActor (const MWWorld::Ptr& ptr, const std::string& mesh);
@ -61,7 +75,7 @@ namespace MWPhysics
void stepSimulation(float dt);
std::vector<std::string> getCollisions(const MWWorld::Ptr &ptr, int collisionGroup, int collisionMask); ///< get handles this object collides with
Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr, float maxHeight);
osg::Vec3f traceDown(const MWWorld::Ptr &ptr, float maxHeight);
std::pair<std::string,Ogre::Vector3> getHitContact(const std::string &name,
const Ogre::Vector3 &origin,
@ -76,7 +90,7 @@ namespace MWPhysics
/// Queues velocity movement for a Ptr. If a Ptr is already queued, its velocity will
/// be overwritten. Valid until the next call to applyQueuedMovement.
void queueObjectMovement(const MWWorld::Ptr &ptr, const Ogre::Vector3 &velocity);
void queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity);
/// Apply all queued movements, then clear the list.
const PtrVelocityList& applyQueuedMovement(float dt);
@ -111,6 +125,14 @@ namespace MWPhysics
btCollisionDispatcher* mDispatcher;
btDiscreteDynamicsWorld* mDynamicsWorld;
std::auto_ptr<NifBullet::BulletShapeManager> mShapeManager;
typedef std::map<MWWorld::Ptr, Object*> ObjectMap;
ObjectMap mObjects;
typedef std::map<MWWorld::Ptr, Actor*> ActorMap;
ActorMap mActors;
typedef std::map<std::pair<int, int>, HeightField*> HeightFieldMap;
HeightFieldMap mHeightFields;
@ -121,9 +143,9 @@ namespace MWPhysics
// Tracks all movement collisions happening during a single frame. <actor handle, collided handle>
// This will detect e.g. running against a vertical wall. It will not detect climbing up stairs,
// stepping up small objects, etc.
std::map<std::string, std::string> mCollisions;
std::map<std::string, std::string> mCollisions; // FIXME: reimplement
std::map<std::string, std::string> mStandingCollisions;
std::map<std::string, std::string> mStandingCollisions; // FIXME: reimplement
PtrVelocityList mMovementQueue;
PtrVelocityList mMovementResults;

@ -0,0 +1,128 @@
#include "trace.h"
#include <map>
#include <btBulletDynamicsCommon.h>
#include <btBulletCollisionCommon.h>
#include "collisiontype.hpp"
#include "actor.hpp"
#include "convert.hpp"
namespace MWPhysics
{
class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback
{
public:
ClosestNotMeConvexResultCallback(btCollisionObject *me, const btVector3 &up, btScalar minSlopeDot)
: btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)),
mMe(me), mUp(up), mMinSlopeDot(minSlopeDot)
{
}
virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace)
{
if(convexResult.m_hitCollisionObject == mMe)
return btScalar( 1 );
btVector3 hitNormalWorld;
if(normalInWorldSpace)
hitNormalWorld = convexResult.m_hitNormalLocal;
else
{
///need to transform normal into worldspace
hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal;
}
btScalar dotUp = mUp.dot(hitNormalWorld);
if(dotUp < mMinSlopeDot)
return btScalar(1);
return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace);
}
protected:
btCollisionObject *mMe;
const btVector3 mUp;
const btScalar mMinSlopeDot;
};
void ActorTracer::doTrace(btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, btDynamicsWorld* world)
{
const btVector3 btstart = toBullet(start);
const btVector3 btend = toBullet(end);
const btTransform &trans = actor->getWorldTransform();
btTransform from(trans);
btTransform to(trans);
from.setOrigin(btstart);
to.setOrigin(btend);
ClosestNotMeConvexResultCallback newTraceCallback(actor, btstart-btend, btScalar(0.0));
// Inherit the actor's collision group and mask
newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup;
newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask;
btCollisionShape *shape = actor->getCollisionShape();
assert(shape->isConvex());
world->convexSweepTest(static_cast<btConvexShape*>(shape),
from, to, newTraceCallback);
// Copy the hit data over to our trace results struct:
if(newTraceCallback.hasHit())
{
const btVector3& tracehitnormal = newTraceCallback.m_hitNormalWorld;
mFraction = newTraceCallback.m_closestHitFraction;
mPlaneNormal = osg::Vec3f(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z());
mEndPos = (end-start)*mFraction + start;
mHitObject = newTraceCallback.m_hitCollisionObject;
}
else
{
mEndPos = end;
mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f);
mFraction = 1.0f;
mHitObject = NULL;
}
}
void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, btDynamicsWorld* world)
{
const btVector3 btstart(start.x(), start.y(), start.z()+1.0f);
const btVector3 btend(end.x(), end.y(), end.z()+1.0f);
const btTransform &trans = actor->getCollisionObject()->getWorldTransform();
btTransform from(trans.getBasis(), btstart);
btTransform to(trans.getBasis(), btend);
ClosestNotMeConvexResultCallback newTraceCallback(actor->getCollisionObject(), btstart-btend, btScalar(0.0));
// Inherit the actor's collision group and mask
newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup;
newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask;
newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor;
btVector3 halfExtents = toBullet(actor->getHalfExtents());
halfExtents[2] = 1.0f;
btCylinderShapeZ base(halfExtents);
world->convexSweepTest(&base, from, to, newTraceCallback);
if(newTraceCallback.hasHit())
{
const btVector3& tracehitnormal = newTraceCallback.m_hitNormalWorld;
mFraction = newTraceCallback.m_closestHitFraction;
mPlaneNormal = osg::Vec3f(tracehitnormal.x(), tracehitnormal.y(), tracehitnormal.z());
mEndPos = (end-start)*mFraction + start;
mEndPos[2] += 1.0f;
}
else
{
mEndPos = end;
mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f);
mFraction = 1.0f;
}
}
}

@ -0,0 +1,27 @@
#ifndef OENGINE_BULLET_TRACE_H
#define OENGINE_BULLET_TRACE_H
#include <osg/Vec3f>
class btCollisionObject;
class btDynamicsWorld;
namespace MWPhysics
{
class Actor;
struct ActorTracer
{
osg::Vec3f mEndPos;
osg::Vec3f mPlaneNormal;
const btCollisionObject* mHitObject;
float mFraction;
void doTrace(btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, btDynamicsWorld* world);
void findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, btDynamicsWorld* world);
};
}
#endif

@ -425,7 +425,7 @@ namespace MWScript
MWWorld::Ptr targetPtr;
if (creatureStats.getAiSequence().getCombatTarget (targetPtr))
{
if (targetPtr.getRefData().getHandle() == testedTargetId)
if (targetPtr.getCellRef().getRefId() == testedTargetId)
targetsAreEqual = true;
}
runtime.push(int(targetsAreEqual));

@ -811,10 +811,10 @@ namespace MWScript
const std::string script = ptr.getClass().getScript(ptr);
if(script.empty())
str<< ptr.getCellRef().getRefId()<<" ("<<ptr.getRefData().getHandle()<<") does not have a script.";
str<< ptr.getCellRef().getRefId()<<" does not have a script.";
else
{
str<< "Local variables for "<<ptr.getCellRef().getRefId()<<" ("<<ptr.getRefData().getHandle()<<")";
str<< "Local variables for "<<ptr.getCellRef().getRefId();
const Locals &locals = ptr.getRefData().getLocals();
const Compiler::Locals &complocals = MWBase::Environment::get().getScriptManager()->getLocals(script);

@ -6,25 +6,21 @@
#include "ptr.hpp"
namespace ESM
{
class CellRef;
}
namespace MWWorld
{
/// List all (Ogre-)handles, then reset RefData::mBaseNode to 0.
struct ListAndResetHandles
struct ListAndResetObjects
{
std::vector<Ogre::SceneNode*> mHandles;
std::vector<MWWorld::Ptr> mObjects;
bool operator() (MWWorld::Ptr ptr)
{
Ogre::SceneNode* handle = ptr.getRefData().getBaseNodeOld();
if (handle)
mHandles.push_back (handle);
if (ptr.getRefData().getBaseNode())
{
ptr.getRefData().setBaseNode(NULL);
mObjects.push_back (ptr);
}
ptr.getRefData().setBaseNodeOld(0);
return true;
}
};

@ -60,7 +60,7 @@ namespace
rot = rot * osg::Quat(y, osg::Vec3(0,-1,0)) * osg::Quat(x, osg::Vec3(-1,0,0));
rendering.rotateObject(ptr, rot * worldRotQuat);
//physics.rotateObject(ptr);
physics.updateRotation(ptr);
}
}
@ -190,14 +190,13 @@ namespace MWWorld
void Scene::unloadCell (CellStoreCollection::iterator iter)
{
std::cout << "Unloading cell\n";
ListAndResetHandles functor;
ListAndResetObjects functor;
(*iter)->forEach<ListAndResetHandles>(functor);
for (std::vector<Ogre::SceneNode*>::const_iterator iter2 (functor.mHandles.begin());
iter2!=functor.mHandles.end(); ++iter2)
(*iter)->forEach<ListAndResetObjects>(functor);
for (std::vector<MWWorld::Ptr>::const_iterator iter2 (functor.mObjects.begin());
iter2!=functor.mObjects.end(); ++iter2)
{
//Ogre::SceneNode* node = *iter2;
//mPhysics->removeObject (node->getName());
mPhysics->remove(*iter2);
}
if ((*iter)->getCell()->isExterior())
@ -258,17 +257,16 @@ namespace MWWorld
insertCell (*cell, true, loadingListener);
mRendering.addCell(cell);
#if 0
bool waterEnabled = cell->getCell()->hasWater();
mRendering.setWaterEnabled(waterEnabled);
//mRendering.setWaterEnabled(waterEnabled);
if (waterEnabled)
{
mPhysics->enableWater(cell->getWaterLevel());
mRendering.setWaterHeight(cell->getWaterLevel());
//mRendering.setWaterHeight(cell->getWaterLevel());
}
else
mPhysics->disableWater();
#endif
if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx))
mRendering.configureAmbient(cell->getCell());
}
@ -564,8 +562,8 @@ namespace MWWorld
{
MWBase::Environment::get().getMechanicsManager()->remove (ptr);
MWBase::Environment::get().getSoundManager()->stopSound3D (ptr);
//mPhysics->removeObject (ptr.getRefData().getHandle());
//mRendering.removeObject (ptr);
mPhysics->remove(ptr);
//Rendering.removeObject (ptr);
}
bool Scene::isCellActive(const CellStore &cell)

@ -415,11 +415,11 @@ void WeatherManager::update(float duration, bool paused)
if (mIsStorm)
{
MWWorld::Ptr player = world->getPlayerPtr();
Ogre::Vector3 playerPos (player.getRefData().getPosition().pos);
Ogre::Vector3 redMountainPos (19950, 72032, 27831);
osg::Vec3f playerPos (player.getRefData().getPosition().asVec3());
osg::Vec3f redMountainPos (19950, 72032, 27831);
mStormDirection = (playerPos - redMountainPos);
mStormDirection.z = 0;
mStormDirection.z() = 0;
//mRendering->getSkyManager()->setStormDirection(mStormDirection);
}
@ -844,7 +844,7 @@ bool WeatherManager::isInStorm() const
return mIsStorm;
}
Ogre::Vector3 WeatherManager::getStormDirection() const
osg::Vec3f WeatherManager::getStormDirection() const
{
return mStormDirection;
}

@ -184,7 +184,7 @@ namespace MWWorld
/// Are we in an ash or blight storm?
bool isInStorm() const;
Ogre::Vector3 getStormDirection() const;
osg::Vec3f getStormDirection() const;
void advanceTime(double hours);
@ -205,7 +205,7 @@ namespace MWWorld
float mHour;
float mWindSpeed;
bool mIsStorm;
Ogre::Vector3 mStormDirection;
osg::Vec3f mStormDirection;
MWBase::SoundPtr mAmbientSound;
std::string mPlayingSoundID;

@ -7,8 +7,6 @@
#else
#include <tr1/unordered_map>
#endif
#include "../mwbase/scriptmanager.hpp"
#include "../mwscript/globalscripts.hpp"
#include <OgreSceneNode.h>
#include <osg/Group>
@ -19,6 +17,7 @@
#include <components/compiler/locals.hpp>
#include <components/esm/cellid.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
#include <boost/math/special_functions/sign.hpp>
@ -26,6 +25,7 @@
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/scriptmanager.hpp"
#include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/movement.hpp"
@ -39,9 +39,12 @@
#include "../mwrender/renderingmanager.hpp"
#include "../mwscript/interpretercontext.hpp"
#include "../mwscript/globalscripts.hpp"
#include "../mwclass/door.hpp"
#include "../mwphysics/physicssystem.hpp"
#include "player.hpp"
#include "manualref.hpp"
#include "cellstore.hpp"
@ -51,7 +54,6 @@
#include "actionteleport.hpp"
//#include "projectilemanager.hpp"
#include "weather.hpp"
#include "../mwphysics/physicssystem.hpp"
#include "contentloader.hpp"
#include "esmloader.hpp"
@ -156,7 +158,7 @@ namespace MWWorld
mStartCell (startCell), mStartupScript(startupScript),
mScriptsEnabled(true)
{
mPhysics = new MWPhysics::PhysicsSystem(rootNode);
mPhysics = new MWPhysics::PhysicsSystem(resourceSystem->getVFS(), rootNode);
//mPhysEngine = mPhysics->getEngine();
#if 0
mProjectileManager.reset(new ProjectileManager(renderer.getScene(), *mPhysEngine));
@ -1216,7 +1218,7 @@ namespace MWWorld
if (haveToMove && ptr.getRefData().getBaseNode())
{
mRendering->moveObject(ptr, osg::Vec3f(vec.x, vec.y, vec.z));
//mPhysics->moveObject (ptr);
mPhysics->updatePosition(ptr);
}
if (isPlayer)
{
@ -1343,9 +1345,9 @@ namespace MWWorld
if (force || !isFlying(ptr))
{
//Ogre::Vector3 traced = mPhysics->traceDown(ptr, 500);
//if (traced.z < pos.pos[2])
// pos.pos[2] = traced.z;
osg::Vec3f traced = mPhysics->traceDown(ptr, 500);
if (traced.z() < pos.pos[2])
pos.pos[2] = traced.z();
}
moveObject(ptr, ptr.getCell(), pos.pos[0], pos.pos[1], pos.pos[2]);
@ -1358,8 +1360,8 @@ namespace MWWorld
pos.pos[2] += dist;
actor.getRefData().setPosition(pos);
Ogre::Vector3 traced;// = mPhysics->traceDown(actor, dist*1.1f);
moveObject(actor, actor.getCell(), traced.x, traced.y, traced.z);
osg::Vec3f traced = mPhysics->traceDown(actor, dist*1.1f);
moveObject(actor, actor.getCell(), traced.x(), traced.y(), traced.z());
}
void World::rotateObject (const Ptr& ptr,float x,float y,float z, bool adjust)
@ -1397,34 +1399,32 @@ namespace MWWorld
cellY = static_cast<int>(std::floor(y / cellSize));
}
void World::queueMovement(const Ptr &ptr, const Ogre::Vector3 &velocity)
void World::queueMovement(const Ptr &ptr, const osg::Vec3f &velocity)
{
//mPhysics->queueObjectMovement(ptr, velocity);
mPhysics->queueObjectMovement(ptr, velocity);
}
void World::doPhysics(float duration)
{
mPhysics->stepSimulation(duration);
#if 0
processDoors(duration);
mProjectileManager->update(duration);
//mProjectileManager->update(duration);
const PtrVelocityList &results = mPhysics->applyQueuedMovement(duration);
PtrVelocityList::const_iterator player(results.end());
for(PtrVelocityList::const_iterator iter(results.begin());iter != results.end();++iter)
const MWPhysics::PtrVelocityList &results = mPhysics->applyQueuedMovement(duration);
MWPhysics::PtrVelocityList::const_iterator player(results.end());
for(MWPhysics::PtrVelocityList::const_iterator iter(results.begin());iter != results.end();++iter)
{
if(iter->first == getPlayerPtr())
{
/* Handle player last, in case a cell transition occurs */
// Handle player last, in case a cell transition occurs
player = iter;
continue;
}
moveObjectImp(iter->first, iter->second.x, iter->second.y, iter->second.z);
moveObjectImp(iter->first, iter->second.x(), iter->second.y(), iter->second.z());
}
if(player != results.end())
moveObjectImp(player->first, player->second.x, player->second.y, player->second.z);
#endif
moveObjectImp(player->first, player->second.x(), player->second.y(), player->second.z());
}
bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2)
@ -2312,12 +2312,12 @@ namespace MWWorld
return false;
}
Ogre::Vector3 World::getStormDirection() const
osg::Vec3f World::getStormDirection() const
{
if (isCellExterior() || isCellQuasiExterior())
return mWeatherManager->getStormDirection();
else
return Ogre::Vector3(0,1,0);
return osg::Vec3f(0,1,0);
}
void World::getContainersOwnedBy (const MWWorld::Ptr& npc, std::vector<MWWorld::Ptr>& out)

@ -380,7 +380,7 @@ namespace MWWorld
virtual void positionToIndex (float x, float y, int &cellX, int &cellY) const;
///< Convert position to cell numbers
virtual void queueMovement(const Ptr &ptr, const Ogre::Vector3 &velocity);
virtual void queueMovement(const Ptr &ptr, const osg::Vec3f &velocity);
///< Queues movement for \a ptr (in local space), to be applied in the next call to
/// doPhysics.
@ -650,7 +650,7 @@ namespace MWWorld
virtual bool isInStorm() const;
/// @see MWWorld::WeatherManager::getStormDirection
virtual Ogre::Vector3 getStormDirection() const;
virtual osg::Vec3f getStormDirection() const;
/// Resets all actors in the current active cells to their original location within that cell.
virtual void resetActors();

@ -52,9 +52,9 @@ add_component_dir (nifosg
nifloader controller particle userdata
)
#add_component_dir (nifbullet
# bulletnifloader
# )
add_component_dir (nifbullet
bulletnifloader bulletshapemanager
)
add_component_dir (to_utf8
to_utf8
@ -171,6 +171,7 @@ target_link_libraries(components
${Boost_LIBRARIES}
${OGRE_LIBRARIES}
${OPENSCENEGRAPH_LIBRARIES}
${BULLET_LIBRARIES}
# For MyGUI platform
${OPENGL_gl_LIBRARY}
)

@ -1,31 +1,11 @@
/*
OpenMW - The completely unofficial reimplementation of Morrowind
Copyright (C) 2008-2010 Nicolay Korslund
Email: < korslund@gmail.com >
WWW: http://openmw.sourceforge.net/
This file (ogre_nif_loader.cpp) is part of the OpenMW package.
OpenMW is distributed as free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License
version 3, as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
version 3 along with this program. If not, see
http://www.gnu.org/licenses/ .
*/
#include "bulletnifloader.hpp"
#include <cstdio>
#include <vector>
#include <list>
#include <iostream>
#include <stdexcept>
#include <OgreMatrix4.h>
#include <components/misc/stringops.hpp>
#include "../nif/niffile.hpp"
@ -34,12 +14,7 @@ http://www.gnu.org/licenses/ .
#include "../nif/property.hpp"
#include "../nif/controller.hpp"
#include "../nif/extra.hpp"
#include <libs/platform/strings.h>
#include <vector>
#include <list>
// For warning messages
#include <iostream>
namespace
{
@ -51,53 +26,40 @@ osg::Matrixf getWorldTransform(const Nif::Node *node)
return node->trafo.toMatrix();
}
btVector3 getbtVector(const osg::Vec3f &v)
{
return btVector3(v.x(), v.y(), v.z());
}
}
namespace NifBullet
{
ManualBulletShapeLoader::~ManualBulletShapeLoader()
BulletNifLoader::BulletNifLoader()
: mCompoundShape(NULL)
, mStaticMesh(NULL)
{
}
btVector3 ManualBulletShapeLoader::getbtVector(Ogre::Vector3 const &v)
BulletNifLoader::~BulletNifLoader()
{
return btVector3(v[0], v[1], v[2]);
}
void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource)
osg::ref_ptr<BulletShape> BulletNifLoader::load(const Nif::NIFFilePtr nif)
{
mShape = static_cast<OEngine::Physic::BulletShape *>(resource);
mResourceName = mShape->getName();
mBoundingBox = NULL;
mShape->mBoxTranslation = osg::Vec3f(0,0,0);
mShape->mBoxRotation = osg::Quat();
mShape = new BulletShape;
mCompoundShape = NULL;
mStaticMesh = NULL;
Nif::NIFFilePtr pnif;// (Nif::Cache::getInstance().load(mResourceName.substr(0, mResourceName.length()-7)));
Nif::NIFFile & nif = *pnif.get ();
if (nif.numRoots() < 1)
if (nif->numRoots() < 1)
{
warn("Found no root nodes in NIF.");
return;
}
// Have to load controlled nodes from the .kf
// FIXME: the .kf has to be loaded both for rendering and physics, ideally it should be opened once and then reused
mControlledNodes.clear();
std::string kfname = mResourceName.substr(0, mResourceName.length()-7);
Misc::StringUtils::toLower(kfname);
if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0)
kfname.replace(kfname.size()-4, 4, ".kf");
if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(kfname))
{
Nif::NIFFilePtr kf;// (Nif::Cache::getInstance().load(kfname));
//extractControlledNodes(kf, mControlledNodes);
return mShape;
}
Nif::Record *r = nif.getRoot(0);
Nif::Record *r = nif->getRoot(0);
assert(r != NULL);
Nif::Node *node = dynamic_cast<Nif::Node*>(r);
@ -105,29 +67,40 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource)
{
warn("First root in file was not a node, but a " +
r->recName + ". Skipping file.");
return;
return mShape;
}
mShape->mAutogenerated = hasAutoGeneratedCollision(node);
if (findBoundingBox(node))
{
std::auto_ptr<btCompoundShape> compound (new btCompoundShape);
//do a first pass
handleNode(node,0,false,false);
btBoxShape* boxShape = new btBoxShape(getbtVector(mShape->mCollisionBoxHalfExtents));
btTransform transform = btTransform::getIdentity();
transform.setOrigin(getbtVector(mShape->mCollisionBoxTranslate));
compound->addChildShape(transform, boxShape);
if(mBoundingBox != NULL)
{
mShape->mCollisionShape = mBoundingBox;
delete mStaticMesh;
if (mCompoundShape)
{
int n = mCompoundShape->getNumChildShapes();
for(int i=0; i <n;i++)
delete (mCompoundShape->getChildShape(i));
delete mCompoundShape;
mShape->mAnimatedShapes.clear();
}
mShape->mCollisionShape = compound.release();
return mShape;
}
else
{
/*
// Have to load controlled nodes from the .kf
mControlledNodes.clear();
std::string kfname = mResourceName.substr(0, mResourceName.length()-7);
Misc::StringUtils::toLower(kfname);
if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0)
kfname.replace(kfname.size()-4, 4, ".kf");
if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(kfname))
{
Nif::NIFFilePtr kf;// (Nif::Cache::getInstance().load(kfname));
//extractControlledNodes(kf, mControlledNodes);
}
*/
bool autogenerated = hasAutoGeneratedCollision(node);
handleNode(node, 0, autogenerated, false, autogenerated);
if (mCompoundShape)
{
mShape->mCollisionShape = mCompoundShape;
@ -140,10 +113,46 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource)
}
else if (mStaticMesh)
mShape->mCollisionShape = new TriangleMeshShape(mStaticMesh,true);
return mShape;
}
}
bool ManualBulletShapeLoader::hasAutoGeneratedCollision(Nif::Node const * rootNode)
// Find a boundingBox in the node hierarchy.
// Return: use bounding box for collision?
bool BulletNifLoader::findBoundingBox(const Nif::Node* node, int flags)
{
flags |= node->flags;
if (node->hasBounds)
{
mShape->mCollisionBoxHalfExtents = node->boundXYZ;
mShape->mCollisionBoxTranslate = node->boundPos;
if (flags & Nif::NiNode::Flag_BBoxCollision)
{
return true;
}
}
const Nif::NiNode *ninode = dynamic_cast<const Nif::NiNode*>(node);
if(ninode)
{
const Nif::NodeList &list = ninode->children;
for(size_t i = 0;i < list.length();i++)
{
if(!list[i].empty())
{
bool found = findBoundingBox (list[i].getPtr());
if (found)
return true;
}
}
}
return false;
}
bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node* rootNode)
{
const Nif::NiNode *ninode = dynamic_cast<const Nif::NiNode*>(rootNode);
if(ninode)
@ -161,8 +170,8 @@ bool ManualBulletShapeLoader::hasAutoGeneratedCollision(Nif::Node const * rootNo
return true;
}
void ManualBulletShapeLoader::handleNode(const Nif::Node *node, int flags,
bool isCollisionNode, bool isAnimated)
void BulletNifLoader::handleNode(const Nif::Node *node, int flags,
bool isCollisionNode, bool isAnimated, bool autogenerated)
{
// Accumulate the flags from all the child nodes. This works for all
// the flags we currently use, at least.
@ -201,24 +210,21 @@ void ManualBulletShapeLoader::handleNode(const Nif::Node *node, int flags,
// No collision. Use an internal flag setting to mark this.
flags |= 0x800;
}
else if (sd->string == "MRK" && autogenerated)
{
// Marker can still have collision if the model explicitely specifies it via a RootCollisionNode.
return;
}
}
}
if (isCollisionNode || (mShape->mAutogenerated))
if (isCollisionNode)
{
// NOTE: a trishape with hasBounds=true, but no BBoxCollision flag should NOT go through handleNiTriShape!
// It must be ignored completely.
// (occurs in tr_ex_imp_wall_arch_04.nif)
if(node->hasBounds)
{
if (flags & Nif::NiNode::Flag_BBoxCollision)
{
mShape->mBoxTranslation = node->boundPos;
//mShape->mBoxRotation = node->boundRot;
//mBoundingBox = new btBoxShape(getbtVector(node->boundXYZ));
}
}
else if(node->recType == Nif::RC_NiTriShape)
if(!node->hasBounds && node->recType == Nif::RC_NiTriShape)
{
handleNiTriShape(static_cast<const Nif::NiTriShape*>(node), flags, getWorldTransform(node), isAnimated);
}
@ -237,7 +243,7 @@ void ManualBulletShapeLoader::handleNode(const Nif::Node *node, int flags,
}
}
void ManualBulletShapeLoader::handleNiTriShape(const Nif::NiTriShape *shape, int flags, const osg::Matrixf &transform, bool isAnimated)
void BulletNifLoader::handleNiTriShape(const Nif::NiTriShape *shape, int flags, const osg::Matrixf &transform, bool isAnimated)
{
assert(shape != NULL);
@ -261,6 +267,7 @@ void ManualBulletShapeLoader::handleNiTriShape(const Nif::NiTriShape *shape, int
if (!shape->skin.empty())
isAnimated = false;
/*
if (isAnimated)
{
if (!mCompoundShape)
@ -281,7 +288,7 @@ void ManualBulletShapeLoader::handleNiTriShape(const Nif::NiTriShape *shape, int
osg::Vec3f b1 = vertices[triangles[i+0]];
osg::Vec3f b2 = vertices[triangles[i+1]];
osg::Vec3f b3 = vertices[triangles[i+2]];
childMesh->addTriangle(btVector3(b1.x(),b1.y(),b1.z()),btVector3(b2.x(),b2.y(),b2.z()),btVector3(b3.x(),b3.y(),b3.z()));
childMesh->addTriangle(getbtVector(b1), getbtVector(b2), getbtVector(b3));
}
TriangleMeshShape* childShape = new TriangleMeshShape(childMesh,true);
@ -304,9 +311,10 @@ void ManualBulletShapeLoader::handleNiTriShape(const Nif::NiTriShape *shape, int
mCompoundShape->addChildShape(trans, childShape);
}
else
*/
{
if (!mStaticMesh)
mStaticMesh = new btTriangleMesh();
mStaticMesh = new btTriangleMesh(false);
// Static shape, just transform all vertices into position
const Nif::NiTriShapeData *data = shape->data.getPtr();
@ -315,61 +323,99 @@ void ManualBulletShapeLoader::handleNiTriShape(const Nif::NiTriShape *shape, int
for(size_t i = 0;i < data->triangles.size();i+=3)
{
osg::Vec3f b1 = transform*vertices[triangles[i+0]];
osg::Vec3f b2 = transform*vertices[triangles[i+1]];
osg::Vec3f b3 = transform*vertices[triangles[i+2]];
mStaticMesh->addTriangle(btVector3(b1.x(),b1.y(),b1.z()),btVector3(b2.x(),b2.y(),b2.z()),btVector3(b3.x(),b3.y(),b3.z()));
osg::Vec3f b1 = vertices[triangles[i+0]]*transform;
osg::Vec3f b2 = vertices[triangles[i+1]]*transform;
osg::Vec3f b3 = vertices[triangles[i+2]]*transform;
mStaticMesh->addTriangle(getbtVector(b1), getbtVector(b2), getbtVector(b3));
}
}
}
bool findBoundingBox (const Nif::Node* node, Ogre::Vector3& halfExtents, Ogre::Vector3& translation, Ogre::Quaternion& orientation)
BulletShape::BulletShape()
: mCollisionShape(NULL)
{
if(node->hasBounds)
}
BulletShape::~BulletShape()
{
deleteShape(mCollisionShape);
}
void BulletShape::deleteShape(btCollisionShape* shape)
{
if(shape!=NULL)
{
if (!(node->flags & Nif::NiNode::Flag_Hidden))
if(shape->isCompound())
{
//translation = node->boundPos;
//orientation = node->boundRot;
//halfExtents = node->boundXYZ;
return true;
btCompoundShape* ms = static_cast<btCompoundShape*>(shape);
int a = ms->getNumChildShapes();
for(int i=0; i <a;i++)
deleteShape(ms->getChildShape(i));
}
delete shape;
}
}
const Nif::NiNode *ninode = dynamic_cast<const Nif::NiNode*>(node);
if(ninode)
btCollisionShape* BulletShape::duplicateCollisionShape(btCollisionShape *shape) const
{
if(shape->isCompound())
{
const Nif::NodeList &list = ninode->children;
for(size_t i = 0;i < list.length();i++)
btCompoundShape *comp = static_cast<btCompoundShape*>(shape);
btCompoundShape *newShape = new btCompoundShape;
int numShapes = comp->getNumChildShapes();
for(int i = 0;i < numShapes;++i)
{
if(!list[i].empty())
if (findBoundingBox(list[i].getPtr(), halfExtents, translation, orientation))
return true;
btCollisionShape *child = duplicateCollisionShape(comp->getChildShape(i));
btTransform trans = comp->getChildTransform(i);
newShape->addChildShape(trans, child);
}
}
return false;
}
bool getBoundingBox(const std::string& nifFile, Ogre::Vector3& halfExtents, Ogre::Vector3& translation, Ogre::Quaternion& orientation)
{
Nif::NIFFilePtr pnif;// (Nif::Cache::getInstance().load(nifFile));
Nif::NIFFile & nif = *pnif.get ();
return newShape;
}
if (nif.numRoots() < 1)
if(btBvhTriangleMeshShape* trishape = dynamic_cast<btBvhTriangleMeshShape*>(shape))
{
return false;
#if BT_BULLET_VERSION >= 283
btScaledBvhTriangleMeshShape* newShape = new btScaledBvhTriangleMeshShape(trishape, btVector3(1.f, 1.f, 1.f));
#else
// work around btScaledBvhTriangleMeshShape bug ( https://code.google.com/p/bullet/issues/detail?id=371 ) in older bullet versions
btTriangleMesh* oldMesh = static_cast<btTriangleMesh*>(trishape->getMeshInterface());
btTriangleMesh* newMesh = new btTriangleMesh(*oldMesh);
NifBullet::TriangleMeshShape* newShape = new NifBullet::TriangleMeshShape(newMesh, true);
#endif
return newShape;
}
Nif::Record *r = nif.getRoot(0);
assert(r != NULL);
Nif::Node *node = dynamic_cast<Nif::Node*>(r);
if (node == NULL)
if (btBoxShape* boxshape = dynamic_cast<btBoxShape*>(shape))
{
return false;
return new btBoxShape(*boxshape);
}
return findBoundingBox(node, halfExtents, translation, orientation);
throw std::logic_error(std::string("Unhandled Bullet shape duplication: ")+shape->getName());
}
btCollisionShape *BulletShape::getCollisionShape()
{
return mCollisionShape;
}
osg::ref_ptr<BulletShapeInstance> BulletShape::makeInstance()
{
osg::ref_ptr<BulletShapeInstance> instance (new BulletShapeInstance(this));
return instance;
}
BulletShapeInstance::BulletShapeInstance(osg::ref_ptr<BulletShape> source)
: BulletShape()
, mSource(source)
{
mCollisionBoxHalfExtents = source->mCollisionBoxHalfExtents;
mCollisionBoxTranslate = source->mCollisionBoxTranslate;
if (source->mCollisionShape)
mCollisionShape = duplicateCollisionShape(source->mCollisionShape);
}
} // namespace NifBullet

@ -1,41 +1,22 @@
/*
OpenMW - The completely unofficial reimplementation of Morrowind
Copyright (C) 2008-2010 Nicolay Korslund
Email: < korslund@gmail.com >
WWW: http://openmw.sourceforge.net/
This file (ogre_nif_loader.h) is part of the OpenMW package.
OpenMW is distributed as free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License
version 3, as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
version 3 along with this program. If not, see
http://www.gnu.org/licenses/ .
*/
#ifndef OPENMW_COMPONENTS_NIFBULLET_BULLETNIFLOADER_HPP
#define OPENMW_COMPONENTS_NIFBULLET_BULLETNIFLOADER_HPP
#include <cassert>
#include <string>
#include <set>
#include <iostream>
#include <map>
#include <BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h>
#include <BulletCollision/CollisionShapes/btConvexTriangleMeshShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <btBulletDynamicsCommon.h>
#include <osg/Matrixf>
#include <osg/BoundingBox>
#include <osg/ref_ptr>
#include <osg/Referenced>
// For warning messages
#include <iostream>
#include <components/nif/niffile.hpp>
namespace Nif
{
@ -47,6 +28,48 @@ namespace Nif
namespace NifBullet
{
class BulletShapeInstance;
class BulletShape : public osg::Referenced
{
public:
BulletShape();
virtual ~BulletShape();
btCollisionShape* mCollisionShape;
// Used for actors. Note, ideally actors would use a separate loader - as it is
// we have to keep a redundant copy of the actor model around in mCollisionShape, which isn't used.
// For now, use one file <-> one resource for simplicity.
osg::Vec3f mCollisionBoxHalfExtents;
osg::Vec3f mCollisionBoxTranslate;
// Stores animated collision shapes. If any collision nodes in the NIF are animated, then mCollisionShape
// will be a btCompoundShape (which consists of one or more child shapes).
// In this map, for each animated collision shape,
// we store the node's record index mapped to the child index of the shape in the btCompoundShape.
std::map<int, int> mAnimatedShapes;
osg::ref_ptr<BulletShapeInstance> makeInstance();
btCollisionShape* duplicateCollisionShape(btCollisionShape* shape) const;
btCollisionShape* getCollisionShape();
private:
void deleteShape(btCollisionShape* shape);
};
// An instance of a BulletShape that may have its own unique scaling set on the mCollisionShape.
// Vertex data is shallow-copied where possible. A ref_ptr to the original shape needs to be held to keep vertex pointers intact.
class BulletShapeInstance : public BulletShape
{
public:
BulletShapeInstance(osg::ref_ptr<BulletShape> source);
private:
osg::ref_ptr<BulletShape> mSource;
};
// Subclass btBhvTriangleMeshShape to auto-delete the meshInterface
struct TriangleMeshShape : public btBvhTriangleMeshShape
{
@ -66,18 +89,12 @@ struct TriangleMeshShape : public btBvhTriangleMeshShape
/**
*Load bulletShape from NIF files.
*/
class ManualBulletShapeLoader
class BulletNifLoader
{
public:
ManualBulletShapeLoader(bool showMarkers=false)
: mCompoundShape(NULL)
, mStaticMesh(NULL)
, mBoundingBox(NULL)
, mShowMarkers(showMarkers)
{
}
BulletNifLoader();
virtual ~ManualBulletShapeLoader();
virtual ~BulletNifLoader();
void warn(const std::string &msg)
{
@ -90,44 +107,26 @@ public:
abort();
}
osg::ref_ptr<BulletShape> load(const Nif::NIFFilePtr file);
private:
btVector3 getbtVector(Ogre::Vector3 const &v);
bool findBoundingBox(const Nif::Node* node, int flags = 0);
/**
*Parse a node.
*/
void handleNode(Nif::Node const *node, int flags, bool isCollisionNode, bool isAnimated=false);
void handleNode(Nif::Node const *node, int flags, bool isCollisionNode, bool isAnimated=false, bool autogenerated=false);
/**
*Helper function
*/
bool hasAutoGeneratedCollision(const Nif::Node *rootNode);
/**
*convert a NiTriShape to a bullet trishape.
*/
void handleNiTriShape(const Nif::NiTriShape *shape, int flags, const osg::Matrixf& transform, bool isAnimated);
std::string mResourceName;
//OEngine::Physic::BulletShape* mShape;//current shape
btCompoundShape* mCompoundShape;
btTriangleMesh* mStaticMesh;
btBoxShape *mBoundingBox;
std::set<std::string> mControlledNodes;
bool mShowMarkers;
osg::ref_ptr<BulletShape> mShape;
};
bool getBoundingBox(const std::string& nifFile, Ogre::Vector3& halfExtents, Ogre::Vector3& translation, Ogre::Quaternion& orientation);
bool findBoundingBox(const Nif::Node* node, Ogre::Vector3& halfExtents, Ogre::Vector3& translation, Ogre::Quaternion& orientation);
}
#endif

@ -0,0 +1,47 @@
#include "bulletshapemanager.hpp"
#include <components/vfs/manager.hpp>
#include <components/nifbullet/bulletnifloader.hpp>
namespace NifBullet
{
BulletShapeManager::BulletShapeManager(const VFS::Manager* vfs)
: mVFS(vfs)
{
}
BulletShapeManager::~BulletShapeManager()
{
}
osg::ref_ptr<BulletShapeInstance> BulletShapeManager::createInstance(const std::string &name)
{
std::string normalized = name;
mVFS->normalizeFilename(normalized);
osg::ref_ptr<BulletShape> shape;
Index::iterator it = mIndex.find(normalized);
if (it == mIndex.end())
{
Files::IStreamPtr file = mVFS->get(normalized);
// TODO: add support for non-NIF formats
BulletNifLoader loader;
// might be worth sharing NIFFiles with SceneManager in some way
shape = loader.load(Nif::NIFFilePtr(new Nif::NIFFile(file, normalized)));
mIndex[normalized] = shape;
}
else
shape = it->second;
osg::ref_ptr<BulletShapeInstance> instance = shape->makeInstance();
return instance;
}
}

@ -0,0 +1,37 @@
#ifndef OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H
#define OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H
#include <map>
#include <string>
#include <osg/ref_ptr>
namespace VFS
{
class Manager;
}
namespace NifBullet
{
class BulletShape;
class BulletShapeInstance;
class BulletShapeManager
{
public:
BulletShapeManager(const VFS::Manager* vfs);
~BulletShapeManager();
osg::ref_ptr<BulletShapeInstance> createInstance(const std::string& name);
private:
const VFS::Manager* mVFS;
typedef std::map<std::string, osg::ref_ptr<BulletShape> > Index;
Index mIndex;
};
}
#endif
Loading…
Cancel
Save