diff --git a/examples/attachedEntitiesManager.js b/examples/attachedEntitiesManager.js index 4dda76f4df..ef85f8cb98 100644 --- a/examples/attachedEntitiesManager.js +++ b/examples/attachedEntitiesManager.js @@ -213,7 +213,6 @@ function AttachedEntitiesManager() { var props = Entities.getEntityProperties(entityID); if (props.parentID == MyAvatar.sessionUUID) { grabData = getEntityCustomData('grabKey', entityID, {}); - grabbableData = getEntityCustomData('grabbableKey', entityID, {}); var wearableData = getEntityCustomData('wearable', entityID, DEFAULT_WEARABLE_DATA); var currentJointName = MyAvatar.getJointNames()[props.parentJointIndex]; wearableData.joints[currentJointName] = [props.localPosition, props.localRotation]; diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 964fca4136..43c18da72d 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -1006,7 +1006,7 @@ function MyController(hand) { // else this thing isn't physical. grab it by reparenting it (but not if we've already // grabbed it). - if (grabbableData.refCount < 1) { + if (refCount < 1) { this.setState(this.state == STATE_SEARCHING ? STATE_NEAR_GRABBING : STATE_EQUIP); return; } else { @@ -1120,7 +1120,6 @@ function MyController(hand) { var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); - var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); if (this.state == STATE_CONTINUE_DISTANCE_HOLDING && this.bumperSqueezed() && this.hasPresetOffsets()) { @@ -1307,7 +1306,6 @@ function MyController(hand) { this.nearGrabbing = function() { var now = Date.now(); - var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { this.setState(STATE_RELEASE); @@ -1330,10 +1328,9 @@ function MyController(hand) { var handRotation = (this.hand === RIGHT_HAND) ? MyAvatar.getRightPalmRotation() : MyAvatar.getLeftPalmRotation(); var handPosition = this.getHandPosition(); - var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); - var hasPresetPosition = false; if (this.state != STATE_NEAR_GRABBING && this.hasPresetOffsets()) { + var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); // if an object is "equipped" and has a predefined offset, use it. this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false; this.offsetPosition = this.getPresetPosition(); @@ -1676,7 +1673,6 @@ function MyController(hand) { }; this.activateEntity = function(entityID, grabbedProperties, wasLoaded) { - var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA); var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); var now = Date.now(); diff --git a/examples/data_visualization/photo_sphere.js b/examples/data_visualization/photo_sphere.js new file mode 100644 index 0000000000..c62fb7d122 --- /dev/null +++ b/examples/data_visualization/photo_sphere.js @@ -0,0 +1,60 @@ +// photo_sphere.js +// +// Created by James B. Pollack @imgntn on 3/11/2015 +// Copyright 2016 High Fidelity, Inc. +// +// This script creates a photo sphere around you. +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var photoSphere, light; + +//equirectangular +var url = 'http://hifi-content.s3.amazonaws.com/james/projection_objects/IMG_9167.JPG'; + +var MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/projection_objects/photosphere2.fbx'; + +function createPhotoSphere() { + + var textureString = 'photo:"' + url + '"' + + var properties = { + type: 'Model', + modelURL: MODEL_URL, + name: 'hifi-photo-sphere', + dimensions: { + x: 32, + y: 32, + z: 32 + }, + position: MyAvatar.position, + textures: textureString + } + photoSphere = Entities.addEntity(properties); +} + +function createLight() { + var properties = { + name: 'hifi-photo-sphere-light', + type: 'Light', + dimensions: { + x: 36, + y: 36, + z: 36, + }, + intensity: 4.0, + falloffRadius: 22, + position: MyAvatar.position + } + light = Entities.addEntity(properties); +} + +function cleanup() { + Entities.deleteEntity(photoSphere); + Entities.deleteEntity(light); +} + +Script.scriptEnding.connect(cleanup); +createPhotoSphere(); +createLight(); diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index 1101a08acb..e3a64da5d4 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -243,6 +243,41 @@ } + function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, defaultValue) { + var properties = {}; + var parsedData = {}; + try { + parsedData = JSON.parse(userDataElement.value); + } catch(e) {} + + if (!(groupName in parsedData)) { + parsedData[groupName] = {} + } + delete parsedData[groupName][keyName]; + if (checkBoxElement.checked !== defaultValue) { + parsedData[groupName][keyName] = checkBoxElement.checked; + } + + if (Object.keys(parsedData[groupName]).length == 0) { + delete parsedData[groupName]; + } + if (Object.keys(parsedData).length > 0) { + properties['userData'] = JSON.stringify(parsedData); + } else { + properties['userData'] = ''; + } + + userDataElement.value = properties['userData']; + + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties, + }) + ); + }; + + function loaded() { openEventBridge(function() { var allSections = []; @@ -305,6 +340,11 @@ var elCollideMyAvatar = document.getElementById("property-collide-myAvatar"); var elCollideOtherAvatar = document.getElementById("property-collide-otherAvatar"); var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); + + var elGrabbable = document.getElementById("property-grabbable"); + var elWantsTrigger = document.getElementById("property-wants-trigger"); + var elIgnoreIK = document.getElementById("property-ignore-ik"); + var elLifetime = document.getElementById("property-lifetime"); var elScriptURL = document.getElementById("property-script-url"); var elScriptTimestamp = document.getElementById("property-script-timestamp"); @@ -408,7 +448,7 @@ var elXTextureURL = document.getElementById("property-x-texture-url"); var elYTextureURL = document.getElementById("property-y-texture-url"); var elZTextureURL = document.getElementById("property-z-texture-url"); - + var elPreviewCameraButton = document.getElementById("preview-camera-button"); if (window.EventBridge !== undefined) { @@ -518,13 +558,30 @@ elCollisionless.checked = properties.collisionless; elDynamic.checked = properties.dynamic; - elCollideStatic.checked = properties.collidesWith.indexOf("static") > -1; elCollideKinematic.checked = properties.collidesWith.indexOf("kinematic") > -1; elCollideDynamic.checked = properties.collidesWith.indexOf("dynamic") > -1; elCollideMyAvatar.checked = properties.collidesWith.indexOf("myAvatar") > -1; elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; + elGrabbable.checked = properties.dynamic; + elWantsTrigger.checked = false; + elIgnoreIK.checked = false; + var parsedUserData = {} + try { + parsedUserData = JSON.parse(properties.userData); + } catch(e) {} + if ("grabbableKey" in parsedUserData) { + if ("grabbable" in parsedUserData["grabbableKey"]) { + elGrabbable.checked = parsedUserData["grabbableKey"].grabbable; + } + if ("wantsTrigger" in parsedUserData["grabbableKey"]) { + elWantsTrigger.checked = parsedUserData["grabbableKey"].wantsTrigger; + } + if ("ignoreIK" in parsedUserData["grabbableKey"]) { + elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; + } + } elCollisionSoundURL.value = properties.collisionSoundURL; elLifetime.value = properties.lifetime; @@ -737,9 +794,6 @@ elCollisionless.addEventListener('change', createEmitCheckedPropertyUpdateFunction('collisionless')); elDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('dynamic')); - - - elCollideDynamic.addEventListener('change', function() { updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideDynamic, 'dynamic'); }); @@ -758,6 +812,15 @@ updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideOtherAvatar, 'otherAvatar'); }); + elGrabbable.addEventListener('change', function() { + userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic); + }); + elWantsTrigger.addEventListener('change', function() { + userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false); + }); + elIgnoreIK.addEventListener('change', function() { + userDataChanger("grabbableKey", "ignoreIK", elIgnoreIK, elUserData, false); + }); elCollisionSoundURL.addEventListener('change', createEmitTextPropertyUpdateFunction('collisionSoundURL')); @@ -954,7 +1017,7 @@ action: "previewCamera" })); }); - + window.onblur = function() { // Fake a change event var ev = document.createEvent("HTMLEvents"); @@ -1476,6 +1539,29 @@ +
Grabbable:
+
+
+ grabbable + + + +
+ +
+ triggerable + + + +
+ +
+ ignore inverse-kinematics + + + +
+
diff --git a/examples/utilities/tools/developerMenuItems.js b/examples/utilities/tools/developerMenuItems.js index 2e6c5a1141..549bed0bc4 100644 --- a/examples/utilities/tools/developerMenuItems.js +++ b/examples/utilities/tools/developerMenuItems.js @@ -36,11 +36,11 @@ var AUDIO_LISTENER_MODE_CUSTOM = "Audio from custom position"; // be sure that the audio listener options are in the right order (same as the enumerator) var AUDIO_LISTENER_OPTIONS = [ - // MyAvatar.FROM_HEAD (0) + // MyAvatar.audioListenerModeHead (0) AUDIO_LISTENER_MODE_FROM_HEAD, - // MyAvatar.FROM_CAMERA (1) + // MyAvatar.audioListenerModeCamera (1) AUDIO_LISTENER_MODE_FROM_CAMERA, - // MyAvatar.CUSTOM (2) + // MyAvatar.audioListenerCustom (2) AUDIO_LISTENER_MODE_CUSTOM ]; var AUDIO_STEREO_INPUT = "Stereo Input"; diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index e50f43674e..696695de68 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -166,7 +166,8 @@ Item { color: root.fontColor; font.pixelSize: root.fontSize visible: root.expanded; - text: "Downloads: "; + text: "Downloads: " + root.downloads + "/" + root.downloadLimit + + ", Pending: " + root.downloadsPending; } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ce8f4e48d9..e1f2b2a49b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2058,18 +2058,21 @@ void Application::keyPressEvent(QKeyEvent* event) { if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::MiniMirror); } else { - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)); - if (!Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { + bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); + if (isMirrorChecked) { Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); } cameraMenuChanged(); } break; - case Qt::Key_P: - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)); - cameraMenuChanged(); - break; + case Qt::Key_P: { + bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked); + cameraMenuChanged(); + break; + } case Qt::Key_Slash: Menu::getInstance()->triggerOption(MenuOption::Stats); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ca242a2ca2..2a94ed30e2 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -972,7 +972,6 @@ void Avatar::renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, g glm::vec3 perpCos = glm::normalize(glm::cross(axis, perpSin)); perpSin = glm::cross(perpCos, axis); - float anglea = 0.0f; float angleb = 0.0f; QVector points; @@ -980,7 +979,7 @@ void Avatar::renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, g // the rectangles that comprise the sides of the cone section are // referenced by "a" and "b" in one dimension, and "1", and "2" in the other dimension. - anglea = angleb; + int anglea = angleb; angleb = ((float)(i+1) / (float)NUM_BODY_CONE_SIDES) * TWO_PI; float sa = sinf(anglea); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 7020de377f..01548c9066 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -57,7 +57,7 @@ class Avatar : public AvatarData { Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset) public: - Avatar(RigPointer rig = nullptr); + explicit Avatar(RigPointer rig = nullptr); ~Avatar(); typedef render::Payload Payload; diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 5087f7955d..629b3aac12 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -308,7 +308,6 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) { hand = _hand; } - ok = true; auto myAvatar = DependencyManager::get()->getMyAvatar(); holderID = myAvatar->getSessionUUID(); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 8e48237b8e..bcb54d6c52 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -63,11 +63,11 @@ void AvatarManager::registerMetaTypes(QScriptEngine* engine) { } AvatarManager::AvatarManager(QObject* parent) : - _avatarFades() + _avatarFades(), + _myAvatar(std::make_shared(std::make_shared())) { // register a meta type for the weak pointer we'll use for the owning avatar mixer for each avatar qRegisterMetaType >("NodeWeakPointer"); - _myAvatar = std::make_shared(std::make_shared()); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 2aff98a1d2..57fc1022ea 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -71,8 +71,8 @@ public slots: void updateAvatarRenderStatus(bool shouldRenderAvatars); private: - AvatarManager(QObject* parent = 0); - AvatarManager(const AvatarManager& other); + explicit AvatarManager(QObject* parent = 0); + explicit AvatarManager(const AvatarManager& other); void simulateAvatarFades(float deltaTime); diff --git a/interface/src/avatar/AvatarUpdate.cpp b/interface/src/avatar/AvatarUpdate.cpp index 4881e3eaec..a52b584527 100644 --- a/interface/src/avatar/AvatarUpdate.cpp +++ b/interface/src/avatar/AvatarUpdate.cpp @@ -16,7 +16,7 @@ #include #include "InterfaceLogging.h" -AvatarUpdate::AvatarUpdate() : GenericThread(), _lastAvatarUpdate(0) { +AvatarUpdate::AvatarUpdate() : GenericThread(), _lastAvatarUpdate(0), _isHMDMode(false) { setObjectName("Avatar Update"); // GenericThread::initialize uses this to set the thread name. Settings settings; const int DEFAULT_TARGET_AVATAR_SIMRATE = 60; diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index ec88b295f7..614e286329 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -28,7 +28,7 @@ class Avatar; class Head : public HeadData { public: - Head(Avatar* owningAvatar); + explicit Head(Avatar* owningAvatar); void init(); void reset(); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9302c3b47d..7198f32422 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1522,9 +1522,9 @@ glm::vec3 MyAvatar::applyKeyboardMotor(float deltaTime, const glm::vec3& localVe // (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; + 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 || @@ -1787,7 +1787,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, << newOrientation.x << ", " << newOrientation.y << ", " << newOrientation.z << ", " << newOrientation.w; // orient the user to face the target - glm::quat quatOrientation = newOrientation; + glm::quat quatOrientation = cancelOutRollAndPitch(newOrientation); if (shouldFaceLocation) { quatOrientation = newOrientation * glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index f834a627b2..37a2e752e6 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -63,9 +63,9 @@ class MyAvatar : public Avatar { Q_PROPERTY(AudioListenerMode audioListenerMode READ getAudioListenerMode WRITE setAudioListenerMode) Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) - Q_PROPERTY(AudioListenerMode FROM_HEAD READ getAudioListenerModeHead) - Q_PROPERTY(AudioListenerMode FROM_CAMERA READ getAudioListenerModeCamera) - Q_PROPERTY(AudioListenerMode CUSTOM READ getAudioListenerModeCustom) + Q_PROPERTY(AudioListenerMode audioListenerModeHead READ getAudioListenerModeHead) + Q_PROPERTY(AudioListenerMode audioListenerModeCamera READ getAudioListenerModeCamera) + Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom) //TODO: make gravity feature work Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setGravity) @@ -84,7 +84,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float energy READ getEnergy WRITE setEnergy) public: - MyAvatar(RigPointer rig); + explicit MyAvatar(RigPointer rig); ~MyAvatar(); virtual void simulateAttachments(float deltaTime) override; diff --git a/interface/src/avatar/MyCharacterController.cpp b/interface/src/avatar/MyCharacterController.cpp index ee77859337..6e52f4a949 100644 --- a/interface/src/avatar/MyCharacterController.cpp +++ b/interface/src/avatar/MyCharacterController.cpp @@ -46,14 +46,15 @@ void MyCharacterController::updateShapeIfNecessary() { // NOTE: _shapeLocalOffset is already computed if (_radius > 0.0f) { - // HACK: use some simple mass property defaults for now - float mass = 100.0f; - btVector3 inertia(30.0f, 8.0f, 30.0f); - // create RigidBody if it doesn't exist if (!_rigidBody) { + + // HACK: use some simple mass property defaults for now + const float DEFAULT_AVATAR_MASS = 100.0f; + const btVector3 DEFAULT_AVATAR_INERTIA_TENSOR(30.0f, 8.0f, 30.0f); + btCollisionShape* shape = new btCapsuleShape(_radius, 2.0f * _halfHeight); - _rigidBody = new btRigidBody(mass, nullptr, shape, inertia); + _rigidBody = new btRigidBody(DEFAULT_AVATAR_MASS, nullptr, shape, DEFAULT_AVATAR_INERTIA_TENSOR); } else { btCollisionShape* shape = _rigidBody->getCollisionShape(); if (shape) { diff --git a/interface/src/avatar/MyCharacterController.h b/interface/src/avatar/MyCharacterController.h index 39f0f99917..265406bc6f 100644 --- a/interface/src/avatar/MyCharacterController.h +++ b/interface/src/avatar/MyCharacterController.h @@ -21,7 +21,7 @@ class MyAvatar; class MyCharacterController : public CharacterController { public: - MyCharacterController(MyAvatar* avatar); + explicit MyCharacterController(MyAvatar* avatar); ~MyCharacterController (); virtual void updateShapeIfNecessary() override; diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index ac47c55eca..23d2b87194 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -193,6 +193,7 @@ void Stats::updateStats(bool force) { } STAT_UPDATE(downloads, ResourceCache::getLoadingRequests().size()); + STAT_UPDATE(downloadLimit, ResourceCache::getRequestLimit()) STAT_UPDATE(downloadsPending, ResourceCache::getPendingRequestCount()); // TODO fix to match original behavior //stringstream downloads; diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index ebcb80c404..c6bc13b52c 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -56,6 +56,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioMixerKbps, 0) STATS_PROPERTY(int, audioMixerPps, 0) STATS_PROPERTY(int, downloads, 0) + STATS_PROPERTY(int, downloadLimit, 0) STATS_PROPERTY(int, downloadsPending, 0) STATS_PROPERTY(int, triangles, 0) STATS_PROPERTY(int, quads, 0) @@ -135,6 +136,7 @@ signals: void audioMixerKbpsChanged(); void audioMixerPpsChanged(); void downloadsChanged(); + void downloadLimitChanged(); void downloadsPendingChanged(); void trianglesChanged(); void quadsChanged(); diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index 468217f5b3..87fb3ca20c 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -21,7 +21,7 @@ class AnimExpression { public: friend class AnimTests; - AnimExpression(const QString& str); + explicit AnimExpression(const QString& str); protected: struct Token { enum Type { @@ -49,8 +49,8 @@ protected: Comma, Error }; - Token(Type type) : type {type} {} - Token(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} + explicit Token(Type type) : type {type} {} + explicit Token(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} explicit Token(int val) : type {Type::Int}, intVal {val} {} explicit Token(bool val) : type {Type::Bool}, intVal {val} {} explicit Token(float val) : type {Type::Float}, floatVal {val} {} @@ -82,7 +82,7 @@ protected: Modulus, UnaryMinus }; - OpCode(Type type) : type {type} {} + explicit OpCode(Type type) : type {type} {} explicit OpCode(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} explicit OpCode(const QString& str) : type {Type::Identifier}, strVal {str} {} explicit OpCode(int val) : type {Type::Int}, intVal {val} {} diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 6a29ad61ac..f4df7ada82 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -23,6 +23,8 @@ AnimInverseKinematics::AnimInverseKinematics(const QString& id) : AnimNode(AnimN AnimInverseKinematics::~AnimInverseKinematics() { clearConstraints(); + _accumulators.clear(); + _targetVarVec.clear(); } void AnimInverseKinematics::loadDefaultPoses(const AnimPoseVec& poses) { @@ -394,6 +396,17 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars } _relativePoses[i].trans = underPoses[i].trans; } + + if (!_relativePoses.empty()) { + // Sometimes the underpose itself can violate the constraints. Rather than + // clamp the animation we dynamically expand each constraint to accomodate it. + std::map::iterator constraintItr = _constraints.begin(); + while (constraintItr != _constraints.end()) { + int index = constraintItr->first; + constraintItr->second->dynamicallyAdjustLimits(_relativePoses[index].rot); + ++constraintItr; + } + } } if (!_relativePoses.empty()) { diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index aeb718668a..182e7b3492 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -25,7 +25,7 @@ class RotationConstraint; class AnimInverseKinematics : public AnimNode { public: - AnimInverseKinematics(const QString& id); + explicit AnimInverseKinematics(const QString& id); virtual ~AnimInverseKinematics() override; void loadDefaultPoses(const AnimPoseVec& poses); diff --git a/libraries/animation/src/AnimNodeLoader.h b/libraries/animation/src/AnimNodeLoader.h index bc7d574c39..27b94f81bb 100644 --- a/libraries/animation/src/AnimNodeLoader.h +++ b/libraries/animation/src/AnimNodeLoader.h @@ -25,7 +25,7 @@ class AnimNodeLoader : public QObject { Q_OBJECT public: - AnimNodeLoader(const QUrl& url); + explicit AnimNodeLoader(const QUrl& url); signals: void success(AnimNode::Pointer node); diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index fc246bc4c0..e2cd20d63e 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -23,8 +23,8 @@ public: using Pointer = std::shared_ptr; using ConstPointer = std::shared_ptr; - AnimSkeleton(const FBXGeometry& fbxGeometry); - AnimSkeleton(const std::vector& joints); + explicit AnimSkeleton(const FBXGeometry& fbxGeometry); + explicit AnimSkeleton(const std::vector& joints); int nameToJointIndex(const QString& jointName) const; const QString& getJointName(int jointIndex) const; int getNumJoints() const; diff --git a/libraries/animation/src/AnimStateMachine.h b/libraries/animation/src/AnimStateMachine.h index 6a28ef1825..d92b94d1b5 100644 --- a/libraries/animation/src/AnimStateMachine.h +++ b/libraries/animation/src/AnimStateMachine.h @@ -110,7 +110,7 @@ protected: public: - AnimStateMachine(const QString& id); + explicit AnimStateMachine(const QString& id); virtual ~AnimStateMachine() override; virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h index 531e2c4a2d..3466013ff6 100644 --- a/libraries/animation/src/AnimVariant.h +++ b/libraries/animation/src/AnimVariant.h @@ -37,12 +37,12 @@ public: static const AnimVariant False; AnimVariant() : _type(Type::Bool) { memset(&_val, 0, sizeof(_val)); } - AnimVariant(bool value) : _type(Type::Bool) { _val.boolVal = value; } - AnimVariant(int value) : _type(Type::Int) { _val.intVal = value; } - AnimVariant(float value) : _type(Type::Float) { _val.floats[0] = value; } - AnimVariant(const glm::vec3& value) : _type(Type::Vec3) { *reinterpret_cast(&_val) = value; } - AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast(&_val) = value; } - AnimVariant(const QString& value) : _type(Type::String) { _stringVal = value; } + explicit AnimVariant(bool value) : _type(Type::Bool) { _val.boolVal = value; } + explicit AnimVariant(int value) : _type(Type::Int) { _val.intVal = value; } + explicit AnimVariant(float value) : _type(Type::Float) { _val.floats[0] = value; } + explicit AnimVariant(const glm::vec3& value) : _type(Type::Vec3) { *reinterpret_cast(&_val) = value; } + explicit AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast(&_val) = value; } + explicit AnimVariant(const QString& value) : _type(Type::String) { _stringVal = value; } bool isBool() const { return _type == Type::Bool; } bool isInt() const { return _type == Type::Int; } @@ -250,7 +250,7 @@ public: qCDebug(animation) << " " << pair.first << "=" << pair.second.getString(); break; default: - assert("AnimVariant::Type" == "valid"); + assert(("invalid AnimVariant::Type", false)); } } } diff --git a/libraries/animation/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h index 6143d9b42e..e6a795c864 100644 --- a/libraries/animation/src/AnimationCache.h +++ b/libraries/animation/src/AnimationCache.h @@ -38,7 +38,7 @@ protected: virtual QSharedPointer createResource(const QUrl& url, const QSharedPointer& fallback, bool delayLoad, const void* extra); private: - AnimationCache(QObject* parent = NULL); + explicit AnimationCache(QObject* parent = NULL); virtual ~AnimationCache() { } }; @@ -51,7 +51,7 @@ class Animation : public Resource { public: - Animation(const QUrl& url); + explicit Animation(const QUrl& url); const FBXGeometry& getGeometry() const { return *_geometry; } diff --git a/libraries/animation/src/AnimationLoop.cpp b/libraries/animation/src/AnimationLoop.cpp index f6a2877966..3d7bca863f 100644 --- a/libraries/animation/src/AnimationLoop.cpp +++ b/libraries/animation/src/AnimationLoop.cpp @@ -40,6 +40,7 @@ AnimationLoop::AnimationLoop(const AnimationDetails& animationDetails) : _lastFrame(animationDetails.lastFrame), _running(animationDetails.running), _currentFrame(animationDetails.currentFrame), + _maxFrameIndexHint(MAXIMUM_POSSIBLE_FRAME), _resetOnRunning(true), _lastSimulated(usecTimestampNow()) { @@ -55,6 +56,7 @@ AnimationLoop::AnimationLoop(float fps, bool loop, bool hold, bool startAutomati _lastFrame(lastFrame), _running(running), _currentFrame(currentFrame), + _maxFrameIndexHint(MAXIMUM_POSSIBLE_FRAME), _resetOnRunning(true), _lastSimulated(usecTimestampNow()) { diff --git a/libraries/animation/src/AnimationLoop.h b/libraries/animation/src/AnimationLoop.h index 10664c47e7..6adfbf35e2 100644 --- a/libraries/animation/src/AnimationLoop.h +++ b/libraries/animation/src/AnimationLoop.h @@ -19,7 +19,7 @@ public: static const float MAXIMUM_POSSIBLE_FRAME; AnimationLoop(); - AnimationLoop(const AnimationDetails& animationDetails); + explicit AnimationLoop(const AnimationDetails& animationDetails); AnimationLoop(float fps, bool loop, bool hold, bool startAutomatically, float firstFrame, float lastFrame, bool running, float currentFrame); diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index bca90b242a..ae9adc71c2 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -37,17 +37,7 @@ static bool isEqual(const glm::quat& p, const glm::quat& q) { return 1.0f - fabsf(glm::dot(p, q)) <= EPSILON; } -#ifdef NDEBUG -#define ASSERT(cond) -#else -#define ASSERT(cond) \ - do { \ - if (!(cond)) { \ - int* ptr = nullptr; \ - *ptr = 10; \ - } \ - } while (0) -#endif +#define ASSERT(cond) assert(cond) // 2 meter tall dude const glm::vec3 DEFAULT_RIGHT_EYE_POS(-0.3f, 0.9f, 0.0f); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 50313d10e7..1f9a02d8ab 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -83,6 +83,7 @@ public: Hover }; + Rig() {} virtual ~Rig() {} void destroyAnimGraph(); diff --git a/libraries/animation/src/RotationConstraint.h b/libraries/animation/src/RotationConstraint.h index 0745500582..9e34537cab 100644 --- a/libraries/animation/src/RotationConstraint.h +++ b/libraries/animation/src/RotationConstraint.h @@ -31,6 +31,10 @@ public: /// \return true if this constraint is part of lower spine virtual bool isLowerSpine() const { return false; } + /// \param rotation rotation to allow + /// \brief clear previous adjustment and adjust constraint limits to allow rotation + virtual void dynamicallyAdjustLimits(const glm::quat& rotation) {} + protected: glm::quat _referenceRotation = glm::quat(); }; diff --git a/libraries/animation/src/SwingTwistConstraint.cpp b/libraries/animation/src/SwingTwistConstraint.cpp index 3bf661612e..d6d8c87344 100644 --- a/libraries/animation/src/SwingTwistConstraint.cpp +++ b/libraries/animation/src/SwingTwistConstraint.cpp @@ -13,6 +13,7 @@ #include #include +#include #include @@ -24,32 +25,152 @@ const int LAST_CLAMP_NO_BOUNDARY = 0; const int LAST_CLAMP_HIGH_BOUNDARY = 1; SwingTwistConstraint::SwingLimitFunction::SwingLimitFunction() { - setCone(PI); + _minDots.push_back(-1.0f); + _minDots.push_back(-1.0f); + + _minDotIndexA = -1; + _minDotIndexB = -1; } -void SwingTwistConstraint::SwingLimitFunction::setCone(float maxAngle) { - _minDots.clear(); - float minDot = glm::clamp(maxAngle, MIN_MINDOT, MAX_MINDOT); - _minDots.push_back(minDot); - // push the first value to the back to establish cyclic boundary conditions - _minDots.push_back(minDot); -} +// In order to support the dynamic adjustment to swing limits we require +// that minDots have a minimum number of elements: +const int MIN_NUM_DOTS = 8; void SwingTwistConstraint::SwingLimitFunction::setMinDots(const std::vector& minDots) { - uint32_t numDots = (uint32_t)minDots.size(); + int numDots = (int)minDots.size(); _minDots.clear(); if (numDots == 0) { - // push two copies of MIN_MINDOT - _minDots.push_back(MIN_MINDOT); + // push multiple copies of MIN_MINDOT + for (int i = 0; i < MIN_NUM_DOTS; ++i) { + _minDots.push_back(MIN_MINDOT); + } + // push one more for cyclic boundary conditions _minDots.push_back(MIN_MINDOT); } else { - _minDots.reserve(numDots); - for (uint32_t i = 0; i < numDots; ++i) { - _minDots.push_back(glm::clamp(minDots[i], MIN_MINDOT, MAX_MINDOT)); + // for minimal fidelity in the dynamic adjustment we expand the swing limit data until + // we have enough data points + int trueNumDots = numDots; + int numFiller = 0; + while(trueNumDots < MIN_NUM_DOTS) { + numFiller++; + trueNumDots += numDots; } - // push the first value to the back to establish cyclic boundary conditions + _minDots.reserve(trueNumDots); + + for (int i = 0; i < numDots; ++i) { + // push the next value + _minDots.push_back(glm::clamp(minDots[i], MIN_MINDOT, MAX_MINDOT)); + + if (numFiller > 0) { + // compute endpoints of line segment + float nearDot = glm::clamp(minDots[i], MIN_MINDOT, MAX_MINDOT); + int k = (i + 1) % numDots; + float farDot = glm::clamp(minDots[k], MIN_MINDOT, MAX_MINDOT); + + // fill the gap with interpolated values + for (int j = 0; j < numFiller; ++j) { + float delta = (float)(j + 1) / float(numFiller + 1); + _minDots.push_back((1.0f - delta) * nearDot + delta * farDot); + } + } + } + // push the first value to the back to for cyclic boundary conditions _minDots.push_back(_minDots[0]); } + _minDotIndexA = -1; + _minDotIndexB = -1; +} + +/// \param angle radian angle to update +/// \param minDotAdjustment minimum dot limit at that angle +void SwingTwistConstraint::SwingLimitFunction::dynamicallyAdjustMinDots(float theta, float minDotAdjustment) { + // What does "dynamic adjustment" mean? + // + // Consider a limitFunction that looks like this: + // + // 1+ + // | valid space + // | + // +-----+-----+-----+-----+-----+-----+-----+-----+ + // | + // | invalid space + // 0+------------------------------------------------ + // 0 pi/2 pi 3pi/2 2pi + // theta ---> + // + // If we wanted to modify the envelope to accept a single invalid point X + // then we would need to modify neighboring values A and B accordingly: + // + // 1+ adjustment for X at some thetaX + // | | + // | | + // +-----+. V .+-----+-----+-----+-----+ + // | - - + // | ' A--X--B ' + // 0+------------------------------------------------ + // 0 pi/2 pi 3pi/2 2pi + // + // The code below computes the values of A and B such that the line between them + // passes through the point X, and we get reasonable interpolation for nearby values + // of theta. The old AB values are saved for later restore. + + if (_minDotIndexA > -1) { + // retstore old values + _minDots[_minDotIndexA] = _minDotA; + _minDots[_minDotIndexB] = _minDotB; + + // handle cyclic boundary conditions + int lastIndex = (int)_minDots.size() - 1; + if (_minDotIndexA == 0) { + _minDots[lastIndex] = _minDotA; + } else if (_minDotIndexB == lastIndex) { + _minDots[0] = _minDotB; + } + } + + // extract the positive normalized fractional part of the theta + float integerPart; + float normalizedAngle = modff(theta / TWO_PI, &integerPart); + if (normalizedAngle < 0.0f) { + normalizedAngle += 1.0f; + } + + // interpolate between the two nearest points in the curve + float delta = modff(normalizedAngle * (float)(_minDots.size() - 1), &integerPart); + int indexA = (int)(integerPart); + int indexB = (indexA + 1) % _minDots.size(); + float interpolatedDot = _minDots[indexA] * (1.0f - delta) + _minDots[indexB] * delta; + + if (minDotAdjustment < interpolatedDot) { + // minDotAdjustment is outside the existing bounds so we must modify + + // remember the indices + _minDotIndexA = indexA; + _minDotIndexB = indexB; + + // save the old minDots + _minDotA = _minDots[_minDotIndexA]; + _minDotB = _minDots[_minDotIndexB]; + + // compute replacement values to _minDots that will provide a line segment + // that passes through minDotAdjustment while balancing the distortion between A and B. + // Note: the derivation of these formulae is left as an exercise to the reader. + float twiceUndershoot = 2.0f * (minDotAdjustment - interpolatedDot); + _minDots[_minDotIndexA] -= twiceUndershoot * (delta + 0.5f) * (delta - 1.0f); + _minDots[_minDotIndexB] -= twiceUndershoot * delta * (delta - 1.5f); + + // handle cyclic boundary conditions + int lastIndex = (int)_minDots.size() - 1; + if (_minDotIndexA == 0) { + _minDots[lastIndex] = _minDots[_minDotIndexA]; + } else if (_minDotIndexB == lastIndex) { + _minDots[0] = _minDots[_minDotIndexB]; + } + } else { + // minDotAdjustment is inside bounds so there is nothing to do + _minDotIndexA = -1; + _minDotIndexB = -1; + } } float SwingTwistConstraint::SwingLimitFunction::getMinDot(float theta) const { @@ -90,15 +211,14 @@ void SwingTwistConstraint::setSwingLimits(const std::vector& swungDir }; std::vector limits; - uint32_t numLimits = (uint32_t)swungDirections.size(); + int numLimits = (int)swungDirections.size(); limits.reserve(numLimits); // compute the limit pairs: - const glm::vec3 yAxis = glm::vec3(0.0f, 1.0f, 0.0f); - for (uint32_t i = 0; i < numLimits; ++i) { + for (int i = 0; i < numLimits; ++i) { float directionLength = glm::length(swungDirections[i]); if (directionLength > EPSILON) { - glm::vec3 swingAxis = glm::cross(yAxis, swungDirections[i]); + glm::vec3 swingAxis = glm::cross(Vectors::UNIT_Y, swungDirections[i]); float theta = atan2f(-swingAxis.z, swingAxis.x); if (theta < 0.0f) { theta += TWO_PI; @@ -108,7 +228,7 @@ void SwingTwistConstraint::setSwingLimits(const std::vector& swungDir } std::vector minDots; - numLimits = (uint32_t)limits.size(); + numLimits = (int)limits.size(); if (numLimits == 0) { // trivial case: nearly free constraint std::vector minDots; @@ -126,10 +246,10 @@ void SwingTwistConstraint::setSwingLimits(const std::vector& swungDir // extrapolate evenly distributed limits for fast lookup table float deltaTheta = TWO_PI / (float)(numLimits); - uint32_t rightIndex = 0; - for (uint32_t i = 0; i < numLimits; ++i) { + int rightIndex = 0; + for (int i = 0; i < numLimits; ++i) { float theta = (float)i * deltaTheta; - uint32_t leftIndex = (rightIndex - 1) % numLimits; + int leftIndex = (rightIndex - 1 + numLimits) % numLimits; while (rightIndex < numLimits && theta > limits[rightIndex]._theta) { leftIndex = rightIndex++; } @@ -165,51 +285,57 @@ void SwingTwistConstraint::setTwistLimits(float minTwist, float maxTwist) { _maxTwist = glm::max(minTwist, maxTwist); _lastTwistBoundary = LAST_CLAMP_NO_BOUNDARY; + _twistAdjusted = false; +} + +// private +float SwingTwistConstraint::handleTwistBoundaryConditions(float twistAngle) const { + // adjust measured twistAngle according to clamping history + switch (_lastTwistBoundary) { + case LAST_CLAMP_LOW_BOUNDARY: + // clamp to min + if (twistAngle > _maxTwist) { + twistAngle -= TWO_PI; + } + break; + case LAST_CLAMP_HIGH_BOUNDARY: + // clamp to max + if (twistAngle < _minTwist) { + twistAngle += TWO_PI; + } + break; + default: // LAST_CLAMP_NO_BOUNDARY + // clamp to nearest boundary + float midBoundary = 0.5f * (_maxTwist + _minTwist + TWO_PI); + if (twistAngle > midBoundary) { + // lower boundary is closer --> phase down one cycle + twistAngle -= TWO_PI; + } else if (twistAngle < midBoundary - TWO_PI) { + // higher boundary is closer --> phase up one cycle + twistAngle += TWO_PI; + } + break; + } + return twistAngle; } bool SwingTwistConstraint::apply(glm::quat& rotation) const { // decompose the rotation into first twist about yAxis, then swing about something perp - const glm::vec3 yAxis(0.0f, 1.0f, 0.0f); // NOTE: rotation = postRotation * referenceRotation glm::quat postRotation = rotation * glm::inverse(_referenceRotation); glm::quat swingRotation, twistRotation; - swingTwistDecomposition(postRotation, yAxis, swingRotation, twistRotation); + swingTwistDecomposition(postRotation, Vectors::UNIT_Y, swingRotation, twistRotation); // NOTE: postRotation = swingRotation * twistRotation - // compute twistAngle + // compute raw twistAngle float twistAngle = 2.0f * acosf(fabsf(twistRotation.w)); - const glm::vec3 xAxis = glm::vec3(1.0f, 0.0f, 0.0f); - glm::vec3 twistedX = twistRotation * xAxis; - twistAngle *= copysignf(1.0f, glm::dot(glm::cross(xAxis, twistedX), yAxis)); + glm::vec3 twistedX = twistRotation * Vectors::UNIT_X; + twistAngle *= copysignf(1.0f, glm::dot(glm::cross(Vectors::UNIT_X, twistedX), Vectors::UNIT_Y)); bool somethingClamped = false; if (_minTwist != _maxTwist) { - // adjust measured twistAngle according to clamping history - switch (_lastTwistBoundary) { - case LAST_CLAMP_LOW_BOUNDARY: - // clamp to min - if (twistAngle > _maxTwist) { - twistAngle -= TWO_PI; - } - break; - case LAST_CLAMP_HIGH_BOUNDARY: - // clamp to max - if (twistAngle < _minTwist) { - twistAngle += TWO_PI; - } - break; - default: // LAST_CLAMP_NO_BOUNDARY - // clamp to nearest boundary - float midBoundary = 0.5f * (_maxTwist + _minTwist + TWO_PI); - if (twistAngle > midBoundary) { - // lower boundary is closer --> phase down one cycle - twistAngle -= TWO_PI; - } else if (twistAngle < midBoundary - TWO_PI) { - // higher boundary is closer --> phase up one cycle - twistAngle += TWO_PI; - } - break; - } + // twist limits apply --> figure out which limit we're hitting, if any + twistAngle = handleTwistBoundaryConditions(twistAngle); // clamp twistAngle float clampedTwistAngle = glm::clamp(twistAngle, _minTwist, _maxTwist); @@ -226,15 +352,15 @@ bool SwingTwistConstraint::apply(glm::quat& rotation) const { // clamp the swing // The swingAxis is always perpendicular to the reference axis (yAxis in the constraint's frame). - glm::vec3 swungY = swingRotation * yAxis; - glm::vec3 swingAxis = glm::cross(yAxis, swungY); + glm::vec3 swungY = swingRotation * Vectors::UNIT_Y; + glm::vec3 swingAxis = glm::cross(Vectors::UNIT_Y, swungY); float axisLength = glm::length(swingAxis); if (axisLength > EPSILON) { // The limit of swing is a function of "theta" which can be computed from the swingAxis // (which is in the constraint's ZX plane). float theta = atan2f(-swingAxis.z, swingAxis.x); float minDot = _swingLimitFunction.getMinDot(theta); - if (glm::dot(swungY, yAxis) < minDot) { + if (glm::dot(swungY, Vectors::UNIT_Y) < minDot) { // The swing limits are violated so we extract the angle from midDot and // use it to supply a new rotation. swingAxis /= axisLength; @@ -245,13 +371,53 @@ bool SwingTwistConstraint::apply(glm::quat& rotation) const { if (somethingClamped) { // update the rotation - twistRotation = glm::angleAxis(twistAngle, yAxis); + twistRotation = glm::angleAxis(twistAngle, Vectors::UNIT_Y); rotation = swingRotation * twistRotation * _referenceRotation; return true; } return false; } +void SwingTwistConstraint::dynamicallyAdjustLimits(const glm::quat& rotation) { + glm::quat postRotation = rotation * glm::inverse(_referenceRotation); + glm::quat swingRotation, twistRotation; + + swingTwistDecomposition(postRotation, Vectors::UNIT_Y, swingRotation, twistRotation); + + // adjust swing limits + glm::vec3 swungY = swingRotation * Vectors::UNIT_Y; + glm::vec3 swingAxis = glm::cross(Vectors::UNIT_Y, swungY); + float theta = atan2f(-swingAxis.z, swingAxis.x); + _swingLimitFunction.dynamicallyAdjustMinDots(theta, swungY.y); + + // restore twist limits + if (_twistAdjusted) { + _minTwist = _oldMinTwist; + _maxTwist = _oldMaxTwist; + _twistAdjusted = false; + } + + if (_minTwist != _maxTwist) { + // compute twistAngle + float twistAngle = 2.0f * acosf(fabsf(twistRotation.w)); + glm::vec3 twistedX = twistRotation * Vectors::UNIT_X; + twistAngle *= copysignf(1.0f, glm::dot(glm::cross(Vectors::UNIT_X, twistedX), Vectors::UNIT_Y)); + twistAngle = handleTwistBoundaryConditions(twistAngle); + + if (twistAngle < _minTwist || twistAngle > _maxTwist) { + // expand twist limits + _twistAdjusted = true; + _oldMinTwist = _minTwist; + _oldMaxTwist = _maxTwist; + if (twistAngle < _minTwist) { + _minTwist = twistAngle; + } else if (twistAngle > _maxTwist) { + _maxTwist = twistAngle; + } + } + } +} + void SwingTwistConstraint::clearHistory() { _lastTwistBoundary = LAST_CLAMP_NO_BOUNDARY; } diff --git a/libraries/animation/src/SwingTwistConstraint.h b/libraries/animation/src/SwingTwistConstraint.h index f73bbfb233..93d7449d8f 100644 --- a/libraries/animation/src/SwingTwistConstraint.h +++ b/libraries/animation/src/SwingTwistConstraint.h @@ -53,24 +53,45 @@ public: void setLowerSpine(bool lowerSpine) { _lowerSpine = lowerSpine; } virtual bool isLowerSpine() const override { return _lowerSpine; } + /// \param rotation rotation to allow + /// \brief clear previous adjustment and adjust constraint limits to allow rotation + virtual void dynamicallyAdjustLimits(const glm::quat& rotation) override; + + // for testing purposes + const std::vector& getMinDots() { return _swingLimitFunction.getMinDots(); } + // SwingLimitFunction is an implementation of the constraint check described in the paper: // "The Parameterization of Joint Rotation with the Unit Quaternion" by Quang Liu and Edmond C. Prakash + // + // The "dynamic adjustment" feature allows us to change the limits on the fly for one particular theta angle. + // class SwingLimitFunction { public: SwingLimitFunction(); - /// \brief use a uniform conical swing limit - void setCone(float maxAngle); - /// \brief use a vector of lookup values for swing limits void setMinDots(const std::vector& minDots); + /// \param theta radian angle to new minDot + /// \param minDot minimum dot limit + /// \brief updates swing constraint to permit minDot at theta + void dynamicallyAdjustMinDots(float theta, float minDot); + /// \return minimum dotProduct between reference and swung axes float getMinDot(float theta) const; - protected: + // for testing purposes + const std::vector& getMinDots() { return _minDots; } + + private: // the limits are stored in a lookup table with cyclic boundary conditions std::vector _minDots; + + // these values used to restore dynamic adjustment + float _minDotA; + float _minDotB; + int8_t _minDotIndexA; + int8_t _minDotIndexB; }; /// \return reference to SwingLimitFunction instance for unit-testing @@ -79,15 +100,22 @@ public: /// \brief exposed for unit testing void clearHistory(); +private: + float handleTwistBoundaryConditions(float twistAngle) const; + protected: SwingLimitFunction _swingLimitFunction; float _minTwist; float _maxTwist; + float _oldMinTwist; + float _oldMaxTwist; + // We want to remember the LAST clamped boundary, so we an use it even when the far boundary is closer. // This reduces "pops" when the input twist angle goes far beyond and wraps around toward the far boundary. mutable int _lastTwistBoundary; bool _lowerSpine { false }; + bool _twistAdjusted { false }; }; #endif // hifi_SwingTwistConstraint_h diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 067ab0603b..adb942417d 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1115,7 +1115,7 @@ void AvatarData::detachOne(const QString& modelURL, const QString& jointName) { return; } QVector attachmentData = getAttachmentData(); - for (QVector::iterator it = attachmentData.begin(); it != attachmentData.end(); it++) { + for (QVector::iterator it = attachmentData.begin(); it != attachmentData.end(); ++it) { if (it->modelURL == modelURL && (jointName.isEmpty() || it->jointName == jointName)) { attachmentData.erase(it); setAttachmentData(attachmentData); @@ -1134,7 +1134,7 @@ void AvatarData::detachAll(const QString& modelURL, const QString& jointName) { if (it->modelURL == modelURL && (jointName.isEmpty() || it->jointName == jointName)) { it = attachmentData.erase(it); } else { - it++; + ++it; } } setAttachmentData(attachmentData); diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 1d664aa3ff..b98112d6e0 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -42,6 +42,8 @@ HeadData::HeadData(AvatarData* owningAvatar) : _rightEyeBlink(0.0f), _averageLoudness(0.0f), _browAudioLift(0.0f), + _audioAverageLoudness(0.0f), + _pupilDilation(0.0f), _owningAvatar(owningAvatar) { diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index dac266f4a2..fef77c6f8f 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -32,7 +32,7 @@ class QJsonObject; class HeadData { public: - HeadData(AvatarData* owningAvatar); + explicit HeadData(AvatarData* owningAvatar); virtual ~HeadData() { }; // degrees diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 3c918a0a77..b79fd7107d 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -515,7 +515,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // we can confidently ignore this packet EntityTreePointer tree = getTree(); if (tree && tree->isDeletedEntity(_id)) { - qDebug() << "Recieved packet for previously deleted entity [" << _id << "] ignoring. (inside " << __FUNCTION__ << ")"; + #ifdef WANT_DEBUG + qDebug() << "Recieved packet for previously deleted entity [" << _id << "] ignoring. " + "(inside " << __FUNCTION__ << ")"; + #endif ignoreServerPacket = true; } diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index dabf1b098b..e345bed81c 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -56,6 +56,15 @@ void ResourceCache::refresh(const QUrl& url) { } } +void ResourceCache::setRequestLimit(int limit) { + _requestLimit = limit; + + // Now go fill any new request spots + while (attemptHighestPriorityRequest()) { + // just keep looping until we reach the new limit or no more pending requests + } +} + void ResourceCache::getResourceAsynchronously(const QUrl& url) { qCDebug(networking) << "ResourceCache::getResourceAsynchronously" << url.toString(); _resourcesToBeGottenLock.lockForWrite(); @@ -150,31 +159,37 @@ void ResourceCache::clearUnusedResource() { } } -void ResourceCache::attemptRequest(Resource* resource) { +bool ResourceCache::attemptRequest(Resource* resource) { auto sharedItems = DependencyManager::get(); // Disable request limiting for ATP if (resource->getURL().scheme() != URL_SCHEME_ATP) { - if (_requestLimit <= 0) { + if (_requestsActive >= _requestLimit) { // wait until a slot becomes available sharedItems->_pendingRequests.append(resource); - return; + return false; } - --_requestLimit; - } + ++_requestsActive; + } sharedItems->_loadingRequests.append(resource); resource->makeRequest(); + return true; } void ResourceCache::requestCompleted(Resource* resource) { auto sharedItems = DependencyManager::get(); sharedItems->_loadingRequests.removeOne(resource); if (resource->getURL().scheme() != URL_SCHEME_ATP) { - ++_requestLimit; + --_requestsActive; } - + + attemptHighestPriorityRequest(); +} + +bool ResourceCache::attemptHighestPriorityRequest() { + auto sharedItems = DependencyManager::get(); // look for the highest priority pending request int highestIndex = -1; float highestPriority = -FLT_MAX; @@ -191,13 +206,12 @@ void ResourceCache::requestCompleted(Resource* resource) { } i++; } - if (highestIndex >= 0) { - attemptRequest(sharedItems->_pendingRequests.takeAt(highestIndex)); - } + return (highestIndex >= 0) && attemptRequest(sharedItems->_pendingRequests.takeAt(highestIndex)); } const int DEFAULT_REQUEST_LIMIT = 10; int ResourceCache::_requestLimit = DEFAULT_REQUEST_LIMIT; +int ResourceCache::_requestsActive = 0; Resource::Resource(const QUrl& url, bool delayLoad) : _url(url), diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 6fbf54c49d..a8ab51b7f7 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -67,8 +67,10 @@ class ResourceCache : public QObject { Q_OBJECT public: - static void setRequestLimit(int limit) { _requestLimit = limit; } + static void setRequestLimit(int limit); static int getRequestLimit() { return _requestLimit; } + + static int getRequestsActive() { return _requestsActive; } void setUnusedResourceCacheSize(qint64 unusedResourcesMaxSize); qint64 getUnusedResourceCacheSize() const { return _unusedResourcesMaxSize; } @@ -105,8 +107,11 @@ protected: void reserveUnusedResource(qint64 resourceSize); void clearUnusedResource(); - Q_INVOKABLE static void attemptRequest(Resource* resource); + /// Attempt to load a resource if requests are below the limit, otherwise queue the resource for loading + /// \return true if the resource began loading, otherwise false if the resource is in the pending queue + Q_INVOKABLE static bool attemptRequest(Resource* resource); static void requestCompleted(Resource* resource); + static bool attemptHighestPriorityRequest(); private: friend class Resource; @@ -115,6 +120,7 @@ private: int _lastLRUKey = 0; static int _requestLimit; + static int _requestsActive; void getResourceAsynchronously(const QUrl& url); QReadWriteLock _resourcesToBeGottenLock; diff --git a/libraries/networking/src/udt/CongestionControl.cpp b/libraries/networking/src/udt/CongestionControl.cpp index c1feae3911..0f8a9f24f6 100644 --- a/libraries/networking/src/udt/CongestionControl.cpp +++ b/libraries/networking/src/udt/CongestionControl.cpp @@ -34,7 +34,6 @@ void CongestionControl::setPacketSendPeriod(double newSendPeriod) { } DefaultCC::DefaultCC() : - _slowStartLastAck(_sendCurrSeqNum), _lastDecreaseMaxSeq(SequenceNumber {SequenceNumber::MAX }) { _mss = udt::MAX_PACKET_SIZE_WITH_UDP_HEADER; @@ -63,11 +62,11 @@ void DefaultCC::onACK(SequenceNumber ackNum) { if (_slowStart) { // we are in slow start phase - increase the congestion window size by the number of packets just ACKed - _congestionWindowSize += seqlen(_slowStartLastAck, ackNum); + _congestionWindowSize += seqlen(_slowStartLastACK, ackNum); // update the last ACK - _slowStartLastAck = ackNum; - + _slowStartLastACK = ackNum; + // check if we can get out of slow start (is our new congestion window size bigger than the max) if (_congestionWindowSize > _maxCongestionWindowSize) { _slowStart = false; diff --git a/libraries/networking/src/udt/CongestionControl.h b/libraries/networking/src/udt/CongestionControl.h index fa1bf73ecf..69b7a32d2d 100644 --- a/libraries/networking/src/udt/CongestionControl.h +++ b/libraries/networking/src/udt/CongestionControl.h @@ -50,6 +50,7 @@ protected: void setMaxCongestionWindowSize(int window) { _maxCongestionWindowSize = window; } void setBandwidth(int bandwidth) { _bandwidth = bandwidth; } void setMaxBandwidth(int maxBandwidth) { _maxBandwidth = maxBandwidth; } + virtual void setInitialSendSequenceNumber(SequenceNumber seqNum) = 0; void setSendCurrentSequenceNumber(SequenceNumber seqNum) { _sendCurrSeqNum = seqNum; } void setReceiveRate(int rate) { _receiveRate = rate; } void setRTT(int rtt) { _rtt = rtt; } @@ -104,14 +105,17 @@ public: virtual void onACK(SequenceNumber ackNum); virtual void onLoss(SequenceNumber rangeStart, SequenceNumber rangeEnd); virtual void onTimeout(); - + +protected: + virtual void setInitialSendSequenceNumber(SequenceNumber seqNum) { _slowStartLastACK = seqNum; } + private: void stopSlowStart(); // stops the slow start on loss or timeout p_high_resolution_clock::time_point _lastRCTime = p_high_resolution_clock::now(); // last rate increase time bool _slowStart { true }; // if in slow start phase - SequenceNumber _slowStartLastAck; // last ACKed seq num + SequenceNumber _slowStartLastACK; // last ACKed seq num from previous slow start check bool _loss { false }; // if loss happened since last rate increase SequenceNumber _lastDecreaseMaxSeq; // max pkt seq num sent out when last decrease happened double _lastDecreasePeriod { 1 }; // value of _packetSendPeriod when last decrease happened diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp index e6a15aa6a0..95afbedb86 100644 --- a/libraries/networking/src/udt/Connection.cpp +++ b/libraries/networking/src/udt/Connection.cpp @@ -104,6 +104,9 @@ SendQueue& Connection::getSendQueue() { _sendQueue->setSyncInterval(_synInterval); _sendQueue->setEstimatedTimeout(estimatedTimeout()); _sendQueue->setFlowWindowSize(std::min(_flowWindowSize, (int) _congestionControl->_congestionWindowSize)); + + // give the randomized sequence number to the congestion control object + _congestionControl->setInitialSendSequenceNumber(_sendQueue->getCurrentSequenceNumber()); } return *_sendQueue; @@ -282,7 +285,7 @@ void Connection::sendACK(bool wasCausedBySyncTimeout) { // grab the up to date packet receive speed and estimated bandwidth int32_t packetReceiveSpeed = _receiveWindow.getPacketReceiveSpeed(); int32_t estimatedBandwidth = _receiveWindow.getEstimatedBandwidth(); - + // update those values in our connection stats _stats.recordReceiveRate(packetReceiveSpeed); _stats.recordEstimatedBandwidth(estimatedBandwidth); @@ -541,7 +544,7 @@ void Connection::processACK(std::unique_ptr controlPacket) { // read the ACK sub-sequence number SequenceNumber currentACKSubSequenceNumber; controlPacket->readPrimitive(¤tACKSubSequenceNumber); - + // Check if we need send an ACK2 for this ACK // This will be the case if it has been longer than the sync interval OR // it looks like they haven't received our ACK2 for this ACK diff --git a/libraries/render-utils/src/sdf_text3D.slf b/libraries/render-utils/src/sdf_text3D.slf index 1e11cd984d..854221a5cf 100644 --- a/libraries/render-utils/src/sdf_text3D.slf +++ b/libraries/render-utils/src/sdf_text3D.slf @@ -21,12 +21,10 @@ in vec3 _normal; in vec2 _texCoord0; const float gamma = 2.2; -const float smoothing = 256.0; +const float smoothing = 32.0; const float interiorCutoff = 0.8; const float outlineExpansion = 0.2; - - void main() { // retrieve signed distance float sdf = texture(Font, _texCoord0).g; diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 7d74529b64..eabdc99338 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -240,7 +240,9 @@ public: const Varying getInput() const { return _input; } const Varying getOutput() const { return _output; } - Model(const Varying& input, Data data = Data()) : Concept(std::make_shared()), _data(data), _input(input), _output(Output()) { + template + Model(const Varying& input, A&&... args) : + Concept(std::make_shared()), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { applyConfiguration(); } @@ -308,7 +310,10 @@ public: const Varying getInput() const { return _input; } const Varying getOutput() const { return _output; } - Model(const Varying& input, Data data = Data()) : Concept(data._config), _data(data), _input(input), _output(Output()) { + template + Model(const Varying& input, A&&... args) : + Concept(nullptr), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { + _config = _data._config; std::static_pointer_cast(_config)->init(&_data); applyConfiguration(); } @@ -337,9 +342,7 @@ public: // Create a new job in the container's queue; returns the job's output template const Varying addJob(std::string name, const Varying& input, A&&... args) { - _jobs.emplace_back(name, std::make_shared( - input, - typename T::JobModel::Data(std::forward(args)...))); + _jobs.emplace_back(name, std::make_shared(input, std::forward(args)...)); QConfigPointer config = _jobs.back().getConfiguration(); config->setParent(_config.get()); config->setObjectName(name.c_str()); diff --git a/tests/animation/src/AnimInverseKinematicsTests.cpp b/tests/animation/src/AnimInverseKinematicsTests.cpp index bb15a1d257..2b10892f82 100644 --- a/tests/animation/src/AnimInverseKinematicsTests.cpp +++ b/tests/animation/src/AnimInverseKinematicsTests.cpp @@ -29,7 +29,7 @@ const glm::quat identity = glm::quat(); const glm::quat quaterTurnAroundZ = glm::angleAxis(0.5f * PI, zAxis); -void makeTestFBXJoints(std::vector& fbxJoints) { +void makeTestFBXJoints(FBXGeometry& geometry) { FBXJoint joint; joint.isFree = false; joint.freeLineage.clear(); @@ -61,29 +61,29 @@ void makeTestFBXJoints(std::vector& fbxJoints) { joint.name = "A"; joint.parentIndex = -1; joint.translation = origin; - fbxJoints.push_back(joint); + geometry.joints.push_back(joint); joint.name = "B"; joint.parentIndex = 0; joint.translation = xAxis; - fbxJoints.push_back(joint); + geometry.joints.push_back(joint); joint.name = "C"; joint.parentIndex = 1; joint.translation = xAxis; - fbxJoints.push_back(joint); + geometry.joints.push_back(joint); joint.name = "D"; joint.parentIndex = 2; joint.translation = xAxis; - fbxJoints.push_back(joint); + geometry.joints.push_back(joint); // compute each joint's transform - for (int i = 1; i < (int)fbxJoints.size(); ++i) { - FBXJoint& j = fbxJoints[i]; + for (int i = 1; i < (int)geometry.joints.size(); ++i) { + FBXJoint& j = geometry.joints[i]; int parentIndex = j.parentIndex; // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) - j.transform = fbxJoints[parentIndex].transform * + j.transform = geometry.joints[parentIndex].transform * glm::translate(j.translation) * j.preTransform * glm::mat4_cast(j.preRotation * j.rotation * j.postRotation) * @@ -94,14 +94,14 @@ void makeTestFBXJoints(std::vector& fbxJoints) { } void AnimInverseKinematicsTests::testSingleChain() { - std::vector fbxJoints; - makeTestFBXJoints(fbxJoints); + FBXGeometry geometry; + makeTestFBXJoints(geometry); // create a skeleton and doll AnimPose offset; - AnimSkeleton* skeleton = new AnimSkeleton(fbxJoints, offset); - AnimSkeleton::Pointer skeletonPtr(skeleton); + AnimSkeleton::Pointer skeletonPtr = std::make_shared(geometry); AnimInverseKinematics ikDoll("doll"); + ikDoll.setSkeleton(skeletonPtr); { // easy test IK of joint C @@ -113,11 +113,11 @@ void AnimInverseKinematicsTests::testSingleChain() { pose.rot = identity; pose.trans = origin; - std::vector poses; + AnimPoseVec poses; poses.push_back(pose); pose.trans = xAxis; - for (int i = 1; i < (int)fbxJoints.size(); ++i) { + for (int i = 1; i < (int)geometry.joints.size(); ++i) { poses.push_back(pose); } ikDoll.loadPoses(poses); @@ -134,7 +134,8 @@ void AnimInverseKinematicsTests::testSingleChain() { AnimVariantMap varMap; varMap.set("positionD", targetPosition); varMap.set("rotationD", targetRotation); - ikDoll.setTargetVars("D", "positionD", "rotationD"); + varMap.set("targetType", (int)IKTarget::Type::RotationAndPosition); + ikDoll.setTargetVars(QString("D"), QString("positionD"), QString("rotationD"), QString("targetType")); AnimNode::Triggers triggers; // the IK solution should be: @@ -144,38 +145,49 @@ void AnimInverseKinematicsTests::testSingleChain() { // | // A------>B------>C // + + // iterate several times float dt = 1.0f; - ikDoll.evaluate(varMap, dt, triggers); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + const AnimPoseVec& relativePoses = ikDoll.overlay(varMap, dt, triggers, poses); // verify absolute results // NOTE: since we expect this solution to converge very quickly (one loop) // we can impose very tight error thresholds. - std::vector absolutePoses; + AnimPoseVec absolutePoses; + for (auto pose : poses) { + absolutePoses.push_back(pose); + } ikDoll.computeAbsolutePoses(absolutePoses); - float acceptableAngle = 0.0001f; - QCOMPARE_QUATS(absolutePoses[0].rot, identity, acceptableAngle); - QCOMPARE_QUATS(absolutePoses[1].rot, identity, acceptableAngle); - QCOMPARE_QUATS(absolutePoses[2].rot, quaterTurnAroundZ, acceptableAngle); - QCOMPARE_QUATS(absolutePoses[3].rot, quaterTurnAroundZ, acceptableAngle); + const float acceptableAngleError = 0.001f; + QCOMPARE_QUATS(absolutePoses[0].rot, identity, acceptableAngleError); + QCOMPARE_QUATS(absolutePoses[1].rot, identity, acceptableAngleError); + QCOMPARE_QUATS(absolutePoses[2].rot, quaterTurnAroundZ, acceptableAngleError); + QCOMPARE_QUATS(absolutePoses[3].rot, quaterTurnAroundZ, acceptableAngleError); - QCOMPARE_WITH_ABS_ERROR(absolutePoses[0].trans, origin, EPSILON); - QCOMPARE_WITH_ABS_ERROR(absolutePoses[1].trans, xAxis, EPSILON); - QCOMPARE_WITH_ABS_ERROR(absolutePoses[2].trans, 2.0f * xAxis, EPSILON); - QCOMPARE_WITH_ABS_ERROR(absolutePoses[3].trans, targetPosition, EPSILON); + const float acceptableTranslationError = 0.025f; + QCOMPARE_WITH_ABS_ERROR(absolutePoses[0].trans, origin, acceptableTranslationError); + QCOMPARE_WITH_ABS_ERROR(absolutePoses[1].trans, xAxis, acceptableTranslationError); + QCOMPARE_WITH_ABS_ERROR(absolutePoses[2].trans, 2.0f * xAxis, acceptableTranslationError); + QCOMPARE_WITH_ABS_ERROR(absolutePoses[3].trans, targetPosition, acceptableTranslationError); // verify relative results - const std::vector& relativePoses = ikDoll.getRelativePoses(); - QCOMPARE_QUATS(relativePoses[0].rot, identity, acceptableAngle); - QCOMPARE_QUATS(relativePoses[1].rot, identity, acceptableAngle); - QCOMPARE_QUATS(relativePoses[2].rot, quaterTurnAroundZ, acceptableAngle); - QCOMPARE_QUATS(relativePoses[3].rot, identity, acceptableAngle); + QCOMPARE_QUATS(relativePoses[0].rot, identity, acceptableAngleError); + QCOMPARE_QUATS(relativePoses[1].rot, identity, acceptableAngleError); + QCOMPARE_QUATS(relativePoses[2].rot, quaterTurnAroundZ, acceptableAngleError); + QCOMPARE_QUATS(relativePoses[3].rot, identity, acceptableAngleError); - QCOMPARE_WITH_ABS_ERROR(relativePoses[0].trans, origin, EPSILON); - QCOMPARE_WITH_ABS_ERROR(relativePoses[1].trans, xAxis, EPSILON); - QCOMPARE_WITH_ABS_ERROR(relativePoses[2].trans, xAxis, EPSILON); - QCOMPARE_WITH_ABS_ERROR(relativePoses[3].trans, xAxis, EPSILON); + QCOMPARE_WITH_ABS_ERROR(relativePoses[0].trans, origin, acceptableTranslationError); + QCOMPARE_WITH_ABS_ERROR(relativePoses[1].trans, xAxis, acceptableTranslationError); + QCOMPARE_WITH_ABS_ERROR(relativePoses[2].trans, xAxis, acceptableTranslationError); + QCOMPARE_WITH_ABS_ERROR(relativePoses[3].trans, xAxis, acceptableTranslationError); } - { // hard test IK of joint C // load intial poses that look like this: // @@ -188,8 +200,8 @@ void AnimInverseKinematicsTests::testSingleChain() { pose.scale = glm::vec3(1.0f); pose.rot = identity; pose.trans = origin; - - std::vector poses; + + AnimPoseVec poses; poses.push_back(pose); pose.trans = xAxis; @@ -211,15 +223,26 @@ void AnimInverseKinematicsTests::testSingleChain() { AnimVariantMap varMap; varMap.set("positionD", targetPosition); varMap.set("rotationD", targetRotation); - ikDoll.setTargetVars("D", "positionD", "rotationD"); + varMap.set("targetType", (int)IKTarget::Type::RotationAndPosition); + ikDoll.setTargetVars(QString("D"), QString("positionD"), QString("rotationD"), QString("targetType")); AnimNode::Triggers triggers; // the IK solution should be: // // A------>B------>C------>D // + + // iterate several times float dt = 1.0f; - ikDoll.evaluate(varMap, dt, triggers); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + ikDoll.overlay(varMap, dt, triggers, poses); + const AnimPoseVec& relativePoses = ikDoll.overlay(varMap, dt, triggers, poses); // verify absolute results // NOTE: the IK algorithm doesn't converge very fast for full-reach targets, @@ -228,31 +251,33 @@ void AnimInverseKinematicsTests::testSingleChain() { // NOTE: constraints may help speed up convergence since some joints may get clamped // to maximum extension. TODO: experiment with tightening the error thresholds when // constraints are working. - std::vector absolutePoses; + AnimPoseVec absolutePoses; + for (auto pose : poses) { + absolutePoses.push_back(pose); + } ikDoll.computeAbsolutePoses(absolutePoses); - float acceptableAngle = 0.1f; // radians + float acceptableAngle = 0.01f; // radians QCOMPARE_QUATS(absolutePoses[0].rot, identity, acceptableAngle); QCOMPARE_QUATS(absolutePoses[1].rot, identity, acceptableAngle); QCOMPARE_QUATS(absolutePoses[2].rot, identity, acceptableAngle); QCOMPARE_QUATS(absolutePoses[3].rot, identity, acceptableAngle); - float acceptableDistance = 0.4f; - QCOMPARE_WITH_ABS_ERROR(absolutePoses[0].trans, origin, EPSILON); + float acceptableDistance = 0.03f; + QCOMPARE_WITH_ABS_ERROR(absolutePoses[0].trans, origin, acceptableDistance); QCOMPARE_WITH_ABS_ERROR(absolutePoses[1].trans, xAxis, acceptableDistance); QCOMPARE_WITH_ABS_ERROR(absolutePoses[2].trans, 2.0f * xAxis, acceptableDistance); QCOMPARE_WITH_ABS_ERROR(absolutePoses[3].trans, 3.0f * xAxis, acceptableDistance); // verify relative results - const std::vector& relativePoses = ikDoll.getRelativePoses(); QCOMPARE_QUATS(relativePoses[0].rot, identity, acceptableAngle); QCOMPARE_QUATS(relativePoses[1].rot, identity, acceptableAngle); QCOMPARE_QUATS(relativePoses[2].rot, identity, acceptableAngle); QCOMPARE_QUATS(relativePoses[3].rot, identity, acceptableAngle); - QCOMPARE_WITH_ABS_ERROR(relativePoses[0].trans, origin, EPSILON); - QCOMPARE_WITH_ABS_ERROR(relativePoses[1].trans, xAxis, EPSILON); - QCOMPARE_WITH_ABS_ERROR(relativePoses[2].trans, xAxis, EPSILON); - QCOMPARE_WITH_ABS_ERROR(relativePoses[3].trans, xAxis, EPSILON); + QCOMPARE_WITH_ABS_ERROR(relativePoses[0].trans, origin, acceptableDistance); + QCOMPARE_WITH_ABS_ERROR(relativePoses[1].trans, xAxis, acceptableDistance); + QCOMPARE_WITH_ABS_ERROR(relativePoses[2].trans, xAxis, acceptableDistance); + QCOMPARE_WITH_ABS_ERROR(relativePoses[3].trans, xAxis, acceptableDistance); } } diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 6812bb63b6..130b692fb0 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -38,8 +38,9 @@ void AnimTests::testClipInternalState() { float endFrame = 20.0f; float timeScale = 1.1f; bool loopFlag = true; + bool mirrorFlag = false; - AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag, mirrorFlag); QVERIFY(clip.getID() == id); QVERIFY(clip.getType() == AnimNode::Type::Clip); @@ -49,6 +50,7 @@ void AnimTests::testClipInternalState() { QVERIFY(clip._endFrame == endFrame); QVERIFY(clip._timeScale == timeScale); QVERIFY(clip._loopFlag == loopFlag); + QVERIFY(clip._mirrorFlag == mirrorFlag); } static float framesToSec(float secs) { @@ -62,12 +64,13 @@ void AnimTests::testClipEvaulate() { float startFrame = 2.0f; float endFrame = 22.0f; float timeScale = 1.0f; - float loopFlag = true; + bool loopFlag = true; + bool mirrorFlag = false; auto vars = AnimVariantMap(); vars.set("FalseVar", false); - AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag, mirrorFlag); AnimNode::Triggers triggers; clip.evaluate(vars, framesToSec(10.0f), triggers); @@ -97,7 +100,8 @@ void AnimTests::testClipEvaulateWithVars() { float startFrame = 2.0f; float endFrame = 22.0f; float timeScale = 1.0f; - float loopFlag = true; + bool loopFlag = true; + bool mirrorFlag = false; float startFrame2 = 22.0f; float endFrame2 = 100.0f; @@ -110,7 +114,7 @@ void AnimTests::testClipEvaulateWithVars() { vars.set("timeScale2", timeScale2); vars.set("loopFlag2", loopFlag2); - AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag, mirrorFlag); clip.setStartFrameVar("startFrame2"); clip.setEndFrameVar("endFrame2"); clip.setTimeScaleVar("timeScale2"); @@ -583,23 +587,23 @@ void AnimTests::testExpressionEvaluator() { TEST_BOOL_EXPR(false && false); TEST_BOOL_EXPR(false && true); - TEST_BOOL_EXPR(true || false && true); - TEST_BOOL_EXPR(true || false && false); - TEST_BOOL_EXPR(true || true && true); - TEST_BOOL_EXPR(true || true && false); - TEST_BOOL_EXPR(false || false && true); - TEST_BOOL_EXPR(false || false && false); - TEST_BOOL_EXPR(false || true && true); - TEST_BOOL_EXPR(false || true && false); + TEST_BOOL_EXPR(true || (false && true)); + TEST_BOOL_EXPR(true || (false && false)); + TEST_BOOL_EXPR(true || (true && true)); + TEST_BOOL_EXPR(true || (true && false)); + TEST_BOOL_EXPR(false || (false && true)); + TEST_BOOL_EXPR(false || (false && false)); + TEST_BOOL_EXPR(false || (true && true)); + TEST_BOOL_EXPR(false || (true && false)); - TEST_BOOL_EXPR(true && false || true); - TEST_BOOL_EXPR(true && false || false); - TEST_BOOL_EXPR(true && true || true); - TEST_BOOL_EXPR(true && true || false); - TEST_BOOL_EXPR(false && false || true); - TEST_BOOL_EXPR(false && false || false); - TEST_BOOL_EXPR(false && true || true); - TEST_BOOL_EXPR(false && true || false); + TEST_BOOL_EXPR((true && false) || true); + TEST_BOOL_EXPR((true && false) || false); + TEST_BOOL_EXPR((true && true) || true); + TEST_BOOL_EXPR((true && true) || false); + TEST_BOOL_EXPR((false && false) || true); + TEST_BOOL_EXPR((false && false) || false); + TEST_BOOL_EXPR((false && true) || true); + TEST_BOOL_EXPR((false && true) || false); TEST_BOOL_EXPR(t || false); TEST_BOOL_EXPR(t || true); @@ -610,14 +614,14 @@ void AnimTests::testExpressionEvaluator() { TEST_BOOL_EXPR(!false); TEST_BOOL_EXPR(!true || true); - TEST_BOOL_EXPR(!true && !false || !true); - TEST_BOOL_EXPR(!true && !false || true); - TEST_BOOL_EXPR(!true && false || !true); - TEST_BOOL_EXPR(!true && false || true); - TEST_BOOL_EXPR(true && !false || !true); - TEST_BOOL_EXPR(true && !false || true); - TEST_BOOL_EXPR(true && false || !true); - TEST_BOOL_EXPR(true && false || true); + TEST_BOOL_EXPR((!true && !false) || !true); + TEST_BOOL_EXPR((!true && !false) || true); + TEST_BOOL_EXPR((!true && false) || !true); + TEST_BOOL_EXPR((!true && false) || true); + TEST_BOOL_EXPR((true && !false) || !true); + TEST_BOOL_EXPR((true && !false) || true); + TEST_BOOL_EXPR((true && false) || !true); + TEST_BOOL_EXPR((true && false) || true); TEST_BOOL_EXPR(!(true && f) || !t); TEST_BOOL_EXPR(!!!(t) && (!!f || true)); diff --git a/tests/animation/src/RotationConstraintTests.cpp b/tests/animation/src/RotationConstraintTests.cpp index 7aacf26826..f828201a81 100644 --- a/tests/animation/src/RotationConstraintTests.cpp +++ b/tests/animation/src/RotationConstraintTests.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -56,7 +57,7 @@ void RotationConstraintTests::testElbowConstraint() { float startAngle = minAngle + smallAngle; float endAngle = maxAngle - smallAngle; float deltaAngle = (endAngle - startAngle) / (float)(numChecks - 1); - + for (float angle = startAngle; angle < endAngle + 0.5f * deltaAngle; angle += deltaAngle) { glm::quat inputRotation = glm::angleAxis(angle, hingeAxis) * referenceRotation; glm::quat outputRotation = inputRotation; @@ -115,9 +116,9 @@ void RotationConstraintTests::testSwingTwistConstraint() { shoulder.setTwistLimits(minTwistAngle, maxTwistAngle); float lowDot = 0.25f; float highDot = 0.75f; - // The swing constriants are more interesting: a vector of minimum dot products + // The swing constriants are more interesting: a vector of minimum dot products // as a function of theta around the twist axis. Our test function will be shaped - // like the square wave with amplitudes 0.25 and 0.75: + // like a square wave with amplitudes 0.25 and 0.75: // // | // 0.75 - o---o---o---o @@ -308,3 +309,344 @@ void RotationConstraintTests::testSwingTwistConstraint() { } } +void RotationConstraintTests::testDynamicSwingLimitFunction() { + SwingTwistConstraint::SwingLimitFunction limitFunction; + const float ACCEPTABLE_ERROR = 1.0e-6f; + + const float adjustmentDot = -0.5f; + + const float MIN_DOT = 0.5f; + { // initialize limitFunction + std::vector minDots; + minDots.push_back(MIN_DOT); + limitFunction.setMinDots(minDots); + } + + std::vector referenceDots; + { // verify limits and initialize referenceDots + const int MIN_NUM_DOTS = 8; + const std::vector& minDots = limitFunction.getMinDots(); + QVERIFY(minDots.size() >= MIN_NUM_DOTS); + + int numDots = (int)minDots.size(); + for (int i = 0; i < numDots; ++i) { + QCOMPARE_WITH_RELATIVE_ERROR(minDots[i], MIN_DOT, ACCEPTABLE_ERROR); + referenceDots.push_back(minDots[i]); + } + } + { // dynamically adjust limits + const std::vector& minDots = limitFunction.getMinDots(); + int numDots = (int)minDots.size(); + + float deltaTheta = TWO_PI / (float)(numDots - 1); + int indexA = 2; + int indexB = (indexA + 1) % numDots; + + { // dynamically adjust a data point + float theta = deltaTheta * (float)indexA; + float interpolatedDot = limitFunction.getMinDot(theta); + + // change indexA + limitFunction.dynamicallyAdjustMinDots(theta, adjustmentDot); + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), adjustmentDot, ACCEPTABLE_ERROR); // adjustmentDot at theta + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], adjustmentDot, ACCEPTABLE_ERROR); // indexA has changed + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], referenceDots[indexB], ACCEPTABLE_ERROR); // indexB has not changed + + // change indexB + theta = deltaTheta * (float)indexB; + limitFunction.dynamicallyAdjustMinDots(theta, adjustmentDot); + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), adjustmentDot, ACCEPTABLE_ERROR); // adjustmentDot at theta + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], referenceDots[indexA], ACCEPTABLE_ERROR); // indexA has been restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], adjustmentDot, ACCEPTABLE_ERROR); // indexB has changed + + // restore + limitFunction.dynamicallyAdjustMinDots(theta, referenceDots[indexB] + 0.01f); // restore with a larger dot + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), interpolatedDot, ACCEPTABLE_ERROR); // restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], referenceDots[indexA], ACCEPTABLE_ERROR); // indexA is restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], referenceDots[indexB], ACCEPTABLE_ERROR); // indexB is restored + } + { // dynamically adjust halfway between data points + float theta = deltaTheta * 0.5f * (float)(indexA + indexB); // halfway between two points + float interpolatedDot = limitFunction.getMinDot(theta); + float deltaDot = adjustmentDot - interpolatedDot; + limitFunction.dynamicallyAdjustMinDots(theta, adjustmentDot); + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), adjustmentDot, ACCEPTABLE_ERROR); // adjustmentDot at theta + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], referenceDots[indexA] + deltaDot, ACCEPTABLE_ERROR); // indexA has changed + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], referenceDots[indexB] + deltaDot, ACCEPTABLE_ERROR); // indexB has changed + + limitFunction.dynamicallyAdjustMinDots(theta, interpolatedDot + 0.01f); // reset with something larger + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), interpolatedDot, ACCEPTABLE_ERROR); // restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], referenceDots[indexA], ACCEPTABLE_ERROR); // indexA is restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], referenceDots[indexB], ACCEPTABLE_ERROR); // indexB is restored + } + { // dynamically adjust one-quarter between data points + float theta = deltaTheta * ((float)indexA + 0.25f); // one quarter past A towards B + float interpolatedDot = limitFunction.getMinDot(theta); + limitFunction.dynamicallyAdjustMinDots(theta, adjustmentDot); + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), adjustmentDot, ACCEPTABLE_ERROR); // adjustmentDot at theta + QVERIFY(minDots[indexA] < adjustmentDot); // indexA should be less than minDot + QVERIFY(minDots[indexB] > adjustmentDot); // indexB should be larger than minDot + QVERIFY(minDots[indexB] < referenceDots[indexB]); // indexB should be less than what it was + + limitFunction.dynamicallyAdjustMinDots(theta, interpolatedDot + 0.01f); // reset with something larger + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), interpolatedDot, ACCEPTABLE_ERROR); // restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], referenceDots[indexA], ACCEPTABLE_ERROR); // indexA is restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], referenceDots[indexB], ACCEPTABLE_ERROR); // indexB is restored + } + { // halfway between first two data points (boundary condition) + indexA = 0; + indexB = 1; + int indexZ = minDots.size() - 1; // far boundary condition + float theta = deltaTheta * 0.5f * (float)(indexA + indexB); // halfway between two points + float interpolatedDot = limitFunction.getMinDot(theta); + float deltaDot = adjustmentDot - interpolatedDot; + limitFunction.dynamicallyAdjustMinDots(theta, adjustmentDot); + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), adjustmentDot, ACCEPTABLE_ERROR); // adjustmentDot at theta + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], referenceDots[indexA] + deltaDot, ACCEPTABLE_ERROR); // indexA has changed + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], referenceDots[indexB] + deltaDot, ACCEPTABLE_ERROR); // indexB has changed + QCOMPARE_WITH_ABS_ERROR(minDots[indexZ], referenceDots[indexZ] + deltaDot, ACCEPTABLE_ERROR); // indexZ has changed + + limitFunction.dynamicallyAdjustMinDots(theta, interpolatedDot + 0.01f); // reset with something larger + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), interpolatedDot, ACCEPTABLE_ERROR); // restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], referenceDots[indexA], ACCEPTABLE_ERROR); // indexA is restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], referenceDots[indexB], ACCEPTABLE_ERROR); // indexB is restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexZ], referenceDots[indexZ], ACCEPTABLE_ERROR); // indexZ is restored + } + { // halfway between first two data points (boundary condition) + indexB = minDots.size() - 1; + indexA = indexB - 1; + int indexZ = 0; // far boundary condition + float theta = deltaTheta * 0.5f * (float)(indexA + indexB); // halfway between two points + float interpolatedDot = limitFunction.getMinDot(theta); + float deltaDot = adjustmentDot - interpolatedDot; + limitFunction.dynamicallyAdjustMinDots(theta, adjustmentDot); + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), adjustmentDot, ACCEPTABLE_ERROR); // adjustmentDot at theta + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], referenceDots[indexA] + deltaDot, ACCEPTABLE_ERROR); // indexA has changed + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], referenceDots[indexB] + deltaDot, ACCEPTABLE_ERROR); // indexB has changed + QCOMPARE_WITH_ABS_ERROR(minDots[indexZ], referenceDots[indexZ] + deltaDot, ACCEPTABLE_ERROR); // indexZ has changed + + limitFunction.dynamicallyAdjustMinDots(theta, interpolatedDot + 0.01f); // reset with something larger + QCOMPARE_WITH_ABS_ERROR(limitFunction.getMinDot(theta), interpolatedDot, ACCEPTABLE_ERROR); // restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexA], referenceDots[indexA], ACCEPTABLE_ERROR); // indexA is restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexB], referenceDots[indexB], ACCEPTABLE_ERROR); // indexB is restored + QCOMPARE_WITH_ABS_ERROR(minDots[indexZ], referenceDots[indexZ], ACCEPTABLE_ERROR); // indexZ is restored + } + } +} + +void RotationConstraintTests::testDynamicSwing() { + const float ACCEPTABLE_ERROR = 1.0e-6f; + + // referenceRotation is the default rotation + float referenceAngle = 1.23f; + glm::vec3 referenceAxis = glm::normalize(glm::vec3(1.0f, 2.0f, -3.0f)); + glm::quat referenceRotation = glm::angleAxis(referenceAngle, referenceAxis); + + // the angle limits of the constriant about the hinge axis + float minTwistAngle = -PI / 2.0f; + float maxTwistAngle = PI / 2.0f; + + // build the constraint + SwingTwistConstraint shoulder; + shoulder.setReferenceRotation(referenceRotation); + shoulder.setTwistLimits(minTwistAngle, maxTwistAngle); + std::vector minDots; + const float MIN_DOT = 0.5f; + minDots.push_back(MIN_DOT); + shoulder.setSwingLimits(minDots); + + // verify resolution of the swing limits + const std::vector& shoulderMinDots = shoulder.getMinDots(); + const int MIN_NUM_DOTS = 8; + int numDots = shoulderMinDots.size(); + QVERIFY(numDots >= MIN_NUM_DOTS); + + // verify values of the swing limits + QCOMPARE_WITH_ABS_ERROR(shoulderMinDots[0], shoulderMinDots[numDots - 1], ACCEPTABLE_ERROR); // endpoints should be the same + for (int i = 0; i < numDots; ++i) { + QCOMPARE_WITH_ABS_ERROR(shoulderMinDots[i], MIN_DOT, ACCEPTABLE_ERROR); // all values should be the same + } + + float deltaTheta = TWO_PI / (float)(numDots - 1); + float theta = 1.5f * deltaTheta; + glm::vec3 swingAxis(cosf(theta), 0.0f, sinf(theta)); + float deltaSwing = 0.1f; + + { // compute rotation that should NOT be constrained + float swingAngle = acosf(MIN_DOT) - deltaSwing; + glm::quat swingRotation = glm::angleAxis(swingAngle, swingAxis); + glm::quat totalRotation = swingRotation * referenceRotation; + + // verify rotation is NOT constrained + glm::quat constrainedRotation = totalRotation; + QVERIFY(!shoulder.apply(constrainedRotation)); + } + + { // compute a rotation that should be barely constrained + float swingAngle = acosf(MIN_DOT) + deltaSwing; + glm::quat swingRotation = glm::angleAxis(swingAngle, swingAxis); + glm::quat totalRotation = swingRotation * referenceRotation; + + // verify rotation is constrained + glm::quat constrainedRotation = totalRotation; + QVERIFY(shoulder.apply(constrainedRotation)); + } + + { // make a dynamic adjustment to the swing limits + const float SMALLER_MIN_DOT = -0.5f; + float swingAngle = acosf(SMALLER_MIN_DOT); + glm::quat swingRotation = glm::angleAxis(swingAngle, swingAxis); + glm::quat badRotation = swingRotation * referenceRotation; + + { // verify rotation is constrained + glm::quat constrainedRotation = badRotation; + QVERIFY(shoulder.apply(constrainedRotation)); + + // now poke the SMALLER_MIN_DOT into the swing limits + shoulder.dynamicallyAdjustLimits(badRotation); + + // verify that if rotation is constrained then it is only by a little bit + constrainedRotation = badRotation; + bool constrained = shoulder.apply(constrainedRotation); + if (constrained) { + // Note: Q1 = dQ * Q0 --> dQ = Q1 * Q0^ + glm::quat dQ = constrainedRotation * glm::inverse(badRotation); + const float acceptableClampAngle = 0.01f; + float deltaAngle = glm::angle(dQ); + QVERIFY(deltaAngle < acceptableClampAngle); + } + } + + { // verify that other swing axes still use the old non-adjusted limits + float deltaTheta = TWO_PI / (float)(numDots - 1); + float otherTheta = 3.5f * deltaTheta; + glm::vec3 otherSwingAxis(cosf(otherTheta), 0.0f, sinf(otherTheta)); + + { // inside rotations should be unconstrained + float goodAngle = acosf(MIN_DOT) - deltaSwing; + glm::quat goodRotation = glm::angleAxis(goodAngle, otherSwingAxis) * referenceRotation; + QVERIFY(!shoulder.apply(goodRotation)); + } + { // outside rotations should be constrained + float badAngle = acosf(MIN_DOT) + deltaSwing; + glm::quat otherBadRotation = glm::angleAxis(badAngle, otherSwingAxis) * referenceRotation; + QVERIFY(shoulder.apply(otherBadRotation)); + + float constrainedAngle = glm::angle(otherBadRotation); + QCOMPARE_WITH_ABS_ERROR(constrainedAngle, acosf(MIN_DOT), 0.1f * deltaSwing); + } + } + + { // clear dynamic adjustment + float goodAngle = acosf(MIN_DOT) - deltaSwing; + glm::quat goodRotation = glm::angleAxis(goodAngle, swingAxis) * referenceRotation; + + // when we update with a goodRotation the dynamic adjustment is cleared + shoulder.dynamicallyAdjustLimits(goodRotation); + + // verify that the old badRotation, which was not constrained dynamically, is now constrained + glm::quat constrainedRotation = badRotation; + QVERIFY(shoulder.apply(constrainedRotation)); + + // and the good rotation should not be constrained + constrainedRotation = goodRotation; + QVERIFY(!shoulder.apply(constrainedRotation)); + } + } +} + +void RotationConstraintTests::testDynamicTwist() { + // referenceRotation is the default rotation + float referenceAngle = 1.23f; + glm::vec3 referenceAxis = glm::normalize(glm::vec3(1.0f, 2.0f, -3.0f)); + glm::quat referenceRotation = glm::angleAxis(referenceAngle, referenceAxis); + + // the angle limits of the constriant about the hinge axis + const float minTwistAngle = -PI / 2.0f; + const float maxTwistAngle = PI / 2.0f; + + // build the constraint + SwingTwistConstraint shoulder; + shoulder.setReferenceRotation(referenceRotation); + shoulder.setTwistLimits(minTwistAngle, maxTwistAngle); + + glm::vec3 twistAxis = Vectors::UNIT_Y; + float deltaTwist = 0.1f; + + { // compute min rotation that should NOT be constrained + float twistAngle = minTwistAngle + deltaTwist; + glm::quat twistRotation = glm::angleAxis(twistAngle, twistAxis); + glm::quat totalRotation = twistRotation * referenceRotation; + + // verify rotation is NOT constrained + glm::quat constrainedRotation = totalRotation; + QVERIFY(!shoulder.apply(constrainedRotation)); + } + { // compute max rotation that should NOT be constrained + float twistAngle = maxTwistAngle - deltaTwist; + glm::quat twistRotation = glm::angleAxis(twistAngle, twistAxis); + glm::quat totalRotation = twistRotation * referenceRotation; + + // verify rotation is NOT constrained + glm::quat constrainedRotation = totalRotation; + QVERIFY(!shoulder.apply(constrainedRotation)); + } + { // compute a min rotation that should be barely constrained + float twistAngle = minTwistAngle - deltaTwist; + glm::quat twistRotation = glm::angleAxis(twistAngle, twistAxis); + glm::quat totalRotation = twistRotation * referenceRotation; + + // verify rotation is constrained + glm::quat constrainedRotation = totalRotation; + QVERIFY(shoulder.apply(constrainedRotation)); + + // adjust the constraint and verify rotation is NOT constrained + shoulder.dynamicallyAdjustLimits(totalRotation); + constrainedRotation = totalRotation; + bool constrained = shoulder.apply(constrainedRotation); + if (constrained) { + // or, if it is constrained then the adjustment is very small + // Note: Q1 = dQ * Q0 --> dQ = Q1 * Q0^ + glm::quat dQ = constrainedRotation * glm::inverse(totalRotation); + const float acceptableClampAngle = 0.01f; + float deltaAngle = glm::angle(dQ); + QVERIFY(deltaAngle < acceptableClampAngle); + } + + // clear the adjustment using a null rotation + shoulder.dynamicallyAdjustLimits(glm::quat()); + + // verify that rotation is constrained again + constrainedRotation = totalRotation; + QVERIFY(shoulder.apply(constrainedRotation)); + } + { // compute a min rotation that should be barely constrained + float twistAngle = maxTwistAngle + deltaTwist; + glm::quat twistRotation = glm::angleAxis(twistAngle, twistAxis); + glm::quat totalRotation = twistRotation * referenceRotation; + + // verify rotation is constrained + glm::quat constrainedRotation = totalRotation; + QVERIFY(shoulder.apply(constrainedRotation)); + + // adjust the constraint and verify rotation is NOT constrained + shoulder.dynamicallyAdjustLimits(totalRotation); + constrainedRotation = totalRotation; + bool constrained = shoulder.apply(constrainedRotation); + if (constrained) { + // or, if it is constrained then the adjustment is very small + // Note: Q1 = dQ * Q0 --> dQ = Q1 * Q0^ + glm::quat dQ = constrainedRotation * glm::inverse(totalRotation); + const float acceptableClampAngle = 0.01f; + float deltaAngle = glm::angle(dQ); + QVERIFY(deltaAngle < acceptableClampAngle); + } + + // clear the adjustment using a null rotation + shoulder.dynamicallyAdjustLimits(glm::quat()); + + // verify that rotation is constrained again + constrainedRotation = totalRotation; + QVERIFY(shoulder.apply(constrainedRotation)); + } +} diff --git a/tests/animation/src/RotationConstraintTests.h b/tests/animation/src/RotationConstraintTests.h index 4fed3588e4..e63d08bc1f 100644 --- a/tests/animation/src/RotationConstraintTests.h +++ b/tests/animation/src/RotationConstraintTests.h @@ -15,10 +15,13 @@ class RotationConstraintTests : public QObject { Q_OBJECT - + private slots: void testElbowConstraint(); void testSwingTwistConstraint(); + void testDynamicSwingLimitFunction(); + void testDynamicSwing(); + void testDynamicTwist(); }; #endif // hifi_RotationConstraintTests_h diff --git a/unpublishedScripts/DomainContent/Home/fishTank/createFishTank.js b/unpublishedScripts/DomainContent/Home/fishTank/createFishTank.js index cd03b1efa2..1a4c95c622 100644 --- a/unpublishedScripts/DomainContent/Home/fishTank/createFishTank.js +++ b/unpublishedScripts/DomainContent/Home/fishTank/createFishTank.js @@ -1,18 +1,18 @@ // // createTank.js -// +// // // created by James b. Pollack @imgntn on 3/9/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Adds a fish tank and base, decorations, particle bubble systems, and a bubble sound. Attaches a script that does fish swimming. -// +// Copyright 2016 High Fidelity, Inc. +// +// Adds a fish tank and base, decorations, particle bubble systems, and a bubble sound. Attaches a script that does fish swimming. +// // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var fishTank, tankBase, bubbleSystem, secondBubbleSystem, thirdBubbleSystem, innerContainer, bubbleInjector, lowerCorner, upperCorner, urchin, treasure, rocks; +var fishTank, tankBase, bubbleSystem, secondBubbleSystem, thirdBubbleSystem, innerContainer, bubbleInjector, lowerCorner, upperCorner, anemone, treasure, rocks; var CLEANUP = true; var TANK_DIMENSIONS = { @@ -34,7 +34,15 @@ var DEBUG_COLOR = { blue: 255 } -var center = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), 1 * TANK_WIDTH)); + +var centerVertical = { + x: 0, + y: 1, + z: 0 +} + +var upCenter = Vec3.sum(centerVertical, MyAvatar.position); +var center = Vec3.sum(upCenter, Vec3.multiply(Quat.getFront(MyAvatar.orientation), 2)); var TANK_POSITION = center; @@ -52,7 +60,7 @@ var TANK_BASE_DIMENSIONS = { z: 2.1936 }; -var BASE_VERTICAL_OFFSET = 0.42; +var BASE_VERTICAL_OFFSET = 0.47; var BUBBLE_SYSTEM_FORWARD_OFFSET = TANK_DIMENSIONS.x + 0.06; var BUBBLE_SYSTEM_LATERAL_OFFSET = 0.025; @@ -68,14 +76,14 @@ var BUBBLE_SOUND_URL = "http://hifi-content.s3.amazonaws.com/DomainContent/Home/ var bubbleSound = SoundCache.getSound(BUBBLE_SOUND_URL); -var URCHIN_FORWARD_OFFSET = TANK_DIMENSIONS.x - 0.35; -var URCHIN_LATERAL_OFFSET = -0.05; -var URCHIN_VERTICAL_OFFSET = -0.12; +var ANEMONE_FORWARD_OFFSET = TANK_DIMENSIONS.x - 0.35; +var ANEMONE_LATERAL_OFFSET = -0.05; +var ANEMONE_VERTICAL_OFFSET = -0.12; -var URCHIN_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/DomainContent/Home/fishTank/Urchin.fbx'; - -var URCHIN_DIMENSIONS = { +var ANEMONE_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/DomainContent/Home/fishTank/anemone.fbx'; +var ANEMONE_ANIMATION_URL = 'http://hifi-content.s3.amazonaws.com/DomainContent/Home/fishTank/anemone.fbx'; +var ANEMONE_DIMENSIONS = { x: 0.4, y: 0.4, z: 0.4 @@ -303,19 +311,29 @@ function createRocks() { } function createUrchin() { - var finalPosition = getOffsetFromTankCenter(URCHIN_VERTICAL_OFFSET, URCHIN_FORWARD_OFFSET, URCHIN_LATERAL_OFFSET); + var finalPosition = getOffsetFromTankCenter(ANEMONE_VERTICAL_OFFSET, ANEMONE_FORWARD_OFFSET, ANEMONE_LATERAL_OFFSET); var properties = { - name: 'hifi-home-fishtank-urchin', + name: 'hifi-home-fishtank-anemone', type: 'Model', + animationURL: ANEMONE_ANIMATION_URL, + animationIsPlaying: true, + animationFPS: 15, + animationSettings: JSON.stringify({ + hold: false, + loop: true, + running: true, + startAutomatically: true + }), parentID: fishTank, - modelURL: URCHIN_MODEL_URL, + modelURL: ANEMONE_MODEL_URL, position: finalPosition, shapeType: 'Sphere', - dimensions: URCHIN_DIMENSIONS + rotation: Quat.fromPitchYawRollDegrees(0, 90, 0), + dimensions: ANEMONE_DIMENSIONS } - urchin = Entities.addEntity(properties); + anemone = Entities.addEntity(properties); } @@ -398,7 +416,7 @@ function cleanup() { Entities.deleteEntity(innerContainer); Entities.deleteEntity(lowerCorner); Entities.deleteEntity(upperCorner); - Entities.deleteEntity(urchin); + Entities.deleteEntity(anemone); Entities.deleteEntity(rocks); bubbleInjector.stop(); bubbleInjector = null; @@ -449,4 +467,4 @@ function getEntityCustomData(customKey, id, defaultValue) { } else { return defaultValue; } -} \ No newline at end of file +}