merge fix

This commit is contained in:
Philip Rosedale 2014-09-18 08:37:14 -07:00
commit 432622d168
26 changed files with 580 additions and 281 deletions

View file

@ -309,8 +309,15 @@ void DomainServer::setupDynamicIPAddressUpdating() {
// send public socket changes to the data server so nodes can find us at our new IP
connect(nodeList, &LimitedNodeList::publicSockAddrChanged, this, &DomainServer::sendNewPublicSocketToDataServer);
// check our IP address right away
requestCurrentIPAddressViaSTUN();
if (!AccountManager::getInstance().hasValidAccessToken()) {
// we don't have an access token to talk to data-web yet, so
// check our IP address as soon as we get an AccountManager access token
connect(&AccountManager::getInstance(), &AccountManager::loginComplete,
this, &DomainServer::requestCurrentIPAddressViaSTUN);
} else {
// access token good to go, attempt to update our IP now
requestCurrentIPAddressViaSTUN();
}
} else {
qDebug() << "Cannot enable dynamic domain-server IP address updating without a domain ID."

View file

@ -16,17 +16,30 @@ var debug = false;
var movingWithHead = false;
var headStartPosition, headStartDeltaPitch, headStartFinalPitch, headStartRoll, headStartYaw;
var HEAD_MOVE_DEAD_ZONE = 0.0;
var HEAD_MOVE_DEAD_ZONE = 0.10;
var HEAD_STRAFE_DEAD_ZONE = 0.0;
var HEAD_ROTATE_DEAD_ZONE = 0.0;
var HEAD_THRUST_FWD_SCALE = 12000.0;
var HEAD_THRUST_STRAFE_SCALE = 2000.0;
//var HEAD_THRUST_FWD_SCALE = 12000.0;
//var HEAD_THRUST_STRAFE_SCALE = 0.0;
var HEAD_YAW_RATE = 1.0;
var HEAD_PITCH_RATE = 1.0;
var HEAD_ROLL_THRUST_SCALE = 75.0;
var HEAD_PITCH_LIFT_THRUST = 3.0;
//var HEAD_ROLL_THRUST_SCALE = 75.0;
//var HEAD_PITCH_LIFT_THRUST = 3.0;
var WALL_BOUNCE = 4000.0;
// Modify these values to tweak the strength of the motion.
// A larger *FACTOR increases the speed.
// A lower SHORT_TIMESCALE makes the motor achieve full speed faster.
var HEAD_VELOCITY_FWD_FACTOR = 20.0;
var HEAD_VELOCITY_LEFT_FACTOR = 20.0;
var HEAD_VELOCITY_UP_FACTOR = 20.0;
var SHORT_TIMESCALE = 0.125;
var VERY_LARGE_TIMESCALE = 1000000.0;
var xAxis = {x:1.0, y:0.0, z:0.0 };
var yAxis = {x:0.0, y:1.0, z:0.0 };
var zAxis = {x:0.0, y:0.0, z:1.0 };
// If these values are set to something
var maxVelocity = 1.25;
var noFly = true;
@ -51,8 +64,8 @@ function isInRoom(position) {
}
function moveWithHead(deltaTime) {
var thrust = { x: 0, y: 0, z: 0 };
var position = MyAvatar.position;
var motorTimescale = VERY_LARGE_TIMESCALE;
if (movingWithHead) {
var deltaYaw = MyAvatar.getHeadFinalYaw() - headStartYaw;
var deltaPitch = MyAvatar.getHeadDeltaPitch() - headStartDeltaPitch;
@ -64,44 +77,36 @@ function moveWithHead(deltaTime) {
headDelta = Vec3.multiplyQbyV(Quat.inverse(Camera.getOrientation()), headDelta);
headDelta.y = 0.0; // Don't respond to any of the vertical component of head motion
var forward = Quat.getFront(Camera.getOrientation());
var right = Quat.getRight(Camera.getOrientation());
var up = Quat.getUp(Camera.getOrientation());
if (noFly) {
forward.y = 0.0;
forward = Vec3.normalize(forward);
right.y = 0.0;
right = Vec3.normalize(right);
up = { x: 0, y: 1, z: 0};
}
// Thrust based on leaning forward and side-to-side
var targetVelocity = {x:0.0, y:0.0, z:0.0};
if (Math.abs(headDelta.z) > HEAD_MOVE_DEAD_ZONE) {
if (Math.abs(Vec3.dot(velocity, forward)) < maxVelocity) {
thrust = Vec3.sum(thrust, Vec3.multiply(forward, -headDelta.z * HEAD_THRUST_FWD_SCALE * deltaTime));
}
targetVelocity = Vec3.multiply(zAxis, -headDelta.z * HEAD_VELOCITY_FWD_FACTOR);
}
if (Math.abs(headDelta.x) > HEAD_STRAFE_DEAD_ZONE) {
if (Math.abs(Vec3.dot(velocity, right)) < maxVelocity) {
thrust = Vec3.sum(thrust, Vec3.multiply(right, headDelta.x * HEAD_THRUST_STRAFE_SCALE * deltaTime));
}
var deltaVelocity = Vec3.multiply(xAxis, -headDelta.x * HEAD_VELOCITY_LEFT_FACTOR);
targetVelocity = Vec3.sum(targetVelocity, deltaVelocity);
}
if (Math.abs(deltaYaw) > HEAD_ROTATE_DEAD_ZONE) {
var orientation = Quat.multiply(Quat.angleAxis((deltaYaw + deltaRoll) * HEAD_YAW_RATE * deltaTime, {x:0, y: 1, z:0}), MyAvatar.orientation);
var orientation = Quat.multiply(Quat.angleAxis((deltaYaw + deltaRoll) * HEAD_YAW_RATE * deltaTime, yAxis), MyAvatar.orientation);
MyAvatar.orientation = orientation;
}
// Thrust Up/Down based on head pitch
if (!noFly) {
if ((Math.abs(Vec3.dot(velocity, up)) < maxVelocity)) {
thrust = Vec3.sum(thrust, Vec3.multiply({ x:0, y:1, z:0 }, (MyAvatar.getHeadFinalPitch() - headStartFinalPitch) * HEAD_PITCH_LIFT_THRUST * deltaTime));
}
var deltaVelocity = Vec3.multiply(yAxis, headDelta.y * HEAD_VELOCITY_UP_FACTOR);
targetVelocity = Vec3.sum(targetVelocity, deltaVelocity);
}
// For head trackers, adjust pitch by head pitch
MyAvatar.headPitch += deltaPitch * HEAD_PITCH_RATE * deltaTime;
// apply the motor
MyAvatar.motorVelocity = targetVelocity;
motorTimescale = SHORT_TIMESCALE;
}
// Check against movement box limits
if (isInRoom(position)) {
// Impose constraints to keep you in the space
var thrust = { x: 0, y: 0, z: 0 };
// use thrust to constrain the avatar to the space
if (position.x < roomLimits.xMin) {
thrust.x += (roomLimits.xMin - position.x) * WALL_BOUNCE * deltaTime;
} else if (position.x > roomLimits.xMax) {
@ -112,11 +117,14 @@ function moveWithHead(deltaTime) {
} else if (position.z > roomLimits.zMax) {
thrust.z += (roomLimits.zMax - position.z) * WALL_BOUNCE * deltaTime;
}
MyAvatar.addThrust(thrust);
if (movingWithHead && Vec3.length(thrust) > 0.0) {
// reduce the timescale of the motor so that it won't defeat the thrust code
Vec3.print("adebug room containment thrust = ", thrust);
motorTimescale = 4.0 * SHORT_TIMESCALE;
}
}
// Check against movement box limits
MyAvatar.addThrust(thrust);
MyAvatar.motorTimescale = motorTimescale;
}
Controller.keyPressEvent.connect(function(event) {
@ -126,13 +134,21 @@ Controller.keyPressEvent.connect(function(event) {
headStartDeltaPitch = MyAvatar.getHeadDeltaPitch();
headStartFinalPitch = MyAvatar.getHeadFinalPitch();
headStartRoll = MyAvatar.getHeadFinalRoll();
headStartYaw = MyAvatar.getHeadFinalYaw();
}
headStartYaw = MyAvatar.getHeadFinalYaw();
// start with disabled motor -- it will be updated shortly
MyAvatar.motorTimescale = VERY_LARGE_TIMESCALE;
MyAvatar.motorVelocity = {x:0.0, y:0.0, z:0.0};
MyAvatar.motorReferenceFrame = "camera"; // alternatives are: "avatar" and "world"
}
});
Controller.keyReleaseEvent.connect(function(event) {
if (event.text == "SPACE") {
movingWithHead = false;
}
// disable motor by giving it an obnoxiously large timescale
MyAvatar.motorTimescale = VERY_LARGE_TIMESCALE;
MyAvatar.motorVelocity = {x:0.0, y:0.0, z:0.0};
}
});
Script.update.connect(moveWithHead);

View file

@ -288,6 +288,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
// set the account manager's root URL and trigger a login request if we don't have the access token
accountManager.setAuthURL(DEFAULT_NODE_AUTH_URL);
UserActivityLogger::getInstance().launch(applicationVersion());
// grab the location manager instance early so it lives in our thread
LocationManager::getInstance();
// once the event loop has started, check and signal for an access token
QMetaObject::invokeMethod(&accountManager, "checkAndSignalForAccessToken", Qt::QueuedConnection);
@ -927,6 +930,13 @@ void Application::keyPressEvent(QKeyEvent* event) {
}
break;
case Qt::Key_N:
if (isMeta) {
Menu::getInstance()->triggerOption(MenuOption::NameLocation);
}
break;
case Qt::Key_Up:
if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
@ -3890,7 +3900,7 @@ void Application::stopAllScripts(bool restart) {
// HACK: ATM scripts cannot set/get their animation priorities, so we clear priorities
// whenever a script stops in case it happened to have been setting joint rotations.
// TODO: expose animation priorities and provide a layered animation control system.
_myAvatar->clearJointAnimationPriorities();
_myAvatar->clearScriptableSettings();
}
void Application::stopScript(const QString &scriptName) {
@ -3903,6 +3913,9 @@ void Application::stopScript(const QString &scriptName) {
// TODO: expose animation priorities and provide a layered animation control system.
_myAvatar->clearJointAnimationPriorities();
}
if (_scriptEnginesHash.empty()) {
_myAvatar->clearScriptableSettings();
}
}
void Application::reloadAllScripts() {

View file

@ -268,13 +268,17 @@ Menu::Menu() :
SLOT(resetSize()));
QObject* avatar = appInstance->getAvatar();
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::KeyboardMotorControl,
Qt::CTRL | Qt::SHIFT | Qt::Key_K, true, avatar, SLOT(updateMotionBehavior()));
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::ScriptedMotorControl, 0, true,
avatar, SLOT(updateMotionBehavior()));
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::ChatCircling, 0, false);
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::GlowWhenSpeaking, 0, true);
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::BlueSpeechSphere, 0, true);
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::ObeyEnvironmentalGravity, Qt::SHIFT | Qt::Key_G, false,
avatar, SLOT(updateMotionBehaviorsFromMenu()));
avatar, SLOT(updateMotionBehavior()));
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::StandOnNearbyFloors, 0, true,
avatar, SLOT(updateMotionBehaviorsFromMenu()));
avatar, SLOT(updateMotionBehavior()));
QMenu* collisionsMenu = avatarMenu->addMenu("Collide With...");
addCheckableActionToQMenuAndActionHash(collisionsMenu, MenuOption::CollideAsRagdoll, 0, false,
@ -328,9 +332,9 @@ Menu::Menu() :
addCheckableActionToQMenuAndActionHash(nodeBordersMenu, MenuOption::ShowBordersVoxelNodes,
Qt::CTRL | Qt::SHIFT | Qt::Key_1, false,
&nodeBounds, SLOT(setShowVoxelNodes(bool)));
addCheckableActionToQMenuAndActionHash(nodeBordersMenu, MenuOption::ShowBordersModelNodes,
addCheckableActionToQMenuAndActionHash(nodeBordersMenu, MenuOption::ShowBordersEntityNodes,
Qt::CTRL | Qt::SHIFT | Qt::Key_2, false,
&nodeBounds, SLOT(setShowModelNodes(bool)));
&nodeBounds, SLOT(setShowEntityNodes(bool)));
addCheckableActionToQMenuAndActionHash(nodeBordersMenu, MenuOption::ShowBordersParticleNodes,
Qt::CTRL | Qt::SHIFT | Qt::Key_3, false,
&nodeBounds, SLOT(setShowParticleNodes(bool)));
@ -744,9 +748,11 @@ void Menu::loadSettings(QSettings* settings) {
// MyAvatar caches some menu options, so we have to update them whenever we load settings.
// TODO: cache more settings in MyAvatar that are checked with very high frequency.
setIsOptionChecked(MenuOption::KeyboardMotorControl , true);
MyAvatar* myAvatar = Application::getInstance()->getAvatar();
myAvatar->updateCollisionGroups();
myAvatar->onToggleRagdoll();
myAvatar->updateMotionBehavior();
if (lockedSettings) {
Application::getInstance()->unlockSettings();

View file

@ -399,6 +399,7 @@ namespace MenuOption {
const QString HeadMouse = "Head Mouse";
const QString IncreaseAvatarSize = "Increase Avatar Size";
const QString IncreaseVoxelSize = "Increase Voxel Size";
const QString KeyboardMotorControl = "Enable Keyboard Motor Control";
const QString LoadScript = "Open and Run Script File...";
const QString LoadScriptURL = "Open and Run Script from URL...";
const QString LodTools = "LOD Tools";
@ -437,9 +438,10 @@ namespace MenuOption {
const QString RunningScripts = "Running Scripts";
const QString RunTimingTests = "Run Timing Tests";
const QString ScriptEditor = "Script Editor...";
const QString ScriptedMotorControl = "Enable Scripted Motor Control";
const QString SettingsExport = "Export Settings";
const QString SettingsImport = "Import Settings";
const QString ShowBordersModelNodes = "Show Model Nodes";
const QString ShowBordersEntityNodes = "Show Entity Nodes";
const QString ShowBordersParticleNodes = "Show Particle Nodes";
const QString ShowBordersVoxelNodes = "Show Voxel Nodes";
const QString ShowIKConstraints = "Show IK Constraints";

View file

@ -42,17 +42,15 @@ ScriptsModel::ScriptsModel(QObject* parent) :
_localDirectory(),
_fsWatcher(),
_localFiles(),
_remoteFiles() {
QString scriptPath = Menu::getInstance()->getScriptsLocation();
_localDirectory.setPath(scriptPath);
_remoteFiles()
{
_localDirectory.setFilter(QDir::Files | QDir::Readable);
_localDirectory.setNameFilters(QStringList("*.js"));
_fsWatcher.addPath(_localDirectory.absolutePath());
updateScriptsLocation(Menu::getInstance()->getScriptsLocation());
connect(&_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &ScriptsModel::reloadLocalFiles);
connect(Menu::getInstance(), &Menu::scriptLocationChanged, this, &ScriptsModel::updateScriptsLocation);
reloadLocalFiles();
@ -88,8 +86,13 @@ int ScriptsModel::rowCount(const QModelIndex& parent) const {
void ScriptsModel::updateScriptsLocation(const QString& newPath) {
_fsWatcher.removePath(_localDirectory.absolutePath());
_localDirectory.setPath(newPath);
_fsWatcher.addPath(_localDirectory.absolutePath());
if (!_localDirectory.absolutePath().isEmpty()) {
_fsWatcher.addPath(_localDirectory.absolutePath());
}
reloadLocalFiles();
}

View file

@ -57,15 +57,15 @@ void FaceModel::maybeUpdateNeckRotation(const JointState& parentState, const FBX
* joint.rotation, DEFAULT_PRIORITY);
}
void FaceModel::maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) {
void FaceModel::maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, JointState& state) {
// likewise with the eye joints
// NOTE: at the moment we do the math in the world-frame, hence the inverse transform is more complex than usual.
glm::mat4 inverse = glm::inverse(glm::mat4_cast(_rotation) * parentState.getTransform() *
glm::mat4 inverse = glm::inverse(glm::mat4_cast(model->getRotation()) * parentState.getTransform() *
glm::translate(state.getDefaultTranslationInConstrainedFrame()) *
joint.preTransform * glm::mat4_cast(joint.preRotation * joint.rotation));
glm::vec3 front = glm::vec3(inverse * glm::vec4(_owningHead->getFinalOrientationInWorldFrame() * IDENTITY_FRONT, 0.0f));
glm::vec3 lookAt = glm::vec3(inverse * glm::vec4(_owningHead->getLookAtPosition() +
_owningHead->getSaccade() - _translation, 1.0f));
_owningHead->getSaccade() - model->getTranslation(), 1.0f));
glm::quat between = rotationBetween(front, lookAt);
const float MAX_ANGLE = 30.0f * RADIANS_PER_DEGREE;
state.setRotationInConstrainedFrame(glm::angleAxis(glm::clamp(glm::angle(between), -MAX_ANGLE, MAX_ANGLE), glm::axis(between)) *
@ -82,7 +82,7 @@ void FaceModel::updateJointState(int index) {
maybeUpdateNeckRotation(parentState, joint, state);
} else if (index == geometry.leftEyeJointIndex || index == geometry.rightEyeJointIndex) {
maybeUpdateEyeRotation(parentState, joint, state);
maybeUpdateEyeRotation(this, parentState, joint, state);
}
}

View file

@ -27,7 +27,7 @@ public:
virtual void simulate(float deltaTime, bool fullUpdate = true);
virtual void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state);
virtual void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state);
virtual void maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, JointState& state);
virtual void updateJointState(int index);
/// Retrieve the positions of up to two eye meshes.

View file

@ -194,8 +194,8 @@ void Head::relaxLean(float deltaTime) {
}
void Head::render(float alpha, Model::RenderMode mode) {
if (_faceModel.render(alpha, mode, Menu::getInstance()->isOptionChecked(MenuOption::AvatarsReceiveShadows)) &&
_renderLookatVectors && mode != Model::SHADOW_RENDER_MODE) {
_faceModel.render(alpha, mode, Menu::getInstance()->isOptionChecked(MenuOption::AvatarsReceiveShadows));
if (_renderLookatVectors && mode != Model::SHADOW_RENDER_MODE) {
renderLookatVectors(_leftEyePosition, _rightEyePosition, _lookAtPosition);
}
}

View file

@ -54,9 +54,14 @@ const float MAX_WALKING_SPEED = 3.0f * MIN_KEYBOARD_CONTROL_SPEED;
// TODO: normalize avatar speed for standard avatar size, then scale all motion logic
// to properly follow avatar size.
float DEFAULT_MOTOR_TIMESCALE = 0.25f;
float MAX_AVATAR_SPEED = 300.0f;
float MAX_MOTOR_SPEED = MAX_AVATAR_SPEED;
float MAX_KEYBOARD_MOTOR_SPEED = MAX_AVATAR_SPEED;
float DEFAULT_KEYBOARD_MOTOR_TIMESCALE = 0.25f;
float MIN_SCRIPTED_MOTOR_TIMESCALE = 0.005f;
float DEFAULT_SCRIPTED_MOTOR_TIMESCALE = 1.0e6f;
const int SCRIPTED_MOTOR_AVATAR_FRAME = 0;
const int SCRIPTED_MOTOR_CAMERA_FRAME = 1;
const int SCRIPTED_MOTOR_WORLD_FRAME = 2;
MyAvatar::MyAvatar() :
Avatar(),
@ -71,9 +76,11 @@ MyAvatar::MyAvatar() :
_isBraking(false),
_trapDuration(0.0f),
_thrust(0.0f),
_motorVelocity(0.0f),
_motorTimescale(DEFAULT_MOTOR_TIMESCALE),
_maxMotorSpeed(MAX_MOTOR_SPEED),
_keyboardMotorVelocity(0.0f),
_keyboardMotorTimescale(DEFAULT_KEYBOARD_MOTOR_TIMESCALE),
_scriptedMotorVelocity(0.0f),
_scriptedMotorTimescale(DEFAULT_SCRIPTED_MOTOR_TIMESCALE),
_scriptedMotorFrame(SCRIPTED_MOTOR_CAMERA_FRAME),
_motionBehaviors(AVATAR_MOTION_DEFAULTS),
_lookAtTargetAvatar(),
_shouldRender(true),
@ -1028,6 +1035,48 @@ void MyAvatar::setAttachmentData(const QVector<AttachmentData>& attachmentData)
_billboardValid = false;
}
QString MyAvatar::getScriptedMotorFrame() const {
QString frame = "avatar";
if (_scriptedMotorFrame == SCRIPTED_MOTOR_CAMERA_FRAME) {
frame = "camera";
} else if (_scriptedMotorFrame == SCRIPTED_MOTOR_WORLD_FRAME) {
frame = "world";
}
return frame;
}
void MyAvatar::setScriptedMotorVelocity(const glm::vec3& velocity) {
float MAX_SCRIPTED_MOTOR_SPEED = 500.0f;
_scriptedMotorVelocity = velocity;
float speed = glm::length(_scriptedMotorVelocity);
if (speed > MAX_SCRIPTED_MOTOR_SPEED) {
_scriptedMotorVelocity *= MAX_SCRIPTED_MOTOR_SPEED / speed;
}
}
void MyAvatar::setScriptedMotorTimescale(float timescale) {
// we clamp the timescale on the large side (instead of just the low side) to prevent
// obnoxiously large values from introducing NaN into avatar's velocity
_scriptedMotorTimescale = glm::clamp(timescale, MIN_SCRIPTED_MOTOR_TIMESCALE,
DEFAULT_SCRIPTED_MOTOR_TIMESCALE);
}
void MyAvatar::setScriptedMotorFrame(QString frame) {
if (frame.toLower() == "avatar") {
_scriptedMotorFrame = SCRIPTED_MOTOR_AVATAR_FRAME;
} else if (frame.toLower() == "camera") {
_scriptedMotorFrame = SCRIPTED_MOTOR_CAMERA_FRAME;
} else if (frame.toLower() == "world") {
_scriptedMotorFrame = SCRIPTED_MOTOR_WORLD_FRAME;
}
}
void MyAvatar::clearScriptableSettings() {
clearJointAnimationPriorities();
_scriptedMotorVelocity = glm::vec3(0.0f);
_scriptedMotorTimescale = DEFAULT_SCRIPTED_MOTOR_TIMESCALE;
}
void MyAvatar::attach(const QString& modelURL, const QString& jointName, const glm::vec3& translation,
const glm::quat& rotation, float scale, bool allowDuplicates, bool useSaved) {
if (QThread::currentThread() != thread()) {
@ -1139,41 +1188,133 @@ void MyAvatar::updateOrientation(float deltaTime) {
setOrientation(orientation);
}
glm::vec3 MyAvatar::applyKeyboardMotor(float deltaTime, const glm::vec3& localVelocity, bool hasFloor) {
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)
float MIN_KEYBOARD_MOTOR_TIMESCALE = 0.125f;
float MAX_KEYBOARD_MOTOR_TIMESCALE = 0.4f;
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 break if anything is pushing the avatar around
timescale = _keyboardMotorTimescale;
_isBraking = false;
} else {
float speed = glm::length(localVelocity);
_isBraking = _wasPushing || (_isBraking && speed > MIN_KEYBOARD_BRAKE_SPEED);
if (_isBraking) {
timescale = MIN_KEYBOARD_MOTOR_TIMESCALE;
}
}
_wasPushing = _isPushing || isThrust;
_isPushing = false;
float motorEfficiency = glm::clamp(deltaTime / timescale, 0.0f, 1.0f);
float keyboardInput = fabsf(_driveKeys[FWD] - _driveKeys[BACK]) +
(fabsf(_driveKeys[RIGHT] - _driveKeys[LEFT])) +
fabsf(_driveKeys[UP] - _driveKeys[DOWN]);
if (keyboardInput) {
// Compute keyboard input
glm::vec3 front = (_driveKeys[FWD] - _driveKeys[BACK]) * IDENTITY_FRONT;
glm::vec3 right = (_driveKeys[RIGHT] - _driveKeys[LEFT]) * IDENTITY_RIGHT;
glm::vec3 up = (_driveKeys[UP] - _driveKeys[DOWN]) * IDENTITY_UP;
glm::vec3 direction = front + right + up;
float directionLength = glm::length(direction);
// Compute motor magnitude
if (directionLength > EPSILON) {
direction /= directionLength;
// Compute the target keyboard velocity (which ramps up slowly, and damps very quickly)
// the max magnitude of which depends on what we're doing:
float finalMaxMotorSpeed = hasFloor ? _scale * MAX_WALKING_SPEED : _scale * MAX_KEYBOARD_MOTOR_SPEED;
float motorLength = glm::length(_keyboardMotorVelocity);
if (motorLength < _scale * MIN_KEYBOARD_CONTROL_SPEED) {
// an active keyboard motor should never be slower than this
_keyboardMotorVelocity = _scale * MIN_KEYBOARD_CONTROL_SPEED * direction;
motorEfficiency = 1.0f;
} else {
float KEYBOARD_MOTOR_LENGTH_TIMESCALE = 2.0f;
float INCREASE_FACTOR = 1.8f;
motorLength *= 1.0f + glm::clamp(deltaTime / KEYBOARD_MOTOR_LENGTH_TIMESCALE, 0.0f, 1.0f) * INCREASE_FACTOR;
if (motorLength > finalMaxMotorSpeed) {
motorLength = finalMaxMotorSpeed;
}
_keyboardMotorVelocity = motorLength * direction;
}
_isPushing = true;
}
} else {
_keyboardMotorVelocity = glm::vec3(0.0f);
}
// apply keyboard motor
return localVelocity + motorEfficiency * (_keyboardMotorVelocity - localVelocity);
}
glm::vec3 MyAvatar::applyScriptedMotor(float deltaTime, const glm::vec3& localVelocity) {
if (! (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED)) {
return localVelocity;
}
float motorEfficiency = glm::clamp(deltaTime / _scriptedMotorTimescale, 0.0f, 1.0f);
return localVelocity + motorEfficiency * (_scriptedMotorVelocity - localVelocity);
}
const float NEARBY_FLOOR_THRESHOLD = 5.0f;
void MyAvatar::updatePosition(float deltaTime) {
// check for floor by casting a ray straight down from avatar's position
float heightAboveFloor = FLT_MAX;
bool hasFloor = false;
const CapsuleShape& boundingShape = _skeletonModel.getBoundingShape();
const float maxFloorDistance = boundingShape.getBoundingRadius() * NEARBY_FLOOR_THRESHOLD;
RayIntersectionInfo intersection;
// NOTE: avatar is center of PhysicsSimulation, so rayStart is the origin for the purposes of the raycast
intersection._rayStart = glm::vec3(0.0f);
intersection._rayDirection = - _worldUpDirection;
intersection._rayLength = 5.0f * boundingShape.getBoundingRadius();
intersection._rayLength = 4.0f * boundingShape.getBoundingRadius();
if (_physicsSimulation.findFloorRayIntersection(intersection)) {
// NOTE: heightAboveFloor is the distance between the bottom of the avatar and the floor
heightAboveFloor = intersection._hitDistance - boundingShape.getBoundingRadius();
heightAboveFloor = intersection._hitDistance - boundingShape.getBoundingRadius()
+ _skeletonModel.getBoundingShapeOffset().y;
if (heightAboveFloor < maxFloorDistance) {
hasFloor = true;
}
}
// velocity is initialized to the measured _velocity but will be modified by friction, external thrust, etc
glm::vec3 velocity = _velocity;
bool pushingUp = (_driveKeys[UP] - _driveKeys[DOWN] > 0.0f);
bool walkingOnFloor = false;
bool pushingUp = (_driveKeys[UP] - _driveKeys[DOWN] > 0.0f) || _scriptedMotorVelocity.y > 0.0f;
if (_motionBehaviors & AVATAR_MOTION_STAND_ON_NEARBY_FLOORS) {
const float MAX_SPEED_UNDER_GRAVITY = 2.0f * _scale * MAX_WALKING_SPEED;
if (pushingUp || glm::length2(velocity) > MAX_SPEED_UNDER_GRAVITY * MAX_SPEED_UNDER_GRAVITY) {
// we're pushing up or moving quickly, so disable gravity
setLocalGravity(glm::vec3(0.0f));
hasFloor = false;
} else {
const float maxFloorDistance = boundingShape.getBoundingRadius() * NEARBY_FLOOR_THRESHOLD;
if (heightAboveFloor > maxFloorDistance) {
// disable local gravity when floor is too far away
setLocalGravity(glm::vec3(0.0f));
hasFloor = false;
} else {
// enable gravity
walkingOnFloor = true;
setLocalGravity(-_worldUpDirection);
}
}
@ -1182,76 +1323,35 @@ void MyAvatar::updatePosition(float deltaTime) {
bool zeroDownwardVelocity = false;
bool gravityEnabled = (glm::length2(_gravity) > EPSILON);
if (gravityEnabled) {
if (heightAboveFloor < 0.0f) {
// Gravity is in effect so we assume that the avatar is colliding against the world and we need
// to lift avatar out of floor, but we don't want to do it too fast (keep it smooth).
float distanceToLift = glm::min(-heightAboveFloor, MAX_WALKING_SPEED * deltaTime);
// We don't use applyPositionDelta() for this lift distance because we don't want the avatar
// to come flying out of the floor. Instead we update position directly, and set a boolean
// that will remind us later to zero any downward component of the velocity.
_position += (distanceToLift - EPSILON) * _worldUpDirection;
const float SLOP = 0.002f;
if (heightAboveFloor < SLOP) {
if (heightAboveFloor < 0.0) {
// Gravity is in effect so we assume that the avatar is colliding against the world and we need
// to lift avatar out of floor, but we don't want to do it too fast (keep it smooth).
float distanceToLift = glm::min(-heightAboveFloor, MAX_WALKING_SPEED * deltaTime);
// We don't use applyPositionDelta() for this lift distance because we don't want the avatar
// to come flying out of the floor. Instead we update position directly, and set a boolean
// that will remind us later to zero any downward component of the velocity.
_position += distanceToLift * _worldUpDirection;
}
zeroDownwardVelocity = true;
}
velocity += (deltaTime * GRAVITY_EARTH) * _gravity;
}
float motorEfficiency = glm::clamp(deltaTime / computeMotorTimescale(velocity), 0.0f, 1.0f);
// compute targetVelocity
glm::vec3 targetVelocity(0.0f);
if (_motionBehaviors & AVATAR_MOTION_MOTOR_KEYBOARD_ENABLED) {
float keyboardInput = fabsf(_driveKeys[FWD] - _driveKeys[BACK]) +
(fabsf(_driveKeys[RIGHT] - _driveKeys[LEFT])) +
fabsf(_driveKeys[UP] - _driveKeys[DOWN]);
if (keyboardInput) {
// Compute keyboard input
glm::vec3 front = (_driveKeys[FWD] - _driveKeys[BACK]) * IDENTITY_FRONT;
glm::vec3 right = (_driveKeys[RIGHT] - _driveKeys[LEFT]) * IDENTITY_RIGHT;
glm::vec3 up = (_driveKeys[UP] - _driveKeys[DOWN]) * IDENTITY_UP;
glm::vec3 direction = front + right + up;
float directionLength = glm::length(direction);
// Compute motor magnitude
if (directionLength > EPSILON) {
direction /= directionLength;
// Compute the target keyboard velocity (which ramps up slowly, and damps very quickly)
// the max magnitude of which depends on what we're doing:
float finalMaxMotorSpeed = walkingOnFloor ? _scale * MAX_WALKING_SPEED : _scale * _maxMotorSpeed;
float motorLength = glm::length(_motorVelocity);
if (motorLength < _scale * MIN_KEYBOARD_CONTROL_SPEED) {
// an active keyboard motor should never be slower than this
_motorVelocity = _scale * MIN_KEYBOARD_CONTROL_SPEED * direction;
motorEfficiency = 1.0f;
} else {
float MOTOR_LENGTH_TIMESCALE = 2.0f;
float INCREASE_FACTOR = 1.8f;
motorLength *= 1.0f + glm::clamp(deltaTime / MOTOR_LENGTH_TIMESCALE, 0.0f, 1.0f) * INCREASE_FACTOR;
if (motorLength > finalMaxMotorSpeed) {
motorLength = finalMaxMotorSpeed;
}
_motorVelocity = motorLength * direction;
}
_isPushing = true;
}
targetVelocity = _motorVelocity;
} else {
_motorVelocity = glm::vec3(0.0f);
if (!zeroDownwardVelocity) {
velocity += (deltaTime * GRAVITY_EARTH) * _gravity;
}
}
targetVelocity = getHead()->getCameraOrientation() * targetVelocity;
glm::vec3 deltaVelocity = targetVelocity - velocity;
// rotate velocity into camera frame
glm::quat rotation = getHead()->getCameraOrientation();
glm::vec3 localVelocity = glm::inverse(rotation) * velocity;
if (walkingOnFloor && !pushingUp) {
// remove vertical component of deltaVelocity
deltaVelocity -= glm::dot(deltaVelocity, _worldUpDirection) * _worldUpDirection;
}
// apply motors in camera frame
glm::vec3 newLocalVelocity = applyKeyboardMotor(deltaTime, localVelocity, hasFloor);
newLocalVelocity = applyScriptedMotor(deltaTime, newLocalVelocity);
// apply motor
velocity += motorEfficiency * deltaVelocity;
// rotate back into world-frame
velocity = rotation * newLocalVelocity;
// apply thrust
velocity += _thrust * deltaTime;
@ -1260,8 +1360,8 @@ void MyAvatar::updatePosition(float deltaTime) {
// remove downward velocity so we don't push into floor
if (zeroDownwardVelocity) {
float verticalSpeed = glm::dot(velocity, _worldUpDirection);
if (verticalSpeed < 0.0f) {
velocity += verticalSpeed * _worldUpDirection;
if (verticalSpeed < 0.0f || !pushingUp) {
velocity -= verticalSpeed * _worldUpDirection;
}
}
@ -1286,37 +1386,6 @@ void MyAvatar::updatePosition(float deltaTime) {
measureMotionDerivatives(deltaTime);
}
float MyAvatar::computeMotorTimescale(const glm::vec3& velocity) {
// The timescale of the motor is the approximate time it takes for the motor to
// accomplish its intended velocity. 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)
float MIN_MOTOR_TIMESCALE = 0.125f;
float MAX_MOTOR_TIMESCALE = 0.4f;
float MIN_BRAKE_SPEED = 0.3f;
float timescale = MAX_MOTOR_TIMESCALE;
bool isThrust = (glm::length2(_thrust) > EPSILON);
if (_isPushing || isThrust) {
timescale = _motorTimescale;
_isBraking = false;
} else {
float speed = glm::length(velocity);
_isBraking = _wasPushing || (_isBraking && speed > MIN_BRAKE_SPEED);
if (_isBraking) {
timescale = MIN_MOTOR_TIMESCALE;
}
}
_wasPushing = _isPushing || isThrust;
_isPushing = false;
return timescale;
}
void MyAvatar::updateCollisionWithEnvironment(float deltaTime, float radius) {
glm::vec3 up = getBodyUpDirection();
const float ENVIRONMENT_SURFACE_ELASTICITY = 0.0f;
@ -1436,10 +1505,10 @@ void MyAvatar::updateCollisionWithVoxels(float deltaTime, float radius) {
if (highestStep > MIN_STEP_HEIGHT && highestStep < MAX_STEP_HEIGHT && verticalPenetration <= 0.0f) {
// we're colliding against an edge
// rotate _motorVelocity into world frame
glm::vec3 targetVelocity = _motorVelocity;
// rotate _keyboardMotorVelocity into world frame
glm::vec3 targetVelocity = _keyboardMotorVelocity;
glm::quat rotation = getHead()->getCameraOrientation();
targetVelocity = rotation * _motorVelocity;
targetVelocity = rotation * _keyboardMotorVelocity;
if (_wasPushing && glm::dot(targetVelocity, totalPenetration) > EPSILON) {
// we're puhing into the edge, so we want to lift
@ -1799,7 +1868,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition,
emit transformChanged();
}
void MyAvatar::updateMotionBehaviorsFromMenu() {
void MyAvatar::updateMotionBehavior() {
Menu* menu = Menu::getInstance();
if (menu->isOptionChecked(MenuOption::ObeyEnvironmentalGravity)) {
_motionBehaviors |= AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY;
@ -1822,6 +1891,16 @@ void MyAvatar::updateMotionBehaviorsFromMenu() {
if (!(_collisionGroups | COLLISION_GROUP_VOXELS)) {
_voxelShapeManager.clearShapes();
}
if (menu->isOptionChecked(MenuOption::KeyboardMotorControl)) {
_motionBehaviors |= AVATAR_MOTION_KEYBOARD_MOTOR_ENABLED;
} else {
_motionBehaviors &= ~AVATAR_MOTION_KEYBOARD_MOTOR_ENABLED;
}
if (menu->isOptionChecked(MenuOption::ScriptedMotorControl)) {
_motionBehaviors |= AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED;
} else {
_motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED;
}
}
void MyAvatar::onToggleRagdoll() {
@ -1867,25 +1946,6 @@ void MyAvatar::setCollisionGroups(quint32 collisionGroups) {
}
}
void MyAvatar::setMotionBehaviorsByScript(quint32 flags) {
// start with the defaults
_motionBehaviors = AVATAR_MOTION_DEFAULTS;
// add the set scriptable bits
_motionBehaviors += flags & AVATAR_MOTION_SCRIPTABLE_BITS;
// reconcile incompatible settings from menu (if any)
Menu* menu = Menu::getInstance();
menu->setIsOptionChecked(MenuOption::ObeyEnvironmentalGravity, (bool)(_motionBehaviors & AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY));
// Environmental and Local gravities are incompatible. Environmental setting trumps local.
if (_motionBehaviors & AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY) {
_motionBehaviors &= ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY;
setGravity(Application::getInstance()->getEnvironment()->getGravity(getPosition()));
} else if (! (_motionBehaviors & (AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY | AVATAR_MOTION_OBEY_LOCAL_GRAVITY))) {
setGravity(glm::vec3(0.0f));
}
}
void MyAvatar::applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration) {
glm::vec3 leverAxis = contactPoint - getPosition();
float leverLength = glm::length(leverAxis);

View file

@ -33,7 +33,9 @@ enum AvatarHandState
class MyAvatar : public Avatar {
Q_OBJECT
Q_PROPERTY(bool shouldRenderLocally READ getShouldRenderLocally WRITE setShouldRenderLocally)
Q_PROPERTY(quint32 motionBehaviors READ getMotionBehaviorsForScript WRITE setMotionBehaviorsByScript)
Q_PROPERTY(glm::vec3 motorVelocity READ getScriptedMotorVelocity WRITE setScriptedMotorVelocity)
Q_PROPERTY(float motorTimescale READ getScriptedMotorTimescale WRITE setScriptedMotorTimescale)
Q_PROPERTY(QString motorReferenceFrame READ getScriptedMotorFrame WRITE setScriptedMotorFrame)
Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setLocalGravity)
public:
@ -131,15 +133,22 @@ public:
void clearJointAnimationPriorities();
glm::vec3 getScriptedMotorVelocity() const { return _scriptedMotorVelocity; }
float getScriptedMotorTimescale() const { return _scriptedMotorTimescale; }
QString getScriptedMotorFrame() const;
void setScriptedMotorVelocity(const glm::vec3& velocity);
void setScriptedMotorTimescale(float timescale);
void setScriptedMotorFrame(QString frame);
void clearScriptableSettings();
virtual void attach(const QString& modelURL, const QString& jointName = QString(),
const glm::vec3& translation = glm::vec3(), const glm::quat& rotation = glm::quat(), float scale = 1.0f,
bool allowDuplicates = false, bool useSaved = true);
virtual void setCollisionGroups(quint32 collisionGroups);
void setMotionBehaviorsByScript(quint32 flags);
quint32 getMotionBehaviorsForScript() const { return _motionBehaviors & AVATAR_MOTION_SCRIPTABLE_BITS; }
void applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration);
/// Renders a laser pointer for UI picking
@ -165,7 +174,7 @@ public slots:
void setVelocity(const glm::vec3 velocity) { _velocity = velocity; }
void updateMotionBehaviorsFromMenu();
void updateMotionBehavior();
void onToggleRagdoll();
glm::vec3 getLeftPalmPosition();
@ -204,9 +213,11 @@ private:
float _trapDuration; // seconds that avatar has been trapped by collisions
glm::vec3 _thrust; // impulse accumulator for outside sources
glm::vec3 _motorVelocity; // intended velocity of avatar motion (relative to what it's standing on)
float _motorTimescale; // timescale for avatar motor to achieve its desired velocity
float _maxMotorSpeed;
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)
float _scriptedMotorTimescale; // timescale for avatar to achieve its target velocity
int _scriptedMotorFrame;
quint32 _motionBehaviors;
QWeakPointer<AvatarData> _lookAtTargetAvatar;
@ -223,8 +234,9 @@ private:
// private methods
void updateOrientation(float deltaTime);
glm::vec3 applyKeyboardMotor(float deltaTime, const glm::vec3& velocity, bool walkingOnFloor);
glm::vec3 applyScriptedMotor(float deltaTime, const glm::vec3& velocity);
void updatePosition(float deltaTime);
float computeMotorTimescale(const glm::vec3& velocity);
void updateCollisionWithAvatars(float deltaTime);
void updateCollisionWithEnvironment(float deltaTime, float radius);
void updateCollisionWithVoxels(float deltaTime, float radius);

View file

@ -281,7 +281,7 @@ void SkeletonModel::maybeUpdateNeckRotation(const JointState& parentState, const
}
void SkeletonModel::maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) {
_owningAvatar->getHead()->getFaceModel().maybeUpdateEyeRotation(parentState, joint, state);
_owningAvatar->getHead()->getFaceModel().maybeUpdateEyeRotation(this, parentState, joint, state);
}
void SkeletonModel::renderJointConstraints(int jointIndex) {

View file

@ -109,6 +109,7 @@ public:
void renderJointCollisionShapes(float alpha);
float getBoundingShapeRadius() const { return _boundingShape.getRadius(); }
const CapsuleShape& getBoundingShape() const { return _boundingShape; }
const glm::vec3 getBoundingShapeOffset() const { return _boundingShapeLocalOffset; }
void resetShapePositionsToDefaultPose(); // DEBUG method

View file

@ -9,10 +9,14 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <qhttpmultipart.h>
#include <qjsonobject.h>
#include <AccountManager.h>
#include "Application.h"
#include "ui/Snapshot.h"
#include "LocationManager.h"
const QString POST_LOCATION_CREATE = "/api/v1/locations/";
@ -24,13 +28,17 @@ LocationManager& LocationManager::getInstance() {
const QString UNKNOWN_ERROR_MESSAGE = "Unknown error creating named location. Please try again!";
void LocationManager::namedLocationDataReceived(const QJsonObject& data) {
if (data.isEmpty()) {
return;
}
const QString LOCATION_OBJECT_KEY = "location";
const QString LOCATION_ID_KEY = "id";
if (data.contains("status") && data["status"].toString() == "success") {
void LocationManager::namedLocationDataReceived(const QJsonObject& rootObject) {
if (rootObject.contains("status") && rootObject["status"].toString() == "success") {
emit creationCompleted(QString());
// successfuly created a location - grab the ID from the response and create a snapshot to upload
QString locationIDString = rootObject[LOCATION_OBJECT_KEY].toObject()[LOCATION_ID_KEY].toString();
updateSnapshotForExistingLocation(locationIDString);
} else {
emit creationCompleted(UNKNOWN_ERROR_MESSAGE);
}
@ -87,3 +95,57 @@ void LocationManager::errorDataReceived(QNetworkReply& errorReply) {
creationCompleted(UNKNOWN_ERROR_MESSAGE);
}
}
void LocationManager::locationImageUpdateSuccess(const QJsonObject& rootObject) {
qDebug() << "Successfuly updated a location image.";
}
void LocationManager::updateSnapshotForExistingLocation(const QString& locationID) {
// first create a snapshot and save it
Application* application = Application::getInstance();
QTemporaryFile* tempImageFile = Snapshot::saveTempSnapshot(application->getGLWidget(), application->getAvatar());
if (tempImageFile && tempImageFile->open()) {
AccountManager& accountManager = AccountManager::getInstance();
// setup a multipart that is in the AccountManager thread - we need this so it can be cleaned up after the QNetworkReply
QHttpMultiPart* imageFileMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
imageFileMultiPart->moveToThread(accountManager.thread());
// parent the temp file to the QHttpMultipart after moving it to account manager thread
tempImageFile->moveToThread(accountManager.thread());
tempImageFile->setParent(imageFileMultiPart);
qDebug() << "Uploading a snapshot from" << QFileInfo(*tempImageFile).absoluteFilePath()
<< "as location image for" << locationID;
const QString LOCATION_IMAGE_NAME = "location[image]";
QHttpPart imagePart;
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"" + LOCATION_IMAGE_NAME + "\";"
" filename=\"" + QFileInfo(tempImageFile->fileName()).fileName().toUtf8() + "\""));
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream"));
imagePart.setBodyDevice(tempImageFile);
imageFileMultiPart->append(imagePart);
const QString LOCATION_IMAGE_PUT_PATH = "api/v1/locations/%1/image";
JSONCallbackParameters imageCallbackParams;
imageCallbackParams.jsonCallbackReceiver = this;
imageCallbackParams.jsonCallbackMethod = "locationImageUpdateSuccess";
// make an authenticated request via account manager to upload the image
// don't do anything with error or success for now
AccountManager::getInstance().authenticatedRequest(LOCATION_IMAGE_PUT_PATH.arg(locationID),
QNetworkAccessManager::PutOperation,
JSONCallbackParameters(), QByteArray(), imageFileMultiPart);
} else {
qDebug() << "Couldn't open snapshot file to upload as location image. No location image will be stored.";
return;
}
}

View file

@ -35,8 +35,12 @@ signals:
void creationCompleted(const QString& errorMessage);
private slots:
void namedLocationDataReceived(const QJsonObject& data);
void namedLocationDataReceived(const QJsonObject& jsonObject);
void errorDataReceived(QNetworkReply& errorReply);
void locationImageUpdateSuccess(const QJsonObject& jsonObject);
private:
void updateSnapshotForExistingLocation(const QString& locationID);
};

View file

@ -20,6 +20,10 @@
#include "Model.h"
#include "world.h"
GeometryCache::GeometryCache() :
_pendingBlenders(0) {
}
GeometryCache::~GeometryCache() {
foreach (const VerticesIndices& vbo, _hemisphereVBOs) {
glDeleteBuffers(1, &vbo.first);
@ -296,10 +300,30 @@ QSharedPointer<NetworkGeometry> GeometryCache::getGeometry(const QUrl& url, cons
return getResource(url, fallback, delayLoad).staticCast<NetworkGeometry>();
}
void GeometryCache::setBlendedVertices(const QPointer<Model>& model, const QWeakPointer<NetworkGeometry>& geometry,
const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& normals) {
void GeometryCache::noteRequiresBlend(Model* model) {
if (_pendingBlenders < QThread::idealThreadCount()) {
if (model->maybeStartBlender()) {
_pendingBlenders++;
}
return;
}
if (!_modelsRequiringBlends.contains(model)) {
_modelsRequiringBlends.append(model);
}
}
void GeometryCache::setBlendedVertices(const QPointer<Model>& model, int blendNumber,
const QWeakPointer<NetworkGeometry>& geometry, const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& normals) {
if (!model.isNull()) {
model->setBlendedVertices(geometry, vertices, normals);
model->setBlendedVertices(blendNumber, geometry, vertices, normals);
}
_pendingBlenders--;
while (!_modelsRequiringBlends.isEmpty()) {
Model* nextModel = _modelsRequiringBlends.takeFirst();
if (nextModel && nextModel->maybeStartBlender()) {
_pendingBlenders++;
return;
}
}
}

View file

@ -35,6 +35,7 @@ class GeometryCache : public ResourceCache {
public:
GeometryCache();
virtual ~GeometryCache();
void renderHemisphere(int slices, int stacks);
@ -47,9 +48,12 @@ public:
/// \param delayLoad if true, don't load the geometry immediately; wait until load is first requested
QSharedPointer<NetworkGeometry> getGeometry(const QUrl& url, const QUrl& fallback = QUrl(), bool delayLoad = false);
/// Adds the specified model to the list requiring vertex blends.
void noteRequiresBlend(Model* model);
public slots:
void setBlendedVertices(const QPointer<Model>& model, const QWeakPointer<NetworkGeometry>& geometry,
void setBlendedVertices(const QPointer<Model>& model, int blendNumber, const QWeakPointer<NetworkGeometry>& geometry,
const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& normals);
protected:
@ -68,6 +72,9 @@ private:
QHash<IntPair, QOpenGLBuffer> _gridBuffers;
QHash<QUrl, QWeakPointer<NetworkGeometry> > _networkGeometry;
QList<QPointer<Model> > _modelsRequiringBlends;
int _pendingBlenders;
};
/// Geometry loaded from the network.

View file

@ -62,8 +62,8 @@ Model::Model(QObject* parent) :
_lodDistance(0.0f),
_pupilDilation(0.0f),
_url("http://invalid.com"),
_blenderPending(false),
_blendRequired(false) {
_blendNumber(0),
_appliedBlendNumber(0) {
// we may have been created in the network thread, but we live in the main thread
moveToThread(Application::getInstance()->thread());
@ -826,7 +826,7 @@ void Model::updateShapePositions() {
class Blender : public QRunnable {
public:
Blender(Model* model, const QWeakPointer<NetworkGeometry>& geometry,
Blender(Model* model, int blendNumber, const QWeakPointer<NetworkGeometry>& geometry,
const QVector<FBXMesh>& meshes, const QVector<float>& blendshapeCoefficients);
virtual void run();
@ -834,55 +834,55 @@ public:
private:
QPointer<Model> _model;
int _blendNumber;
QWeakPointer<NetworkGeometry> _geometry;
QVector<FBXMesh> _meshes;
QVector<float> _blendshapeCoefficients;
};
Blender::Blender(Model* model, const QWeakPointer<NetworkGeometry>& geometry,
Blender::Blender(Model* model, int blendNumber, const QWeakPointer<NetworkGeometry>& geometry,
const QVector<FBXMesh>& meshes, const QVector<float>& blendshapeCoefficients) :
_model(model),
_blendNumber(blendNumber),
_geometry(geometry),
_meshes(meshes),
_blendshapeCoefficients(blendshapeCoefficients) {
}
void Blender::run() {
// make sure the model still exists
if (_model.isNull()) {
return;
}
QVector<glm::vec3> vertices, normals;
int offset = 0;
foreach (const FBXMesh& mesh, _meshes) {
if (mesh.blendshapes.isEmpty()) {
continue;
}
vertices += mesh.vertices;
normals += mesh.normals;
glm::vec3* meshVertices = vertices.data() + offset;
glm::vec3* meshNormals = normals.data() + offset;
offset += mesh.vertices.size();
const float NORMAL_COEFFICIENT_SCALE = 0.01f;
for (int i = 0, n = qMin(_blendshapeCoefficients.size(), mesh.blendshapes.size()); i < n; i++) {
float vertexCoefficient = _blendshapeCoefficients.at(i);
if (vertexCoefficient < EPSILON) {
if (!_model.isNull()) {
int offset = 0;
foreach (const FBXMesh& mesh, _meshes) {
if (mesh.blendshapes.isEmpty()) {
continue;
}
float normalCoefficient = vertexCoefficient * NORMAL_COEFFICIENT_SCALE;
const FBXBlendshape& blendshape = mesh.blendshapes.at(i);
for (int j = 0; j < blendshape.indices.size(); j++) {
int index = blendshape.indices.at(j);
meshVertices[index] += blendshape.vertices.at(j) * vertexCoefficient;
meshNormals[index] += blendshape.normals.at(j) * normalCoefficient;
vertices += mesh.vertices;
normals += mesh.normals;
glm::vec3* meshVertices = vertices.data() + offset;
glm::vec3* meshNormals = normals.data() + offset;
offset += mesh.vertices.size();
const float NORMAL_COEFFICIENT_SCALE = 0.01f;
for (int i = 0, n = qMin(_blendshapeCoefficients.size(), mesh.blendshapes.size()); i < n; i++) {
float vertexCoefficient = _blendshapeCoefficients.at(i);
if (vertexCoefficient < EPSILON) {
continue;
}
float normalCoefficient = vertexCoefficient * NORMAL_COEFFICIENT_SCALE;
const FBXBlendshape& blendshape = mesh.blendshapes.at(i);
for (int j = 0; j < blendshape.indices.size(); j++) {
int index = blendshape.indices.at(j);
meshVertices[index] += blendshape.vertices.at(j) * vertexCoefficient;
meshNormals[index] += blendshape.normals.at(j) * normalCoefficient;
}
}
}
}
// post the result to the geometry cache, which will dispatch to the model if still alive
QMetaObject::invokeMethod(Application::getInstance()->getGeometryCache(), "setBlendedVertices",
Q_ARG(const QPointer<Model>&, _model), Q_ARG(const QWeakPointer<NetworkGeometry>&, _geometry),
Q_ARG(const QVector<glm::vec3>&, vertices), Q_ARG(const QVector<glm::vec3>&, normals));
Q_ARG(const QPointer<Model>&, _model), Q_ARG(int, _blendNumber),
Q_ARG(const QWeakPointer<NetworkGeometry>&, _geometry), Q_ARG(const QVector<glm::vec3>&, vertices),
Q_ARG(const QVector<glm::vec3>&, normals));
}
void Model::setScaleToFit(bool scaleToFit, const glm::vec3& dimensions) {
@ -894,6 +894,10 @@ void Model::setScaleToFit(bool scaleToFit, const glm::vec3& dimensions) {
}
void Model::setScaleToFit(bool scaleToFit, float largestDimension) {
if (!isActive()) {
return;
}
if (_scaleToFit != scaleToFit || glm::length(_scaleToFitDimensions) != largestDimension) {
_scaleToFit = scaleToFit;
@ -1016,14 +1020,9 @@ void Model::simulateInternal(float deltaTime) {
}
// post the blender if we're not currently waiting for one to finish
if (geometry.hasBlendedMeshes()) {
if (_blenderPending) {
_blendRequired = true;
} else {
_blendRequired = false;
_blenderPending = true;
QThreadPool::globalInstance()->start(new Blender(this, _geometry, geometry.meshes, _blendshapeCoefficients));
}
if (geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) {
_blendedBlendshapeCoefficients = _blendshapeCoefficients;
Application::getInstance()->getGeometryCache()->noteRequiresBlend(this);
}
}
@ -1286,22 +1285,23 @@ void Model::renderJointCollisionShapes(float alpha) {
// implement this when we have shapes for regular models
}
void Model::setBlendedVertices(const QWeakPointer<NetworkGeometry>& geometry, const QVector<glm::vec3>& vertices,
const QVector<glm::vec3>& normals) {
_blenderPending = false;
// start the next blender if required
bool Model::maybeStartBlender() {
const FBXGeometry& fbxGeometry = _geometry->getFBXGeometry();
if (_blendRequired) {
_blendRequired = false;
if (fbxGeometry.hasBlendedMeshes()) {
_blenderPending = true;
QThreadPool::globalInstance()->start(new Blender(this, _geometry, fbxGeometry.meshes, _blendshapeCoefficients));
}
if (fbxGeometry.hasBlendedMeshes()) {
QThreadPool::globalInstance()->start(new Blender(this, ++_blendNumber, _geometry,
fbxGeometry.meshes, _blendshapeCoefficients));
return true;
}
if (_geometry != geometry || _blendedVertexBuffers.isEmpty()) {
return false;
}
void Model::setBlendedVertices(int blendNumber, const QWeakPointer<NetworkGeometry>& geometry,
const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& normals) {
if (_geometry != geometry || _blendedVertexBuffers.isEmpty() || blendNumber < _appliedBlendNumber) {
return;
}
_appliedBlendNumber = blendNumber;
const FBXGeometry& fbxGeometry = _geometry->getFBXGeometry();
int index = 0;
for (int i = 0; i < fbxGeometry.meshes.size(); i++) {
const FBXMesh& mesh = fbxGeometry.meshes.at(i);
@ -1354,6 +1354,8 @@ void Model::deleteGeometry() {
if (_geometry) {
_geometry->clearLoadPriority(this);
}
_blendedBlendshapeCoefficients.clear();
}
void Model::renderMeshes(RenderMode mode, bool translucent, bool receiveShadows) {

View file

@ -165,9 +165,11 @@ public:
virtual void renderJointCollisionShapes(float alpha);
bool maybeStartBlender();
/// Sets blended vertices computed in a separate thread.
void setBlendedVertices(const QWeakPointer<NetworkGeometry>& geometry, const QVector<glm::vec3>& vertices,
const QVector<glm::vec3>& normals);
void setBlendedVertices(int blendNumber, const QWeakPointer<NetworkGeometry>& geometry,
const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& normals);
class LocalLight {
public:
@ -285,8 +287,9 @@ private:
glm::vec4 _localLightColors[MAX_LOCAL_LIGHTS];
glm::vec4 _localLightDirections[MAX_LOCAL_LIGHTS];
bool _blenderPending;
bool _blendRequired;
QVector<float> _blendedBlendshapeCoefficients;
int _blendNumber;
int _appliedBlendNumber;
static ProgramObject _program;
static ProgramObject _normalMapProgram;

View file

@ -65,6 +65,24 @@ SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) {
}
QString Snapshot::saveSnapshot(QGLWidget* widget, Avatar* avatar) {
QFile* snapshotFile = savedFileForSnapshot(widget, avatar, false);
// we don't need the snapshot file, so close it, grab its filename and delete it
snapshotFile->close();
QString snapshotPath = QFileInfo(*snapshotFile).absoluteFilePath();
delete snapshotFile;
return snapshotPath;
}
QTemporaryFile* Snapshot::saveTempSnapshot(QGLWidget* widget, Avatar* avatar) {
// return whatever we get back from saved file for snapshot
return static_cast<QTemporaryFile*>(savedFileForSnapshot(widget, avatar, true));;
}
QFile* Snapshot::savedFileForSnapshot(QGLWidget* widget, Avatar* avatar, bool isTemporary) {
QImage shot = widget->grabFrameBuffer();
glm::vec3 location = avatar->getPosition();
@ -91,16 +109,40 @@ QString Snapshot::saveSnapshot(QGLWidget* widget, Avatar* avatar) {
username.replace(QRegExp("[^A-Za-z0-9_]"), "-");
QDateTime now = QDateTime::currentDateTime();
QString fileName = Menu::getInstance()->getSnapshotsLocation();
if (!fileName.endsWith(QDir::separator())) {
fileName.append(QDir::separator());
}
fileName.append(QString(FILENAME_PATH_FORMAT.arg(username, now.toString(DATETIME_FORMAT), formattedLocation)));
shot.save(fileName, 0, 100);
return fileName;
QString filename = FILENAME_PATH_FORMAT.arg(username, now.toString(DATETIME_FORMAT), formattedLocation);
const int IMAGE_QUALITY = 100;
if (!isTemporary) {
QString snapshotFullPath = Menu::getInstance()->getSnapshotsLocation();
if (!snapshotFullPath.endsWith(QDir::separator())) {
snapshotFullPath.append(QDir::separator());
}
snapshotFullPath.append(filename);
QFile* imageFile = new QFile(snapshotFullPath);
imageFile->open(QIODevice::WriteOnly);
shot.save(imageFile, 0, IMAGE_QUALITY);
imageFile->close();
return imageFile;
} else {
QTemporaryFile* imageTempFile = new QTemporaryFile(QDir::tempPath() + "/XXXXXX-" + filename);
if (!imageTempFile->open()) {
qDebug() << "Unable to open QTemporaryFile for temp snapshot. Will not save.";
return NULL;
}
shot.save(imageTempFile, 0, IMAGE_QUALITY);
imageTempFile->close();
return imageTempFile;
}
}

View file

@ -42,7 +42,11 @@ class Snapshot {
public:
static QString saveSnapshot(QGLWidget* widget, Avatar* avatar);
static QTemporaryFile* saveTempSnapshot(QGLWidget* widget, Avatar* avatar);
static SnapshotMetaData* parseSnapshotData(QString snapshotPath);
private:
static QFile* savedFileForSnapshot(QGLWidget* widget, Avatar* avatar, bool isTemporary);
};
#endif // hifi_Snapshot_h

View file

@ -55,18 +55,21 @@ typedef unsigned long long quint64;
#include "HandData.h"
// avatar motion behaviors
const quint32 AVATAR_MOTION_MOTOR_KEYBOARD_ENABLED = 1U << 0;
const quint32 AVATAR_MOTION_KEYBOARD_MOTOR_ENABLED = 1U << 0;
const quint32 AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED = 1U << 1;
const quint32 AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY = 1U << 1;
const quint32 AVATAR_MOTION_OBEY_LOCAL_GRAVITY = 1U << 2;
const quint32 AVATAR_MOTION_STAND_ON_NEARBY_FLOORS = 1U << 3;
const quint32 AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY = 1U << 2;
const quint32 AVATAR_MOTION_OBEY_LOCAL_GRAVITY = 1U << 3;
const quint32 AVATAR_MOTION_STAND_ON_NEARBY_FLOORS = 1U << 4;
const quint32 AVATAR_MOTION_DEFAULTS =
AVATAR_MOTION_MOTOR_KEYBOARD_ENABLED |
AVATAR_MOTION_KEYBOARD_MOTOR_ENABLED |
AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED |
AVATAR_MOTION_STAND_ON_NEARBY_FLOORS;
// these bits will be expanded as features are exposed
const quint32 AVATAR_MOTION_SCRIPTABLE_BITS =
AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED |
AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY |
AVATAR_MOTION_OBEY_LOCAL_GRAVITY |
AVATAR_MOTION_STAND_ON_NEARBY_FLOORS;

View file

@ -1755,12 +1755,24 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping)
// look for an unused slot in the weights vector
glm::vec4& weights = extracted.mesh.clusterWeights[it.value()];
for (int k = 0; k < 4; k++) {
int lowestIndex = -1;
float lowestWeight = FLT_MAX;
int k = 0;
for (; k < 4; k++) {
if (weights[k] == 0.0f) {
extracted.mesh.clusterIndices[it.value()][k] = i;
weights[k] = weight;
break;
}
if (weights[k] < lowestWeight) {
lowestIndex = k;
lowestWeight = weights[k];
}
}
if (k == 4) {
// no space for an additional weight; we must replace the lowest
weights[lowestIndex] = weight;
extracted.mesh.clusterIndices[it.value()][lowestIndex] = i;
}
}
}
@ -1769,6 +1781,14 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping)
maxJointIndex = jointIndex;
}
}
// normalize the weights if they don't add up to one
for (int i = 0; i < extracted.mesh.clusterWeights.size(); i++) {
glm::vec4& weights = extracted.mesh.clusterWeights[i];
float total = weights.x + weights.y + weights.z + weights.w;
if (total != 1.0f && total != 0.0f) {
weights /= total;
}
}
} else {
int jointIndex = maxJointIndex;
FBXJoint& joint = geometry.joints[jointIndex];

View file

@ -25,6 +25,8 @@
const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false;
const QByteArray ACCESS_TOKEN_AUTHORIZATION_HEADER = "Authorization";
AccountManager& AccountManager::getInstance() {
static AccountManager sharedInstance;
return sharedInstance;
@ -188,7 +190,8 @@ void AccountManager::invokedRequest(const QString& path,
if (requiresAuthentication) {
if (hasValidAccessToken()) {
requestURL.setQuery("access_token=" + _accountInfo.getAccessToken().token);
networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER,
_accountInfo.getAccessToken().authorizationHeaderValue());
} else {
qDebug() << "No valid access token present. Bailing on authenticated invoked request.";
return;
@ -405,9 +408,11 @@ void AccountManager::requestProfile() {
QUrl profileURL = _authURL;
profileURL.setPath("/api/v1/users/profile");
profileURL.setQuery("access_token=" + _accountInfo.getAccessToken().token);
QNetworkRequest profileRequest(profileURL);
profileRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, _accountInfo.getAccessToken().authorizationHeaderValue());
QNetworkReply* profileReply = networkAccessManager.get(QNetworkRequest(profileURL));
QNetworkReply* profileReply = networkAccessManager.get(profileRequest);
connect(profileReply, &QNetworkReply::finished, this, &AccountManager::requestProfileFinished);
connect(profileReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestProfileError(QNetworkReply::NetworkError)));
}

View file

@ -23,6 +23,8 @@ public:
OAuthAccessToken(const QJsonObject& jsonObject);
OAuthAccessToken(const OAuthAccessToken& otherToken);
OAuthAccessToken& operator=(const OAuthAccessToken& otherToken);
QByteArray authorizationHeaderValue() const { return QString("Bearer %1").arg(token).toUtf8(); }
bool isExpired() const { return expiryTimestamp <= QDateTime::currentMSecsSinceEpoch(); }

View file

@ -614,6 +614,7 @@ void ScriptEngine::timerFired() {
if (!callingTimer->isActive()) {
// this timer is done, we can kill it
_timerFunctionMap.remove(callingTimer);
delete callingTimer;
}
}