diff --git a/cmake/externals/polyvox/CMakeLists.txt b/cmake/externals/polyvox/CMakeLists.txt index 4563bf5918..14712e5537 100644 --- a/cmake/externals/polyvox/CMakeLists.txt +++ b/cmake/externals/polyvox/CMakeLists.txt @@ -5,7 +5,7 @@ ExternalProject_Add( ${EXTERNAL_NAME} URL http://hifi-public.s3.amazonaws.com/dependencies/polyvox-master-2015-7-15.zip URL_MD5 9ec6323b87e849ae36e562ae1c7494a9 - CMAKE_ARGS -DENABLE_EXAMPLES=OFF -DCMAKE_INSTALL_PREFIX:PATH= + CMAKE_ARGS -DENABLE_EXAMPLES=OFF -DENABLE_BINDINGS=OFF -DCMAKE_INSTALL_PREFIX:PATH= BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build LOG_DOWNLOAD 1 LOG_CONFIGURE 1 diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 2ebea75abc..523cb4eff9 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -13,7 +13,6 @@ Script.include("../libraries/utils.js"); - var RIGHT_HAND_CLICK = Controller.findAction("RIGHT_HAND_CLICK"); var rightTriggerAction = RIGHT_HAND_CLICK; @@ -22,6 +21,10 @@ var GRAB_USER_DATA_KEY = "grabKey"; var LEFT_HAND_CLICK = Controller.findAction("LEFT_HAND_CLICK"); var leftTriggerAction = LEFT_HAND_CLICK; +var LIFETIME = 10; +var EXTRA_TIME = 5; +var POINTER_CHECK_TIME = 5000; + var ZERO_VEC = { x: 0, y: 0, @@ -42,7 +45,7 @@ var INTERSECT_COLOR = { blue: 10 }; -var GRAB_RADIUS = 1.0; +var GRAB_RADIUS = 1.5; var GRAB_COLOR = { red: 250, @@ -59,8 +62,15 @@ var TRACTOR_BEAM_VELOCITY_THRESHOLD = 0.5; var RIGHT = 1; var LEFT = 0; -var rightController = new controller(RIGHT, rightTriggerAction, right4Action, "right") -var leftController = new controller(LEFT, leftTriggerAction, left4Action, "left") +var rightController = new controller(RIGHT, rightTriggerAction, right4Action, "right"); +var leftController = new controller(LEFT, leftTriggerAction, left4Action, "left"); + + +//Need to wait before calling these methods for some reason... +Script.setTimeout(function() { + rightController.checkPointer(); + leftController.checkPointer(); +}, 100) function controller(side, triggerAction, pullAction, hand) { this.hand = hand; @@ -92,7 +102,9 @@ function controller(side, triggerAction, pullAction, hand) { z: 1000 }, visible: false, + lifetime: LIFETIME }); + } @@ -127,6 +139,16 @@ controller.prototype.updateLine = function() { } +controller.prototype.checkPointer = function() { + var self = this; + Script.setTimeout(function() { + var props = Entities.getEntityProperties(self.pointer); + Entities.editEntity(self.pointer, { + lifetime: props.age + EXTRA_TIME + }); + self.checkPointer(); + }, POINTER_CHECK_TIME); +} controller.prototype.checkForIntersections = function(origin, direction) { var pickRay = { @@ -241,14 +263,22 @@ controller.prototype.grabEntity = function() { this.closeGrabbing = true; //check if our entity has instructions on how to be grabbed, otherwise, just use default relative position and rotation var userData = getEntityUserData(this.grabbedEntity); - var relativePosition = ZERO_VEC; - var relativeRotation = Quat.fromPitchYawRollDegrees(0, 0, 0); - if(userData.spatialKey) { - if(userData.spatialKey.relativePosition) { - relativePosition = userData.spatialKey.relativePosition; + + var objectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation; + var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); + + var objectPosition = Entities.getEntityProperties(this.grabbedEntity).position; + var offset = Vec3.subtract(objectPosition, handPosition); + var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset); + + var relativePosition = offsetPosition; + var relativeRotation = offsetRotation; + if (userData.grabFrame) { + if (userData.grabFrame.relativePosition) { + relativePosition = userData.grabFrame.relativePosition; } - if(userData.spatialKey.relativeRotation) { - relativeRotation = userData.spatialKey.relativeRotation; + if (userData.grabFrame.relativeRotation) { + relativeRotation = userData.grabFrame.relativeRotation; } } this.actionID = Entities.addAction("hold", this.grabbedEntity, { diff --git a/examples/entityScripts/boombox.js b/examples/entityScripts/boombox.js index d1d18ec615..cc0dc23a23 100644 --- a/examples/entityScripts/boombox.js +++ b/examples/entityScripts/boombox.js @@ -71,7 +71,7 @@ if (_this.injector == null) { _this.injector = Audio.playSound(_this.song, { position: props.position, // position of boombox entity - volume: 0.5, + volume: 0.1, loop: true }); } else { diff --git a/examples/entityScripts/breakdanceEntity.js b/examples/entityScripts/breakdanceEntity.js index ece3e57062..5d4d418fec 100644 --- a/examples/entityScripts/breakdanceEntity.js +++ b/examples/entityScripts/breakdanceEntity.js @@ -31,6 +31,7 @@ // if we're currently being grabbed and if the person grabbing us is the current interfaces avatar. // we will watch this for state changes and print out if we're being grabbed or released when it changes. update: function() { + //print("BreakdanceEntity.update() _this.entityID:" + _this.entityID); var GRAB_USER_DATA_KEY = "grabKey"; // because the update() signal doesn't have a valid this, we need to use our memorized _this to access our entityID @@ -45,14 +46,15 @@ // if the grabData says we're being grabbed, and the owner ID is our session, then we are being grabbed by this interface if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { - + //print("BreakdanceEntity.update() [I'm being grabbed] _this.entityID:" + _this.entityID); if (!_this.beingGrabbed) { + print("I'm was grabbed... _this.entityID:" + _this.entityID); + // remember we're being grabbed so we can detect being released _this.beingGrabbed = true; var props = Entities.getEntityProperties(entityID); var puppetPosition = getPuppetPosition(props); breakdanceStart(props.modelURL, puppetPosition); - print("I'm was grabbed..."); } else { breakdanceUpdate(); } @@ -62,7 +64,7 @@ // if we are not being grabbed, and we previously were, then we were just released, remember that // and print out a message _this.beingGrabbed = false; - print("I'm was released..."); + print("I'm was released... _this.entityID:" + _this.entityID); breakdanceEnd(); } }, @@ -73,6 +75,7 @@ // * connecting to the update signal so we can check our grabbed state preload: function(entityID) { this.entityID = entityID; + print("BreakdanceEntity.preload() this.entityID:" + this.entityID); Script.update.connect(this.update); }, @@ -80,6 +83,7 @@ // or because we've left the domain or quit the application. In all cases we want to unhook our connection // to the update signal unload: function(entityID) { + print("BreakdanceEntity.unload() this.entityID:" + this.entityID); Script.update.disconnect(this.update); }, diff --git a/examples/entityScripts/sprayPaintCan.js b/examples/entityScripts/sprayPaintCan.js index 914e855349..c42a9bd659 100644 --- a/examples/entityScripts/sprayPaintCan.js +++ b/examples/entityScripts/sprayPaintCan.js @@ -1,6 +1,6 @@ (function() { Script.include("../libraries/utils.js"); - SPATIAL_USER_DATA_KEY = "spatialKey"; + GRAB_FRAME_USER_DATA_KEY = "grabFrame"; this.userData = {}; var TIP_OFFSET_Z = 0.14; @@ -62,13 +62,14 @@ this.sprayStream = function() { var forwardVec = Quat.getFront(self.properties.rotation); forwardVec = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, 90, 0), forwardVec); + forwardVec = Vec3.normalize(forwardVec); var upVec = Quat.getUp(self.properties.rotation); var position = Vec3.sum(self.properties.position, Vec3.multiply(forwardVec, TIP_OFFSET_Z)); position = Vec3.sum(position, Vec3.multiply(upVec, TIP_OFFSET_Y)) Entities.editEntity(self.paintStream, { position: position, - emitVelocity: forwardVec + emitVelocity: Vec3.multiply(forwardVec, 4) }); //Now check for an intersection with an entity @@ -155,12 +156,16 @@ if (this.userData.grabKey && this.userData.grabKey.activated) { this.activated = true; } - if(!this.userData.spatialKey) { + if (!this.userData.grabFrame) { var data = { - relativePosition: {x: 0, y: 0, z: 0}, - relativeRotation: Quat.fromPitchYawRollDegrees(0, 0,0) + relativePosition: { + x: 0, + y: 0, + z: 0 + }, + relativeRotation: Quat.fromPitchYawRollDegrees(0, 0, 0) } - setEntityCustomData(SPATIAL_USER_DATA_KEY, this.entityId, data); + setEntityCustomData(GRAB_FRAME_USER_DATA_KEY, this.entityId, data); } this.initialize(); } @@ -174,7 +179,6 @@ running: false }); - this.paintStream = Entities.addEntity({ type: "ParticleEffect", animationSettings: animationSettings, diff --git a/examples/example/tests/test-includes/a.js b/examples/example/tests/test-includes/a.js new file mode 100644 index 0000000000..1d99a08525 --- /dev/null +++ b/examples/example/tests/test-includes/a.js @@ -0,0 +1,7 @@ +// a.js: +Script.include('b.js'); +if (a === undefined) { +a = 0; +} +a++; +print('script a:' + a); diff --git a/examples/example/tests/test-includes/b.js b/examples/example/tests/test-includes/b.js new file mode 100644 index 0000000000..ea3f9664ba --- /dev/null +++ b/examples/example/tests/test-includes/b.js @@ -0,0 +1,6 @@ +// b.js: +if (b === undefined) { +b = 0; +} +b++; +print('script b: ' + b); \ No newline at end of file diff --git a/examples/example/tests/test-includes/start.js b/examples/example/tests/test-includes/start.js new file mode 100644 index 0000000000..8060ffcf71 --- /dev/null +++ b/examples/example/tests/test-includes/start.js @@ -0,0 +1,5 @@ +// start.js: +var a, b; +print('initially: a:' + a + ' b:' + b); +Script.include(['a.js', '../test-includes/a.js', 'b.js', 'a.js']); +print('finally a:' + a + ' b:' + b); diff --git a/examples/libraries/utils.js b/examples/libraries/utils.js index 10694b11f5..1275975fd8 100644 --- a/examples/libraries/utils.js +++ b/examples/libraries/utils.js @@ -96,20 +96,44 @@ mergeObjects = function(proto, custom) { return result; } +LOG_WARN = 1; + logWarn = function(str) { - print(str); + if (LOG_WARN) { + print(str); + } } +LOG_ERROR = 1; + logError = function(str) { - print(str); + if (LOG_ERROR) { + print(str); + } } +LOG_INFO = 1; + logInfo = function(str) { - print(str); + if (LOG_INFO) { + print(str); + } } +LOG_DEBUG = 0; + logDebug = function(str) { - print(str); + if (LOG_DEBUG) { + print(str); + } +} + +LOG_TRACE = 0; + +logTrace = function(str) { + if (LOG_TRACE) { + print(str); + } } // Computes the penetration between a point and a sphere (centered at the origin) diff --git a/examples/toys/breakdanceCore.js b/examples/toys/breakdanceCore.js index 57a679ff51..6a69e8d35a 100644 --- a/examples/toys/breakdanceCore.js +++ b/examples/toys/breakdanceCore.js @@ -10,8 +10,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - -function getPuppetPosition(props) { +getPuppetPosition = function(props) { var MAX_DISTANCE = 10; var searchPosition = MyAvatar.position; diff --git a/examples/toys/flashlight/createFlashlight.js b/examples/toys/flashlight/createFlashlight.js new file mode 100644 index 0000000000..bf03de547b --- /dev/null +++ b/examples/toys/flashlight/createFlashlight.js @@ -0,0 +1,45 @@ +// +// createFlashligh.js +// examples/entityScripts +// +// Created by Sam Gateau on 9/9/15. +// Copyright 2015 High Fidelity, Inc. +// +// This is a toy script that create a flashlight entity that lit when grabbed +// This can be run from an interface and the flashlight will get deleted from the domain when quitting +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("https://hifi-public.s3.amazonaws.com/scripts/utilities.js"); + + +var scriptURL = "https://hifi-public.s3.amazonaws.com/scripts/toys/flashlight/flashlight.js?"+randInt(0,1000); + +var modelURL = "https://hifi-public.s3.amazonaws.com/models/props/flashlight.fbx"; + + +var center = Vec3.sum(Vec3.sum(MyAvatar.position, {x: 0, y: 0.5, z: 0}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); + +var flashlight = Entities.addEntity({ + type: "Model", + modelURL: modelURL, + position: center, + dimensions: { + x: 0.04, + y: 0.15, + z: 0.04 + }, + collisionsWillMove: true, + shapeType: 'box', + script: scriptURL +}); + + +function cleanup() { + Entities.deleteEntity(flashlight); +} + + +Script.scriptEnding.connect(cleanup); \ No newline at end of file diff --git a/examples/toys/flashlight/flashlight.js b/examples/toys/flashlight/flashlight.js new file mode 100644 index 0000000000..e0106dbd49 --- /dev/null +++ b/examples/toys/flashlight/flashlight.js @@ -0,0 +1,154 @@ +// +// flashligh.js +// examples/entityScripts +// +// Created by Sam Gateau on 9/9/15. +// Copyright 2015 High Fidelity, Inc. +// +// This is a toy script that can be added to the Flashlight model entity: +// "https://hifi-public.s3.amazonaws.com/models/props/flashlight.fbx" +// that creates a spotlight attached with the flashlight model while the entity is grabbed +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + + function debugPrint(message) { + // print(message); + } + + Script.include("../../libraries/utils.js"); + + var _this; + + // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember + // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) + Flashlight = function() { + _this = this; + _this._hasSpotlight = false; + _this._spotlight = null; + }; + + // These constants define the Spotlight position and orientation relative to the model + var MODEL_LIGHT_POSITION = {x: 0, y: 0, z: 0}; + var MODEL_LIGHT_ROTATION = Quat.angleAxis (-90, {x: 1, y: 0, z: 0}); + + // Evaluate the world light entity position and orientation from the model ones + function evalLightWorldTransform(modelPos, modelRot) { + return {p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, MODEL_LIGHT_POSITION)), + q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION)}; + }; + + Flashlight.prototype = { + + // update() will be called regulary, because we've hooked the update signal in our preload() function + // we will check out userData for the grabData. In the case of the hydraGrab script, it will tell us + // if we're currently being grabbed and if the person grabbing us is the current interfaces avatar. + // we will watch this for state changes and print out if we're being grabbed or released when it changes. + update: function() { + var GRAB_USER_DATA_KEY = "grabKey"; + + // because the update() signal doesn't have a valid this, we need to use our memorized _this to access our entityID + var entityID = _this.entityID; + + // we want to assume that if there is no grab data, then we are not being grabbed + var defaultGrabData = { activated: false, avatarId: null }; + + // this handy function getEntityCustomData() is available in utils.js and it will return just the specific section + // of user data we asked for. If it's not available it returns our default data. + var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, defaultGrabData); + + + // if the grabData says we're being grabbed, and the owner ID is our session, then we are being grabbed by this interface + if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { + + // remember we're being grabbed so we can detect being released + _this.beingGrabbed = true; + + var modelProperties = Entities.getEntityProperties(entityID); + var lightTransform = evalLightWorldTransform(modelProperties.position, modelProperties.rotation); + + // Create the spot light driven by this model if we don;t have one yet + // Or make sure to keep it's position in sync + if (!_this._hasSpotlight) { + + _this._spotlight = Entities.addEntity({ + type: "Light", + position: lightTransform.p, + rotation: lightTransform.q, + isSpotlight: true, + dimensions: { x: 2, y: 2, z: 20 }, + color: { red: 255, green: 255, blue: 255 }, + intensity: 2, + exponent: 0.3, + cutoff: 20 + }); + _this._hasSpotlight = true; + + _this._startModelPosition = modelProperties.position; + _this._startModelRotation = modelProperties.rotation; + + debugPrint("Flashlight:: creating a spotlight"); + } else { + // Updating the spotlight + Entities.editEntity(_this._spotlight, {position: lightTransform.p, rotation: lightTransform.q}); + + debugPrint("Flashlight:: updating the spotlight"); + } + + debugPrint("I'm being grabbed..."); + + } else if (_this.beingGrabbed) { + + if (_this._hasSpotlight) { + Entities.deleteEntity(_this._spotlight); + debugPrint("Destroying flashlight spotlight..."); + } + _this._hasSpotlight = false; + _this._spotlight = null; + + // Reset model to initial position + Entities.editEntity(_this.entityID, {position: _this._startModelPosition, rotation: _this._startModelRotation, + velocity: {x: 0, y: 0, z: 0}, angularVelocity: {x: 0, y: 0, z: 0}}); + + // if we are not being grabbed, and we previously were, then we were just released, remember that + // and print out a message + _this.beingGrabbed = false; + debugPrint("I'm was released..."); + } + }, + + // preload() will be called when the entity has become visible (or known) to the interface + // it gives us a chance to set our local JavaScript object up. In this case it means: + // * remembering our entityID, so we can access it in cases where we're called without an entityID + // * connecting to the update signal so we can check our grabbed state + preload: function(entityID) { + _this.entityID = entityID; + + var modelProperties = Entities.getEntityProperties(entityID); + _this._startModelPosition = modelProperties.position; + _this._startModelRotation = modelProperties.rotation; + + Script.update.connect(this.update); + }, + + // unload() will be called when our entity is no longer available. It may be because we were deleted, + // or because we've left the domain or quit the application. In all cases we want to unhook our connection + // to the update signal + unload: function(entityID) { + + if (_this._hasSpotlight) { + Entities.deleteEntity(_this._spotlight); + } + _this._hasSpotlight = false; + _this._spotlight = null; + + Script.update.disconnect(this.update); + }, + }; + + // entity scripts always need to return a newly constructed object of our type + return new Flashlight(); +}) diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 878d784bdf..c3829c7be2 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -52,6 +52,11 @@ Item { font.pixelSize: root.fontSize text: "Simrate: " + root.simrate } + Text { + color: root.fontColor; + font.pixelSize: root.fontSize + text: "Avatar Simrate: " + root.avatarSimrate + } Text { color: root.fontColor; font.pixelSize: root.fontSize diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 08a8af89f0..793a1034f4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -786,6 +786,14 @@ void Application::aboutToQuit() { } void Application::cleanupBeforeQuit() { + // Terminate third party processes so that they're not left running in the event of a subsequent shutdown crash +#ifdef HAVE_DDE + DependencyManager::destroy(); +#endif +#ifdef HAVE_IVIEWHMD + DependencyManager::destroy(); +#endif + if (_keyboardFocusHighlightID > 0) { getOverlays().deleteOverlay(_keyboardFocusHighlightID); _keyboardFocusHighlightID = -1; @@ -802,6 +810,7 @@ void Application::cleanupBeforeQuit() { // first stop all timers directly or by invokeMethod // depending on what thread they run in + _avatarUpdate->terminate(); locationUpdateTimer->stop(); balanceUpdateTimer->stop(); identityPacketTimer->stop(); @@ -833,13 +842,6 @@ void Application::cleanupBeforeQuit() { // destroy the AudioClient so it and its thread have a chance to go down safely DependencyManager::destroy(); - -#ifdef HAVE_DDE - DependencyManager::destroy(); -#endif -#ifdef HAVE_IVIEWHMD - DependencyManager::destroy(); -#endif } void Application::emptyLocalCache() { @@ -1068,7 +1070,7 @@ void Application::paintGL() { // Before anything else, let's sync up the gpuContext with the true glcontext used in case anything happened renderArgs._context->syncCache(); - + if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { auto primaryFbo = DependencyManager::get()->getPrimaryFramebufferDepthColor(); @@ -1101,6 +1103,7 @@ void Application::paintGL() { _applicationOverlay.renderOverlay(&renderArgs); } + _myAvatar->startCapture(); if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON || _myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, _myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN); Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !(_myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN)); @@ -1150,7 +1153,7 @@ void Application::paintGL() { if (!isHMDMode()) { _myCamera.update(1.0f / _fps); } - + _myAvatar->endCapture(); // Primary rendering pass auto framebufferCache = DependencyManager::get(); @@ -2172,6 +2175,18 @@ float Application::getAverageSimsPerSecond() { } return _simsPerSecondReport; } +void Application::setAvatarSimrateSample(float sample) { + _avatarSimsPerSecond.updateAverage(sample); +} +float Application::getAvatarSimrate() { + uint64_t now = usecTimestampNow(); + + if (now - _lastAvatarSimsPerSecondUpdate > USECS_PER_SECOND) { + _avatarSimsPerSecondReport = _avatarSimsPerSecond.getAverage(); + _lastAvatarSimsPerSecondUpdate = now; + } + return _avatarSimsPerSecondReport; +} void Application::setLowVelocityFilter(bool lowVelocityFilter) { InputDevice::setLowVelocityFilter(lowVelocityFilter); @@ -2457,8 +2472,20 @@ void Application::init() { // Make sure any new sounds are loaded as soon as know about them. connect(tree, &EntityTree::newCollisionSoundURL, DependencyManager::get().data(), &SoundCache::getSound); connect(_myAvatar, &MyAvatar::newCollisionSoundURL, DependencyManager::get().data(), &SoundCache::getSound); + + setAvatarUpdateThreading(Menu::getInstance()->isOptionChecked(MenuOption::EnableAvatarUpdateThreading)); } +void Application::setAvatarUpdateThreading(bool isThreaded) { + if (_avatarUpdate) { + getMyAvatar()->destroyAnimGraph(); + _avatarUpdate->terminate(); + } + _avatarUpdate = new AvatarUpdate(); + _avatarUpdate->initialize(isThreaded); +} + + void Application::closeMirrorView() { if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { Menu::getInstance()->triggerOption(MenuOption::Mirror); @@ -2535,18 +2562,19 @@ void Application::updateMyAvatarLookAtPosition() { auto eyeTracker = DependencyManager::get(); bool isLookingAtSomeone = false; + bool isHMD = _avatarUpdate->isHMDMode(); glm::vec3 lookAtSpot; if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { // When I am in mirror mode, just look right at the camera (myself); don't switch gaze points because when physically // looking in a mirror one's eyes appear steady. - if (!isHMDMode()) { + if (!isHMD) { lookAtSpot = _myCamera.getPosition(); } else { lookAtSpot = _myCamera.getPosition() + transformPoint(_myAvatar->getSensorToWorldMatrix(), extractTranslation(getHMDSensorPose())); } - } else if (eyeTracker->isTracking() && (isHMDMode() || eyeTracker->isSimulating())) { + } else if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { // Look at the point that the user is looking at. - if (isHMDMode()) { + if (isHMD) { glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); glm::quat hmdRotation = glm::quat_cast(headPose); lookAtSpot = _myCamera.getPosition() + @@ -2589,8 +2617,8 @@ void Application::updateMyAvatarLookAtPosition() { } } else { // I am not looking at anyone else, so just look forward - if (isHMDMode()) { - glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); + if (isHMD) { + glm::mat4 headPose = _avatarUpdate->getHeadPose() ; glm::quat headRotation = glm::quat_cast(headPose); lookAtSpot = _myCamera.getPosition() + _myAvatar->getOrientation() * (headRotation * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); @@ -2817,9 +2845,6 @@ void Application::update(float deltaTime) { updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... - //loop through all the other avatars and simulate them... - DependencyManager::get()->updateOtherAvatars(deltaTime); - updateCamera(deltaTime); // handle various camera tweaks like off axis projection updateDialogs(deltaTime); // update various stats dialogs if present updateCursor(deltaTime); // Handle cursor updates @@ -2891,12 +2916,7 @@ void Application::update(float deltaTime) { _overlays.update(deltaTime); } - { - PerformanceTimer perfTimer("myAvatar"); - updateMyAvatarLookAtPosition(); - // Sample hardware, update view frustum if needed, and send avatar data to mixer/nodes - DependencyManager::get()->updateMyAvatar(deltaTime); - } + _avatarUpdate->synchronousProcess(); { PerformanceTimer perfTimer("emitSimulating"); @@ -3477,7 +3497,10 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se // FIXME: This preRender call is temporary until we create a separate render::scene for the mirror rendering. // Then we can move this logic into the Avatar::simulate call. + _myAvatar->startRender(); _myAvatar->preRender(renderArgs); + _myAvatar->endRender(); + activeRenderingThread = QThread::currentThread(); PROFILE_RANGE(__FUNCTION__); @@ -3591,7 +3614,9 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se _renderEngine->setRenderContext(renderContext); // Before the deferred pass, let's try to use the render engine + _myAvatar->startRenderRun(); _renderEngine->run(); + _myAvatar->endRenderRun(); auto engineRC = _renderEngine->getRenderContext(); sceneInterface->setEngineFeedOpaqueItems(engineRC->_numFeedOpaqueItems); @@ -4761,9 +4786,11 @@ void Application::updateDisplayMode() { qDebug() << "Deferring plugin switch until out of painting"; // Have the old plugin stop requesting renders oldDisplayPlugin->stop(); - QCoreApplication::postEvent(this, new LambdaEvent([this] { + QTimer* timer = new QTimer(); + timer->singleShot(500, [this, timer] { + timer->deleteLater(); updateDisplayMode(); - })); + }); return; } diff --git a/interface/src/Application.h b/interface/src/Application.h index 81fd8415ed..6056323aa9 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -48,6 +48,7 @@ #include "Menu.h" #include "Physics.h" #include "Stars.h" +#include "avatar/AvatarUpdate.h" #include "avatar/Avatar.h" #include "avatar/MyAvatar.h" #include "scripting/ControllerScriptingInterface.h" @@ -335,6 +336,12 @@ public: const QRect& getMirrorViewRect() const { return _mirrorViewRect; } + void updateMyAvatarLookAtPosition(); + AvatarUpdate* getAvatarUpdater() { return _avatarUpdate; } + MyAvatar* getMyAvatar() { return _myAvatar; } + float getAvatarSimrate(); + void setAvatarSimrateSample(float sample); + float getAverageSimsPerSecond(); signals: @@ -405,6 +412,7 @@ public slots: void openUrl(const QUrl& url); void updateMyAvatarTransform(); + void setAvatarUpdateThreading(bool isThreaded); void domainSettingsReceived(const QJsonObject& domainSettingsObject); @@ -482,7 +490,6 @@ private: // Various helper functions called during update() void updateLOD(); void updateMouseRay(); - void updateMyAvatarLookAtPosition(); void updateThreads(float deltaTime); void updateCamera(float deltaTime); void updateDialogs(float deltaTime); @@ -551,6 +558,10 @@ private: KeyboardMouseDevice* _keyboardMouseDevice{ nullptr }; // Default input device, the good old keyboard mouse and maybe touchpad MyAvatar* _myAvatar; // TODO: move this and relevant code to AvatarManager (or MyAvatar as the case may be) + AvatarUpdate* _avatarUpdate {nullptr}; + SimpleMovingAverage _avatarSimsPerSecond {10}; + int _avatarSimsPerSecondReport {0}; + quint64 _lastAvatarSimsPerSecondUpdate {0}; Camera _myCamera; // My view onto the world Camera _mirrorCamera; // Cammera for mirror view QRect _mirrorViewRect; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 6e3480b2cf..d54f69b56d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -447,6 +447,8 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderFocusIndicator, 0, false); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowWhosLookingAtMe, 0, false); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableAvatarUpdateThreading, 0, false, + qApp, SLOT(setAvatarUpdateThreading(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableRigAnimations, 0, true, avatar, SLOT(setEnableRigAnimations(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableAnimGraph, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 0c416f4cc2..7d63c64446 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -188,6 +188,7 @@ namespace MenuOption { const QString EchoServerAudio = "Echo Server Audio"; const QString EditEntitiesHelp = "Edit Entities Help..."; const QString Enable3DTVMode = "Enable 3DTV Mode"; + const QString EnableAvatarUpdateThreading = "Enable Avatar Update Threading"; const QString EnableAnimGraph = "Enable Anim Graph"; const QString EnableCharacterController = "Enable avatar collisions"; const QString EnableRigAnimations = "Enable Rig Animations"; diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 55699bccbe..086178ec6c 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -316,6 +316,7 @@ void Avatar::removeFromScene(AvatarSharedPointer self, std::shared_ptrupdate(); } @@ -390,6 +391,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { } if (frustum->sphereInFrustum(getPosition(), boundingRadius) == ViewFrustum::OUTSIDE) { + endRender(); return; } @@ -540,6 +542,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { if (!isMyAvatar() || cameraMode != CAMERA_MODE_FIRST_PERSON) { renderDisplayName(batch, *renderArgs->_viewFrustum, renderArgs->_viewport); } + endRender(); } glm::quat Avatar::computeRotationFromBodyToWorldUp(float proportion) const { @@ -1019,6 +1022,7 @@ void Avatar::setBillboard(const QByteArray& billboard) { } int Avatar::parseDataFromBuffer(const QByteArray& buffer) { + startUpdate(); if (!_initialized) { // now that we have data for this Avatar we are go for init init(); @@ -1034,6 +1038,7 @@ int Avatar::parseDataFromBuffer(const QByteArray& buffer) { if (_moving && _motionState) { _motionState->addDirtyFlags(EntityItem::DIRTY_POSITION); } + endUpdate(); return bytesRead; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index a7cece2b45..6cfbf939a2 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -156,6 +156,7 @@ public: void scaleVectorRelativeToPosition(glm::vec3 &positionToScale) const; void slamPosition(const glm::vec3& position); + virtual void updateAttitude() { _skeletonModel.updateAttitude(); } // Call this when updating Avatar position with a delta. This will allow us to // _accurately_ measure position changes and compute the resulting velocity diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1644f22b09..67668a549d 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -129,7 +129,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { _avatarFades.push_back(avatarIterator.value()); avatarIterator = _avatarHash.erase(avatarIterator); } else { + avatar->startUpdate(); avatar->simulate(deltaTime); + avatar->endUpdate(); ++avatarIterator; } } @@ -148,6 +150,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { render::PendingChanges pendingChanges; while (fadingIterator != _avatarFades.end()) { auto avatar = std::static_pointer_cast(*fadingIterator); + avatar->startUpdate(); avatar->setTargetScale(avatar->getScale() * SHRINK_RATE, true); if (avatar->getTargetScale() < MIN_FADE_SCALE) { avatar->removeFromScene(*fadingIterator, scene, pendingChanges); @@ -156,6 +159,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { avatar->simulate(deltaTime); ++fadingIterator; } + avatar->endUpdate(); } scene->enqueuePendingChanges(pendingChanges); } diff --git a/interface/src/avatar/AvatarUpdate.cpp b/interface/src/avatar/AvatarUpdate.cpp new file mode 100644 index 0000000000..a4391172a7 --- /dev/null +++ b/interface/src/avatar/AvatarUpdate.cpp @@ -0,0 +1,75 @@ +// +// AvatarUpdate.cpp +// interface/src/avatar +// +// Created by Howard Stearns on 8/18/15. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// + +#include +#include "Application.h" +#include "AvatarManager.h" +#include "AvatarUpdate.h" +#include +#include "InterfaceLogging.h" + +AvatarUpdate::AvatarUpdate() : GenericThread(), _lastAvatarUpdate(0) { + setObjectName("Avatar Update"); // GenericThread::initialize uses this to set the thread name. + Settings settings; + const int DEFAULT_TARGET_AVATAR_SIMRATE = 60; + _targetInterval = USECS_PER_SECOND / settings.value("AvatarUpdateTargetSimrate", DEFAULT_TARGET_AVATAR_SIMRATE).toInt(); +} +// We could have the constructor call initialize(), but GenericThread::initialize can take parameters. +// Keeping it separately called by the client allows that client to pass those without our +// constructor needing to know about them. + +void AvatarUpdate::synchronousProcess() { + + // Keep our own updated value, so that our asynchronous code can consult it. + _isHMDMode = Application::getInstance()->isHMDMode(); + _headPose = Application::getInstance()->getActiveDisplayPlugin()->getHeadPose(); + + if (_updateBillboard) { + Application::getInstance()->getMyAvatar()->doUpdateBillboard(); + } + + if (!isThreaded()) { + process(); + } +} + +bool AvatarUpdate::process() { + PerformanceTimer perfTimer("AvatarUpdate"); + quint64 start = usecTimestampNow(); + quint64 deltaMicroseconds = start - _lastAvatarUpdate; + _lastAvatarUpdate = start; + float deltaSeconds = (float) deltaMicroseconds / (float) USECS_PER_SECOND; + Application::getInstance()->setAvatarSimrateSample(1.0f / deltaSeconds); + + QSharedPointer manager = DependencyManager::get(); + MyAvatar* myAvatar = manager->getMyAvatar(); + + //loop through all the other avatars and simulate them... + //gets current lookat data, removes missing avatars, etc. + manager->updateOtherAvatars(deltaSeconds); + + myAvatar->startUpdate(); + Application::getInstance()->updateMyAvatarLookAtPosition(); + // Sample hardware, update view frustum if needed, and send avatar data to mixer/nodes + manager->updateMyAvatar(deltaSeconds); + myAvatar->endUpdate(); + + if (!isThreaded()) { + return true; + } + int elapsed = (usecTimestampNow() - start); + int usecToSleep = _targetInterval - elapsed; + if (usecToSleep < 0) { + usecToSleep = 1; // always yield + } + usleep(usecToSleep); + return true; +} diff --git a/interface/src/avatar/AvatarUpdate.h b/interface/src/avatar/AvatarUpdate.h new file mode 100644 index 0000000000..27c88b6617 --- /dev/null +++ b/interface/src/avatar/AvatarUpdate.h @@ -0,0 +1,43 @@ +// +// AvatarUpdate.h +// interface/src/avatar +// +// Created by Howard Stearns on 8/18/15. +/// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// + +#ifndef __hifi__AvatarUpdate__ +#define __hifi__AvatarUpdate__ + +#include +#include + +// Home for the avatarUpdate operations (e.g., whether on a separate thread, pipelined in various ways, etc.) +// This might get folded into AvatarManager. +class AvatarUpdate : public GenericThread { + Q_OBJECT +public: + AvatarUpdate(); + void synchronousProcess(); + void setRequestBillboardUpdate(bool needsUpdate) { _updateBillboard = needsUpdate; } + +private: + virtual bool process(); // No reason for other classes to invoke this. + quint64 _lastAvatarUpdate; // microsoeconds + quint64 _targetInterval; // microseconds + bool _updateBillboard; + + // Goes away if Application::getActiveDisplayPlugin() and friends are made thread safe: +public: + bool isHMDMode() { return _isHMDMode; } + glm::mat4 getHeadPose() { return _headPose; } +private: + bool _isHMDMode; + glm::mat4 _headPose; +}; + + +#endif /* defined(__hifi__AvatarUpdate__) */ diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 3806dd6edc..09ac893e65 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -389,7 +389,7 @@ glm::quat Head::getCameraOrientation() const { // to change the driving direction while in Oculus mode. It is used to support driving toward where you're // head is looking. Note that in oculus mode, your actual camera view and where your head is looking is not // always the same. - if (qApp->isHMDMode()) { + if (qApp->getAvatarUpdater()->isHMDMode()) { MyAvatar* myAvatar = dynamic_cast(_owningAvatar); if (myAvatar && myAvatar->getStandingHMDSensorMode()) { return glm::quat_cast(myAvatar->getSensorToWorldMatrix()) * myAvatar->getHMDSensorOrientation(); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c792aec0c8..bafca2844d 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -266,8 +266,7 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { if (getStandingHMDSensorMode()) { // set the body position/orientation to reflect motion due to the head. auto worldMat = _sensorToWorldMatrix * _bodySensorMatrix; - setPosition(extractTranslation(worldMat)); - setOrientation(glm::quat_cast(worldMat)); + nextAttitude(extractTranslation(worldMat), glm::quat_cast(worldMat)); } } @@ -285,7 +284,7 @@ void MyAvatar::updateSensorToWorldMatrix() { void MyAvatar::updateFromTrackers(float deltaTime) { glm::vec3 estimatedPosition, estimatedRotation; - bool inHmd = qApp->isHMDMode(); + bool inHmd = qApp->getAvatarUpdater()->isHMDMode(); if (isPlaying() && inHmd) { return; @@ -705,19 +704,46 @@ float loadSetting(QSettings& settings, const char* name, float defaultValue) { return value; } +// Resource loading is not yet thread safe. If an animation is not loaded when requested by other than tha main thread, +// we block in AnimationHandle::setURL => AnimationCache::getAnimation. +// Meanwhile, the main thread will also eventually lock as it tries to render us. +// If we demand the animation from the update thread while we're locked, we'll deadlock. +// Until we untangle this, code puts the updates back on the main thread temporarilly and starts all the loading. +void MyAvatar::safelyLoadAnimations() { + qApp->setAvatarUpdateThreading(false); + _rig->addAnimationByRole("idle"); + _rig->addAnimationByRole("walk"); + _rig->addAnimationByRole("backup"); + _rig->addAnimationByRole("leftTurn"); + _rig->addAnimationByRole("rightTurn"); + _rig->addAnimationByRole("leftStrafe"); + _rig->addAnimationByRole("rightStrafe"); +} + void MyAvatar::setEnableRigAnimations(bool isEnabled) { + if (isEnabled) { + safelyLoadAnimations(); + } _rig->setEnableRig(isEnabled); if (!isEnabled) { _rig->deleteAnimations(); + } else if (Menu::getInstance()->isOptionChecked(MenuOption::EnableAvatarUpdateThreading)) { + qApp->setAvatarUpdateThreading(true); } } void MyAvatar::setEnableAnimGraph(bool isEnabled) { + if (isEnabled) { + safelyLoadAnimations(); + } _rig->setEnableAnimGraph(isEnabled); if (isEnabled) { if (_skeletonModel.readyToAddToScene()) { initAnimGraph(); } + if (Menu::getInstance()->isOptionChecked(MenuOption::EnableAvatarUpdateThreading)) { + qApp->setAvatarUpdateThreading(true); + } } else { destroyAnimGraph(); } @@ -804,6 +830,9 @@ void MyAvatar::loadData() { settings.endGroup(); _rig->setEnableRig(Menu::getInstance()->isOptionChecked(MenuOption::EnableRigAnimations)); + setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible)); + setEnableDebugDrawBindPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawBindPose)); + setEnableDebugDrawAnimPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawAnimPose)); } void MyAvatar::saveAttachmentData(const AttachmentData& attachment) const { @@ -1248,7 +1277,7 @@ void MyAvatar::initAnimGraph() { // or run a local web-server // python -m SimpleHTTPServer& auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/e58e0a24cc341ad5d060/raw/2a994bef7726ce8e9efcee7622b8b1a1b6b67490/ik-avatar.json"); - _skeletonModel.initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry()); + _rig->initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry()); } void MyAvatar::destroyAnimGraph() { @@ -1346,7 +1375,7 @@ void MyAvatar::updateOrientation(float deltaTime) { setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, _bodyYawDelta * deltaTime, 0.0f)))); - if (qApp->isHMDMode()) { + if (qApp->getAvatarUpdater()->isHMDMode()) { glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation(); glm::quat bodyOrientation = getWorldBodyOrientation(); glm::quat localOrientation = glm::inverse(bodyOrientation) * orientation; @@ -1567,6 +1596,7 @@ bool findAvatarAvatarPenetration(const glm::vec3 positionA, float radiusA, float } void MyAvatar::maybeUpdateBillboard() { + qApp->getAvatarUpdater()->setRequestBillboardUpdate(false); if (_billboardValid || !(_skeletonModel.isLoadedWithTextures() && getHead()->getFaceModel().isLoadedWithTextures())) { return; } @@ -1575,7 +1605,9 @@ void MyAvatar::maybeUpdateBillboard() { return; } } - + qApp->getAvatarUpdater()->setRequestBillboardUpdate(true); +} +void MyAvatar::doUpdateBillboard() { RenderArgs renderArgs(qApp->getGPUContext()); QImage image = qApp->renderAvatarBillboard(&renderArgs); _billboard.clear(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e743dea860..bb3c6385f9 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -19,7 +19,6 @@ #include "Avatar.h" class ModelItemID; -class AnimNode; enum eyeContactTarget { LEFT_EYE, @@ -151,6 +150,8 @@ public: static const float ZOOM_DEFAULT; bool getStandingHMDSensorMode() const { return _standingHMDSensorMode; } + void doUpdateBillboard(); + void destroyAnimGraph(); public slots: void increaseSize(); @@ -290,7 +291,7 @@ private: void maybeUpdateBillboard(); void initHeadBones(); void initAnimGraph(); - void destroyAnimGraph(); + void safelyLoadAnimations(); // Avatar Preferences QUrl _fullAvatarURLFromPreferences; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 5ffd0f8dec..8a05b15c4c 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -103,13 +103,15 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); } Model::updateRig(deltaTime, parentTransform); + Head* head = _owningAvatar->getHead(); if (_owningAvatar->isMyAvatar()) { + MyAvatar* myAvatar = static_cast(_owningAvatar); const FBXGeometry& geometry = _geometry->getFBXGeometry(); - Head* head = _owningAvatar->getHead(); Rig::HeadParameters params; params.modelRotation = getRotation(); params.modelTranslation = getTranslation(); + params.enableLean = qApp->getAvatarUpdater()->isHMDMode() && !myAvatar->getStandingHMDSensorMode(); params.leanSideways = head->getFinalLeanSideways(); params.leanForward = head->getFinalLeanForward(); params.torsoTwist = head->getTorsoTwist(); @@ -133,7 +135,6 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { // However, in the !isLookingAtMe case, the eyes aren't rotating the way they should right now. // We will revisit that as priorities allow, and particularly after the new rig/animation/joints. const FBXGeometry& geometry = _geometry->getFBXGeometry(); - Head* head = _owningAvatar->getHead(); // If the head is not positioned, updateEyeJoints won't get the math right glm::quat headOrientation; _rig->getJointRotation(geometry.headJointIndex, headOrientation); @@ -147,13 +148,17 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { } } -// Called by Avatar::simulate after it has set the joint states (fullUpdate true if changed), -// but just before head has been simulated. -void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { +void SkeletonModel::updateAttitude() { setTranslation(_owningAvatar->getSkeletonPosition()); static const glm::quat refOrientation = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); setRotation(_owningAvatar->getOrientation() * refOrientation); setScale(glm::vec3(1.0f, 1.0f, 1.0f) * _owningAvatar->getScale()); +} + +// Called by Avatar::simulate after it has set the joint states (fullUpdate true if changed), +// but just before head has been simulated. +void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { + updateAttitude(); setBlendshapeCoefficients(_owningAvatar->getHead()->getBlendshapeCoefficients()); Model::simulate(deltaTime, fullUpdate); @@ -544,14 +549,24 @@ void SkeletonModel::computeBoundingShape() { totalExtents.addPoint(glm::vec3(0.0f)); int numStates = _rig->getJointStateCount(); for (int i = 0; i < numStates; i++) { - // compute the default transform of this joint const JointState& state = _rig->getJointState(i); - // Each joint contributes a sphere at its position - glm::vec3 axis(state.getBoneRadius()); - glm::vec3 jointPosition = state.getPosition(); - totalExtents.addPoint(jointPosition + axis); - totalExtents.addPoint(jointPosition - axis); + const glm::mat4& jointTransform = state.getTransform(); + float scale = extractUniformScale(jointTransform); + + // Each joint contributes a capsule defined by FBXJoint.shapeInfo. + // For totalExtents we use the capsule endpoints expanded by the radius. + const FBXJointShapeInfo& shapeInfo = geometry.joints.at(i).shapeInfo; + for (int j = 0; j < shapeInfo.points.size(); ++j) { + glm::vec3 transformedPoint = extractTranslation(jointTransform * glm::translate(shapeInfo.points[j])); + vec3 radius(scale * shapeInfo.radius); + totalExtents.addPoint(transformedPoint + radius); + totalExtents.addPoint(transformedPoint - radius); + } + // HACK so that default legless robot doesn't knuckle-drag + if (shapeInfo.points.size() == 0 && (state.getName() == "LeftFoot" || state.getName() == "RightFoot")) { + totalExtents.addPoint(extractTranslation(jointTransform)); + } } // compute bounding shape parameters @@ -607,6 +622,3 @@ bool SkeletonModel::hasSkeleton() { void SkeletonModel::onInvalidate() { } -void SkeletonModel::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) { - _rig->initAnimGraph(url, fbxGeometry); -} diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 389b1e7d36..e2c3ab8f83 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -31,6 +31,7 @@ public: virtual void simulate(float deltaTime, bool fullUpdate = true); virtual void updateRig(float deltaTime, glm::mat4 parentTransform); + void updateAttitude(); void renderIKConstraints(gpu::Batch& batch); @@ -105,8 +106,6 @@ public: virtual void onInvalidate() override; - void initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry); - signals: void skeletonLoaded(); diff --git a/interface/src/devices/DdeFaceTracker.cpp b/interface/src/devices/DdeFaceTracker.cpp index 396539c3cf..2ddd8d9d04 100644 --- a/interface/src/devices/DdeFaceTracker.cpp +++ b/interface/src/devices/DdeFaceTracker.cpp @@ -236,29 +236,25 @@ void DdeFaceTracker::setEnabled(bool enabled) { cancelCalibration(); } - // isOpen() does not work as one might expect on QUdpSocket; don't test isOpen() before closing socket. _udpSocket.close(); - if (enabled) { - _udpSocket.bind(_host, _serverPort); - } + // Terminate any existing DDE process, perhaps left running after an Interface crash. + // Do this even if !enabled in case user reset their settings after crash. const char* DDE_EXIT_COMMAND = "exit"; + _udpSocket.bind(_host, _serverPort); + _udpSocket.writeDatagram(DDE_EXIT_COMMAND, DDE_SERVER_ADDR, _controlPort); if (enabled && !_ddeProcess) { - // Terminate any existing DDE process, perhaps left running after an Interface crash - _udpSocket.writeDatagram(DDE_EXIT_COMMAND, DDE_SERVER_ADDR, _controlPort); _ddeStopping = false; - qCDebug(interfaceapp) << "DDE Face Tracker: Starting"; _ddeProcess = new QProcess(qApp); connect(_ddeProcess, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(processFinished(int, QProcess::ExitStatus))); _ddeProcess->start(QCoreApplication::applicationDirPath() + DDE_PROGRAM_PATH, DDE_ARGUMENTS); } - + if (!enabled && _ddeProcess) { _ddeStopping = true; - _udpSocket.writeDatagram(DDE_EXIT_COMMAND, DDE_SERVER_ADDR, _controlPort); qCDebug(interfaceapp) << "DDE Face Tracker: Stopping"; } #endif diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 465b24ff80..8ddb767537 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -115,6 +115,7 @@ void Stats::updateStats() { STAT_UPDATE(serverCount, nodeList->size()); STAT_UPDATE(framerate, (int)qApp->getFps()); STAT_UPDATE(simrate, (int)Application::getInstance()->getAverageSimsPerSecond()); + STAT_UPDATE(avatarSimrate, (int)qApp->getAvatarSimrate()); auto bandwidthRecorder = DependencyManager::get(); STAT_UPDATE(packetInCount, bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 096469a84d..e5c273926b 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -31,6 +31,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, serverCount, 0) STATS_PROPERTY(int, framerate, 0) STATS_PROPERTY(int, simrate, 0) + STATS_PROPERTY(int, avatarSimrate, 0) STATS_PROPERTY(int, avatarCount, 0) STATS_PROPERTY(int, packetInCount, 0) STATS_PROPERTY(int, packetOutCount, 0) @@ -98,6 +99,7 @@ signals: void serverCountChanged(); void framerateChanged(); void simrateChanged(); + void avatarSimrateChanged(); void avatarCountChanged(); void packetInCountChanged(); void packetOutCountChanged(); diff --git a/libraries/animation/src/JointState.cpp b/libraries/animation/src/JointState.cpp index edea3b462d..9597a46726 100644 --- a/libraries/animation/src/JointState.cpp +++ b/libraries/animation/src/JointState.cpp @@ -41,7 +41,6 @@ void JointState::copyState(const JointState& other) { // DO NOT copy _constraint _name = other._name; _isFree = other._isFree; - _boneRadius = other._boneRadius; _parentIndex = other._parentIndex; _defaultRotation = other._defaultRotation; _inverseDefaultRotation = other._inverseDefaultRotation; @@ -58,7 +57,6 @@ JointState::JointState(const FBXJoint& joint) { _rotationInConstrainedFrame = joint.rotation; _name = joint.name; _isFree = joint.isFree; - _boneRadius = joint.boneRadius; _parentIndex = joint.parentIndex; _translation = joint.translation; _defaultRotation = joint.rotation; diff --git a/libraries/animation/src/JointState.h b/libraries/animation/src/JointState.h index 4f45661eb2..07ed010104 100644 --- a/libraries/animation/src/JointState.h +++ b/libraries/animation/src/JointState.h @@ -118,7 +118,6 @@ public: const glm::quat& getDefaultRotation() const { return _defaultRotation; } const glm::quat& getInverseDefaultRotation() const { return _inverseDefaultRotation; } const QString& getName() const { return _name; } - float getBoneRadius() const { return _boneRadius; } bool getIsFree() const { return _isFree; } float getAnimationPriority() const { return _animationPriority; } void setAnimationPriority(float priority) { _animationPriority = priority; } @@ -149,7 +148,6 @@ private: QString _name; int _parentIndex; bool _isFree; - float _boneRadius; glm::vec3 _rotationMin; glm::vec3 _rotationMax; glm::quat _preRotation; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index cc5ceac31f..e0a2b31622 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -477,7 +477,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos } if (glm::length(localVel) > moveThresh) { - if (fabs(forwardSpeed) > 0.5f * fabs(lateralSpeed)) { + if (fabsf(forwardSpeed) > 0.5f * fabsf(lateralSpeed)) { if (forwardSpeed > 0.0f) { // forward _animVars.set("isMovingForward", true); @@ -501,7 +501,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos } _state = RigRole::Move; } else { - if (fabs(turningSpeed) > turnThresh) { + if (fabsf(turningSpeed) > turnThresh) { if (turningSpeed > 0.0f) { // turning right _animVars.set("isTurningRight", true); @@ -928,13 +928,6 @@ void Rig::updateVisibleJointStates() { } } -void Rig::setJointTransform(int jointIndex, glm::mat4 newTransform) { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { - return; - } - _jointStates[jointIndex].setTransform(newTransform); -} - void Rig::setJointVisibleTransform(int jointIndex, glm::mat4 newTransform) { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return; @@ -957,7 +950,9 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { } void Rig::updateFromHeadParameters(const HeadParameters& params) { - updateLeanJoint(params.leanJointIndex, params.leanSideways, params.leanForward, params.torsoTwist); + if (params.enableLean) { + updateLeanJoint(params.leanJointIndex, params.leanSideways, params.leanForward, params.torsoTwist); + } updateNeckJoint(params.neckJointIndex, params.localHeadOrientation, params.leanSideways, params.leanForward, params.torsoTwist); updateEyeJoints(params.leftEyeJointIndex, params.rightEyeJointIndex, params.modelTranslation, params.modelRotation, params.worldHeadOrientation, params.eyeLookAt, params.eyeSaccade); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 254d708eaa..0a3ebad5d2 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -56,6 +56,7 @@ public: float leanSideways = 0.0f; // degrees float leanForward = 0.0f; // degrees float torsoTwist = 0.0f; // degrees + bool enableLean = false; glm::quat modelRotation = glm::quat(); glm::quat localHeadOrientation = glm::quat(); glm::quat worldHeadOrientation = glm::quat(); @@ -132,7 +133,6 @@ public: glm::vec3 translation, glm::quat rotation) const; bool getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& result, glm::quat rotation) const; glm::mat4 getJointTransform(int jointIndex) const; - void setJointTransform(int jointIndex, glm::mat4 newTransform); glm::mat4 getJointVisibleTransform(int jointIndex) const; void setJointVisibleTransform(int jointIndex, glm::mat4 newTransform); // Start or stop animations as needed. diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 9430c73091..e1bb6ae8a0 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -110,6 +110,53 @@ void AvatarData::setOrientation(const glm::quat& orientation, bool overideRefere } } +// There are a number of possible strategies for this set of tools through endRender, below. +void AvatarData::nextAttitude(glm::vec3 position, glm::quat orientation) { + avatarLock.lock(); + setPosition(position, true); + setOrientation(orientation, true); + avatarLock.unlock(); +} +void AvatarData::startCapture() { + avatarLock.lock(); + assert(_nextAllowed); + _nextAllowed = false; + _nextPosition = getPosition(); + _nextOrientation = getOrientation(); +} +void AvatarData::endCapture() { + avatarLock.unlock(); +} +void AvatarData::startUpdate() { + avatarLock.lock(); +} +void AvatarData::endUpdate() { + avatarLock.unlock(); +} +void AvatarData::startRenderRun() { + // I'd like to get rid of this and just (un)lock at (end-)startRender. + // But somehow that causes judder in rotations. + avatarLock.lock(); +} +void AvatarData::endRenderRun() { + avatarLock.unlock(); +} +void AvatarData::startRender() { + glm::vec3 pos = getPosition(); + glm::quat rot = getOrientation(); + setPosition(_nextPosition, true); + setOrientation(_nextOrientation, true); + updateAttitude(); + _nextPosition = pos; + _nextOrientation = rot; +} +void AvatarData::endRender() { + setPosition(_nextPosition, true); + setOrientation(_nextOrientation, true); + updateAttitude(); + _nextAllowed = true; +} + float AvatarData::getTargetScale() const { if (_referential) { _referential->update(); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 7abaf32a71..81d252c622 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -200,6 +200,17 @@ public: glm::quat getOrientation() const; virtual void setOrientation(const glm::quat& orientation, bool overideReferential = false); + void nextAttitude(glm::vec3 position, glm::quat orientation); // Can be safely called at any time. + void startCapture(); // start/end of the period in which the latest values are about to be captured for camera, etc. + void endCapture(); + void startUpdate(); // start/end of update iteration + void endUpdate(); + void startRender(); // start/end of rendering of this object + void startRenderRun(); // start/end of entire scene. + void endRenderRun(); + void endRender(); + virtual void updateAttitude() {} // Tell skeleton mesh about changes + glm::quat getHeadOrientation() const { return _headData->getOrientation(); } void setHeadOrientation(const glm::quat& orientation) { _headData->setOrientation(orientation); } @@ -358,6 +369,10 @@ protected: float _bodyPitch; // degrees float _bodyRoll; // degrees + glm::vec3 _nextPosition {}; + glm::quat _nextOrientation {}; + bool _nextAllowed {true}; + // Body scale float _targetScale; @@ -407,6 +422,8 @@ protected: SimpleMovingAverage _averageBytesReceived; + QMutex avatarLock; // Name is redundant, but it aids searches. + private: static QUrl _defaultFullAvatarModelUrl; // privatize the copy constructor and assignment operator so they cannot be called diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 35390a8e44..5410859340 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1257,21 +1257,7 @@ QString getString(const QVariant& value) { return list.isEmpty() ? value.toString() : list.at(0).toString(); } -class JointShapeInfo { -public: - JointShapeInfo() : numVertices(0), - sumVertexWeights(0.0f), sumWeightedRadii(0.0f), numVertexWeights(0), - boneBegin(0.0f), averageRadius(0.0f) { - } - - // NOTE: the points here are in the "joint frame" which has the "jointEnd" at the origin - int numVertices; // num vertices from contributing meshes - float sumVertexWeights; // sum of all vertex weights - float sumWeightedRadii; // sum of weighted vertices - int numVertexWeights; // num vertices that contributed to sums - glm::vec3 boneBegin; // parent joint location (in joint frame) - float averageRadius; -}; +typedef std::vector ShapeVertices; class AnimationCurve { public: @@ -2282,22 +2268,21 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping joint.postTransform = model.postTransform; joint.rotationMin = model.rotationMin; joint.rotationMax = model.rotationMax; - glm::quat combinedRotation = model.preRotation * model.rotation * model.postRotation; + glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; if (joint.parentIndex == -1) { - joint.transform = geometry.offset * glm::translate(model.translation) * model.preTransform * - glm::mat4_cast(combinedRotation) * model.postTransform; + joint.transform = geometry.offset * glm::translate(joint.translation) * joint.preTransform * + glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation); - joint.distanceToParent = 0.0f; + joint.distanceToParent = 0.0f; } else { const FBXJoint& parentJoint = geometry.joints.at(joint.parentIndex); - joint.transform = parentJoint.transform * glm::translate(model.translation) * - model.preTransform * glm::mat4_cast(combinedRotation) * model.postTransform; + joint.transform = parentJoint.transform * glm::translate(joint.translation) * + joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation) * parentJoint.inverseDefaultRotation; joint.distanceToParent = glm::distance(extractTranslation(parentJoint.transform), extractTranslation(joint.transform)); } - joint.boneRadius = 0.0f; joint.inverseBindRotation = joint.inverseDefaultRotation; joint.name = model.name; @@ -2326,9 +2311,10 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping zCurve.values.isEmpty() ? defaultValues.z : zCurve.values.at(i % zCurve.values.size())))); } } - // for each joint we allocate a JointShapeInfo in which we'll store collision shape info - QVector jointShapeInfos; - jointShapeInfos.resize(geometry.joints.size()); + + // NOTE: shapeVertices are in joint-frame + QVector shapeVertices; + shapeVertices.resize(geometry.joints.size()); // find our special joints geometry.leftEyeJointIndex = modelIDs.indexOf(jointEyeLeftID); @@ -2585,8 +2571,10 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping boneDirection /= boneLength; } } - float radiusScale = extractUniformScale(joint.transform * fbxCluster.inverseBindMatrix); - JointShapeInfo& jointShapeInfo = jointShapeInfos[jointIndex]; + + float clusterScale = extractUniformScale(fbxCluster.inverseBindMatrix); + glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; + ShapeVertices& points = shapeVertices[jointIndex]; float totalWeight = 0.0f; for (int j = 0; j < cluster.indices.size(); j++) { @@ -2595,18 +2583,13 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping totalWeight += weight; for (QMultiHash::const_iterator it = extracted.newIndices.constFind(oldIndex); it != extracted.newIndices.end() && it.key() == oldIndex; it++) { - // expand the bone radius for vertices with at least 1/4 weight + + // remember vertices with at least 1/4 weight const float EXPANSION_WEIGHT_THRESHOLD = 0.25f; if (weight > EXPANSION_WEIGHT_THRESHOLD) { - const glm::vec3& vertex = extracted.mesh.vertices.at(it.value()); - float proj = glm::dot(boneDirection, boneEnd - vertex); - float radiusWeight = (proj < 0.0f || proj > boneLength) ? 0.5f * weight : weight; - - jointShapeInfo.sumVertexWeights += radiusWeight; - jointShapeInfo.sumWeightedRadii += radiusWeight * radiusScale * glm::distance(vertex, boneEnd - boneDirection * proj); - ++jointShapeInfo.numVertexWeights; - - ++jointShapeInfo.numVertices; + // transform to joint-frame and save for later + const glm::mat4 vertexTransform = meshToJoint * glm::translate(extracted.mesh.vertices.at(it.value())); + points.push_back(extractTranslation(vertexTransform) * clusterScale); } // look for an unused slot in the weights vector @@ -2649,54 +2632,16 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping // this is a single-mesh joint int jointIndex = maxJointIndex; FBXJoint& joint = geometry.joints[jointIndex]; - JointShapeInfo& jointShapeInfo = jointShapeInfos[jointIndex]; - glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; - glm::vec3 boneEnd = extractTranslation(transformJointToMesh); - glm::vec3 boneBegin = boneEnd; - - glm::vec3 boneDirection; - float boneLength = 0.0f; - if (joint.parentIndex != -1) { - boneBegin = extractTranslation(inverseModelTransform * geometry.joints[joint.parentIndex].bindTransform); - boneDirection = boneEnd - boneBegin; - boneLength = glm::length(boneDirection); - if (boneLength > EPSILON) { - boneDirection /= boneLength; - } - } - float radiusScale = extractUniformScale(joint.transform * firstFBXCluster.inverseBindMatrix); - - // compute average vertex - glm::vec3 averageVertex(0.0f); + // transform cluster vertices to joint-frame and save for later + float clusterScale = extractUniformScale(firstFBXCluster.inverseBindMatrix); + glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; + ShapeVertices& points = shapeVertices[jointIndex]; foreach (const glm::vec3& vertex, extracted.mesh.vertices) { - float proj = glm::dot(boneDirection, boneEnd - vertex); - float radiusWeight = (proj < 0.0f || proj > boneLength) ? 0.5f : 1.0f; - jointShapeInfo.sumVertexWeights += radiusWeight; - jointShapeInfo.sumWeightedRadii += radiusWeight * radiusScale * glm::distance(vertex, boneEnd - boneDirection * proj); - ++jointShapeInfo.numVertexWeights; - averageVertex += vertex; + const glm::mat4 vertexTransform = meshToJoint * glm::translate(vertex); + points.push_back(extractTranslation(vertexTransform) * clusterScale); } - // compute joint's radius - int numVertices = extracted.mesh.vertices.size(); - jointShapeInfo.numVertices = numVertices; - if (numVertices > 0) { - // compute average radius - averageVertex /= (float)jointShapeInfo.numVertices; - float averageRadius = 0.0f; - foreach (const glm::vec3& vertex, extracted.mesh.vertices) { - averageRadius += glm::distance(vertex, averageVertex); - } - averageRadius *= radiusScale / (float)jointShapeInfo.numVertices; - - // final radius is minimum of average and weighted - float weightedRadius = jointShapeInfo.sumWeightedRadii / jointShapeInfo.sumVertexWeights; - jointShapeInfo.averageRadius = glm::min(weightedRadius, averageRadius); - } - - // clear sumVertexWeights (this flags it as a single-mesh joint for later) - jointShapeInfo.sumVertexWeights = 0.0f; } extracted.mesh.isEye = (maxJointIndex == geometry.leftEyeJointIndex || maxJointIndex == geometry.rightEyeJointIndex); @@ -2721,24 +2666,59 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping glm::vec3 defaultCapsuleAxis(0.0f, 1.0f, 0.0f); for (int i = 0; i < geometry.joints.size(); ++i) { FBXJoint& joint = geometry.joints[i]; - JointShapeInfo& jointShapeInfo = jointShapeInfos[i]; - if (joint.parentIndex == -1) { - jointShapeInfo.boneBegin = glm::vec3(0.0f); + // NOTE: points are in joint-frame + // compute average point + ShapeVertices& points = shapeVertices[i]; + glm::vec3 avgPoint = glm::vec3(0.0f); + for (uint32_t j = 0; j < points.size(); ++j) { + avgPoint += points[j]; + } + avgPoint /= (float)points.size(); + + // compute axis from begin to avgPoint + glm::vec3 begin(0.0f); + glm::vec3 end = avgPoint; + glm::vec3 axis = end - begin; + float axisLength = glm::length(axis); + if (axisLength > EPSILON) { + axis /= axisLength; } else { - const FBXJoint& parentJoint = geometry.joints[joint.parentIndex]; - glm::quat inverseRotation = glm::inverse(extractRotation(joint.transform)); - jointShapeInfo.boneBegin = inverseRotation * (extractTranslation(parentJoint.transform) - extractTranslation(joint.transform)); + axis = glm::vec3(0.0f); } - if (jointShapeInfo.sumVertexWeights > 0.0f) { - // mutiple meshes contributed to the bone radius and now that all - // contributing meshes are done we can finally compute the boneRadius - joint.boneRadius = jointShapeInfo.sumWeightedRadii / jointShapeInfo.sumVertexWeights; - } else { - // single-mesh joint - joint.boneRadius = jointShapeInfo.averageRadius; + // measure average cylindrical radius + float avgRadius = 0.0f; + if (points.size() > 0) { + float minProjection = FLT_MAX; + float maxProjection = -FLT_MIN; + for (uint32_t j = 0; j < points.size(); ++j) { + glm::vec3 offset = points[j] - avgPoint; + float projection = glm::dot(offset, axis); + maxProjection = glm::max(maxProjection, projection); + minProjection = glm::min(minProjection, projection); + avgRadius += glm::length(offset - projection * axis); + } + avgRadius /= (float)points.size(); + + // compute endpoints of capsule in joint-frame + glm::vec3 capsuleBegin = avgPoint; + glm::vec3 capsuleEnd = avgPoint; + if (maxProjection - minProjection < 2.0f * avgRadius) { + // the mesh-as-cylinder approximation is too short to collide as a capsule + // so we'll collapse it to a sphere (although that isn't a very good approximation) + capsuleBegin = avgPoint + 0.5f * (maxProjection + minProjection) * axis; + capsuleEnd = capsuleBegin; + } else { + capsuleBegin = avgPoint + (minProjection + avgRadius) * axis; + capsuleEnd = avgPoint + (maxProjection - avgRadius) * axis; + } + + // save points for later + joint.shapeInfo.points.push_back(capsuleBegin); + joint.shapeInfo.points.push_back(capsuleEnd); } + joint.shapeInfo.radius = avgRadius; } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 158b5581c6..cff22676c8 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -55,15 +55,21 @@ public: QVector normals; }; +struct FBXJointShapeInfo { + // same units and frame as FBXJoint.translation + QVector points; + float radius; +}; + /// A single joint (transformation node) extracted from an FBX document. class FBXJoint { public: - bool isFree; + FBXJointShapeInfo shapeInfo; QVector freeLineage; + bool isFree; int parentIndex; float distanceToParent; - float boneRadius; // http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 1dfb7a4587..2645e42874 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -441,7 +441,6 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, geometry.joints[0].isFree = false; geometry.joints[0].parentIndex = -1; geometry.joints[0].distanceToParent = 0; - geometry.joints[0].boneRadius = 0; geometry.joints[0].translation = glm::vec3(0, 0, 0); geometry.joints[0].rotationMin = glm::vec3(0, 0, 0); geometry.joints[0].rotationMax = glm::vec3(0, 0, 0); @@ -617,7 +616,6 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << " freeLineage" << joint.freeLineage; qCDebug(modelformat) << " parentIndex" << joint.parentIndex; qCDebug(modelformat) << " distanceToParent" << joint.distanceToParent; - qCDebug(modelformat) << " boneRadius" << joint.boneRadius; qCDebug(modelformat) << " translation" << joint.translation; qCDebug(modelformat) << " preTransform" << joint.preTransform; qCDebug(modelformat) << " preRotation" << joint.preRotation; diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 959c1562f2..9931bab5ed 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -57,7 +57,8 @@ AssetRequest* AssetClient::createRequest(const QString& hash, const QString& ext SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); if (!assetServer) { - qDebug().nospace() << "Could not request " << hash << "." << extension << " since you are not currently connected to an asset-server."; + qDebug().nospace() << "Could not request " << hash << "." << extension + << " since you are not currently connected to an asset-server."; return nullptr; } @@ -96,10 +97,13 @@ bool AssetClient::getAsset(const QString& hash, const QString& extension, DataOf SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); if (assetServer) { - auto packet = NLPacket::create(PacketType::AssetGet); - + auto messageID = ++_currentID; + auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(uint8_t) + extension.length() + + sizeof(start) + sizeof(end); + auto packet = NLPacket::create(PacketType::AssetGet, payloadSize, true); + qDebug() << "Requesting data from" << start << "to" << end << "of" << hash << "from asset-server."; packet->writePrimitive(messageID); @@ -127,9 +131,11 @@ bool AssetClient::getAssetInfo(const QString& hash, const QString& extension, Ge SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); if (assetServer) { - auto packet = NLPacket::create(PacketType::AssetGetInfo); - auto messageID = ++_currentID; + + auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(uint8_t) + extension.length(); + auto packet = NLPacket::create(PacketType::AssetGetInfo, payloadSize, true); + packet->writePrimitive(messageID); packet->write(QByteArray::fromHex(hash.toLatin1())); packet->writePrimitive(uint8_t(extension.length())); @@ -207,9 +213,6 @@ bool AssetClient::uploadAsset(const QByteArray& data, const QString& extension, packetList->writePrimitive(static_cast(extension.length())); packetList->write(extension.toLatin1().constData(), extension.length()); - qDebug() << "Extension length: " << extension.length(); - qDebug() << "Extension: " << extension; - uint64_t size = data.length(); packetList->writePrimitive(size); packetList->write(data.constData(), size); diff --git a/libraries/networking/src/AssetUpload.cpp b/libraries/networking/src/AssetUpload.cpp index 14e5354f0b..03c8707517 100644 --- a/libraries/networking/src/AssetUpload.cpp +++ b/libraries/networking/src/AssetUpload.cpp @@ -41,6 +41,8 @@ void AssetUpload::start() { // ask the AssetClient to upload the asset and emit the proper signals from the passed callback auto assetClient = DependencyManager::get(); + qDebug() << "Attempting to upload" << _filename << "to asset-server."; + assetClient->uploadAsset(data, _extension, [this](AssetServerError error, const QString& hash){ switch (error) { case AssetServerError::NoError: diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 38c87418c8..b6c3db3562 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -242,8 +242,9 @@ bool LimitedNodeList::packetSourceAndHashMatch(const udt::Packet& packet) { return true; } else { + static const QString UNKNOWN_REGEX = "Packet of type \\d+ \\([\\sa-zA-Z:]+\\) received from unknown node with UUID"; static QString repeatedMessage - = LogHandler::getInstance().addRepeatedMessageRegex("Packet of type \\d+ \\([\\sa-zA-Z]+\\) received from unknown node with UUID"); + = LogHandler::getInstance().addRepeatedMessageRegex(UNKNOWN_REGEX); qCDebug(networking) << "Packet of type" << headerType << "received from unknown node with UUID" << qPrintable(uuidStringWithoutCurlyBraces(sourceID)); diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index c721419c7e..8af33c5463 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -25,6 +25,13 @@ ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const Q return new HTTPResourceRequest(parent, url); } else if (scheme == URL_SCHEME_ATP) { return new AssetResourceRequest(parent, url); + } else { + // check the degenerative file case: on windows we can often have urls of the form c:/filename + // this checks for and works around that case. + QUrl urlWithFileScheme { URL_SCHEME_FILE + ":///" + url.toString() }; + if (!urlWithFileScheme.toLocalFile().isEmpty()) { + return new FileResourceRequest(parent, urlWithFileScheme); + } } qDebug() << "Unknown scheme (" << scheme << ") for URL: " << url.url(); diff --git a/libraries/physics/src/DynamicCharacterController.cpp b/libraries/physics/src/DynamicCharacterController.cpp index 22f84eccec..163cba6aae 100644 --- a/libraries/physics/src/DynamicCharacterController.cpp +++ b/libraries/physics/src/DynamicCharacterController.cpp @@ -409,8 +409,7 @@ void DynamicCharacterController::postSimulation() { glm::quat rotation = bulletToGLM(avatarTransform.getRotation()); glm::vec3 position = bulletToGLM(avatarTransform.getOrigin()); - _avatarData->setOrientation(rotation); - _avatarData->setPosition(position - rotation * _shapeLocalOffset); + _avatarData->nextAttitude(position - rotation * _shapeLocalOffset, rotation); _avatarData->setVelocity(bulletToGLM(_rigidBody->getLinearVelocity())); } } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index d8e3225a73..fb26286f6a 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1291,7 +1291,9 @@ void Model::simulateInternal(float deltaTime) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; updateRig(deltaTime, parentTransform); - +} +void Model::updateClusterMatrices() { + const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 zeroScale(glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), @@ -1305,7 +1307,7 @@ void Model::simulateInternal(float deltaTime) { if (_showTrueJointTransforms) { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - auto jointMatrix =_rig->getJointTransform(cluster.jointIndex); + auto jointMatrix = _rig->getJointTransform(cluster.jointIndex); state.clusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix; // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. @@ -1319,7 +1321,7 @@ void Model::simulateInternal(float deltaTime) { } else { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - auto jointMatrix = _rig->getJointVisibleTransform(cluster.jointIndex); + auto jointMatrix = _rig->getJointVisibleTransform(cluster.jointIndex); // differs from above only in using get...VisibleTransform state.clusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix; // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. @@ -1434,7 +1436,6 @@ AABox Model::getPartBounds(int meshIndex, int partIndex) { return calculateScaledOffsetAABox(_geometry->getFBXGeometry().meshExtents); } } - if (_geometry->getFBXGeometry().meshes.size() > meshIndex) { // FIX ME! - This is currently a hack because for some mesh parts our efforts to calculate the bounding @@ -1489,6 +1490,8 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran return; } + updateClusterMatrices(); + const NetworkMesh& networkMesh = *(networkMeshes.at(meshIndex).get()); const FBXMesh& mesh = geometry.meshes.at(meshIndex); const MeshState& state = _meshStates.at(meshIndex); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index cfd819a76d..348e5cf549 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -111,6 +111,7 @@ public: bool getSnapModelToRegistrationPoint() { return _snapModelToRegistrationPoint; } virtual void simulate(float deltaTime, bool fullUpdate = true); + void updateClusterMatrices(); /// Returns a reference to the shared geometry. const QSharedPointer& getGeometry() const { return _geometry; } diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index eeff6c15f0..83a87b718f 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -860,7 +860,14 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac } QList urls; for (QString file : includeFiles) { - urls.append(resolvePath(file)); + QUrl thisURL { resolvePath(file) }; + if (!_includedURLs.contains(thisURL)) { + urls.append(thisURL); + _includedURLs << thisURL; + } + else { + qCDebug(scriptengine) << "Script.include() ignoring previously included url:" << thisURL; + } } BatchLoader* loader = new BatchLoader(urls); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 65644dceb9..bb2918fb8e 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -152,6 +153,7 @@ protected: Sound* _avatarSound; int _numAvatarSoundSentBytes; bool _isAgent = false; + QSet _includedURLs; private: void stopAllTimers(); diff --git a/libraries/shared/src/GenericQueueThread.h b/libraries/shared/src/GenericQueueThread.h index 2a48ff7418..067d7a7989 100644 --- a/libraries/shared/src/GenericQueueThread.h +++ b/libraries/shared/src/GenericQueueThread.h @@ -24,7 +24,7 @@ class GenericQueueThread : public GenericThread { public: using Queue = QQueue; GenericQueueThread(QObject* parent = nullptr) - : GenericThread(parent) {} + : GenericThread() {} virtual ~GenericQueueThread() {} diff --git a/libraries/shared/src/GenericThread.cpp b/libraries/shared/src/GenericThread.cpp index 18f5224229..66af2e01c8 100644 --- a/libraries/shared/src/GenericThread.cpp +++ b/libraries/shared/src/GenericThread.cpp @@ -14,8 +14,8 @@ #include "GenericThread.h" -GenericThread::GenericThread(QObject* parent) : - QObject(parent), +GenericThread::GenericThread() : + QObject(), _stopThread(false), _isThreaded(false) // assume non-threaded, must call initialize() { diff --git a/libraries/shared/src/GenericThread.h b/libraries/shared/src/GenericThread.h index f261dc5b37..8362d3ba57 100644 --- a/libraries/shared/src/GenericThread.h +++ b/libraries/shared/src/GenericThread.h @@ -23,7 +23,7 @@ class GenericThread : public QObject { Q_OBJECT public: - GenericThread(QObject* parent = nullptr); + GenericThread(); virtual ~GenericThread(); /// Call to start the thread. diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 806560d96f..a08288000e 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -127,21 +127,20 @@ void AnimTests::testClipEvaulateWithVars() { void AnimTests::testLoader() { auto url = QUrl("https://gist.githubusercontent.com/hyperlogic/857129fe04567cbe670f/raw/8ba57a8f0a76f88b39a11f77f8d9df04af9cec95/test.json"); + // NOTE: This will warn about missing "test01.fbx", "test02.fbx", etc. if the resource loading code doesn't handle relative pathnames! + // However, the test will proceed. AnimNodeLoader loader(url); const int timeout = 1000; QEventLoop loop; - QTimer timer; - timer.setInterval(timeout); - timer.setSingleShot(true); AnimNode::Pointer node = nullptr; connect(&loader, &AnimNodeLoader::success, [&](AnimNode::Pointer nodeIn) { node = nodeIn; }); loop.connect(&loader, SIGNAL(success(AnimNode::Pointer)), SLOT(quit())); loop.connect(&loader, SIGNAL(error(int, QString)), SLOT(quit())); - loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); - timer.start(); + QTimer::singleShot(timeout, &loop, SLOT(quit())); + loop.exec(); QVERIFY((bool)node); @@ -184,42 +183,58 @@ void AnimTests::testLoader() { void AnimTests::testVariant() { auto defaultVar = AnimVariant(); - auto boolVar = AnimVariant(true); - auto intVar = AnimVariant(1); - auto floatVar = AnimVariant(1.0f); - auto vec3Var = AnimVariant(glm::vec3(1.0f, 2.0f, 3.0f)); - auto quatVar = AnimVariant(glm::quat(1.0f, 2.0f, 3.0f, 4.0f)); + auto boolVarTrue = AnimVariant(true); + auto boolVarFalse = AnimVariant(false); + auto intVarZero = AnimVariant(0); + auto intVarOne = AnimVariant(1); + auto intVarNegative = AnimVariant(-1); + auto floatVarZero = AnimVariant(0.0f); + auto floatVarOne = AnimVariant(1.0f); + auto floatVarNegative = AnimVariant(-1.0f); + auto vec3Var = AnimVariant(glm::vec3(1.0f, -2.0f, 3.0f)); + auto quatVar = AnimVariant(glm::quat(1.0f, 2.0f, -3.0f, 4.0f)); auto mat4Var = AnimVariant(glm::mat4(glm::vec4(1.0f, 2.0f, 3.0f, 4.0f), - glm::vec4(5.0f, 6.0f, 7.0f, 8.0f), + glm::vec4(5.0f, 6.0f, -7.0f, 8.0f), glm::vec4(9.0f, 10.0f, 11.0f, 12.0f), glm::vec4(13.0f, 14.0f, 15.0f, 16.0f))); QVERIFY(defaultVar.isBool()); QVERIFY(defaultVar.getBool() == false); - QVERIFY(boolVar.isBool()); - QVERIFY(boolVar.getBool() == true); + QVERIFY(boolVarTrue.isBool()); + QVERIFY(boolVarTrue.getBool() == true); + QVERIFY(boolVarFalse.isBool()); + QVERIFY(boolVarFalse.getBool() == false); - QVERIFY(intVar.isInt()); - QVERIFY(intVar.getInt() == 1); + QVERIFY(intVarZero.isInt()); + QVERIFY(intVarZero.getInt() == 0); + QVERIFY(intVarOne.isInt()); + QVERIFY(intVarOne.getInt() == 1); + QVERIFY(intVarNegative.isInt()); + QVERIFY(intVarNegative.getInt() == -1); - QVERIFY(floatVar.isFloat()); - QVERIFY(floatVar.getFloat() == 1.0f); + QVERIFY(floatVarZero.isFloat()); + QVERIFY(floatVarZero.getFloat() == 0.0f); + QVERIFY(floatVarOne.isFloat()); + QVERIFY(floatVarOne.getFloat() == 1.0f); + QVERIFY(floatVarNegative.isFloat()); + QVERIFY(floatVarNegative.getFloat() == -1.0f); QVERIFY(vec3Var.isVec3()); auto v = vec3Var.getVec3(); QVERIFY(v.x == 1.0f); - QVERIFY(v.y == 2.0f); + QVERIFY(v.y == -2.0f); QVERIFY(v.z == 3.0f); QVERIFY(quatVar.isQuat()); auto q = quatVar.getQuat(); QVERIFY(q.w == 1.0f); QVERIFY(q.x == 2.0f); - QVERIFY(q.y == 3.0f); + QVERIFY(q.y == -3.0f); QVERIFY(q.z == 4.0f); QVERIFY(mat4Var.isMat4()); auto m = mat4Var.getMat4(); QVERIFY(m[0].x == 1.0f); + QVERIFY(m[1].z == -7.0f); QVERIFY(m[3].w == 16.0f); }