Merge pull request #7874 from AndrewMeadows/vertical-thrust

allow MyAvatar.addThrust() to raise avatar from standing on ground
This commit is contained in:
Brad Hefta-Gaub 2016-05-18 11:41:37 -07:00
commit 335d9abddd
7 changed files with 308 additions and 222 deletions

View file

@ -500,7 +500,7 @@ Menu::Menu() {
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderSensorToWorldMatrix, 0, false,
avatar, SLOT(setEnableDebugDrawSensorToWorldMatrix(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::KeyboardMotorControl,
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ActionMotorControl,
Qt::CTRL | Qt::SHIFT | Qt::Key_K, true, avatar, SLOT(updateMotionBehaviorFromMenu()),
UNSPECIFIED_POSITION, "Developer");

View file

@ -115,7 +115,7 @@ namespace MenuOption {
const QString IncreaseAvatarSize = "Increase Avatar Size";
const QString IndependentMode = "Independent Mode";
const QString InputMenu = "Avatar>Input Devices";
const QString KeyboardMotorControl = "Enable Keyboard Motor Control";
const QString ActionMotorControl = "Enable Default Motor Control";
const QString LeapMotionOnHMD = "Leap Motion on HMD";
const QString LoadScript = "Open and Run Script File...";
const QString LoadScriptURL = "Open and Run Script from URL...";

View file

@ -59,9 +59,10 @@ using namespace std;
const glm::vec3 DEFAULT_UP_DIRECTION(0.0f, 1.0f, 0.0f);
const float DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES = 30.0f;
const float MAX_WALKING_SPEED = 2.5f; // human walking speed
const float MAX_BOOST_SPEED = 0.5f * MAX_WALKING_SPEED; // keyboard motor gets additive boost below this speed
const float MIN_AVATAR_SPEED = 0.05f; // speed is set to zero below this
const float MAX_WALKING_SPEED = 2.6f; // human walking speed
const float MAX_BOOST_SPEED = 0.5f * MAX_WALKING_SPEED; // action motor gets additive boost below this speed
const float MIN_AVATAR_SPEED = 0.05f;
const float MIN_AVATAR_SPEED_SQUARED = MIN_AVATAR_SPEED * MIN_AVATAR_SPEED; // speed is set to zero below this
const float YAW_SPEED_DEFAULT = 120.0f; // degrees/sec
const float PITCH_SPEED_DEFAULT = 90.0f; // degrees/sec
@ -69,8 +70,7 @@ const float PITCH_SPEED_DEFAULT = 90.0f; // degrees/sec
// TODO: normalize avatar speed for standard avatar size, then scale all motion logic
// to properly follow avatar size.
float MAX_AVATAR_SPEED = 30.0f;
float MAX_KEYBOARD_MOTOR_SPEED = MAX_AVATAR_SPEED;
float DEFAULT_KEYBOARD_MOTOR_TIMESCALE = 0.25f;
float MAX_ACTION_MOTOR_SPEED = MAX_AVATAR_SPEED;
float MIN_SCRIPTED_MOTOR_TIMESCALE = 0.005f;
float DEFAULT_SCRIPTED_MOTOR_TIMESCALE = 1.0e6f;
const int SCRIPTED_MOTOR_CAMERA_FRAME = 0;
@ -86,13 +86,13 @@ MyAvatar::MyAvatar(RigPointer rig) :
Avatar(rig),
_wasPushing(false),
_isPushing(false),
_isBeingPushed(false),
_isBraking(false),
_boomLength(ZOOM_DEFAULT),
_yawSpeed(YAW_SPEED_DEFAULT),
_pitchSpeed(PITCH_SPEED_DEFAULT),
_thrust(0.0f),
_keyboardMotorVelocity(0.0f),
_keyboardMotorTimescale(DEFAULT_KEYBOARD_MOTOR_TIMESCALE),
_actionMotorVelocity(0.0f),
_scriptedMotorVelocity(0.0f),
_scriptedMotorTimescale(DEFAULT_SCRIPTED_MOTOR_TIMESCALE),
_scriptedMotorFrame(SCRIPTED_MOTOR_CAMERA_FRAME),
@ -246,7 +246,6 @@ void MyAvatar::reset(bool andRecenter) {
_follow.deactivate();
_skeletonModel->reset();
getHead()->reset();
_targetVelocity = glm::vec3(0.0f);
setThrust(glm::vec3(0.0f));
if (andRecenter) {
@ -1166,8 +1165,46 @@ controller::Pose MyAvatar::getRightHandControllerPoseInAvatarFrame() const {
return getRightHandControllerPoseInWorldFrame().transform(invAvatarMatrix);
}
void MyAvatar::updateMotors() {
_characterController.clearMotors();
glm::quat motorRotation;
if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) {
if (_characterController.getState() == CharacterController::State::Hover) {
motorRotation = getHead()->getCameraOrientation();
} else {
motorRotation = getOrientation();
}
const float DEFAULT_MOTOR_TIMESCALE = 0.2f;
const float INVALID_MOTOR_TIMESCALE = 1.0e6f;
if (_isPushing || _isBraking || !_isBeingPushed) {
_characterController.addMotor(_actionMotorVelocity, motorRotation, DEFAULT_MOTOR_TIMESCALE, INVALID_MOTOR_TIMESCALE);
} else {
// _isBeingPushed must be true --> disable action motor by giving it a long timescale,
// otherwise it's attempt to "stand in in place" could defeat scripted motor/thrusts
_characterController.addMotor(_actionMotorVelocity, motorRotation, INVALID_MOTOR_TIMESCALE);
}
}
if (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED) {
if (_scriptedMotorFrame == SCRIPTED_MOTOR_CAMERA_FRAME) {
motorRotation = getHead()->getCameraOrientation() * glm::angleAxis(PI, Vectors::UNIT_Y);
} else if (_scriptedMotorFrame == SCRIPTED_MOTOR_AVATAR_FRAME) {
motorRotation = getOrientation() * glm::angleAxis(PI, Vectors::UNIT_Y);
} else {
// world-frame
motorRotation = glm::quat();
}
_characterController.addMotor(_scriptedMotorVelocity, motorRotation, _scriptedMotorTimescale);
}
// legacy support for 'MyAvatar::applyThrust()', which has always been implemented as a
// short-lived linearAcceleration
_characterController.setLinearAcceleration(_thrust);
_thrust = Vectors::ZERO;
}
void MyAvatar::prepareForPhysicsSimulation() {
relayDriveKeysToCharacterController();
updateMotors();
bool success;
glm::vec3 parentVelocity = getParentVelocity(success);
@ -1177,7 +1214,6 @@ void MyAvatar::prepareForPhysicsSimulation() {
}
_characterController.setParentVelocity(parentVelocity);
_characterController.setTargetVelocity(getTargetVelocity());
_characterController.setPositionAndOrientation(getPosition(), getOrientation());
if (qApp->isHMDMode()) {
bool hasDriveInput = fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f;
@ -1190,11 +1226,17 @@ void MyAvatar::prepareForPhysicsSimulation() {
void MyAvatar::harvestResultsFromPhysicsSimulation(float deltaTime) {
glm::vec3 position = getPosition();
glm::quat orientation = getOrientation();
_characterController.getPositionAndOrientation(position, orientation);
if (_characterController.isEnabled()) {
_characterController.getPositionAndOrientation(position, orientation);
}
nextAttitude(position, orientation);
_bodySensorMatrix = _follow.postPhysicsUpdate(*this, _bodySensorMatrix);
setVelocity(_characterController.getLinearVelocity() + _characterController.getFollowVelocity());
if (_characterController.isEnabled()) {
setVelocity(_characterController.getLinearVelocity() + _characterController.getFollowVelocity());
} else {
setVelocity(getVelocity() + _characterController.getFollowVelocity());
}
}
QString MyAvatar::getScriptedMotorFrame() const {
@ -1234,7 +1276,7 @@ void MyAvatar::setScriptedMotorFrame(QString frame) {
}
void MyAvatar::clearScriptableSettings() {
_scriptedMotorVelocity = glm::vec3(0.0f);
_scriptedMotorVelocity = Vectors::ZERO;
_scriptedMotorTimescale = DEFAULT_SCRIPTED_MOTOR_TIMESCALE;
}
@ -1499,163 +1541,97 @@ void MyAvatar::updateOrientation(float deltaTime) {
}
}
glm::vec3 MyAvatar::applyKeyboardMotor(float deltaTime, const glm::vec3& localVelocity, bool isHovering) {
if (! (_motionBehaviors & AVATAR_MOTION_KEYBOARD_MOTOR_ENABLED)) {
return localVelocity;
}
// compute motor efficiency
// The timescale of the motor is the approximate time it takes for the motor to
// accomplish its intended localVelocity. A short timescale makes the motor strong,
// and a long timescale makes it weak. The value of timescale to use depends
// on what the motor is doing:
//
// (1) braking --> short timescale (aggressive motor assertion)
// (2) pushing --> medium timescale (mild motor assertion)
// (3) inactive --> long timescale (gentle friction for low speeds)
const float MIN_KEYBOARD_MOTOR_TIMESCALE = 0.125f;
const float MAX_KEYBOARD_MOTOR_TIMESCALE = 0.4f;
const float MIN_KEYBOARD_BRAKE_SPEED = 0.3f;
float timescale = MAX_KEYBOARD_MOTOR_TIMESCALE;
bool isThrust = (glm::length2(_thrust) > EPSILON);
if (_isPushing || isThrust ||
(_scriptedMotorTimescale < MAX_KEYBOARD_MOTOR_TIMESCALE &&
(_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED))) {
// we don't want to brake if something is pushing the avatar around
timescale = _keyboardMotorTimescale;
void MyAvatar::updateActionMotor(float deltaTime) {
bool thrustIsPushing = (glm::length2(_thrust) > EPSILON);
bool scriptedMotorIsPushing = (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED)
&& _scriptedMotorTimescale < MAX_CHARACTER_MOTOR_TIMESCALE;
_isBeingPushed = thrustIsPushing || scriptedMotorIsPushing;
if (_isPushing || _isBeingPushed) {
// we don't want the motor to brake if a script is pushing the avatar around
// (we assume the avatar is driving itself via script)
_isBraking = false;
} else {
float speed = glm::length(localVelocity);
_isBraking = _wasPushing || (_isBraking && speed > MIN_KEYBOARD_BRAKE_SPEED);
if (_isBraking) {
timescale = MIN_KEYBOARD_MOTOR_TIMESCALE;
}
float speed = glm::length(_actionMotorVelocity);
const float MIN_ACTION_BRAKE_SPEED = 0.1f;
_isBraking = _wasPushing || (_isBraking && speed > MIN_ACTION_BRAKE_SPEED);
}
_wasPushing = _isPushing || isThrust;
_isPushing = false;
float motorEfficiency = glm::clamp(deltaTime / timescale, 0.0f, 1.0f);
glm::vec3 newLocalVelocity = localVelocity;
// compute action input
glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT;
glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT;
// FIXME how do I implement step translation as well?
float keyboardInput = fabsf(_driveKeys[TRANSLATE_Z]) + fabsf(_driveKeys[TRANSLATE_X]) + fabsf(_driveKeys[TRANSLATE_Y]);
if (keyboardInput) {
// Compute keyboard input
glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT;
glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT;
glm::vec3 direction = front + right;
CharacterController::State state = _characterController.getState();
if (state == CharacterController::State::Hover) {
// we're flying --> support vertical motion
glm::vec3 up = (_driveKeys[TRANSLATE_Y]) * IDENTITY_UP;
direction += up;
}
glm::vec3 direction = front + right + up;
float directionLength = glm::length(direction);
_wasPushing = _isPushing;
float directionLength = glm::length(direction);
_isPushing = directionLength > EPSILON;
//qCDebug(interfaceapp, "direction = (%.5f, %.5f, %.5f)", direction.x, direction.y, direction.z);
// Compute motor magnitude
if (directionLength > EPSILON) {
direction /= directionLength;
if (isHovering) {
// we're flying --> complex acceleration curve with high max speed
float motorSpeed = glm::length(_keyboardMotorVelocity);
float finalMaxMotorSpeed = getUniformScale() * MAX_KEYBOARD_MOTOR_SPEED;
float speedGrowthTimescale = 2.0f;
float speedIncreaseFactor = 1.8f;
motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale , 0.0f, 1.0f) * speedIncreaseFactor;
const float maxBoostSpeed = getUniformScale() * MAX_BOOST_SPEED;
if (motorSpeed < maxBoostSpeed) {
// an active keyboard motor should never be slower than this
float boostCoefficient = (maxBoostSpeed - motorSpeed) / maxBoostSpeed;
motorSpeed += MIN_AVATAR_SPEED * boostCoefficient;
motorEfficiency += (1.0f - motorEfficiency) * boostCoefficient;
} else if (motorSpeed > finalMaxMotorSpeed) {
motorSpeed = finalMaxMotorSpeed;
}
_keyboardMotorVelocity = motorSpeed * direction;
} else {
// we're using a floor --> simple exponential decay toward target walk speed
const float WALK_ACCELERATION_TIMESCALE = 0.7f; // seconds to decrease delta to 1/e
_keyboardMotorVelocity = MAX_WALKING_SPEED * direction;
motorEfficiency = glm::clamp(deltaTime / WALK_ACCELERATION_TIMESCALE, 0.0f, 1.0f);
}
_isPushing = true;
}
newLocalVelocity = localVelocity + motorEfficiency * (_keyboardMotorVelocity - localVelocity);
// normalize direction
if (_isPushing) {
direction /= directionLength;
} else {
_keyboardMotorVelocity = glm::vec3(0.0f);
newLocalVelocity = (1.0f - motorEfficiency) * localVelocity;
if (!isHovering && !_wasPushing) {
float speed = glm::length(newLocalVelocity);
if (speed > MIN_AVATAR_SPEED) {
// add small constant friction to help avatar drift to a stop sooner at low speeds
const float CONSTANT_FRICTION_DECELERATION = MIN_AVATAR_SPEED / 0.20f;
newLocalVelocity *= (speed - timescale * CONSTANT_FRICTION_DECELERATION) / speed;
direction = Vectors::ZERO;
}
if (state == CharacterController::State::Hover) {
// we're flying --> complex acceleration curve that builds on top of current motor speed and caps at some max speed
float motorSpeed = glm::length(_actionMotorVelocity);
float finalMaxMotorSpeed = getUniformScale() * MAX_ACTION_MOTOR_SPEED;
float speedGrowthTimescale = 2.0f;
float speedIncreaseFactor = 1.8f;
motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale , 0.0f, 1.0f) * speedIncreaseFactor;
const float maxBoostSpeed = getUniformScale() * MAX_BOOST_SPEED;
if (_isPushing) {
if (motorSpeed < maxBoostSpeed) {
// an active action motor should never be slower than this
float boostCoefficient = (maxBoostSpeed - motorSpeed) / maxBoostSpeed;
motorSpeed += MIN_AVATAR_SPEED * boostCoefficient;
} else if (motorSpeed > finalMaxMotorSpeed) {
motorSpeed = finalMaxMotorSpeed;
}
}
_actionMotorVelocity = motorSpeed * direction;
} else {
// we're interacting with a floor --> simple horizontal speed and exponential decay
_actionMotorVelocity = MAX_WALKING_SPEED * direction;
}
float boomChange = _driveKeys[ZOOM];
_boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange;
_boomLength = glm::clamp<float>(_boomLength, ZOOM_MIN, ZOOM_MAX);
return newLocalVelocity;
}
glm::vec3 MyAvatar::applyScriptedMotor(float deltaTime, const glm::vec3& localVelocity) {
// NOTE: localVelocity is in camera-frame because that's the frame of the default avatar motor
if (! (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED)) {
return localVelocity;
}
glm::vec3 deltaVelocity(0.0f);
if (_scriptedMotorFrame == SCRIPTED_MOTOR_CAMERA_FRAME) {
// camera frame
deltaVelocity = _scriptedMotorVelocity - localVelocity;
} else if (_scriptedMotorFrame == SCRIPTED_MOTOR_AVATAR_FRAME) {
// avatar frame
glm::quat rotation = glm::inverse(getHead()->getCameraOrientation()) * getOrientation();
deltaVelocity = rotation * _scriptedMotorVelocity - localVelocity;
} else {
// world-frame
glm::quat rotation = glm::inverse(getHead()->getCameraOrientation());
deltaVelocity = rotation * _scriptedMotorVelocity - localVelocity;
}
float motorEfficiency = glm::clamp(deltaTime / _scriptedMotorTimescale, 0.0f, 1.0f);
return localVelocity + motorEfficiency * deltaVelocity;
}
void MyAvatar::updatePosition(float deltaTime) {
// rotate velocity into camera frame
glm::quat rotation = getHead()->getCameraOrientation();
glm::vec3 localVelocity = glm::inverse(rotation) * _targetVelocity;
bool isHovering = _characterController.getState() == CharacterController::State::Hover;
glm::vec3 newLocalVelocity = applyKeyboardMotor(deltaTime, localVelocity, isHovering);
newLocalVelocity = applyScriptedMotor(deltaTime, newLocalVelocity);
// rotate back into world-frame
_targetVelocity = rotation * newLocalVelocity;
_targetVelocity += _thrust * deltaTime;
_thrust = glm::vec3(0.0f);
// cap avatar speed
float speed = glm::length(_targetVelocity);
if (speed > MAX_AVATAR_SPEED) {
_targetVelocity *= MAX_AVATAR_SPEED / speed;
speed = MAX_AVATAR_SPEED;
if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) {
updateActionMotor(deltaTime);
}
if (speed > MIN_AVATAR_SPEED && !_characterController.isEnabled()) {
// update position ourselves
applyPositionDelta(deltaTime * _targetVelocity);
vec3 velocity = getVelocity();
const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s
if (!_characterController.isEnabled()) {
// _characterController is not in physics simulation but it can still compute its target velocity
updateMotors();
_characterController.computeNewVelocity(deltaTime, velocity);
float speed2 = glm::length2(velocity);
if (speed2 > MIN_AVATAR_SPEED_SQUARED) {
// update position ourselves
applyPositionDelta(deltaTime * velocity);
}
measureMotionDerivatives(deltaTime);
} // else physics will move avatar later
// update _moving flag based on speed
const float MOVING_SPEED_THRESHOLD = 0.01f;
_moving = speed > MOVING_SPEED_THRESHOLD;
_moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED;
} else {
// physics physics simulation updated elsewhere
float speed2 = glm::length2(velocity);
_moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED;
}
// capture the head rotation, in sensor space, when the user first indicates they would like to move/fly.
if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) {
@ -1802,10 +1778,10 @@ void MyAvatar::updateMotionBehaviorFromMenu() {
}
Menu* menu = Menu::getInstance();
if (menu->isOptionChecked(MenuOption::KeyboardMotorControl)) {
_motionBehaviors |= AVATAR_MOTION_KEYBOARD_MOTOR_ENABLED;
if (menu->isOptionChecked(MenuOption::ActionMotorControl)) {
_motionBehaviors |= AVATAR_MOTION_ACTION_MOTOR_ENABLED;
} else {
_motionBehaviors &= ~AVATAR_MOTION_KEYBOARD_MOTOR_ENABLED;
_motionBehaviors &= ~AVATAR_MOTION_ACTION_MOTOR_ENABLED;
}
if (menu->isOptionChecked(MenuOption::ScriptedMotorControl)) {
_motionBehaviors |= AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED;

View file

@ -218,6 +218,7 @@ public:
MyCharacterController* getCharacterController() { return &_characterController; }
const MyCharacterController* getCharacterController() const { return &_characterController; }
void updateMotors();
void prepareForPhysicsSimulation();
void harvestResultsFromPhysicsSimulation(float deltaTime);
@ -350,6 +351,7 @@ private:
float _driveKeys[MAX_DRIVE_KEYS];
bool _wasPushing;
bool _isPushing;
bool _isBeingPushed;
bool _isBraking;
float _boomLength;
@ -358,9 +360,8 @@ private:
glm::vec3 _thrust; // impulse accumulator for outside sources
glm::vec3 _keyboardMotorVelocity; // target local-frame velocity of avatar (keyboard)
float _keyboardMotorTimescale; // timescale for avatar to achieve its target velocity
glm::vec3 _scriptedMotorVelocity; // target local-frame velocity of avatar (script)
glm::vec3 _actionMotorVelocity; // target local-frame velocity of avatar (default controller actions)
glm::vec3 _scriptedMotorVelocity; // target local-frame velocity of avatar (analog script)
float _scriptedMotorTimescale; // timescale for avatar to achieve its target velocity
int _scriptedMotorFrame;
quint32 _motionBehaviors;
@ -384,8 +385,7 @@ private:
// private methods
void updateOrientation(float deltaTime);
glm::vec3 applyKeyboardMotor(float deltaTime, const glm::vec3& velocity, bool isHovering);
glm::vec3 applyScriptedMotor(float deltaTime, const glm::vec3& velocity);
void updateActionMotor(float deltaTime);
void updatePosition(float deltaTime);
void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency);
void initHeadBones();

View file

@ -65,11 +65,11 @@ using AvatarHash = QHash<QUuid, AvatarSharedPointer>;
using AvatarDataSequenceNumber = uint16_t;
// avatar motion behaviors
const quint32 AVATAR_MOTION_KEYBOARD_MOTOR_ENABLED = 1U << 0;
const quint32 AVATAR_MOTION_ACTION_MOTOR_ENABLED = 1U << 0;
const quint32 AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED = 1U << 1;
const quint32 AVATAR_MOTION_DEFAULTS =
AVATAR_MOTION_KEYBOARD_MOTOR_ENABLED |
AVATAR_MOTION_ACTION_MOTOR_ENABLED |
AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED;
// these bits will be expanded as features are exposed

View file

@ -46,6 +46,20 @@ protected:
btRigidBody* _me;
};
CharacterController::CharacterMotor::CharacterMotor(const glm::vec3& vel, const glm::quat& rot, float horizTimescale, float vertTimescale) {
velocity = glmToBullet(vel);
rotation = glmToBullet(rot);
hTimescale = horizTimescale;
if (hTimescale < MIN_CHARACTER_MOTOR_TIMESCALE) {
hTimescale = MIN_CHARACTER_MOTOR_TIMESCALE;
}
vTimescale = vertTimescale;
if (vTimescale < 0.0f) {
vTimescale = hTimescale;
} else if (vTimescale < MIN_CHARACTER_MOTOR_TIMESCALE) {
vTimescale = MIN_CHARACTER_MOTOR_TIMESCALE;
}
}
CharacterController::CharacterController() {
_halfHeight = 1.0f;
@ -173,60 +187,16 @@ void CharacterController::preStep(btCollisionWorld* collisionWorld) {
_hasSupport = checkForSupport(collisionWorld);
}
const btScalar MIN_TARGET_SPEED = 0.001f;
const btScalar MIN_TARGET_SPEED_SQUARED = MIN_TARGET_SPEED * MIN_TARGET_SPEED;
void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) {
const btScalar MIN_SPEED = 0.001f;
btVector3 actualVelocity = _rigidBody->getLinearVelocity() - _parentVelocity;
if (actualVelocity.length() < MIN_SPEED) {
actualVelocity = btVector3(0.0f, 0.0f, 0.0f);
}
btVector3 desiredVelocity = _targetVelocity;
if (desiredVelocity.length() < MIN_SPEED) {
desiredVelocity = btVector3(0.0f, 0.0f, 0.0f);
}
// decompose into horizontal and vertical components.
btVector3 actualVertVelocity = actualVelocity.dot(_currentUp) * _currentUp;
btVector3 actualHorizVelocity = actualVelocity - actualVertVelocity;
btVector3 desiredVertVelocity = desiredVelocity.dot(_currentUp) * _currentUp;
btVector3 desiredHorizVelocity = desiredVelocity - desiredVertVelocity;
btVector3 finalVelocity;
switch (_state) {
case State::Ground:
case State::Takeoff:
{
// horizontal ground control
const btScalar WALK_ACCELERATION_TIMESCALE = 0.1f;
btScalar tau = dt / WALK_ACCELERATION_TIMESCALE;
finalVelocity = tau * desiredHorizVelocity + (1.0f - tau) * actualHorizVelocity + actualVertVelocity;
}
break;
case State::InAir:
{
// horizontal air control
const btScalar IN_AIR_ACCELERATION_TIMESCALE = 2.0f;
btScalar tau = dt / IN_AIR_ACCELERATION_TIMESCALE;
finalVelocity = tau * desiredHorizVelocity + (1.0f - tau) * actualHorizVelocity + actualVertVelocity;
}
break;
case State::Hover:
{
// vertical and horizontal air control
const btScalar FLY_ACCELERATION_TIMESCALE = 0.2f;
btScalar tau = dt / FLY_ACCELERATION_TIMESCALE;
finalVelocity = tau * desiredVelocity + (1.0f - tau) * actualVelocity;
}
break;
}
_rigidBody->setLinearVelocity(finalVelocity + _parentVelocity);
btVector3 velocity = _rigidBody->getLinearVelocity() - _parentVelocity;
computeNewVelocity(dt, velocity);
_rigidBody->setLinearVelocity(velocity + _parentVelocity);
// Dynamicaly compute a follow velocity to move this body toward the _followDesiredBodyTransform.
// Rather then add this velocity to velocity the RigidBody, we explicitly teleport the RigidBody towards its goal.
// Rather than add this velocity to velocity the RigidBody, we explicitly teleport the RigidBody towards its goal.
// This mirrors the computation done in MyAvatar::FollowHelper::postPhysicsUpdate().
const float MINIMUM_TIME_REMAINING = 0.005f;
@ -393,10 +363,6 @@ void CharacterController::getPositionAndOrientation(glm::vec3& position, glm::qu
}
}
void CharacterController::setTargetVelocity(const glm::vec3& velocity) {
_targetVelocity = glmToBullet(velocity);
}
void CharacterController::setParentVelocity(const glm::vec3& velocity) {
_parentVelocity = glmToBullet(velocity);
}
@ -438,6 +404,122 @@ glm::vec3 CharacterController::getVelocityChange() const {
return velocity;
}
void CharacterController::clearMotors() {
_motors.clear();
}
void CharacterController::addMotor(const glm::vec3& velocity, const glm::quat& rotation, float horizTimescale, float vertTimescale) {
_motors.push_back(CharacterController::CharacterMotor(velocity, rotation, horizTimescale, vertTimescale));
}
void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVelocity, std::vector<btVector3>& velocities, std::vector<btScalar>& weights) {
assert(index < (int)(_motors.size()));
CharacterController::CharacterMotor& motor = _motors[index];
if (motor.hTimescale >= MAX_CHARACTER_MOTOR_TIMESCALE && motor.vTimescale >= MAX_CHARACTER_MOTOR_TIMESCALE) {
// nothing to do
return;
}
// rotate into motor-frame
btVector3 axis = motor.rotation.getAxis();
btScalar angle = motor.rotation.getAngle();
btVector3 velocity = worldVelocity.rotate(axis, -angle);
if (_state == State::Hover || motor.hTimescale == motor.vTimescale) {
// modify velocity
btScalar tau = dt / motor.hTimescale;
if (tau > 1.0f) {
tau = 1.0f;
}
velocity += (motor.velocity - velocity) * tau;
// rotate back into world-frame
velocity = velocity.rotate(axis, angle);
// store the velocity and weight
velocities.push_back(velocity);
weights.push_back(tau);
} else {
// compute local UP
btVector3 up = _currentUp.rotate(axis, -angle);
// split velocity into horizontal and vertical components
btVector3 vVelocity = velocity.dot(up) * up;
btVector3 hVelocity = velocity - vVelocity;
btVector3 vTargetVelocity = motor.velocity.dot(up) * up;
btVector3 hTargetVelocity = motor.velocity - vTargetVelocity;
// modify each component separately
btScalar maxTau = 0.0f;
if (motor.hTimescale < MAX_CHARACTER_MOTOR_TIMESCALE) {
btScalar tau = dt / motor.hTimescale;
if (tau > 1.0f) {
tau = 1.0f;
}
maxTau = tau;
hVelocity += (hTargetVelocity - hVelocity) * tau;
}
if (motor.vTimescale < MAX_CHARACTER_MOTOR_TIMESCALE) {
btScalar tau = dt / motor.vTimescale;
if (tau > 1.0f) {
tau = 1.0f;
}
if (tau > maxTau) {
maxTau = tau;
}
vVelocity += (vTargetVelocity - vVelocity) * tau;
}
// add components back together and rotate into world-frame
velocity = (hVelocity + vVelocity).rotate(axis, angle);
// store velocity and weights
velocities.push_back(velocity);
weights.push_back(maxTau);
}
}
void CharacterController::computeNewVelocity(btScalar dt, btVector3& velocity) {
if (velocity.length2() < MIN_TARGET_SPEED_SQUARED) {
velocity = btVector3(0.0f, 0.0f, 0.0f);
}
// measure velocity changes and their weights
std::vector<btVector3> velocities;
velocities.reserve(_motors.size());
std::vector<btScalar> weights;
weights.reserve(_motors.size());
for (int i = 0; i < (int)_motors.size(); ++i) {
applyMotor(i, dt, velocity, velocities, weights);
}
assert(velocities.size() == weights.size());
// blend velocity changes according to relative weights
btScalar totalWeight = 0.0f;
for (size_t i = 0; i < weights.size(); ++i) {
totalWeight += weights[i];
}
if (totalWeight > 0.0f) {
velocity = btVector3(0.0f, 0.0f, 0.0f);
for (size_t i = 0; i < velocities.size(); ++i) {
velocity += (weights[i] / totalWeight) * velocities[i];
}
}
if (velocity.length2() < MIN_TARGET_SPEED_SQUARED) {
velocity = btVector3(0.0f, 0.0f, 0.0f);
}
// 'thrust' is applied at the very end
velocity += dt * _linearAcceleration;
_targetVelocity = velocity;
}
void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) {
btVector3 btVelocity = glmToBullet(velocity);
computeNewVelocity(dt, btVelocity);
velocity = bulletToGLM(btVelocity);
}
void CharacterController::preSimulation() {
if (_enabled && _dynamicsWorld) {
quint64 now = usecTimestampNow();
@ -447,9 +529,6 @@ void CharacterController::preSimulation() {
btVector3 velocity = _rigidBody->getLinearVelocity();
_preSimulationVelocity = velocity;
btVector3 actualVertVelocity = velocity.dot(_currentUp) * _currentUp;
btVector3 actualHorizVelocity = velocity - actualVertVelocity;
// scan for distant floor
// rayStart is at center of bottom sphere
btVector3 rayStart = _characterBodyTransform.getOrigin() - _halfHeight * _currentUp;
@ -487,6 +566,8 @@ void CharacterController::preSimulation() {
}
bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP;
btVector3 actualHorizVelocity = velocity - velocity.dot(_currentUp) * _currentUp;
bool flyingFast = _state == State::Hover && actualHorizVelocity.length() > (MAX_WALKING_SPEED * 0.75f);
switch (_state) {
@ -513,10 +594,17 @@ void CharacterController::preSimulation() {
case State::InAir: {
if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < JUMP_PROXIMITY_THRESHOLD) || _hasSupport)) {
SET_STATE(State::Ground, "hit ground");
} else if (jumpButtonHeld && (_takeoffJumpButtonID != _jumpButtonDownCount)) {
SET_STATE(State::Hover, "double jump button");
} else if (jumpButtonHeld && (now - _jumpButtonDownStartTime) > JUMP_TO_HOVER_PERIOD) {
SET_STATE(State::Hover, "jump button held");
} else {
btVector3 desiredVelocity = _targetVelocity;
if (desiredVelocity.length2() < MIN_TARGET_SPEED_SQUARED) {
desiredVelocity = btVector3(0.0f, 0.0f, 0.0f);
}
bool vertTargetSpeedIsNonZero = desiredVelocity.dot(_currentUp) > MIN_TARGET_SPEED;
if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (_takeoffJumpButtonID != _jumpButtonDownCount)) {
SET_STATE(State::Hover, "double jump button");
} else if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (now - _jumpButtonDownStartTime) > JUMP_TO_HOVER_PERIOD) {
SET_STATE(State::Hover, "jump button held");
}
}
break;
}

View file

@ -33,6 +33,9 @@ class btDynamicsWorld;
//#define DEBUG_STATE_CHANGE
const btScalar MAX_CHARACTER_MOTOR_TIMESCALE = 60.0f; // one minute
const btScalar MIN_CHARACTER_MOTOR_TIMESCALE = 0.05f;
class CharacterController : public btCharacterControllerInterface {
public:
CharacterController();
@ -62,13 +65,21 @@ public:
virtual void jump() override;
virtual bool onGround() const override;
void clearMotors();
void addMotor(const glm::vec3& velocity, const glm::quat& rotation, float horizTimescale, float vertTimescale = -1.0f);
void applyMotor(int index, btScalar dt, btVector3& worldVelocity, std::vector<btVector3>& velocities, std::vector<btScalar>& weights);
void computeNewVelocity(btScalar dt, btVector3& velocity);
void computeNewVelocity(btScalar dt, glm::vec3& velocity);
// HACK for legacy 'thrust' feature
void setLinearAcceleration(const glm::vec3& acceleration) { _linearAcceleration = glmToBullet(acceleration); }
void preSimulation();
void postSimulation();
void setPositionAndOrientation( const glm::vec3& position, const glm::quat& orientation);
void getPositionAndOrientation(glm::vec3& position, glm::quat& rotation) const;
void setTargetVelocity(const glm::vec3& velocity);
void setParentVelocity(const glm::vec3& parentVelocity);
void setFollowParameters(const glm::mat4& desiredWorldMatrix, float timeRemaining);
float getFollowTime() const { return _followTime; }
@ -110,6 +121,16 @@ protected:
bool checkForSupport(btCollisionWorld* collisionWorld) const;
protected:
struct CharacterMotor {
CharacterMotor(const glm::vec3& vel, const glm::quat& rot, float horizTimescale, float vertTimescale = -1.0f);
btVector3 velocity { btVector3(0.0f, 0.0f, 0.0f) }; // local-frame
btQuaternion rotation; // local-to-world
btScalar hTimescale { MAX_CHARACTER_MOTOR_TIMESCALE }; // horizontal
btScalar vTimescale { MAX_CHARACTER_MOTOR_TIMESCALE }; // vertical
};
std::vector<CharacterMotor> _motors;
btVector3 _currentUp;
btVector3 _targetVelocity;
btVector3 _parentVelocity;
@ -141,6 +162,7 @@ protected:
btScalar _followTime;
btVector3 _followLinearDisplacement;
btQuaternion _followAngularDisplacement;
btVector3 _linearAcceleration;
bool _enabled;
State _state;