diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index 02a4ff04f0..358b963b4b 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include "AssignmentClientMonitor.h" @@ -17,9 +19,21 @@ const char* NUM_FORKS_PARAMETER = "-n"; const QString ASSIGNMENT_CLIENT_MONITOR_TARGET_NAME = "assignment-client-monitor"; +void signalHandler(int param){ + // get the qApp and cast it to an AssignmentClientMonitor + AssignmentClientMonitor* app = qobject_cast(qApp); + + // tell it to stop the child processes and then go down + app->stopChildProcesses(); + app->quit(); +} + AssignmentClientMonitor::AssignmentClientMonitor(int &argc, char **argv, int numAssignmentClientForks) : QCoreApplication(argc, argv) { + // be a signal handler for SIGTERM so we can stop our children when we get it + signal(SIGTERM, signalHandler); + // start the Logging class with the parent's target name LogHandler::getInstance().setTargetName(ASSIGNMENT_CLIENT_MONITOR_TARGET_NAME); @@ -38,9 +52,29 @@ AssignmentClientMonitor::AssignmentClientMonitor(int &argc, char **argv, int num } } +void AssignmentClientMonitor::stopChildProcesses() { + + QList >::Iterator it = _childProcesses.begin(); + while (it != _childProcesses.end()) { + if (!it->isNull()) { + qDebug() << "Monitor is terminating child process" << it->data(); + + // don't re-spawn this child when it goes down + disconnect(it->data(), 0, this, 0); + + it->data()->terminate(); + it->data()->waitForFinished(); + } + + it = _childProcesses.erase(it); + } +} + void AssignmentClientMonitor::spawnChildClient() { QProcess *assignmentClient = new QProcess(this); + _childProcesses.append(QPointer(assignmentClient)); + // make sure that the output from the child process appears in our output assignmentClient->setProcessChannelMode(QProcess::ForwardedChannels); @@ -55,5 +89,10 @@ void AssignmentClientMonitor::spawnChildClient() { void AssignmentClientMonitor::childProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { qDebug("Replacing dead child assignment client with a new one"); + + // remove the old process from our list of child processes + qDebug() << "need to remove" << QPointer(qobject_cast(sender())); + _childProcesses.removeOne(QPointer(qobject_cast(sender()))); + spawnChildClient(); } diff --git a/assignment-client/src/AssignmentClientMonitor.h b/assignment-client/src/AssignmentClientMonitor.h index 1df08a345e..9dbfcc495a 100644 --- a/assignment-client/src/AssignmentClientMonitor.h +++ b/assignment-client/src/AssignmentClientMonitor.h @@ -13,6 +13,7 @@ #define hifi_AssignmentClientMonitor_h #include +#include #include #include @@ -23,10 +24,13 @@ class AssignmentClientMonitor : public QCoreApplication { Q_OBJECT public: AssignmentClientMonitor(int &argc, char **argv, int numAssignmentClientForks); + + void stopChildProcesses(); private slots: void childProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); private: void spawnChildClient(); + QList > _childProcesses; QStringList _childArguments; }; diff --git a/examples/acScripts/ambiance.js b/examples/acScripts/ambiance.js index 0149d5d3ff..fcc3ba0d80 100644 --- a/examples/acScripts/ambiance.js +++ b/examples/acScripts/ambiance.js @@ -14,11 +14,10 @@ var position = { x: 700, y: 25, z: 725 }; var audioOptions = { position: position, volume: 0.4, - loop: true, - stereo: false + loop: true }; -var sound = SoundCache.getSound(soundURL, audioOptions.isStereo); +var sound = SoundCache.getSound(soundURL); var injector = null; var count = 100; diff --git a/examples/html/entityList.html b/examples/html/entityList.html new file mode 100644 index 0000000000..7caa45f19d --- /dev/null +++ b/examples/html/entityList.html @@ -0,0 +1,125 @@ + + + + + + + +
+ +
+ + + + + + + + + + +
TypeURL
+ + + diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index 695879b678..a79edfb181 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -214,7 +214,7 @@ elModelSection.style.display = 'block'; elModelURL.value = properties.modelURL; elModelAnimationURL.value = properties.animationURL; - elModelAnimationPlaying.checked = properties.animationPlaying; + elModelAnimationPlaying.checked = properties.animationIsPlaying; elModelAnimationFPS.value = properties.animationFPS; } diff --git a/examples/html/style.css b/examples/html/style.css index 1625fd094f..b721c31b88 100644 --- a/examples/html/style.css +++ b/examples/html/style.css @@ -93,3 +93,40 @@ input.coord { width: 6em; height: 2em; } + +table#entity-table { + border-collapse: collapse; + font-family: Sans-Serif; + font-size: 12px; + width: 100%; +} + +#entity-table tr { + cursor: pointer; +} + +tr.selected { + background-color: #AAA; +} + +#entity-table th { + background-color: #333; + color: #fff; + border: 0px black solid; + text-align: left; + word-wrap: nowrap; + white-space: nowrap; +} + +#entity-table td { + border: 0px black solid; + word-wrap: nowrap; + white-space: nowrap; +} + +th#entity-type { + width: 60px; +} + +th#entity-url { +} diff --git a/examples/libraries/entityList.js b/examples/libraries/entityList.js new file mode 100644 index 0000000000..65c06c8cea --- /dev/null +++ b/examples/libraries/entityList.js @@ -0,0 +1,70 @@ +EntityListTool = function(opts) { + var that = {}; + + var url = Script.resolvePath('html/entityList.html'); + var webView = new WebWindow('Entities', url, 200, 280); + + var visible = false; + + webView.setVisible(visible); + + that.setVisible = function(newVisible) { + visible = newVisible; + webView.setVisible(visible); + }; + + selectionManager.addEventListener(function() { + var selectedIDs = []; + + for (var i = 0; i < selectionManager.selections.length; i++) { + selectedIDs.push(selectionManager.selections[i].id); + } + + data = { + type: 'selectionUpdate', + selectedIDs: selectedIDs, + }; + webView.eventBridge.emitScriptEvent(JSON.stringify(data)); + }); + + webView.eventBridge.webEventReceived.connect(function(data) { + data = JSON.parse(data); + if (data.type == "selectionUpdate") { + var ids = data.entityIds; + var entityIDs = []; + for (var i = 0; i < ids.length; i++) { + var entityID = Entities.getEntityItemID(ids[i]); + if (entityID.isKnownID) { + entityIDs.push(entityID); + } else { + print("Tried to select invalid entity: " + ids[i]); + } + } + selectionManager.setSelections(entityIDs); + if (data.focus) { + cameraManager.focus(selectionManager.worldPosition, + selectionManager.worldDimensions, + Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); + } + } else if (data.type == "refresh") { + var entities = []; + var ids = Entities.findEntities(MyAvatar.position, 100); + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var properties = Entities.getEntityProperties(id); + entities.push({ + id: id.id, + type: properties.type, + url: properties.type == "Model" ? properties.modelURL : "", + }); + } + var data = { + type: "update", + entities: entities, + }; + webView.eventBridge.emitScriptEvent(JSON.stringify(data)); + } + }); + + return that; +}; diff --git a/examples/libraries/entitySelectionTool.js b/examples/libraries/entitySelectionTool.js index ada876e9b1..e760fb0463 100644 --- a/examples/libraries/entitySelectionTool.js +++ b/examples/libraries/entitySelectionTool.js @@ -1242,7 +1242,7 @@ SelectionDisplay = (function () { Quat.getFront(lastCameraOrientation)); var vector = Vec3.subtract(newIntersection, lastPlaneIntersection); - lastPlaneIntersection = newIntersection; + vector = grid.snapToGrid(vector); // we only care about the Y axis vector.x = 0; @@ -1258,10 +1258,15 @@ SelectionDisplay = (function () { Vec3.print(" newPosition:", newPosition); } for (var i = 0; i < SelectionManager.selections.length; i++) { - var properties = Entities.getEntityProperties(SelectionManager.selections[i]); + var id = SelectionManager.selections[i]; + var properties = selectionManager.savedProperties[id.id]; + var original = properties.position; - properties.position = Vec3.sum(properties.position, vector); - Entities.editEntity(SelectionManager.selections[i], properties); + var newPosition = Vec3.sum(properties.position, vector); + + Entities.editEntity(id, { + position: newPosition, + }); } SelectionManager._update(); diff --git a/examples/lobby.js b/examples/lobby.js index bb033971b3..437cfe40f3 100644 --- a/examples/lobby.js +++ b/examples/lobby.js @@ -39,18 +39,23 @@ var ORB_SHIFT = { x: 0, y: -1.4, z: -0.8}; var HELMET_ATTACHMENT_URL = HIFI_PUBLIC_BUCKET + "models/attachments/IronManMaskOnly.fbx" -var droneSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Lobby/drone.raw") +var droneSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Lobby/drone.stereo.raw") var currentDrone = null; -var latinSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Lobby/latin.raw") -var elevatorSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Lobby/elevator.raw") -var currentMusak = null; +var latinSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Lobby/latin.stereo.raw") +var elevatorSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Lobby/elevator.stereo.raw") +var currentMusakInjector = null; +var currentSound = null; + +var inOculusMode = Menu.isOptionChecked("EnableVRMode"); function reticlePosition() { var RETICLE_DISTANCE = 1; return Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), RETICLE_DISTANCE)); } +var MAX_NUM_PANELS = 21; + function drawLobby() { if (!panelWall) { print("Adding overlays for the lobby panel wall and orb shell."); @@ -64,7 +69,7 @@ function drawLobby() { url: HIFI_PUBLIC_BUCKET + "models/sets/Lobby/LobbyPrototype/Lobby5_PanelsWithFrames.fbx", position: Vec3.sum(orbPosition, Vec3.multiplyQbyV(towardsMe, panelsCenterShift)), rotation: towardsMe, - dimensions: panelsDimensions + dimensions: panelsDimensions, }; var orbShellProps = { @@ -77,21 +82,23 @@ function drawLobby() { avatarStickPosition = MyAvatar.position; - panelWall = Overlays.addOverlay("model", panelWallProps); + panelWall = Overlays.addOverlay("model", panelWallProps); orbShell = Overlays.addOverlay("model", orbShellProps); // for HMD wearers, create a reticle in center of screen - var CURSOR_SCALE = 0.025; - - reticle = Overlays.addOverlay("billboard", { - url: HIFI_PUBLIC_BUCKET + "images/cursor.svg", - position: reticlePosition(), - ignoreRayIntersection: true, - isFacingAvatar: true, - alpha: 1.0, - scale: CURSOR_SCALE - }); + if (inOculusMode) { + var CURSOR_SCALE = 0.025; + reticle = Overlays.addOverlay("billboard", { + url: HIFI_PUBLIC_BUCKET + "images/cursor.svg", + position: reticlePosition(), + ignoreRayIntersection: true, + isFacingAvatar: true, + alpha: 1.0, + scale: CURSOR_SCALE + }); + } + // add an attachment on this avatar so other people see them in the lobby MyAvatar.attach(HELMET_ATTACHMENT_URL, "Neck", {x: 0, y: 0, z: 0}, Quat.fromPitchYawRollDegrees(0, 0, 0), 1.15); @@ -125,39 +132,73 @@ function changeLobbyTextures() { Overlays.editOverlay(panelWall, textureProp); } +var MUSAK_VOLUME = 0.5; + +function playNextMusak() { + if (panelWall) { + if (currentSound == latinSound) { + if (elevatorSound.downloaded) { + currentSound = elevatorSound; + } + } else if (currentSound == elevatorSound) { + if (latinSound.downloaded) { + currentSound = latinSound; + } + } + + currentMusakInjector = Audio.playSound(currentSound, { localOnly: true, volume: MUSAK_VOLUME }); + } +} + function playRandomMusak() { - chosenSound = null; + currentSound = null; if (latinSound.downloaded && elevatorSound.downloaded) { - chosenSound = Math.random < 0.5 ? latinSound : elevatorSound; + currentSound = Math.random() < 0.5 ? latinSound : elevatorSound; } else if (latinSound.downloaded) { - chosenSound = latinSound; + currentSound = latinSound; } else if (elevatorSound.downloaded) { - chosenSound = elevatorSound; + currentSound = elevatorSound; } - if (chosenSound) { - currentMusak = Audio.playSound(chosenSound, { stereo: true, localOnly: true }) + if (currentSound) { + // pick a random number of seconds from 0-10 to offset the musak + var secondOffset = Math.random() * 10; + currentMusakInjector = Audio.playSound(currentSound, { localOnly: true, secondOffset: secondOffset, volume: MUSAK_VOLUME }); } else { - currentMusak = null; + currentMusakInjector = null; } } function cleanupLobby() { + + // for each of the 21 placeholder textures, set them back to default so the cached model doesn't have changed textures + var panelTexturesReset = {}; + panelTexturesReset["textures"] = {}; + + for (var j = 0; j < MAX_NUM_PANELS; j++) { + panelTexturesReset["textures"]["file" + (j + 1)] = HIFI_PUBLIC_BUCKET + "models/sets/Lobby/LobbyPrototype/Texture.jpg"; + }; + + Overlays.editOverlay(panelWall, panelTexturesReset); + Overlays.deleteOverlay(panelWall); Overlays.deleteOverlay(orbShell); - Overlays.deleteOverlay(reticle); - Audio.stopInjector(currentDrone); - currentDrone = null; - - Audio.stopInjector(currentMusak); - currentMusak = null; + if (reticle) { + Overlays.deleteOverlay(reticle); + } panelWall = false; orbShell = false; reticle = false; + Audio.stopInjector(currentDrone); + currentDrone = null; + + Audio.stopInjector(currentMusakInjector); + currentMusakInjector = null; + locations = {}; toggleEnvironmentRendering(true); @@ -218,10 +259,18 @@ function toggleEnvironmentRendering(shouldRender) { function update(deltaTime) { maybeCleanupLobby(); - if (reticle) { - Overlays.editOverlay(reticle, { - position: reticlePosition() - }); + if (panelWall) { + + if (reticle) { + Overlays.editOverlay(reticle, { + position: reticlePosition() + }); + } + + // if the reticle is up then we may need to play the next musak + if (!Audio.isInjectorPlaying(currentMusakInjector)) { + playNextMusak(); + } } } diff --git a/examples/newEditEntities.js b/examples/newEditEntities.js index f866893411..27a3e8a9d2 100644 --- a/examples/newEditEntities.js +++ b/examples/newEditEntities.js @@ -39,6 +39,9 @@ Script.include("libraries/gridTool.js"); var grid = Grid(); gridTool = GridTool({ horizontalGrid: grid }); +Script.include("libraries/entityList.js"); +var entityListTool = EntityListTool(); + selectionManager.addEventListener(selectionDisplay.updateHandles); var windowDimensions = Controller.getViewportDimensions(); @@ -283,6 +286,7 @@ var toolBar = (function () { if (activeButton === toolBar.clicked(clickedOverlay)) { isActive = !isActive; if (!isActive) { + entityListTool.setVisible(false); gridTool.setVisible(false); grid.setEnabled(false); propertiesTool.setVisible(false); @@ -290,6 +294,7 @@ var toolBar = (function () { cameraManager.disable(); } else { cameraManager.enable(); + entityListTool.setVisible(true); gridTool.setVisible(true); grid.setEnabled(true); propertiesTool.setVisible(true); diff --git a/examples/radio.js b/examples/radio.js index 0b62d78b0e..e8300c00b5 100644 --- a/examples/radio.js +++ b/examples/radio.js @@ -12,13 +12,12 @@ Script.include("libraries/globals.js"); var modelURL = HIFI_PUBLIC_BUCKET + "models/entities/radio/Speakers.fbx"; -var soundURL = HIFI_PUBLIC_BUCKET + "sounds/FamilyStereo.raw"; +var soundURL = HIFI_PUBLIC_BUCKET + "sounds/family.stereo.raw"; var AudioRotationOffset = Quat.fromPitchYawRollDegrees(0, -90, 0); var audioOptions = { volume: 0.5, - loop: true, - stereo: true + loop: true } var injector = null; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 66b96cf132..4e8b95862a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2673,6 +2673,14 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node } } +bool Application::isHMDMode() const { + if (OculusManager::isConnected()) { + return true; + } else { + return false; + } +} + ///////////////////////////////////////////////////////////////////////////////////// // loadViewFrustum() // diff --git a/interface/src/Application.h b/interface/src/Application.h index 2c70249b32..c75202d96f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -316,6 +316,11 @@ public: void registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine); + // the isHMDmode is true whenever we use the interface from an HMD and not a standard flat display + // rendering of several elements depend on that + // TODO: carry that information on the Camera as a setting + bool isHMDMode() const; + signals: /// Fired when we're simulating; allows external parties to hook in. diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index f7aabc8f06..1eacfa8363 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -297,6 +297,8 @@ Menu::Menu() : avatar, SLOT(updateMotionBehavior())); addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::StandOnNearbyFloors, 0, true, avatar, SLOT(updateMotionBehavior())); + addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::ShiftHipsForIdleAnimations, 0, false, + avatar, SLOT(updateMotionBehavior())); QMenu* collisionsMenu = avatarMenu->addMenu("Collide With..."); addCheckableActionToQMenuAndActionHash(collisionsMenu, MenuOption::CollideAsRagdoll, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 8590d8580e..7ed8811b4f 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -484,6 +484,7 @@ namespace MenuOption { const QString SixenseMouseInput = "Enable Sixense Mouse Input"; const QString SixenseLasers = "Enable Sixense UI Lasers"; const QString StandOnNearbyFloors = "Stand on nearby floors"; + const QString ShiftHipsForIdleAnimations = "Shift hips for idle animations"; const QString Stars = "Stars"; const QString Stats = "Stats"; const QString StereoAudio = "Stereo Audio"; diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 5741d6b265..df3daba9c8 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -655,7 +655,10 @@ void Avatar::renderDisplayName() { if (_displayName.isEmpty() || _displayNameAlpha == 0.0f) { return; } - + + // which viewing mode? + bool inHMD = Application::getInstance()->isHMDMode(); + glDisable(GL_LIGHTING); glPushMatrix(); @@ -664,17 +667,17 @@ void Avatar::renderDisplayName() { glTranslatef(textPosition.x, textPosition.y, textPosition.z); // we need "always facing camera": we must remove the camera rotation from the stack - glm::quat rotation = Application::getInstance()->getCamera()->getRotation(); - glm::vec3 frontAxis(1.f, 0.f, 0.f); - frontAxis = glm::rotate(rotation, frontAxis); - frontAxis = glm::normalize(glm::vec3(frontAxis.x, 0.f, frontAxis.z)); - - // TODO : test this secodn solution which should be better wfor occulus - //glm::vec3 camPosition = Application::getInstance()->getCamera()->getPosition(); - //glm::vec3 frontAxis = camPosition - textPosition; - //frontAxis = glm::normalize(glm::vec3(frontAxis.z, 0.f, -frontAxis.x)); + glm::vec3 frontAxis(0.f, 0.f, 1.f); + if (inHMD) { + glm::vec3 camPosition = Application::getInstance()->getCamera()->getPosition(); + frontAxis = camPosition - textPosition; + } else { + glm::quat rotation = Application::getInstance()->getCamera()->getRotation(); + frontAxis = glm::rotate(rotation, frontAxis); + } + frontAxis = glm::normalize(glm::vec3(frontAxis.z, 0.f, -frontAxis.x)); float angle = acos(frontAxis.x) * ((frontAxis.z < 0) ? 1.f : -1.f); glRotatef(glm::degrees(angle), 0.0f, 1.0f, 0.0f); @@ -706,10 +709,16 @@ void Avatar::renderDisplayName() { if (success) { double textWindowHeight = abs(result1[1] - result0[1]); - float scaleFactor = Application::getInstance()->getRenderResolutionScale() * // Scale compensate for the resolution - QApplication::desktop()->windowHandle()->devicePixelRatio() * // And the device pixel ratio + // need to scale to compensate for the font resolution due to the device + float scaleFactor = QApplication::desktop()->windowHandle()->devicePixelRatio() * ((textWindowHeight > EPSILON) ? 1.0f / textWindowHeight : 1.0f); - glScalef(scaleFactor, scaleFactor, 1.0); + if (inHMD) { + const float HMDMODE_NAME_SCALE = 0.65f; + scaleFactor *= HMDMODE_NAME_SCALE; + } else { + scaleFactor *= Application::getInstance()->getRenderResolutionScale(); + } + glScalef(scaleFactor, scaleFactor, 1.0); glScalef(1.0f, -1.0f, 1.0f); // TextRenderer::draw paints the text upside down in y axis @@ -787,7 +796,10 @@ void Avatar::setSkeletonOffset(const glm::vec3& offset) { } glm::vec3 Avatar::getSkeletonPosition() const { - return _position + _skeletonOffset; + // The avatar is rotated PI about the yAxis, so we have to correct for it + // to get the skeleton offset contribution in the world-frame. + const glm::quat FLIP = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); + return _position + getOrientation() * FLIP * _skeletonOffset; } QVector Avatar::getJointRotations() const { diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 3b6922d0d1..7c25bbd6b2 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -204,6 +204,8 @@ void Head::simulate(float deltaTime, bool isMine, bool billboard) { _mouth3, _mouth4, _blendshapeCoefficients); + } else { + _saccade = glm::vec3(); } if (!isMine) { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 292cc2fb71..8b5fefc3be 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -89,6 +89,7 @@ MyAvatar::MyAvatar() : _billboardValid(false), _physicsSimulation(), _voxelShapeManager(), + _feetTouchFloor(true), _isLookingAtLeftEye(true) { ShapeCollider::initDispatchTable(); @@ -115,7 +116,7 @@ QByteArray MyAvatar::toByteArray() { if (mode == CAMERA_MODE_THIRD_PERSON || mode == CAMERA_MODE_INDEPENDENT) { // fake the avatar position that is sent up to the AvatarMixer glm::vec3 oldPosition = _position; - _position += _skeletonOffset; + _position = getSkeletonPosition(); QByteArray array = AvatarData::toByteArray(); // copy the correct position back _position = oldPosition; @@ -155,6 +156,9 @@ void MyAvatar::update(float deltaTime) { } simulate(deltaTime); + if (_feetTouchFloor) { + _skeletonModel.updateStandingFoot(); + } } void MyAvatar::simulate(float deltaTime) { @@ -1034,7 +1038,14 @@ void MyAvatar::setAttachmentData(const QVector& attachmentData) glm::vec3 MyAvatar::getSkeletonPosition() const { CameraMode mode = Application::getInstance()->getCamera()->getMode(); if (mode == CAMERA_MODE_THIRD_PERSON || mode == CAMERA_MODE_INDEPENDENT) { - return Avatar::getSkeletonPosition(); + // The avatar is rotated PI about the yAxis, so we have to correct for it + // to get the skeleton offset contribution in the world-frame. + const glm::quat FLIP = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 skeletonOffset = _skeletonOffset; + if (_feetTouchFloor) { + skeletonOffset += _skeletonModel.getStandingOffset(); + } + return _position + getOrientation() * FLIP * skeletonOffset; } return Avatar::getPosition(); } @@ -1939,6 +1950,7 @@ void MyAvatar::updateMotionBehavior() { } else { _motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED; } + _feetTouchFloor = menu->isOptionChecked(MenuOption::ShiftHipsForIdleAnimations); } void MyAvatar::onToggleRagdoll() { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 01bbb556a1..0060c959e4 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -235,6 +235,7 @@ private: PhysicsSimulation _physicsSimulation; VoxelShapeManager _voxelShapeManager; + bool _feetTouchFloor; bool _isLookingAtLeftEye; RecorderPointer _recorder; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 7ca105f483..834f1c2c08 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -22,13 +22,22 @@ #include "SkeletonModel.h" #include "SkeletonRagdoll.h" +enum StandingFootState { + LEFT_FOOT, + RIGHT_FOOT, + NO_FOOT +}; + SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) : Model(parent), _owningAvatar(owningAvatar), _boundingShape(), _boundingShapeLocalOffset(0.0f), _ragdoll(NULL), - _defaultEyeModelPosition(glm::vec3(0.f, 0.f, 0.f)) { + _defaultEyeModelPosition(glm::vec3(0.f, 0.f, 0.f)), + _standingFoot(NO_FOOT), + _standingOffset(0.0f), + _clampedFootPosition(0.0f) { } SkeletonModel::~SkeletonModel() { @@ -607,6 +616,62 @@ void SkeletonModel::updateVisibleJointStates() { } } +/// \return offset of hips after foot animation +void SkeletonModel::updateStandingFoot() { + glm::vec3 offset(0.0f); + int leftFootIndex = _geometry->getFBXGeometry().leftToeJointIndex; + int rightFootIndex = _geometry->getFBXGeometry().rightToeJointIndex; + + if (leftFootIndex != -1 && rightFootIndex != -1) { + glm::vec3 leftPosition, rightPosition; + getJointPosition(leftFootIndex, leftPosition); + getJointPosition(rightFootIndex, rightPosition); + + int lowestFoot = (leftPosition.y < rightPosition.y) ? LEFT_FOOT : RIGHT_FOOT; + const float MIN_STEP_HEIGHT_THRESHOLD = 0.05f; + bool oneFoot = fabsf(leftPosition.y - rightPosition.y) > MIN_STEP_HEIGHT_THRESHOLD; + int currentFoot = oneFoot ? lowestFoot : _standingFoot; + + if (_standingFoot == NO_FOOT) { + currentFoot = lowestFoot; + } + if (currentFoot != _standingFoot) { + if (_standingFoot == NO_FOOT) { + // pick the lowest foot + glm::vec3 lowestPosition = (currentFoot == LEFT_FOOT) ? leftPosition : rightPosition; + // we ignore zero length positions which can happen for a few frames until skeleton is fully loaded + if (glm::length(lowestPosition) > 0.0f) { + _standingFoot = currentFoot; + _clampedFootPosition = lowestPosition; + } + } else { + // swap feet + _standingFoot = currentFoot; + glm::vec3 nextPosition = leftPosition; + glm::vec3 prevPosition = rightPosition; + if (_standingFoot == RIGHT_FOOT) { + nextPosition = rightPosition; + prevPosition = leftPosition; + } + glm::vec3 oldOffset = _clampedFootPosition - prevPosition; + _clampedFootPosition = oldOffset + nextPosition; + offset = _clampedFootPosition - nextPosition; + } + } else { + glm::vec3 nextPosition = (_standingFoot == LEFT_FOOT) ? leftPosition : rightPosition; + offset = _clampedFootPosition - nextPosition; + } + + // clamp the offset to not exceed some max distance + const float MAX_STEP_OFFSET = 1.0f; + float stepDistance = glm::length(offset); + if (stepDistance > MAX_STEP_OFFSET) { + offset *= (MAX_STEP_OFFSET / stepDistance); + } + } + _standingOffset = offset; +} + SkeletonRagdoll* SkeletonModel::buildRagdoll() { if (!_ragdoll) { _ragdoll = new SkeletonRagdoll(this); diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 81e3fcb3b6..ea732acfd5 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -101,6 +101,10 @@ public: /// \return whether or not the head was found. glm::vec3 getDefaultEyeModelPosition() const; + /// skeleton offset caused by moving feet + void updateStandingFoot(); + const glm::vec3& getStandingOffset() const { return _standingOffset; } + virtual void updateVisibleJointStates(); SkeletonRagdoll* buildRagdoll(); @@ -154,6 +158,9 @@ private: SkeletonRagdoll* _ragdoll; glm::vec3 _defaultEyeModelPosition; + int _standingFoot; + glm::vec3 _standingOffset; + glm::vec3 _clampedFootPosition; }; #endif // hifi_SkeletonModel_h diff --git a/interface/src/ui/ToolWindow.cpp b/interface/src/ui/ToolWindow.cpp index 44ea44c47d..de88d75b3d 100644 --- a/interface/src/ui/ToolWindow.cpp +++ b/interface/src/ui/ToolWindow.cpp @@ -17,6 +17,7 @@ const int DEFAULT_WIDTH = 300; ToolWindow::ToolWindow(QWidget* parent) : QMainWindow(parent), + _selfHidden(false), _hasShown(false), _lastGeometry() { diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 1c9c93c00e..d31ab83976 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -78,6 +78,17 @@ float AudioInjector::getLoudness() { } void AudioInjector::injectAudio() { + + // check if we need to offset the sound by some number of seconds + if (_options.secondOffset > 0.0f) { + + // convert the offset into a number of bytes + int byteOffset = (int) floorf(SAMPLE_RATE * _options.secondOffset * (_options.stereo ? 2.0f : 1.0f)); + byteOffset *= sizeof(int16_t); + + _currentSendPosition = byteOffset; + } + if (_options.localOnly) { injectLocally(); } else { @@ -89,10 +100,14 @@ void AudioInjector::injectLocally() { bool success = false; if (_localAudioInterface) { if (_audioData.size() > 0) { + _localBuffer = new AudioInjectorLocalBuffer(_audioData, this); _localBuffer->open(QIODevice::ReadOnly); _localBuffer->setShouldLoop(_options.loop); + // give our current send position to the local buffer + _localBuffer->setCurrentOffset(_currentSendPosition); + QMetaObject::invokeMethod(_localAudioInterface, "outputLocalInjector", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, success), @@ -100,7 +115,8 @@ void AudioInjector::injectLocally() { Q_ARG(qreal, _options.volume), Q_ARG(AudioInjector*, this)); - + // if we're not looping and the buffer tells us it is empty then emit finished + connect(_localBuffer, &AudioInjectorLocalBuffer::bufferEmpty, this, &AudioInjector::stop); if (!success) { qDebug() << "AudioInjector::injectLocally could not output locally via _localAudioInterface"; diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.cpp b/libraries/audio/src/AudioInjectorLocalBuffer.cpp index bdf084091d..a58d686498 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.cpp +++ b/libraries/audio/src/AudioInjectorLocalBuffer.cpp @@ -47,6 +47,11 @@ qint64 AudioInjectorLocalBuffer::readData(char* data, qint64 maxSize) { _currentOffset += bytesRead; } + if (!_shouldLoop && bytesRead == bytesToEnd) { + // we hit the end of the buffer, emit a signal + emit bufferEmpty(); + } + return bytesRead; } else { return 0; diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.h b/libraries/audio/src/AudioInjectorLocalBuffer.h index 8b32c6fbc7..399c7515ec 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.h +++ b/libraries/audio/src/AudioInjectorLocalBuffer.h @@ -26,6 +26,9 @@ public: void setShouldLoop(bool shouldLoop) { _shouldLoop = shouldLoop; } + void setCurrentOffset(int currentOffset) { _currentOffset = currentOffset; } +signals: + void bufferEmpty(); private: qint64 recursiveReadFromFront(char* data, qint64 maxSize); diff --git a/libraries/audio/src/AudioInjectorOptions.cpp b/libraries/audio/src/AudioInjectorOptions.cpp index df435cf2cc..824a816382 100644 --- a/libraries/audio/src/AudioInjectorOptions.cpp +++ b/libraries/audio/src/AudioInjectorOptions.cpp @@ -20,7 +20,8 @@ AudioInjectorOptions::AudioInjectorOptions() : orientation(glm::vec3(0.0f, 0.0f, 0.0f)), stereo(false), ignorePenumbra(false), - localOnly(false) + localOnly(false), + secondOffset(0.0) { } @@ -31,9 +32,9 @@ QScriptValue injectorOptionsToScriptValue(QScriptEngine* engine, const AudioInje obj.setProperty("volume", injectorOptions.volume); obj.setProperty("loop", injectorOptions.loop); obj.setProperty("orientation", quatToScriptValue(engine, injectorOptions.orientation)); - obj.setProperty("stereo", injectorOptions.stereo); obj.setProperty("ignorePenumbra", injectorOptions.ignorePenumbra); obj.setProperty("localOnly", injectorOptions.localOnly); + obj.setProperty("secondOffset", injectorOptions.secondOffset); return obj; } @@ -54,10 +55,6 @@ void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOpt quatFromScriptValue(object.property("orientation"), injectorOptions.orientation); } - if (object.property("stereo").isValid()) { - injectorOptions.stereo = object.property("stereo").toBool(); - } - if (object.property("ignorePenumbra").isValid()) { injectorOptions.ignorePenumbra = object.property("ignorePenumbra").toBool(); } @@ -65,4 +62,8 @@ void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOpt if (object.property("localOnly").isValid()) { injectorOptions.localOnly = object.property("localOnly").toBool(); } + + if (object.property("secondOffset").isValid()) { + injectorOptions.secondOffset = object.property("secondOffset").toNumber(); + } } \ No newline at end of file diff --git a/libraries/audio/src/AudioInjectorOptions.h b/libraries/audio/src/AudioInjectorOptions.h index 4fd3a0b7ae..d06dc9eb63 100644 --- a/libraries/audio/src/AudioInjectorOptions.h +++ b/libraries/audio/src/AudioInjectorOptions.h @@ -27,6 +27,7 @@ public: bool stereo; bool ignorePenumbra; bool localOnly; + float secondOffset; }; Q_DECLARE_METATYPE(AudioInjectorOptions); diff --git a/libraries/audio/src/AudioScriptingInterface.cpp b/libraries/audio/src/AudioScriptingInterface.cpp index 35a11f4dd4..b604e2825b 100644 --- a/libraries/audio/src/AudioScriptingInterface.cpp +++ b/libraries/audio/src/AudioScriptingInterface.cpp @@ -44,7 +44,11 @@ void AudioScriptingInterface::stopAllInjectors() { AudioInjector* AudioScriptingInterface::playSound(Sound* sound, const AudioInjectorOptions& injectorOptions) { if (sound) { - AudioInjector* injector = new AudioInjector(sound, injectorOptions); + // stereo option isn't set from script, this comes from sound metadata or filename + AudioInjectorOptions optionsCopy = injectorOptions; + optionsCopy.stereo = sound->isStereo(); + + AudioInjector* injector = new AudioInjector(sound, optionsCopy); injector->setLocalAudioInterface(_localAudioInterface); QThread* injectorThread = new QThread(); diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 366659b633..666cdee6b3 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -169,9 +169,16 @@ int InboundAudioStream::parseData(const QByteArray& packet) { } int InboundAudioStream::parseStreamProperties(PacketType type, const QByteArray& packetAfterSeqNum, int& numAudioSamples) { - // mixed audio packets do not have any info between the seq num and the audio data. - numAudioSamples = packetAfterSeqNum.size() / sizeof(int16_t); - return 0; + if (type == PacketTypeSilentAudioFrame) { + quint16 numSilentSamples = 0; + memcpy(&numSilentSamples, packetAfterSeqNum.constData(), sizeof(quint16)); + numAudioSamples = numSilentSamples; + return sizeof(quint16); + } else { + // mixed audio packets do not have any info between the seq num and the audio data. + numAudioSamples = packetAfterSeqNum.size() / sizeof(int16_t); + return 0; + } } int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples) { diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index ff54e262f8..2608c333d6 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -64,7 +64,14 @@ void Sound::downloadFinished(QNetworkReply* reply) { interpretAsWav(rawAudioByteArray, outputAudioByteArray); downSample(outputAudioByteArray); } else { - // Process as RAW file + // check if this was a stereo raw file + // since it's raw the only way for us to know that is if the file was called .stereo.raw + if (reply->url().fileName().toLower().endsWith("stereo.raw")) { + _isStereo = true; + qDebug() << "Processing sound from" << reply->url() << "as stereo audio file."; + } + + // Process as RAW file downSample(rawAudioByteArray); } trimFrames(); @@ -206,10 +213,12 @@ void Sound::interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& ou qDebug() << "Currently not supporting non PCM audio files."; return; } - if (qFromLittleEndian(fileHeader.wave.numChannels) != 1) { - qDebug() << "Currently not supporting stereo audio files."; - return; + if (qFromLittleEndian(fileHeader.wave.numChannels) == 2) { + _isStereo = true; + } else if (qFromLittleEndian(fileHeader.wave.numChannels) > 2) { + qDebug() << "Currently not support audio files with more than 2 channels."; } + if (qFromLittleEndian(fileHeader.wave.bitsPerSample) != 16) { qDebug() << "Currently not supporting non 16bit audio files."; return; diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h index c78bf72ff7..02b75417e8 100644 --- a/libraries/audio/src/Sound.h +++ b/libraries/audio/src/Sound.h @@ -25,7 +25,7 @@ class Sound : public Resource { public: Sound(const QUrl& url, bool isStereo = false); - bool isStereo() const { return _isStereo; } + bool isStereo() const { return _isStereo; } bool isReady() const { return _isReady; } const QByteArray& getByteArray() { return _byteArray; } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index d0fafa06b1..98dda6f33f 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -90,6 +90,7 @@ EntityItem::EntityItem(const EntityItemID& entityItemID) { _lastEditedFromRemoteInRemoteTime = 0; _lastUpdated = 0; _created = 0; + _updateFlags = 0; _changedOnServer = 0; initFromEntityItemID(entityItemID); _simulationState = EntityItem::Static; @@ -102,6 +103,7 @@ EntityItem::EntityItem(const EntityItemID& entityItemID, const EntityItemPropert _lastEditedFromRemoteInRemoteTime = 0; _lastUpdated = 0; _created = properties.getCreated(); + _updateFlags = 0; _changedOnServer = 0; initFromEntityItemID(entityItemID); setProperties(properties, true); // force copy @@ -465,7 +467,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef dataAt += propertyFlags.getEncodedLength(); bytesRead += propertyFlags.getEncodedLength(); - READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, _position); + READ_ENTITY_PROPERTY_SETTER(PROP_POSITION, glm::vec3, updatePosition); // Old bitstreams had PROP_RADIUS, new bitstreams have PROP_DIMENSIONS if (args.bitstreamVersion < VERSION_ENTITIES_SUPPORT_DIMENSIONS) { @@ -484,7 +486,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } } else { - READ_ENTITY_PROPERTY(PROP_DIMENSIONS, glm::vec3, _dimensions); + READ_ENTITY_PROPERTY_SETTER(PROP_DIMENSIONS, glm::vec3, setDimensions); if (wantDebug) { qDebug() << " readEntityDataFromBuffer() NEW FORMAT... look for PROP_DIMENSIONS"; } @@ -494,19 +496,19 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef qDebug() << " readEntityDataFromBuffer() _dimensions:" << getDimensionsInMeters() << " in meters"; } - READ_ENTITY_PROPERTY_QUAT(PROP_ROTATION, _rotation); - READ_ENTITY_PROPERTY(PROP_MASS, float, _mass); - READ_ENTITY_PROPERTY(PROP_VELOCITY, glm::vec3, _velocity); - READ_ENTITY_PROPERTY(PROP_GRAVITY, glm::vec3, _gravity); + READ_ENTITY_PROPERTY_QUAT_SETTER(PROP_ROTATION, updateRotation); + READ_ENTITY_PROPERTY_SETTER(PROP_MASS, float, updateMass); + READ_ENTITY_PROPERTY_SETTER(PROP_VELOCITY, glm::vec3, updateVelocity); + READ_ENTITY_PROPERTY_SETTER(PROP_GRAVITY, glm::vec3, updateGravity); READ_ENTITY_PROPERTY(PROP_DAMPING, float, _damping); - READ_ENTITY_PROPERTY(PROP_LIFETIME, float, _lifetime); + READ_ENTITY_PROPERTY_SETTER(PROP_LIFETIME, float, updateLifetime); READ_ENTITY_PROPERTY_STRING(PROP_SCRIPT,setScript); READ_ENTITY_PROPERTY(PROP_REGISTRATION_POINT, glm::vec3, _registrationPoint); - READ_ENTITY_PROPERTY(PROP_ANGULAR_VELOCITY, glm::vec3, _angularVelocity); + READ_ENTITY_PROPERTY_SETTER(PROP_ANGULAR_VELOCITY, glm::vec3, updateAngularVelocity); READ_ENTITY_PROPERTY(PROP_ANGULAR_DAMPING, float, _angularDamping); READ_ENTITY_PROPERTY(PROP_VISIBLE, bool, _visible); - READ_ENTITY_PROPERTY(PROP_IGNORE_FOR_COLLISIONS, bool, _ignoreForCollisions); - READ_ENTITY_PROPERTY(PROP_COLLISIONS_WILL_MOVE, bool, _collisionsWillMove); + READ_ENTITY_PROPERTY_SETTER(PROP_IGNORE_FOR_COLLISIONS, bool, updateIgnoreForCollisions); + READ_ENTITY_PROPERTY_SETTER(PROP_COLLISIONS_WILL_MOVE, bool, updateCollisionsWillMove); READ_ENTITY_PROPERTY(PROP_LOCKED, bool, _locked); READ_ENTITY_PROPERTY_STRING(PROP_USER_DATA,setUserData); @@ -731,11 +733,6 @@ bool EntityItem::lifetimeHasExpired() const { return isMortal() && (getAge() > getLifetime()); } - -void EntityItem::copyChangedProperties(const EntityItem& other) { - *this = other; -} - EntityItemProperties EntityItem::getProperties() const { EntityItemProperties properties; properties._id = getID(); @@ -947,4 +944,111 @@ void EntityItem::recalculateCollisionShape() { _collisionShape.setScale(entityAACube.getScale()); } +void EntityItem::updatePosition(const glm::vec3& value) { + if (_position != value) { + _position = value; + recalculateCollisionShape(); + _updateFlags |= EntityItem::UPDATE_POSITION; + } +} + +void EntityItem::updatePositionInMeters(const glm::vec3& value) { + glm::vec3 position = glm::clamp(value / (float) TREE_SCALE, 0.0f, 1.0f); + if (_position != position) { + _position = position; + recalculateCollisionShape(); + _updateFlags |= EntityItem::UPDATE_POSITION; + } +} + +void EntityItem::updateDimensions(const glm::vec3& value) { + if (_dimensions != value) { + _dimensions = value; + recalculateCollisionShape(); + _updateFlags |= EntityItem::UPDATE_SHAPE; + } +} + +void EntityItem::updateDimensionsInMeters(const glm::vec3& value) { + glm::vec3 dimensions = value / (float) TREE_SCALE; + if (_dimensions != dimensions) { + _dimensions = dimensions; + recalculateCollisionShape(); + _updateFlags |= EntityItem::UPDATE_SHAPE; + } +} + +void EntityItem::updateRotation(const glm::quat& rotation) { + if (_rotation != rotation) { + _rotation = rotation; + recalculateCollisionShape(); + _updateFlags |= EntityItem::UPDATE_POSITION; + } +} + +void EntityItem::updateMass(float value) { + if (_mass != value) { + _mass = value; + _updateFlags |= EntityItem::UPDATE_MASS; + } +} + +void EntityItem::updateVelocity(const glm::vec3& value) { + if (_velocity != value) { + _velocity = value; + _updateFlags |= EntityItem::UPDATE_VELOCITY; + } +} + +void EntityItem::updateVelocityInMeters(const glm::vec3& value) { + glm::vec3 velocity = value / (float) TREE_SCALE; + if (_velocity != velocity) { + _velocity = velocity; + _updateFlags |= EntityItem::UPDATE_VELOCITY; + } +} + +void EntityItem::updateGravity(const glm::vec3& value) { + if (_gravity != value) { + _gravity = value; + _updateFlags |= EntityItem::UPDATE_VELOCITY; + } +} + +void EntityItem::updateGravityInMeters(const glm::vec3& value) { + glm::vec3 gravity = value / (float) TREE_SCALE; + if (_gravity != gravity) { + _gravity = gravity; + _updateFlags |= EntityItem::UPDATE_VELOCITY; + } +} + +void EntityItem::updateAngularVelocity(const glm::vec3& value) { + if (_angularVelocity != value) { + _angularVelocity = value; + _updateFlags |= EntityItem::UPDATE_VELOCITY; + } +} + +void EntityItem::updateIgnoreForCollisions(bool value) { + if (_ignoreForCollisions != value) { + _ignoreForCollisions = value; + _updateFlags |= EntityItem::UPDATE_COLLISION_GROUP; + } +} + +void EntityItem::updateCollisionsWillMove(bool value) { + if (_collisionsWillMove != value) { + _collisionsWillMove = value; + _updateFlags |= EntityItem::UPDATE_MOTION_TYPE; + } +} + +void EntityItem::updateLifetime(float value) { + if (_lifetime != value) { + _lifetime = value; + _updateFlags |= EntityItem::UPDATE_LIFETIME; + } +} + diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index d57f547cc5..3f63c96c4e 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -35,13 +35,23 @@ class EntityTreeElementExtraEncodeData; #define DONT_ALLOW_INSTANTIATION virtual void pureVirtualFunctionPlaceHolder() = 0; #define ALLOW_INSTANTIATION virtual void pureVirtualFunctionPlaceHolder() { }; - /// EntityItem class this is the base class for all entity types. It handles the basic properties and functionality available /// to all other entity types. In particular: postion, size, rotation, age, lifetime, velocity, gravity. You can not instantiate /// one directly, instead you must only construct one of it's derived classes with additional features. class EntityItem { public: + enum EntityUpdateFlags { + UPDATE_POSITION = 0x0001, + UPDATE_VELOCITY = 0x0002, + UPDATE_MASS = 0x0004, + UPDATE_COLLISION_GROUP = 0x0008, + UPDATE_MOTION_TYPE = 0x0010, + UPDATE_SHAPE = 0x0020, + UPDATE_LIFETIME = 0x0040 + //UPDATE_APPEARANCE = 0x8000, + }; + DONT_ALLOW_INSTANTIATION // This class can not be instantiated directly EntityItem(const EntityItemID& entityItemID); @@ -125,9 +135,6 @@ public: virtual void debugDump() const; - // similar to assignment/copy, but it handles keeping lifetime accurate - void copyChangedProperties(const EntityItem& other); - // attributes applicable to all entity types EntityTypes::EntityType getType() const { return _type; } const glm::vec3& getPosition() const { return _position; } /// get position in domain scale units (0.0 - 1.0) @@ -266,6 +273,30 @@ public: virtual const Shape& getCollisionShapeInMeters() const { return _collisionShape; } virtual bool contains(const glm::vec3& point) const { return getAABox().contains(point); } + // updateFoo() methods to be used when changes need to be accumulated in the _updateFlags + void updatePosition(const glm::vec3& value); + void updatePositionInMeters(const glm::vec3& value); + void updateDimensions(const glm::vec3& value); + void updateDimensionsInMeters(const glm::vec3& value); + void updateRotation(const glm::quat& rotation); + void updateMass(float value); + void updateVelocity(const glm::vec3& value); + void updateVelocityInMeters(const glm::vec3& value); + void updateGravity(const glm::vec3& value); + void updateGravityInMeters(const glm::vec3& value); + void updateAngularVelocity(const glm::vec3& value); + void updateIgnoreForCollisions(bool value); + void updateCollisionsWillMove(bool value); + void updateLifetime(float value); + + uint32_t getUpdateFlags() const { return _updateFlags; } + void clearUpdateFlags() { _updateFlags = 0; } + +#ifdef USE_BULLET_PHYSICS + EntityMotionState* getMotionState() const { return _motionState; } + virtual EntityMotionState* createMotionState() { return NULL; } + void destroyMotionState(); +#endif // USE_BULLET_PHYSICS SimulationState getSimulationState() const { return _simulationState; } protected: @@ -314,6 +345,10 @@ protected: AACubeShape _collisionShape; SimulationState _simulationState; // only set by EntityTree + + // UpdateFlags are set whenever a property changes that requires the change to be communicated to other + // data structures. It is the responsibility of the EntityTree to relay changes entity and clear flags. + uint32_t _updateFlags; }; diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index 8ad444a975..3288e56a3d 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -63,6 +63,17 @@ } \ } +#define READ_ENTITY_PROPERTY_QUAT_SETTER(P,M) \ + if (propertyFlags.getHasProperty(P)) { \ + glm::quat fromBuffer; \ + int bytes = unpackOrientationQuatFromBytes(dataAt, fromBuffer); \ + dataAt += bytes; \ + bytesRead += bytes; \ + if (overwriteLocalData) { \ + M(fromBuffer); \ + } \ + } + #define READ_ENTITY_PROPERTY_STRING(P,O) \ if (propertyFlags.getHasProperty(P)) { \ uint16_t length; \ diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 29c4a8b19a..e66da97390 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -44,6 +44,20 @@ EntityItemID EntityScriptingInterface::addEntity(const EntityItemProperties& pro return id; } +EntityItemID EntityScriptingInterface::getEntityItemID(const QString& uuid) { + EntityItemID entityID = EntityItemID(QUuid(uuid), UNKNOWN_ENTITY_TOKEN, false); + + _entityTree->lockForRead(); + EntityItem* entity = const_cast(_entityTree->findEntityByEntityItemID(entityID)); + _entityTree->unlock(); + + if (entity) { + return entity->getEntityItemID(); + } + + return entityID; +} + EntityItemID EntityScriptingInterface::identifyEntity(EntityItemID entityID) { EntityItemID actualID = entityID; diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 2150fa51da..7269579ab0 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -64,6 +64,9 @@ public slots: /// adds a model with the specific properties Q_INVOKABLE EntityItemID addEntity(const EntityItemProperties& properties); + // Get EntityItemID from uuid string + Q_INVOKABLE EntityItemID getEntityItemID(const QString& entityID); + /// identify a recently created model to determine its true ID Q_INVOKABLE EntityItemID identifyEntity(EntityItemID entityID); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index a0b2d6f9c3..b25b153f44 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -657,12 +657,13 @@ void EntityTree::updateChangedEntities(quint64 now, QSet& entities foreach (EntityItem* thisEntity, _changedEntities) { // check to see if the lifetime has expired, for immortal entities this is always false if (thisEntity->lifetimeHasExpired()) { - qDebug() << "Lifetime has expired for thisEntity:" << thisEntity->getEntityItemID(); + qDebug() << "Lifetime has expired for entity:" << thisEntity->getEntityItemID(); entitiesToDelete << thisEntity->getEntityItemID(); clearEntityState(thisEntity); } else { updateEntityState(thisEntity); } + thisEntity->clearUpdateFlags(); } _changedEntities.clear(); } diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 7d4d95bf94..2c189846eb 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -739,8 +739,9 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int EntityTreeElement* currentContainingElement = _myTree->getContainingElement(entityItemID); bytesForThisEntity = entityItem->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); - // TODO: Andrew to only set changed if something has actually changed - _myTree->entityChanged(entityItem); + if (entityItem->getUpdateFlags()) { + _myTree->entityChanged(entityItem); + } bool bestFitAfter = bestFitEntityBounds(entityItem); if (bestFitBefore != bestFitAfter) { diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index 2f70e96c60..9cee11d73c 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -47,6 +47,13 @@ UpdateEntityOperator::UpdateEntityOperator(EntityTree* tree, // the getMaximumAACube is the relaxed form. _oldEntityCube = _existingEntity->getMaximumAACube(); _oldEntityBox = _oldEntityCube.clamp(0.0f, 1.0f); // clamp to domain bounds + + // If the old properties doesn't contain the properties required to calculate a bounding box, + // get them from the existing entity. Registration point is required to correctly calculate + // the bounding box. + if (!_properties.registrationPointChanged()) { + _properties.setRegistrationPoint(_existingEntity->getRegistrationPoint()); + } // If the new properties has position OR dimension changes, but not both, we need to // get the old property value and set it in our properties in order for our bounds diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 4ffd3f6286..e0f175d60d 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1067,6 +1067,8 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) QString jointHeadID; QString jointLeftHandID; QString jointRightHandID; + QString jointLeftToeID; + QString jointRightToeID; QVector humanIKJointNames; for (int i = 0;; i++) { @@ -1166,11 +1168,17 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } else if (name == jointHeadName) { jointHeadID = getID(object.properties); - } else if (name == jointLeftHandName) { + } else if (name == jointLeftHandName || name == "LeftHand" || name == "joint_L_hand") { jointLeftHandID = getID(object.properties); - } else if (name == jointRightHandName) { + } else if (name == jointRightHandName || name == "RightHand" || name == "joint_R_hand") { jointRightHandID = getID(object.properties); + + } else if (name == "LeftToe" || name == "joint_L_toe" || name == "LeftToe_End") { + jointLeftToeID = getID(object.properties); + + } else if (name == "RightToe" || name == "joint_R_toe" || name == "RightToe_End") { + jointRightToeID = getID(object.properties); } int humanIKJointIndex = humanIKJointNames.indexOf(name); if (humanIKJointIndex != -1) { @@ -1595,6 +1603,8 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) geometry.headJointIndex = modelIDs.indexOf(jointHeadID); geometry.leftHandJointIndex = modelIDs.indexOf(jointLeftHandID); geometry.rightHandJointIndex = modelIDs.indexOf(jointRightHandID); + geometry.leftToeJointIndex = modelIDs.indexOf(jointLeftToeID); + geometry.rightToeJointIndex = modelIDs.indexOf(jointRightToeID); foreach (const QString& id, humanIKJointIDs) { geometry.humanIKJointIndices.append(modelIDs.indexOf(id)); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 3c9e918686..4a585c2476 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -201,6 +201,8 @@ public: int headJointIndex; int leftHandJointIndex; int rightHandJointIndex; + int leftToeJointIndex; + int rightToeJointIndex; QVector humanIKJointIndices; diff --git a/libraries/networking/src/PacketHeaders.h b/libraries/networking/src/PacketHeaders.h index 6cca5b505d..7557ea31e1 100644 --- a/libraries/networking/src/PacketHeaders.h +++ b/libraries/networking/src/PacketHeaders.h @@ -21,57 +21,57 @@ // NOTE: if adding a new packet type, you can replace one marked usable or add at the end // NOTE: if you want the name of the packet type to be available for debugging or logging, update nameForPacketType() as well enum PacketType { - PacketTypeUnknown, + PacketTypeUnknown, // 0 PacketTypeStunResponse, PacketTypeDomainList, PacketTypePing, PacketTypePingReply, - PacketTypeKillAvatar, + PacketTypeKillAvatar, // 5 PacketTypeAvatarData, PacketTypeInjectAudio, PacketTypeMixedAudio, PacketTypeMicrophoneAudioNoEcho, - PacketTypeMicrophoneAudioWithEcho, + PacketTypeMicrophoneAudioWithEcho, // 10 PacketTypeBulkAvatarData, PacketTypeSilentAudioFrame, PacketTypeEnvironmentData, PacketTypeDomainListRequest, - PacketTypeRequestAssignment, + PacketTypeRequestAssignment, // 15 PacketTypeCreateAssignment, PacketTypeDomainConnectionDenied, PacketTypeMuteEnvironment, PacketTypeAudioStreamStats, - PacketTypeDataServerConfirm, + PacketTypeDataServerConfirm, // 20 PacketTypeVoxelQuery, PacketTypeVoxelData, PacketTypeVoxelSet, PacketTypeVoxelSetDestructive, - PacketTypeVoxelErase, - PacketTypeOctreeStats, // 26 + PacketTypeVoxelErase, // 25 + PacketTypeOctreeStats, PacketTypeJurisdiction, PacketTypeJurisdictionRequest, UNUSED_1, - UNUSED_2, + UNUSED_2, // 30 UNUSED_3, UNUSED_4, PacketTypeNoisyMute, PacketTypeMetavoxelData, - PacketTypeAvatarIdentity, + PacketTypeAvatarIdentity, // 35 PacketTypeAvatarBillboard, PacketTypeDomainConnectRequest, PacketTypeDomainServerRequireDTLS, PacketTypeNodeJsonStats, - PacketTypeEntityQuery, - PacketTypeEntityData, // 41 + PacketTypeEntityQuery, // 40 + PacketTypeEntityData, PacketTypeEntityAddOrEdit, PacketTypeEntityErase, PacketTypeEntityAddResponse, PacketTypeOctreeDataNack, // 45 PacketTypeVoxelEditNack, PacketTypeAudioEnvironment, - PacketTypeEntityEditNack, // 48 + PacketTypeEntityEditNack, PacketTypeSignedTransactionPayment, - PacketTypeIceServerHeartbeat, + PacketTypeIceServerHeartbeat, // 50 PacketTypeIceServerHeartbeatResponse, PacketTypeUnverifiedPing, PacketTypeUnverifiedPingReply