diff --git a/examples/editModels.js b/examples/editModels.js index 50b0137c4f..ecf398edfa 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +var windowDimensions = Controller.getViewportDimensions(); + var LASER_WIDTH = 4; var LASER_COLOR = { red: 255, green: 0, blue: 0 }; var LASER_LENGTH_FACTOR = 1.5; @@ -16,6 +18,40 @@ var LASER_LENGTH_FACTOR = 1.5; var LEFT = 0; var RIGHT = 1; + +var SPAWN_DISTANCE = 1; +var radiusDefault = 0.10; + +var modelURLs = [ + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Feisar_Ship.FBX", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/birarda/birarda_head.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/pug.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/newInvader16x16-large-purple.svo", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/minotaur/mino_full.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Combat_tank_V01.FBX", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/orc.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/slimer.fbx", + ]; + +var toolIconUrl = "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/tools/"; +var numberOfTools = 1; +var toolHeight = 50; +var toolWidth = 50; +var toolVerticalSpacing = 4; +var toolsHeight = toolHeight * numberOfTools + toolVerticalSpacing * (numberOfTools - 1); +var toolsX = windowDimensions.x - 8 - toolWidth; +var toolsY = (windowDimensions.y - toolsHeight) / 2; + + +var firstModel = Overlays.addOverlay("image", { + x: 0, y: 0, width: toolWidth, height: toolHeight, + subImage: { x: 0, y: toolHeight, width: toolWidth, height: toolHeight }, + imageURL: toolIconUrl + "voxel-tool.svg", + x: toolsX, y: toolsY + ((toolHeight + toolVerticalSpacing) * 0), width: toolWidth, height: toolHeight, + visible: true, + alpha: 0.9 + }); + function controller(wichSide) { this.side = wichSide; this.palm = 2 * wichSide; @@ -46,7 +82,10 @@ function controller(wichSide) { this.pressing = false; // is trigger being pressed (is pressed now but wasn't previously) this.grabbing = false; - this.modelID; + this.modelID = { isKnownID: false }; + this.oldModelRotation; + this.oldModelPosition; + this.oldModelRadius; this.laser = Overlays.addOverlay("line3d", { position: this.palmPosition, @@ -85,23 +124,19 @@ function controller(wichSide) { - this.grab = function (modelID) { - if (!modelID.isKnownID) { - var identify = Models.identifyModel(modelID); - if (!identify.isKnownID) { - print("Unknown ID " + identify.id + "(grab)"); - return; - } - modelID = identify; - } + this.grab = function (modelID, properties) { print("Grabbing " + modelID.id); this.grabbing = true; this.modelID = modelID; + + this.oldModelPosition = properties.position; + this.oldModelRotation = properties.modelRotation; + this.oldModelRadius = properties.radius; } this.release = function () { this.grabbing = false; - this.modelID = 0; + this.modelID.isKnownID = false; } this.checkTrigger = function () { @@ -118,6 +153,34 @@ function controller(wichSide) { } } + this.checkModel = function (properties) { + // P P - Model + // /| A - Palm + // / | d B - unit vector toward tip + // / | X - base of the perpendicular line + // A---X----->B d - distance fom axis + // x x - distance from A + // + // |X-A| = (P-A).B + // X == A + ((P-A).B)B + // d = |P-X| + + var A = this.palmPosition; + var B = this.front; + var P = properties.position; + + var x = Vec3.dot(Vec3.subtract(P, A), B); + var y = Vec3.dot(Vec3.subtract(P, A), this.up); + var z = Vec3.dot(Vec3.subtract(P, A), this.right); + var X = Vec3.sum(A, Vec3.multiply(B, x)); + var d = Vec3.length(Vec3.subtract(P, X)); + + if (d < properties.radius && 0 < x && x < LASER_LENGTH_FACTOR) { + return { valid: true, x: x, y: y, z: z }; + } + return { valid: false }; + } + this.moveLaser = function () { var endPosition = Vec3.sum(this.palmPosition, Vec3.multiply(this.front, LASER_LENGTH_FACTOR)); @@ -143,44 +206,33 @@ function controller(wichSide) { }); } - this.checkModel = function (modelID) { - if (!modelID.isKnownID) { - var identify = Models.identifyModel(modelID); - if (!identify.isKnownID) { - print("Unknown ID " + identify.id + "(checkModel)"); - return; - } - modelID = identify; + this.moveModel = function () { + if (this.grabbing) { + var newPosition = Vec3.sum(this.palmPosition, + Vec3.multiply(this.front, this.x)); + newPosition = Vec3.sum(newPosition, + Vec3.multiply(this.up, this.y)); + newPosition = Vec3.sum(newPosition, + Vec3.multiply(this.right, this.z)); + + var newRotation = Quat.multiply(this.rotation, + Quat.inverse(this.oldRotation)); + newRotation = Quat.multiply(newRotation, + this.oldModelRotation); + + Models.editModel(this.modelID, { + position: newPosition, + modelRotation: newRotation + }); + print("Moving " + this.modelID.id); +// Vec3.print("Old Position: ", this.oldModelPosition); +// Vec3.print("Sav Position: ", newPosition); + Quat.print("Old Rotation: ", this.oldModelRotation); + Quat.print("New Rotation: ", newRotation); + + this.oldModelRotation = newRotation; + this.oldModelPosition = newPosition; } - // P P - Model - // /| A - Palm - // / | d B - unit vector toward tip - // / | X - base of the perpendicular line - // A---X----->B d - distance fom axis - // x x - distance from A - // - // |X-A| = (P-A).B - // X == A + ((P-A).B)B - // d = |P-X| - - var A = this.palmPosition; - var B = this.front; - var P = Models.getModelProperties(modelID).position; - - this.x = Vec3.dot(Vec3.subtract(P, A), B); - this.y = Vec3.dot(Vec3.subtract(P, A), this.up); - this.z = Vec3.dot(Vec3.subtract(P, A), this.right); - var X = Vec3.sum(A, Vec3.multiply(B, this.x)); - var d = Vec3.length(Vec3.subtract(P, X)); - -// Vec3.print("A: ", A); -// Vec3.print("B: ", B); -// Vec3.print("Particle pos: ", P); -// print("d: " + d + ", x: " + this.x); - if (d < Models.getModelProperties(modelID).radius && 0 < this.x && this.x < LASER_LENGTH_FACTOR) { - return true; - } - return false; } this.update = function () { @@ -205,25 +257,40 @@ function controller(wichSide) { this.checkTrigger(); - if (this.pressing) { - Vec3.print("Looking at: ", this.palmPosition); - var foundModels = Models.findModels(this.palmPosition, LASER_LENGTH_FACTOR); - for (var i = 0; i < foundModels.length; i++) { - print("Model found ID (" + foundModels[i].id + ")"); - if (this.checkModel(foundModels[i])) { - if (this.grab(foundModels[i])) { - return; - } - } - } - } + this.moveLaser(); if (!this.pressed && this.grabbing) { // release if trigger not pressed anymore. this.release(); } - - this.moveLaser(); + + if (this.pressing) { + Vec3.print("Looking at: ", this.palmPosition); + var foundModels = Models.findModels(this.palmPosition, LASER_LENGTH_FACTOR); + for (var i = 0; i < foundModels.length; i++) { + + if (!foundModels[i].isKnownID) { + var identify = Models.identifyModel(foundModels[i]); + if (!identify.isKnownID) { + print("Unknown ID " + identify.id + "(update loop)"); + return; + } + foundModels[i] = identify; + } + + var properties = Models.getModelProperties(foundModels[i]); + print("Checking properties: " + properties.id + " " + properties.isKnownID); + + var check = this.checkModel(properties); + if (check.valid) { + this.grab(foundModels[i], properties); + this.x = check.x; + this.y = check.y; + this.z = check.z; + return; + } + } + } } this.cleanup = function () { @@ -238,78 +305,44 @@ var leftController = new controller(LEFT); var rightController = new controller(RIGHT); function moveModels() { - if (leftController.grabbing) { - if (rightController.grabbing) { - var properties = Models.getModelProperties(leftController.modelID); - - var oldLeftPoint = Vec3.sum(leftController.oldPalmPosition, Vec3.multiply(leftController.oldFront, leftController.x)); - var oldRightPoint = Vec3.sum(rightController.oldPalmPosition, Vec3.multiply(rightController.oldFront, rightController.x)); - - var oldMiddle = Vec3.multiply(Vec3.sum(oldLeftPoint, oldRightPoint), 0.5); - var oldLength = Vec3.length(Vec3.subtract(oldLeftPoint, oldRightPoint)); - - - var leftPoint = Vec3.sum(leftController.palmPosition, Vec3.multiply(leftController.front, leftController.x)); - var rightPoint = Vec3.sum(rightController.palmPosition, Vec3.multiply(rightController.front, rightController.x)); - - var middle = Vec3.multiply(Vec3.sum(leftPoint, rightPoint), 0.5); - var length = Vec3.length(Vec3.subtract(leftPoint, rightPoint)); - - var ratio = length / oldLength; - - var newPosition = Vec3.sum(middle, - Vec3.multiply(Vec3.subtract(properties.position, oldMiddle), ratio)); - Vec3.print("Ratio : " + ratio + " New position: ", newPosition); - var rotation = Quat.multiply(leftController.rotation, - Quat.inverse(leftController.oldRotation)); - rotation = Quat.multiply(rotation, properties.modelRotation); - - Models.editModel(leftController.modelID, { - position: newPosition, - //modelRotation: rotation, - radius: properties.radius * ratio - }); - - return; - } else { - var newPosition = Vec3.sum(leftController.palmPosition, - Vec3.multiply(leftController.front, leftController.x)); - newPosition = Vec3.sum(newPosition, - Vec3.multiply(leftController.up, leftController.y)); - newPosition = Vec3.sum(newPosition, - Vec3.multiply(leftController.right, leftController.z)); - - var rotation = Quat.multiply(leftController.rotation, - Quat.inverse(leftController.oldRotation)); - rotation = Quat.multiply(rotation, - Models.getModelProperties(leftController.modelID).modelRotation); - - Models.editModel(leftController.modelID, { - position: newPosition, - modelRotation: rotation - }); - } - } - - - if (rightController.grabbing) { - var newPosition = Vec3.sum(rightController.palmPosition, - Vec3.multiply(rightController.front, rightController.x)); - newPosition = Vec3.sum(newPosition, - Vec3.multiply(rightController.up, rightController.y)); - newPosition = Vec3.sum(newPosition, - Vec3.multiply(rightController.right, rightController.z)); + if (leftController.grabbing && rightController.grabbing && rightController.modelID.id == leftController.modelID.id) { + print("Both controllers"); + var oldLeftPoint = Vec3.sum(leftController.oldPalmPosition, Vec3.multiply(leftController.oldFront, leftController.x)); + var oldRightPoint = Vec3.sum(rightController.oldPalmPosition, Vec3.multiply(rightController.oldFront, rightController.x)); - var rotation = Quat.multiply(rightController.rotation, - Quat.inverse(rightController.oldRotation)); - rotation = Quat.multiply(rotation, - Models.getModelProperties(rightController.modelID).modelRotation); + var oldMiddle = Vec3.multiply(Vec3.sum(oldLeftPoint, oldRightPoint), 0.5); + var oldLength = Vec3.length(Vec3.subtract(oldLeftPoint, oldRightPoint)); - Models.editModel(rightController.modelID, { + + var leftPoint = Vec3.sum(leftController.palmPosition, Vec3.multiply(leftController.front, leftController.x)); + var rightPoint = Vec3.sum(rightController.palmPosition, Vec3.multiply(rightController.front, rightController.x)); + + var middle = Vec3.multiply(Vec3.sum(leftPoint, rightPoint), 0.5); + var length = Vec3.length(Vec3.subtract(leftPoint, rightPoint)); + + var ratio = length / oldLength; + + var newPosition = Vec3.sum(middle, + Vec3.multiply(Vec3.subtract(leftController.oldModelPosition, oldMiddle), ratio)); + Vec3.print("Ratio : " + ratio + " New position: ", newPosition); + var rotation = Quat.multiply(leftController.rotation, + Quat.inverse(leftController.oldRotation)); + rotation = Quat.multiply(rotation, leftController.oldModelRotation); + + Models.editModel(leftController.modelID, { position: newPosition, - modelRotation: rotation + //modelRotation: rotation, + radius: leftController.oldModelRadius * ratio }); + + leftController.oldModelPosition = newPosition; + leftController.oldModelRotation = rotation; + leftController.oldModelRadius *= ratio; + return; } + + leftController.moveModel(); + rightController.moveModel(); } function checkController(deltaTime) { @@ -318,6 +351,8 @@ function checkController(deltaTime) { var numberOfSpatialControls = Controller.getNumberOfSpatialControls(); var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers; + moveOverlays(); + // this is expected for hydras if (!(numberOfButtons==12 && numberOfTriggers == 2 && controllersPerTrigger == 2)) { //print("no hydra connected?"); @@ -329,14 +364,48 @@ function checkController(deltaTime) { moveModels(); } +function moveOverlays() { + windowDimensions = Controller.getViewportDimensions(); + + toolsX = windowDimensions.x - 8 - toolWidth; + toolsY = (windowDimensions.y - toolsHeight) / 2; + + Overlays.addOverlay(firstModel, { + x: toolsX, y: toolsY + ((toolHeight + toolVerticalSpacing) * 0), width: toolWidth, height: toolHeight, + }); +} + +function mousePressEvent(event) { + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + var url; + + if (clickedOverlay == firstModel) { + url = Window.prompt("Model url", modelURLs[Math.floor(Math.random() * modelURLs.length)]); + if (url == null) { + return; } + } else { + print("Didn't click on anything"); + return; + } + + var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); + Models.addModel({ position: position, + radius: radiusDefault, + modelURL: url + }); +} + function scriptEnding() { leftController.cleanup(); rightController.cleanup(); + + Overlays.deleteOverlay(firstModel); } Script.scriptEnding.connect(scriptEnding); // register the call back so it fires before each data send Script.update.connect(checkController); +Controller.mousePressEvent.connect(mousePressEvent); diff --git a/examples/placeModelsWithHands.js b/examples/placeModelsWithHands.js index a6b59ff5c3..41d9d5dc86 100644 --- a/examples/placeModelsWithHands.js +++ b/examples/placeModelsWithHands.js @@ -37,6 +37,7 @@ var radiusMinimum = 0.05; var radiusMaximum = 0.5; var modelURLs = [ + "https://s3-us-west-1.amazonaws.com/highfidelity-public/models/music/EVHFrankenstein.fbx", "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Feisar_Ship.FBX", "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/birarda/birarda_head.fbx", "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/pug.fbx", @@ -51,6 +52,12 @@ var currentModelURL = 1; var numModels = modelURLs.length; +function getNewVoxelPosition() { + var camera = Camera.getPosition(); + var forwardVector = Quat.getFront(MyAvatar.orientation); + var newPosition = Vec3.sum(camera, Vec3.multiply(forwardVector, 2.0)); + return newPosition; +} function keyPressEvent(event) { debugPrint("event.text=" + event.text); @@ -63,7 +70,20 @@ function keyPressEvent(event) { rightRecentlyDeleted = false; rightModelAlreadyInHand = false; } + } else if (event.text == "m") { + var URL = Window.prompt("Model URL", "Enter URL, e.g. http://foo.com/model.fbx"); + Window.alert("Your response was: " + prompt); + var modelPosition = getNewVoxelPosition(); + var properties = { position: { x: modelPosition.x, + y: modelPosition.y, + z: modelPosition.z }, + radius: modelRadius, + modelURL: URL + }; + newModel = Models.addModel(properties); + } else if (event.text == "DELETE" || event.text == "BACKSPACE") { + if (leftModelAlreadyInHand) { print("want to delete leftHandModel=" + leftHandModel); Models.deleteModel(leftHandModel); diff --git a/interface/resources/sounds/mention/Mentioned A.wav b/interface/resources/sounds/mention/Mentioned A.wav new file mode 100644 index 0000000000..5b203d7adf Binary files /dev/null and b/interface/resources/sounds/mention/Mentioned A.wav differ diff --git a/interface/resources/sounds/mention/Mentioned B.wav b/interface/resources/sounds/mention/Mentioned B.wav new file mode 100644 index 0000000000..22a3b8bb64 Binary files /dev/null and b/interface/resources/sounds/mention/Mentioned B.wav differ diff --git a/interface/resources/sounds/mention/Mentioned C.wav b/interface/resources/sounds/mention/Mentioned C.wav new file mode 100644 index 0000000000..78d5fe3f66 Binary files /dev/null and b/interface/resources/sounds/mention/Mentioned C.wav differ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 86054f3fcd..b50e9ef1dc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -63,11 +63,11 @@ #include #include #include -#include #include "Application.h" #include "InterfaceVersion.h" #include "Menu.h" +#include "ModelUploader.h" #include "Util.h" #include "devices/OculusManager.h" #include "devices/TV3DManager.h" @@ -2843,7 +2843,7 @@ void Application::renderRearViewMirror(const QRect& region, bool billboard) { // save absolute translations glm::vec3 absoluteSkeletonTranslation = _myAvatar->getSkeletonModel().getTranslation(); glm::vec3 absoluteFaceTranslation = _myAvatar->getHead()->getFaceModel().getTranslation(); - + // get the eye positions relative to the neck and use them to set the face translation glm::vec3 leftEyePosition, rightEyePosition; _myAvatar->getHead()->getFaceModel().setTranslation(glm::vec3()); @@ -2857,11 +2857,22 @@ void Application::renderRearViewMirror(const QRect& region, bool billboard) { _myAvatar->getSkeletonModel().setTranslation(_myAvatar->getHead()->getFaceModel().getTranslation() - neckPosition); + // update the attachments to match + QVector absoluteAttachmentTranslations; + glm::vec3 delta = _myAvatar->getSkeletonModel().getTranslation() - absoluteSkeletonTranslation; + foreach (Model* attachment, _myAvatar->getAttachmentModels()) { + absoluteAttachmentTranslations.append(attachment->getTranslation()); + attachment->setTranslation(attachment->getTranslation() + delta); + } + displaySide(_mirrorCamera, true); // restore absolute translations _myAvatar->getSkeletonModel().setTranslation(absoluteSkeletonTranslation); _myAvatar->getHead()->getFaceModel().setTranslation(absoluteFaceTranslation); + for (int i = 0; i < absoluteAttachmentTranslations.size(); i++) { + _myAvatar->getAttachmentModels().at(i)->setTranslation(absoluteAttachmentTranslations.at(i)); + } } else { displaySide(_mirrorCamera, true); } @@ -3090,6 +3101,16 @@ void Application::setMenuShortcutsEnabled(bool enabled) { setShortcutsEnabled(_window->menuBar(), enabled); } +void Application::uploadModel(ModelType modelType) { + ModelUploader* uploader = new ModelUploader(modelType); + QThread* thread = new QThread(); + thread->connect(uploader, SIGNAL(destroyed()), SLOT(quit())); + thread->connect(thread, SIGNAL(finished()), SLOT(deleteLater())); + uploader->connect(thread, SIGNAL(started()), SLOT(send())); + + thread->start(); +} + void Application::updateWindowTitle(){ QString buildVersion = " (build " + applicationVersion() + ")"; @@ -3417,22 +3438,16 @@ void Application::toggleRunningScriptsWidget() { } } -void Application::uploadFST(bool isHead) { - ModelUploader* uploader = new ModelUploader(isHead); - QThread* thread = new QThread(); - thread->connect(uploader, SIGNAL(destroyed()), SLOT(quit())); - thread->connect(thread, SIGNAL(finished()), SLOT(deleteLater())); - uploader->connect(thread, SIGNAL(started()), SLOT(send())); - - thread->start(); -} - void Application::uploadHead() { - uploadFST(true); + uploadModel(HEAD_MODEL); } void Application::uploadSkeleton() { - uploadFST(false); + uploadModel(SKELETON_MODEL); +} + +void Application::uploadAttachment() { + uploadModel(ATTACHMENT_MODEL); } ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScriptFromEditor) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 96b472e553..a7073ac4e9 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -71,6 +71,7 @@ #include "scripting/ControllerScriptingInterface.h" #include "ui/BandwidthDialog.h" #include "ui/BandwidthMeter.h" +#include "ui/ModelsBrowser.h" #include "ui/OctreeStatsDialog.h" #include "ui/RearMirrorTools.h" #include "ui/LodToolsDialog.h" @@ -295,9 +296,9 @@ public slots: void reloadAllScripts(); void toggleRunningScriptsWidget(); - void uploadFST(bool isHead); void uploadHead(); void uploadSkeleton(); + void uploadAttachment(); void bumpSettings() { ++_numChangedSettings; } @@ -375,13 +376,11 @@ private: void setMenuShortcutsEnabled(bool enabled); + void uploadModel(ModelType modelType); + static void attachNewHeadToNode(Node *newNode); static void* networkReceive(void* args); // network receive thread - void findAxisAlignment(); - - void displayRearMirrorTools(); - MainWindow* _window; GLCanvas* _glWidget; // our GLCanvas has a couple extra features diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 10e2787d6b..868489b851 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -37,6 +37,7 @@ #include "Menu.h" #include "scripting/MenuScriptingInterface.h" #include "Util.h" +#include "ui/AttachmentsDialog.h" #include "ui/InfoView.h" #include "ui/MetavoxelEditor.h" #include "ui/ModelsBrowser.h" @@ -157,6 +158,8 @@ Menu::Menu() : addDisabledActionAndSeparator(fileMenu, "Upload Avatar Model"); addActionToQMenuAndActionHash(fileMenu, MenuOption::UploadHead, 0, Application::getInstance(), SLOT(uploadHead())); addActionToQMenuAndActionHash(fileMenu, MenuOption::UploadSkeleton, 0, Application::getInstance(), SLOT(uploadSkeleton())); + addActionToQMenuAndActionHash(fileMenu, MenuOption::UploadAttachment, 0, + Application::getInstance(), SLOT(uploadAttachment())); addDisabledActionAndSeparator(fileMenu, "Settings"); addActionToQMenuAndActionHash(fileMenu, MenuOption::SettingsImport, 0, this, SLOT(importSettings())); addActionToQMenuAndActionHash(fileMenu, MenuOption::SettingsExport, 0, this, SLOT(exportSettings())); @@ -187,6 +190,8 @@ Menu::Menu() : SLOT(editPreferences()), QAction::PreferencesRole); + addActionToQMenuAndActionHash(editMenu, MenuOption::Attachments, 0, this, SLOT(editAttachments())); + addDisabledActionAndSeparator(editMenu, "Physics"); QObject* avatar = appInstance->getAvatar(); addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::ObeyEnvironmentalGravity, Qt::SHIFT | Qt::Key_G, true, @@ -210,6 +215,10 @@ Menu::Menu() : toggleChat(); connect(&xmppClient, SIGNAL(connected()), this, SLOT(toggleChat())); connect(&xmppClient, SIGNAL(disconnected()), this, SLOT(toggleChat())); + + QDir::setCurrent(Application::resourcesPath()); + // init chat window to listen chat + _chatWindow = new ChatWindow(Application::getInstance()->getWindow()); #endif QMenu* viewMenu = addMenu("View"); @@ -479,6 +488,7 @@ void Menu::loadSettings(QSettings* settings) { _audioJitterBufferSamples = loadSetting(settings, "audioJitterBufferSamples", 0); _fieldOfView = loadSetting(settings, "fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES); + _realWorldFieldOfView = loadSetting(settings, "realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES); _faceshiftEyeDeflection = loadSetting(settings, "faceshiftEyeDeflection", DEFAULT_FACESHIFT_EYE_DEFLECTION); _maxVoxels = loadSetting(settings, "maxVoxels", DEFAULT_MAX_VOXELS_PER_SYSTEM); _maxVoxelPacketsPerSecond = loadSetting(settings, "maxVoxelsPPS", DEFAULT_MAX_VOXEL_PPS); @@ -831,6 +841,15 @@ void Menu::editPreferences() { } } +void Menu::editAttachments() { + if (!_attachmentsDialog) { + _attachmentsDialog = new AttachmentsDialog(); + _attachmentsDialog->show(); + } else { + _attachmentsDialog->close(); + } +} + void Menu::goToDomain(const QString newDomain) { if (NodeList::getInstance()->getDomainHandler().getHostname() != newDomain) { // send a node kill request, indicating to other clients that they should play the "disappeared" effect diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 9a7dda9111..723d320905 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -64,6 +64,7 @@ struct ViewFrustumOffset { class QSettings; +class AttachmentsDialog; class BandwidthDialog; class LodToolsDialog; class MetavoxelEditor; @@ -84,6 +85,9 @@ public: void setAudioJitterBufferSamples(float audioJitterBufferSamples) { _audioJitterBufferSamples = audioJitterBufferSamples; } float getFieldOfView() const { return _fieldOfView; } void setFieldOfView(float fieldOfView) { _fieldOfView = fieldOfView; } + float getRealWorldFieldOfView() const { return _realWorldFieldOfView; } + void setRealWorldFieldOfView(float realWorldFieldOfView) { _realWorldFieldOfView = realWorldFieldOfView; } + float getFaceshiftEyeDeflection() const { return _faceshiftEyeDeflection; } void setFaceshiftEyeDeflection(float faceshiftEyeDeflection) { _faceshiftEyeDeflection = faceshiftEyeDeflection; } QString getSnapshotsLocation() const; @@ -171,6 +175,7 @@ public slots: private slots: void aboutApp(); void editPreferences(); + void editAttachments(); void goToDomainDialog(); void goToLocation(); void nameLocation(); @@ -228,6 +233,7 @@ private: int _audioJitterBufferSamples; /// number of extra samples to wait before starting audio playback BandwidthDialog* _bandwidthDialog; float _fieldOfView; /// in Degrees, doesn't apply to HMD like Oculus + float _realWorldFieldOfView; // The actual FOV set by the user's monitor size and view distance float _faceshiftEyeDeflection; FrustumDrawMode _frustumDrawMode; ViewFrustumOffset _viewFrustumOffset; @@ -252,6 +258,7 @@ private: SimpleMovingAverage _fastFPSAverage; QAction* _loginAction; QPointer _preferencesDialog; + QPointer _attachmentsDialog; QAction* _chatAction; QString _snapshotsLocation; }; @@ -261,6 +268,7 @@ namespace MenuOption { const QString AlignForearmsWithWrists = "Align Forearms with Wrists"; const QString AmbientOcclusion = "Ambient Occlusion"; const QString Atmosphere = "Atmosphere"; + const QString Attachments = "Attachments..."; const QString AudioNoiseReduction = "Audio Noise Reduction"; const QString AudioScope = "Audio Scope"; const QString AudioScopePause = "Pause Audio Scope"; @@ -362,6 +370,7 @@ namespace MenuOption { const QString TestPing = "Test Ping"; const QString TransmitterDrive = "Transmitter Drive"; const QString TurnWithHead = "Turn using Head"; + const QString UploadAttachment = "Upload Attachment Model"; const QString UploadHead = "Upload Head Model"; const QString UploadSkeleton = "Upload Skeleton Model"; const QString Visage = "Visage"; diff --git a/interface/src/ModelUploader.cpp b/interface/src/ModelUploader.cpp index 0ffd725716..bf6a868368 100644 --- a/interface/src/ModelUploader.cpp +++ b/interface/src/ModelUploader.cpp @@ -59,11 +59,11 @@ static const int MAX_CHECK = 30; static const int QCOMPRESS_HEADER_POSITION = 0; static const int QCOMPRESS_HEADER_SIZE = 4; -ModelUploader::ModelUploader(bool isHead) : +ModelUploader::ModelUploader(ModelType modelType) : _lodCount(-1), _texturesCount(-1), _totalSize(0), - _isHead(isHead), + _modelType(modelType), _readyToSend(false), _dataMultiPart(new QHttpMultiPart(QHttpMultiPart::FormDataType)), _numberOfChecks(MAX_CHECK) @@ -190,7 +190,7 @@ bool ModelUploader::zip() { } // open the dialog to configure the rest - ModelPropertiesDialog properties(_isHead, mapping, basePath, geometry); + ModelPropertiesDialog properties(_modelType, mapping, basePath, geometry); if (properties.exec() == QDialog::Rejected) { return false; } @@ -202,7 +202,7 @@ bool ModelUploader::zip() { textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"model_name\""); textPart.setBody(nameField); _dataMultiPart->append(textPart); - _url = S3_URL + ((_isHead)? "/models/heads/" : "/models/skeletons/") + nameField + ".fst"; + _url = S3_URL + "/models/" + MODEL_TYPE_NAMES[_modelType] + "/" + nameField + ".fst"; } else { QMessageBox::warning(NULL, QString("ModelUploader::zip()"), @@ -260,11 +260,7 @@ bool ModelUploader::zip() { QHttpPart textPart; textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data;" " name=\"model_category\""); - if (_isHead) { - textPart.setBody("heads"); - } else { - textPart.setBody("skeletons"); - } + textPart.setBody(MODEL_TYPE_NAMES[_modelType]); _dataMultiPart->append(textPart); _readyToSend = true; @@ -428,19 +424,24 @@ void ModelUploader::processCheck() { } bool ModelUploader::addTextures(const QString& texdir, const FBXGeometry& geometry) { + QSet added; foreach (FBXMesh mesh, geometry.meshes) { foreach (FBXMeshPart part, mesh.parts) { - if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty()) { + if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty() && + !added.contains(part.diffuseTexture.filename)) { if (!addPart(texdir + "/" + part.diffuseTexture.filename, QString("texture%1").arg(++_texturesCount), true)) { return false; } + added.insert(part.diffuseTexture.filename); } - if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty()) { + if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty() && + !added.contains(part.normalTexture.filename)) { if (!addPart(texdir + "/" + part.normalTexture.filename, QString("texture%1").arg(++_texturesCount), true)) { return false; } + added.insert(part.normalTexture.filename); } } } @@ -510,9 +511,9 @@ bool ModelUploader::addPart(const QFile& file, const QByteArray& contents, const return true; } -ModelPropertiesDialog::ModelPropertiesDialog(bool isHead, const QVariantHash& originalMapping, +ModelPropertiesDialog::ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, const QString& basePath, const FBXGeometry& geometry) : - _isHead(isHead), + _modelType(modelType), _originalMapping(originalMapping), _basePath(basePath), _geometry(geometry) { @@ -531,10 +532,12 @@ ModelPropertiesDialog::ModelPropertiesDialog(bool isHead, const QVariantHash& or _scale->setMaximum(FLT_MAX); _scale->setSingleStep(0.01); - form->addRow("Left Eye Joint:", _leftEyeJoint = createJointBox()); - form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); - form->addRow("Neck Joint:", _neckJoint = createJointBox()); - if (!isHead) { + if (_modelType != ATTACHMENT_MODEL) { + form->addRow("Left Eye Joint:", _leftEyeJoint = createJointBox()); + form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); + form->addRow("Neck Joint:", _neckJoint = createJointBox()); + } + if (_modelType == SKELETON_MODEL) { form->addRow("Root Joint:", _rootJoint = createJointBox()); form->addRow("Lean Joint:", _leanJoint = createJointBox()); form->addRow("Head Joint:", _headJoint = createJointBox()); @@ -573,10 +576,12 @@ QVariantHash ModelPropertiesDialog::getMapping() const { mapping.insert(JOINT_INDEX_FIELD, jointIndices); QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); - insertJointMapping(joints, "jointEyeLeft", _leftEyeJoint->currentText()); - insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); - insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); - if (!_isHead) { + if (_modelType != ATTACHMENT_MODEL) { + insertJointMapping(joints, "jointEyeLeft", _leftEyeJoint->currentText()); + insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); + insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); + } + if (_modelType == SKELETON_MODEL) { insertJointMapping(joints, "jointRoot", _rootJoint->currentText()); insertJointMapping(joints, "jointLean", _leanJoint->currentText()); insertJointMapping(joints, "jointHead", _headJoint->currentText()); @@ -604,10 +609,12 @@ void ModelPropertiesDialog::reset() { _scale->setValue(_originalMapping.value(SCALE_FIELD).toDouble()); QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash(); - setJointText(_leftEyeJoint, jointHash.value("jointEyeLeft").toString()); - setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString()); - setJointText(_neckJoint, jointHash.value("jointNeck").toString()); - if (!_isHead) { + if (_modelType != ATTACHMENT_MODEL) { + setJointText(_leftEyeJoint, jointHash.value("jointEyeLeft").toString()); + setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString()); + setJointText(_neckJoint, jointHash.value("jointNeck").toString()); + } + if (_modelType == SKELETON_MODEL) { setJointText(_rootJoint, jointHash.value("jointRoot").toString()); setJointText(_leanJoint, jointHash.value("jointLean").toString()); setJointText(_headJoint, jointHash.value("jointHead").toString()); diff --git a/interface/src/ModelUploader.h b/interface/src/ModelUploader.h index 11594b3d95..499bfad03b 100644 --- a/interface/src/ModelUploader.h +++ b/interface/src/ModelUploader.h @@ -17,6 +17,8 @@ #include +#include "ui/ModelsBrowser.h" + class QComboBox; class QDoubleSpinBox; class QFileInfo; @@ -30,7 +32,7 @@ class ModelUploader : public QObject { Q_OBJECT public: - ModelUploader(bool isHead); + ModelUploader(ModelType type); ~ModelUploader(); public slots: @@ -49,7 +51,7 @@ private: int _lodCount; int _texturesCount; int _totalSize; - bool _isHead; + ModelType _modelType; bool _readyToSend; QHttpMultiPart* _dataMultiPart; @@ -73,7 +75,7 @@ class ModelPropertiesDialog : public QDialog { Q_OBJECT public: - ModelPropertiesDialog(bool isHead, const QVariantHash& originalMapping, + ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, const QString& basePath, const FBXGeometry& geometry); QVariantHash getMapping() const; @@ -87,7 +89,7 @@ private: QComboBox* createJointBox(bool withNone = true) const; void insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const; - bool _isHead; + ModelType _modelType; QVariantHash _originalMapping; QString _basePath; FBXGeometry _geometry; diff --git a/interface/src/XmppClient.cpp b/interface/src/XmppClient.cpp index 48c1f5d976..666906681c 100644 --- a/interface/src/XmppClient.cpp +++ b/interface/src/XmppClient.cpp @@ -36,6 +36,7 @@ void XmppClient::xmppConnected() { _publicChatRoom = _xmppMUCManager.addRoom(DEFAULT_CHAT_ROOM); _publicChatRoom->setNickName(AccountManager::getInstance().getAccountInfo().getUsername()); _publicChatRoom->join(); + emit joinedPublicChatRoom(); } void XmppClient::xmppError(QXmppClient::Error error) { diff --git a/interface/src/XmppClient.h b/interface/src/XmppClient.h index 8af3204377..cbb06cf992 100644 --- a/interface/src/XmppClient.h +++ b/interface/src/XmppClient.h @@ -28,6 +28,9 @@ public: QXmppClient& getXMPPClient() { return _xmppClient; } const QXmppMucRoom* getPublicChatRoom() const { return _publicChatRoom; } +signals: + void joinedPublicChatRoom(); + private slots: void xmppConnected(); void xmppError(QXmppClient::Error error); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index e8ac93234c..f7bf4595d6 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -126,6 +126,7 @@ void Avatar::simulate(float deltaTime) { _skeletonModel.simulate(deltaTime); } _skeletonModel.simulate(deltaTime, _hasNewJointRotations); + simulateAttachments(deltaTime); _hasNewJointRotations = false; glm::vec3 headPosition = _position; @@ -232,6 +233,17 @@ void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { _skeletonModel.renderBoundingCollisionShapes(0.7f); } } + // If this is the avatar being looked at, render a little ball above their head + if (_isLookAtTarget) { + const float LOOK_AT_INDICATOR_RADIUS = 0.03f; + const float LOOK_AT_INDICATOR_HEIGHT = 0.60f; + const float LOOK_AT_INDICATOR_COLOR[] = { 0.8f, 0.0f, 0.0f, 0.5f }; + glPushMatrix(); + glColor4fv(LOOK_AT_INDICATOR_COLOR); + glTranslatef(_position.x, _position.y + (getSkeletonHeight() * LOOK_AT_INDICATOR_HEIGHT), _position.z); + glutSolidSphere(LOOK_AT_INDICATOR_RADIUS, 15, 15); + glPopMatrix(); + } // quick check before falling into the code below: // (a 10 degree breadth of an almost 2 meter avatar kicks in at about 12m) @@ -338,6 +350,7 @@ void Avatar::renderBody(RenderMode renderMode, float glowLevel) { return; } _skeletonModel.render(1.0f, modelRenderMode); + renderAttachments(modelRenderMode); getHand()->render(false); } getHead()->render(1.0f, modelRenderMode); @@ -347,6 +360,29 @@ bool Avatar::shouldRenderHead(const glm::vec3& cameraPosition, RenderMode render return true; } +void Avatar::simulateAttachments(float deltaTime) { + for (int i = 0; i < _attachmentModels.size(); i++) { + const AttachmentData& attachment = _attachmentData.at(i); + Model* model = _attachmentModels.at(i); + int jointIndex = getJointIndex(attachment.jointName); + glm::vec3 jointPosition; + glm::quat jointRotation; + if (_skeletonModel.getJointPosition(jointIndex, jointPosition) && + _skeletonModel.getJointRotation(jointIndex, jointRotation)) { + model->setTranslation(jointPosition + jointRotation * attachment.translation * _scale); + model->setRotation(jointRotation * attachment.rotation); + model->setScale(_skeletonModel.getScale() * attachment.scale); + model->simulate(deltaTime); + } + } +} + +void Avatar::renderAttachments(Model::RenderMode renderMode) { + foreach (Model* model, _attachmentModels) { + model->render(1.0f, renderMode); + } +} + void Avatar::updateJointMappings() { // no-op; joint mappings come from skeleton model } @@ -667,6 +703,25 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _skeletonModel.setURL(_skeletonModelURL, DEFAULT_SKELETON_MODEL_URL, true, !isMyAvatar()); } +void Avatar::setAttachmentData(const QVector& attachmentData) { + AvatarData::setAttachmentData(attachmentData); + + // make sure we have as many models as attachments + while (_attachmentModels.size() < attachmentData.size()) { + Model* model = new Model(this); + model->init(); + _attachmentModels.append(model); + } + while (_attachmentModels.size() > attachmentData.size()) { + delete _attachmentModels.takeLast(); + } + + // update the urls + for (int i = 0; i < attachmentData.size(); i++) { + _attachmentModels[i]->setURL(attachmentData.at(i).modelURL); + } +} + void Avatar::setDisplayName(const QString& displayName) { AvatarData::setDisplayName(displayName); _displayNameBoundingRect = textRenderer(DISPLAYNAME)->metrics().tightBoundingRect(displayName); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 4263e606a5..edd53e4b8f 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -79,10 +79,11 @@ public: //setters void setDisplayingLookatVectors(bool displayingLookatVectors) { getHead()->setRenderLookatVectors(displayingLookatVectors); } void setMouseRay(const glm::vec3 &origin, const glm::vec3 &direction); - + void setIsLookAtTarget(const bool isLookAtTarget) { _isLookAtTarget = isLookAtTarget; } //getters bool isInitialized() const { return _initialized; } SkeletonModel& getSkeletonModel() { return _skeletonModel; } + const QVector& getAttachmentModels() const { return _attachmentModels; } glm::vec3 getChestPosition() const; float getScale() const { return _scale; } const glm::vec3& getVelocity() const { return _velocity; } @@ -131,6 +132,7 @@ public: virtual void setFaceModelURL(const QUrl& faceModelURL); virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); + virtual void setAttachmentData(const QVector& attachmentData); virtual void setDisplayName(const QString& displayName); virtual void setBillboard(const QByteArray& billboard); @@ -160,6 +162,7 @@ signals: protected: SkeletonModel _skeletonModel; + QVector _attachmentModels; float _bodyYawDelta; glm::vec3 _velocity; float _leanScale; @@ -188,6 +191,9 @@ protected: virtual void renderBody(RenderMode renderMode, float glowLevel = 0.0f); virtual bool shouldRenderHead(const glm::vec3& cameraPosition, RenderMode renderMode) const; + void simulateAttachments(float deltaTime); + void renderAttachments(Model::RenderMode renderMode); + virtual void updateJointMappings(); private: @@ -195,6 +201,7 @@ private: bool _initialized; QScopedPointer _billboardTexture; bool _shouldRenderBillboard; + bool _isLookAtTarget; void renderBillboard(); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index cd799b7d2e..adeaa30962 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -178,6 +178,7 @@ void MyAvatar::simulate(float deltaTime) { getHand()->simulate(deltaTime, true); _skeletonModel.simulate(deltaTime); + simulateAttachments(deltaTime); // copy out the skeleton joints from the model _jointData.resize(_skeletonModel.getJointStateCount()); @@ -248,14 +249,17 @@ void MyAvatar::updateFromGyros(float deltaTime) { } // Set the rotation of the avatar's head (as seen by others, not affecting view frustum) - // to be scaled. Pitch is greater to emphasize nodding behavior / synchrony. - const float AVATAR_HEAD_PITCH_MAGNIFY = 1.0f; - const float AVATAR_HEAD_YAW_MAGNIFY = 1.0f; - const float AVATAR_HEAD_ROLL_MAGNIFY = 1.0f; + // to be scaled such that when the user's physical head is pointing at edge of screen, the + // avatar head is at the edge of the in-world view frustum. So while a real person may move + // their head only 30 degrees or so, this may correspond to a 90 degree field of view. + // Note that roll is magnified by a constant because it is not related to field of view. + + float magnifyFieldOfView = Menu::getInstance()->getFieldOfView() / Menu::getInstance()->getRealWorldFieldOfView(); + Head* head = getHead(); - head->setDeltaPitch(estimatedRotation.x * AVATAR_HEAD_PITCH_MAGNIFY); - head->setDeltaYaw(estimatedRotation.y * AVATAR_HEAD_YAW_MAGNIFY); - head->setDeltaRoll(estimatedRotation.z * AVATAR_HEAD_ROLL_MAGNIFY); + head->setDeltaPitch(estimatedRotation.x * magnifyFieldOfView); + head->setDeltaYaw(estimatedRotation.y * magnifyFieldOfView); + head->setDeltaRoll(estimatedRotation.z); // Update torso lean distance based on accelerometer data const float TORSO_LENGTH = 0.5f; @@ -338,18 +342,15 @@ void MyAvatar::renderHeadMouse(int screenWidth, int screenHeight) const { Faceshift* faceshift = Application::getInstance()->getFaceshift(); + float pixelsPerDegree = screenHeight / Menu::getInstance()->getFieldOfView(); + // Display small target box at center or head mouse target that can also be used to measure LOD float headPitch = getHead()->getFinalPitch(); float headYaw = getHead()->getFinalYaw(); - // - // It should be noted that the following constant is a function - // how far the viewer's head is away from both the screen and the size of the screen, - // which are both things we cannot know without adding a calibration phase. - // - const float PIXELS_PER_VERTICAL_DEGREE = 20.0f; + float aspectRatio = (float) screenWidth / (float) screenHeight; - int headMouseX = screenWidth / 2.f - headYaw * aspectRatio * PIXELS_PER_VERTICAL_DEGREE; - int headMouseY = screenHeight / 2.f - headPitch * PIXELS_PER_VERTICAL_DEGREE; + int headMouseX = screenWidth / 2.f - headYaw * aspectRatio * pixelsPerDegree; + int headMouseY = screenHeight / 2.f - headPitch * pixelsPerDegree; glColor3f(1.0f, 1.0f, 1.0f); glDisable(GL_LINE_SMOOTH); @@ -366,8 +367,8 @@ void MyAvatar::renderHeadMouse(int screenWidth, int screenHeight) const { float avgEyePitch = faceshift->getEstimatedEyePitch(); float avgEyeYaw = faceshift->getEstimatedEyeYaw(); - int eyeTargetX = (screenWidth / 2) - avgEyeYaw * aspectRatio * PIXELS_PER_VERTICAL_DEGREE; - int eyeTargetY = (screenHeight / 2) - avgEyePitch * PIXELS_PER_VERTICAL_DEGREE; + int eyeTargetX = (screenWidth / 2) - avgEyeYaw * aspectRatio * pixelsPerDegree; + int eyeTargetY = (screenHeight / 2) - avgEyePitch * pixelsPerDegree; glColor3f(0.0f, 1.0f, 1.0f); glDisable(GL_LINE_SMOOTH); @@ -422,6 +423,24 @@ void MyAvatar::saveData(QSettings* settings) { settings->setValue("faceModelURL", _faceModelURL); settings->setValue("skeletonModelURL", _skeletonModelURL); + + settings->beginWriteArray("attachmentData"); + for (int i = 0; i < _attachmentData.size(); i++) { + settings->setArrayIndex(i); + const AttachmentData& attachment = _attachmentData.at(i); + settings->setValue("modelURL", attachment.modelURL); + settings->setValue("jointName", attachment.jointName); + settings->setValue("translation_x", attachment.translation.x); + settings->setValue("translation_y", attachment.translation.y); + settings->setValue("translation_z", attachment.translation.z); + glm::vec3 eulers = safeEulerAngles(attachment.rotation); + settings->setValue("rotation_x", eulers.x); + settings->setValue("rotation_y", eulers.y); + settings->setValue("rotation_z", eulers.z); + settings->setValue("scale", attachment.scale); + } + settings->endArray(); + settings->setValue("displayName", _displayName); settings->endGroup(); @@ -450,6 +469,28 @@ void MyAvatar::loadData(QSettings* settings) { setFaceModelURL(settings->value("faceModelURL", DEFAULT_HEAD_MODEL_URL).toUrl()); setSkeletonModelURL(settings->value("skeletonModelURL").toUrl()); + + QVector attachmentData; + int attachmentCount = settings->beginReadArray("attachmentData"); + for (int i = 0; i < attachmentCount; i++) { + settings->setArrayIndex(i); + AttachmentData attachment; + attachment.modelURL = settings->value("modelURL").toUrl(); + attachment.jointName = settings->value("jointName").toString(); + attachment.translation.x = loadSetting(settings, "translation_x", 0.0f); + attachment.translation.y = loadSetting(settings, "translation_y", 0.0f); + attachment.translation.z = loadSetting(settings, "translation_z", 0.0f); + glm::vec3 eulers; + eulers.x = loadSetting(settings, "rotation_x", 0.0f); + eulers.y = loadSetting(settings, "rotation_y", 0.0f); + eulers.z = loadSetting(settings, "rotation_z", 0.0f); + attachment.rotation = glm::quat(eulers); + attachment.scale = loadSetting(settings, "scale", 1.0f); + attachmentData.append(attachment); + } + settings->endArray(); + setAttachmentData(attachmentData); + setDisplayName(settings->value("displayName").toString()); settings->endGroup(); @@ -468,23 +509,6 @@ void MyAvatar::sendKillAvatar() { NodeList::getInstance()->broadcastToNodes(killPacket, NodeSet() << NodeType::AvatarMixer); } -void MyAvatar::orbit(const glm::vec3& position, int deltaX, int deltaY) { - // first orbit horizontally - glm::quat orientation = getOrientation(); - const float ANGULAR_SCALE = 0.5f; - glm::quat rotation = glm::angleAxis(glm::radians(- deltaX * ANGULAR_SCALE), orientation * IDENTITY_UP); - setPosition(position + rotation * (getPosition() - position)); - orientation = rotation * orientation; - setOrientation(orientation); - - // then vertically - float oldPitch = getHead()->getBasePitch(); - getHead()->setBasePitch(oldPitch - deltaY * ANGULAR_SCALE); - rotation = glm::angleAxis(glm::radians((getHead()->getBasePitch() - oldPitch)), orientation * IDENTITY_RIGHT); - - setPosition(position + rotation * (getPosition() - position)); -} - void MyAvatar::updateLookAtTargetAvatar() { // // Look at the avatar whose eyes are closest to the ray in direction of my avatar's head @@ -495,6 +519,7 @@ void MyAvatar::updateLookAtTargetAvatar() { float smallestAngleTo = MIN_LOOKAT_ANGLE; foreach (const AvatarSharedPointer& avatarPointer, Application::getInstance()->getAvatarManager().getAvatarHash()) { Avatar* avatar = static_cast(avatarPointer.data()); + avatar->setIsLookAtTarget(false); if (!avatar->isMyAvatar()) { float angleTo = glm::angle(getHead()->getFinalOrientation() * glm::vec3(0.0f, 0.0f, -1.0f), glm::normalize(avatar->getHead()->getEyePosition() - getHead()->getEyePosition())); @@ -505,6 +530,9 @@ void MyAvatar::updateLookAtTargetAvatar() { } } } + if (_lookAtTargetAvatar) { + static_cast(_lookAtTargetAvatar.data())->setIsLookAtTarget(true); + } } void MyAvatar::clearLookAtTargetAvatar() { @@ -548,7 +576,8 @@ void MyAvatar::renderBody(RenderMode renderMode, float glowLevel) { Model::RenderMode modelRenderMode = (renderMode == SHADOW_RENDER_MODE) ? Model::SHADOW_RENDER_MODE : Model::DEFAULT_RENDER_MODE; _skeletonModel.render(1.0f, modelRenderMode); - + renderAttachments(modelRenderMode); + // Render head so long as the camera isn't inside it if (shouldRenderHead(Application::getInstance()->getCamera()->getPosition(), renderMode)) { getHead()->render(1.0f, modelRenderMode); @@ -732,7 +761,7 @@ void MyAvatar::applyMotor(float deltaTime) { glm::vec3 targetVelocity = _motorVelocity; if (_motionBehaviors & AVATAR_MOTION_MOTOR_USE_LOCAL_FRAME) { // rotate _motorVelocity into world frame - glm::quat rotation = getOrientation(); + glm::quat rotation = getHead()->getCameraOrientation(); targetVelocity = rotation * _motorVelocity; } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0c02e42eb0..f893cc4f47 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -77,8 +77,6 @@ public: static void sendKillAvatar(); - void orbit(const glm::vec3& position, int deltaX, int deltaY); - Q_INVOKABLE glm::vec3 getTargetAvatarPosition() const { return _targetAvatarPosition; } AvatarData* getLookAtTargetAvatar() const { return _lookAtTargetAvatar.data(); } void updateLookAtTargetAvatar(); diff --git a/interface/src/devices/Faceplus.cpp b/interface/src/devices/Faceplus.cpp index f8eec434b7..00bc9d2676 100644 --- a/interface/src/devices/Faceplus.cpp +++ b/interface/src/devices/Faceplus.cpp @@ -220,15 +220,10 @@ void FaceplusReader::update() { if (!_referenceInitialized) { _referenceX = x; _referenceY = y; - _referenceScale = scale; _referenceInitialized = true; } const float TRANSLATION_SCALE = 10.0f; - const float REFERENCE_DISTANCE = 10.0f; - float depthScale = _referenceScale / scale; - float z = REFERENCE_DISTANCE * (depthScale - 1.0f); - glm::vec3 headTranslation((x - _referenceX) * depthScale * TRANSLATION_SCALE, - (y - _referenceY) * depthScale * TRANSLATION_SCALE, z); + glm::vec3 headTranslation((x - _referenceX) * TRANSLATION_SCALE, (y - _referenceY) * TRANSLATION_SCALE, 0.0f); glm::quat headRotation(glm::radians(glm::vec3(-_outputVector.at(_headRotationIndices[0]), _outputVector.at(_headRotationIndices[1]), -_outputVector.at(_headRotationIndices[2])))); float estimatedEyePitch = (_outputVector.at(_leftEyeRotationIndices[0]) + diff --git a/interface/src/devices/Faceplus.h b/interface/src/devices/Faceplus.h index f3c680c2d6..d52740ca5f 100644 --- a/interface/src/devices/Faceplus.h +++ b/interface/src/devices/Faceplus.h @@ -76,7 +76,6 @@ private: int _rightEyeRotationIndices[2]; float _referenceX; float _referenceY; - float _referenceScale; bool _referenceInitialized; QVector _blendshapeCoefficients; #endif diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index 7141ccb32f..b8b4f1f2a0 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -509,6 +509,24 @@ void Model::setURL(const QUrl& url, const QUrl& fallback, bool retainCurrent, bo } } +bool Model::getJointPosition(int jointIndex, glm::vec3& position) const { + if (jointIndex == -1 || _jointStates.isEmpty()) { + return false; + } + position = _translation + extractTranslation(_jointStates[jointIndex].transform); + return true; +} + +bool Model::getJointRotation(int jointIndex, glm::quat& rotation, bool fromBind) const { + if (jointIndex == -1 || _jointStates.isEmpty()) { + return false; + } + rotation = _jointStates[jointIndex].combinedRotation * + (fromBind ? _geometry->getFBXGeometry().joints[jointIndex].inverseBindRotation : + _geometry->getFBXGeometry().joints[jointIndex].inverseDefaultRotation); + return true; +} + void Model::clearShapes() { for (int i = 0; i < _jointShapes.size(); ++i) { delete _jointShapes[i]; @@ -949,24 +967,6 @@ void Model::maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint // nothing by default } -bool Model::getJointPosition(int jointIndex, glm::vec3& position) const { - if (jointIndex == -1 || _jointStates.isEmpty()) { - return false; - } - position = _translation + extractTranslation(_jointStates[jointIndex].transform); - return true; -} - -bool Model::getJointRotation(int jointIndex, glm::quat& rotation, bool fromBind) const { - if (jointIndex == -1 || _jointStates.isEmpty()) { - return false; - } - rotation = _jointStates[jointIndex].combinedRotation * - (fromBind ? _geometry->getFBXGeometry().joints[jointIndex].inverseBindRotation : - _geometry->getFBXGeometry().joints[jointIndex].inverseDefaultRotation); - return true; -} - bool Model::setJointPosition(int jointIndex, const glm::vec3& translation, const glm::quat& rotation, bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment) { if (jointIndex == -1 || _jointStates.isEmpty()) { diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index ae2bcd79b8..6a79772ca7 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -180,6 +180,9 @@ public: /// Returns the extended length from the right hand to its first free ancestor. float getRightArmLength() const; + bool getJointPosition(int jointIndex, glm::vec3& position) const; + bool getJointRotation(int jointIndex, glm::quat& rotation, bool fromBind = false) const; + void clearShapes(); void rebuildShapes(); void updateShapePositions(); @@ -269,9 +272,6 @@ protected: virtual void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state); virtual void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state); - bool getJointPosition(int jointIndex, glm::vec3& position) const; - bool getJointRotation(int jointIndex, glm::quat& rotation, bool fromBind = false) const; - bool setJointPosition(int jointIndex, const glm::vec3& translation, const glm::quat& rotation = glm::quat(), bool useRotation = false, int lastFreeIndex = -1, bool allIntermediatesFree = false, const glm::vec3& alignment = glm::vec3(0.0f, -1.0f, 0.0f)); diff --git a/interface/src/ui/AttachmentsDialog.cpp b/interface/src/ui/AttachmentsDialog.cpp new file mode 100644 index 0000000000..016098699b --- /dev/null +++ b/interface/src/ui/AttachmentsDialog.cpp @@ -0,0 +1,169 @@ +// +// AttachmentsDialog.cpp +// interface/src/ui +// +// Created by Andrzej Kapolka on 5/4/14. +// Copyright 2014 High Fidelity, Inc. +// +// 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 +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "AttachmentsDialog.h" + +AttachmentsDialog::AttachmentsDialog() : + QDialog(Application::getInstance()->getWindow()) { + + setWindowTitle("Edit Attachments"); + setAttribute(Qt::WA_DeleteOnClose); + + QVBoxLayout* layout = new QVBoxLayout(); + setLayout(layout); + + QScrollArea* area = new QScrollArea(); + layout->addWidget(area); + area->setWidgetResizable(true); + QWidget* container = new QWidget(); + container->setLayout(_attachments = new QVBoxLayout()); + container->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); + area->setWidget(container); + _attachments->addStretch(1); + + foreach (const AttachmentData& data, Application::getInstance()->getAvatar()->getAttachmentData()) { + addAttachment(data); + } + + QPushButton* newAttachment = new QPushButton("New Attachment"); + connect(newAttachment, SIGNAL(clicked(bool)), SLOT(addAttachment())); + layout->addWidget(newAttachment); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok); + layout->addWidget(buttons); + connect(buttons, SIGNAL(accepted()), SLOT(deleteLater())); + _ok = buttons->button(QDialogButtonBox::Ok); + + setMinimumSize(600, 600); +} + +void AttachmentsDialog::setVisible(bool visible) { + QDialog::setVisible(visible); + + // un-default the OK button + if (visible) { + _ok->setDefault(false); + } +} + +void AttachmentsDialog::updateAttachmentData() { + QVector data; + for (int i = 0; i < _attachments->count() - 1; i++) { + data.append(static_cast(_attachments->itemAt(i)->widget())->getAttachmentData()); + } + Application::getInstance()->getAvatar()->setAttachmentData(data); +} + +void AttachmentsDialog::addAttachment(const AttachmentData& data) { + _attachments->insertWidget(_attachments->count() - 1, new AttachmentPanel(this, data)); +} + +static QDoubleSpinBox* createTranslationBox(AttachmentsDialog* dialog, float value) { + QDoubleSpinBox* box = new QDoubleSpinBox(); + box->setSingleStep(0.01); + box->setMinimum(-FLT_MAX); + box->setMaximum(FLT_MAX); + box->setValue(value); + dialog->connect(box, SIGNAL(valueChanged(double)), SLOT(updateAttachmentData())); + return box; +} + +static QDoubleSpinBox* createRotationBox(AttachmentsDialog* dialog, float value) { + QDoubleSpinBox* box = new QDoubleSpinBox(); + box->setMinimum(-180.0); + box->setMaximum(180.0); + box->setWrapping(true); + box->setValue(value); + dialog->connect(box, SIGNAL(valueChanged(double)), SLOT(updateAttachmentData())); + return box; +} + +AttachmentPanel::AttachmentPanel(AttachmentsDialog* dialog, const AttachmentData& data) { + setFrameStyle(QFrame::StyledPanel); + + QFormLayout* layout = new QFormLayout(); + layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + setLayout(layout); + + QHBoxLayout* urlBox = new QHBoxLayout(); + layout->addRow("Model URL:", urlBox); + urlBox->addWidget(_modelURL = new QLineEdit(data.modelURL.toString()), 1); + _modelURL->setText(data.modelURL.toString()); + dialog->connect(_modelURL, SIGNAL(returnPressed()), SLOT(updateAttachmentData())); + QPushButton* chooseURL = new QPushButton("Choose"); + urlBox->addWidget(chooseURL); + connect(chooseURL, SIGNAL(clicked(bool)), SLOT(chooseModelURL())); + + layout->addRow("Joint:", _jointName = new QComboBox()); + QSharedPointer geometry = Application::getInstance()->getAvatar()->getSkeletonModel().getGeometry(); + if (geometry && geometry->isLoaded()) { + foreach (const FBXJoint& joint, geometry->getFBXGeometry().joints) { + _jointName->addItem(joint.name); + } + } + _jointName->setCurrentText(data.jointName); + dialog->connect(_jointName, SIGNAL(currentIndexChanged(int)), SLOT(updateAttachmentData())); + + QHBoxLayout* translationBox = new QHBoxLayout(); + translationBox->addWidget(_translationX = createTranslationBox(dialog, data.translation.x)); + translationBox->addWidget(_translationY = createTranslationBox(dialog, data.translation.y)); + translationBox->addWidget(_translationZ = createTranslationBox(dialog, data.translation.z)); + layout->addRow("Translation:", translationBox); + + QHBoxLayout* rotationBox = new QHBoxLayout(); + glm::vec3 eulers = glm::degrees(safeEulerAngles(data.rotation)); + rotationBox->addWidget(_rotationX = createRotationBox(dialog, eulers.x)); + rotationBox->addWidget(_rotationY = createRotationBox(dialog, eulers.y)); + rotationBox->addWidget(_rotationZ = createRotationBox(dialog, eulers.z)); + layout->addRow("Rotation:", rotationBox); + + layout->addRow("Scale:", _scale = new QDoubleSpinBox()); + _scale->setSingleStep(0.01); + _scale->setMaximum(FLT_MAX); + _scale->setValue(data.scale); + dialog->connect(_scale, SIGNAL(valueChanged(double)), SLOT(updateAttachmentData())); + + QPushButton* remove = new QPushButton("Delete"); + layout->addRow(remove); + connect(remove, SIGNAL(clicked(bool)), SLOT(deleteLater())); + dialog->connect(remove, SIGNAL(clicked(bool)), SLOT(updateAttachmentData()), Qt::QueuedConnection); +} + +AttachmentData AttachmentPanel::getAttachmentData() const { + AttachmentData data; + data.modelURL = _modelURL->text(); + data.jointName = _jointName->currentText(); + data.translation = glm::vec3(_translationX->value(), _translationY->value(), _translationZ->value()); + data.rotation = glm::quat(glm::radians(glm::vec3(_rotationX->value(), _rotationY->value(), _rotationZ->value()))); + data.scale = _scale->value(); + return data; +} + +void AttachmentPanel::chooseModelURL() { + ModelsBrowser modelBrowser(ATTACHMENT_MODEL, this); + connect(&modelBrowser, SIGNAL(selected(QString)), SLOT(setModelURL(const QString&))); + modelBrowser.browse(); +} + +void AttachmentPanel::setModelURL(const QString& url) { + _modelURL->setText(url); + emit _modelURL->returnPressed(); +} diff --git a/interface/src/ui/AttachmentsDialog.h b/interface/src/ui/AttachmentsDialog.h new file mode 100644 index 0000000000..4e67ae8882 --- /dev/null +++ b/interface/src/ui/AttachmentsDialog.h @@ -0,0 +1,77 @@ +// +// AttachmentsDialog.h +// interface/src/ui +// +// Created by Andrzej Kapolka on 5/4/14. +// Copyright 2014 High Fidelity, Inc. +// +// 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_AttachmentsDialog_h +#define hifi_AttachmentsDialog_h + +#include +#include + +#include + +class QComboBox; +class QDoubleSpinner; +class QLineEdit; +class QVBoxLayout; + +/// Allows users to edit the avatar attachments. +class AttachmentsDialog : public QDialog { + Q_OBJECT + +public: + + AttachmentsDialog(); + + virtual void setVisible(bool visible); + +public slots: + + void updateAttachmentData(); + +private slots: + + void addAttachment(const AttachmentData& data = AttachmentData()); + +private: + + QVBoxLayout* _attachments; + QPushButton* _ok; +}; + +/// A panel controlling a single attachment. +class AttachmentPanel : public QFrame { + Q_OBJECT + +public: + + AttachmentPanel(AttachmentsDialog* dialog, const AttachmentData& data = AttachmentData()); + + AttachmentData getAttachmentData() const; + +private slots: + + void chooseModelURL(); + void setModelURL(const QString& url); + +private: + + QLineEdit* _modelURL; + QComboBox* _jointName; + QDoubleSpinBox* _translationX; + QDoubleSpinBox* _translationY; + QDoubleSpinBox* _translationZ; + QDoubleSpinBox* _rotationX; + QDoubleSpinBox* _rotationY; + QDoubleSpinBox* _rotationZ; + QDoubleSpinBox* _scale; +}; + +#endif // hifi_AttachmentsDialog_h diff --git a/interface/src/ui/ChatWindow.cpp b/interface/src/ui/ChatWindow.cpp index 3f2df5593b..39db2b734e 100644 --- a/interface/src/ui/ChatWindow.cpp +++ b/interface/src/ui/ChatWindow.cpp @@ -26,17 +26,23 @@ #include "ChatWindow.h" + + const int NUM_MESSAGES_TO_TIME_STAMP = 20; const QRegularExpression regexLinks("((?:(?:ftp)|(?:https?))://\\S+)"); const QRegularExpression regexHifiLinks("([#@]\\S+)"); +const QString mentionSoundsPath("/sounds/mention/"); +const QString mentionRegex("@(\\b%1\\b)"); ChatWindow::ChatWindow(QWidget* parent) : FramelessDialog(parent, 0, POSITION_RIGHT), ui(new Ui::ChatWindow), numMessagesAfterLastTimeStamp(0), _mousePressed(false), - _mouseStartPosition() + _mouseStartPosition(), + _trayIcon(parent), + _effectPlayer() { setAttribute(Qt::WA_DeleteOnClose, false); @@ -77,16 +83,47 @@ ChatWindow::ChatWindow(QWidget* parent) : ui->usersWidget->hide(); ui->messagesScrollArea->hide(); ui->messagePlainTextEdit->hide(); - connect(&xmppClient, SIGNAL(connected()), this, SLOT(connected())); + connect(&XmppClient::getInstance(), SIGNAL(joinedPublicChatRoom()), this, SLOT(connected())); } connect(&xmppClient, SIGNAL(messageReceived(QXmppMessage)), this, SLOT(messageReceived(QXmppMessage))); + connect(&_trayIcon, SIGNAL(messageClicked()), this, SLOT(notificationClicked())); #endif + + QDir mentionSoundsDir(Application::resourcesPath() + mentionSoundsPath); + _mentionSounds = mentionSoundsDir.entryList(QDir::Files); + _trayIcon.setIcon(QIcon( Application::resourcesPath() + "/images/hifi-logo.svg")); +} + +void ChatWindow::notificationClicked() { + if (parentWidget()->isMinimized()) { + parentWidget()->showNormal(); + } + if (isHidden()) { + show(); + } + + // find last mention + int messageCount = ui->messagesVBoxLayout->count(); + for (unsigned int i = messageCount; i > 0; i--) { + ChatMessageArea* area = (ChatMessageArea*)ui->messagesVBoxLayout->itemAt(i - 1)->widget(); + QRegularExpression usernameMention(mentionRegex.arg(AccountManager::getInstance().getAccountInfo().getUsername())); + if (area->toPlainText().contains(usernameMention)) { + int top = area->geometry().top(); + int height = area->geometry().height(); + + QScrollBar* verticalScrollBar = ui->messagesScrollArea->verticalScrollBar(); + verticalScrollBar->setSliderPosition(top - verticalScrollBar->size().height() + height); + return; + } + } + + scrollToBottom(); } ChatWindow::~ChatWindow() { #ifdef HAVE_QXMPP const QXmppClient& xmppClient = XmppClient::getInstance().getXMPPClient(); - disconnect(&xmppClient, SIGNAL(connected()), this, SLOT(connected())); + disconnect(&xmppClient, SIGNAL(joinedPublicChatRoom()), this, SLOT(connected())); disconnect(&xmppClient, SIGNAL(messageReceived(QXmppMessage)), this, SLOT(messageReceived(QXmppMessage))); const QXmppMucRoom* publicChatRoom = XmppClient::getInstance().getPublicChatRoom(); @@ -105,9 +142,15 @@ void ChatWindow::keyPressEvent(QKeyEvent* event) { void ChatWindow::showEvent(QShowEvent* event) { FramelessDialog::showEvent(event); + if (!event->spontaneous()) { ui->messagePlainTextEdit->setFocus(); } + + const QXmppClient& xmppClient = XmppClient::getInstance().getXMPPClient(); + if (xmppClient.isConnected()) { + participantsChanged(); + } } bool ChatWindow::eventFilter(QObject* sender, QEvent* event) { @@ -304,6 +347,21 @@ void ChatWindow::messageReceived(const QXmppMessage& message) { } else { lastMessageStamp = QDateTime::currentDateTime(); } + + QRegularExpression usernameMention(mentionRegex.arg(AccountManager::getInstance().getAccountInfo().getUsername())); + if (isHidden() && message.body().contains(usernameMention)) { + if (_effectPlayer.state() != QMediaPlayer::PlayingState) { + // get random sound + QFileInfo inf = QFileInfo(Application::resourcesPath() + + mentionSoundsPath + + _mentionSounds.at(rand() % _mentionSounds.size())); + _effectPlayer.setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); + _effectPlayer.play(); + } + + _trayIcon.show(); + _trayIcon.showMessage(windowTitle(), message.body()); + } } #endif diff --git a/interface/src/ui/ChatWindow.h b/interface/src/ui/ChatWindow.h index 104fbe1746..1e0f533e9e 100644 --- a/interface/src/ui/ChatWindow.h +++ b/interface/src/ui/ChatWindow.h @@ -14,6 +14,8 @@ #include #include +#include +#include #include #include @@ -63,6 +65,9 @@ private: QDateTime lastMessageStamp; bool _mousePressed; QPoint _mouseStartPosition; + QSystemTrayIcon _trayIcon; + QStringList _mentionSounds; + QMediaPlayer _effectPlayer; private slots: void connected(); @@ -71,6 +76,7 @@ private slots: void error(QXmppClient::Error error); void participantsChanged(); void messageReceived(const QXmppMessage& message); + void notificationClicked(); #endif }; diff --git a/interface/src/ui/ModelsBrowser.cpp b/interface/src/ui/ModelsBrowser.cpp index 77e056bdd3..f65829a8ac 100644 --- a/interface/src/ui/ModelsBrowser.cpp +++ b/interface/src/ui/ModelsBrowser.cpp @@ -22,10 +22,11 @@ #include "ModelsBrowser.h" +const char* MODEL_TYPE_NAMES[] = { "heads", "skeletons", "attachments" }; + static const QString S3_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com"; static const QString PUBLIC_URL = "http://public.highfidelity.io"; -static const QString HEAD_MODELS_LOCATION = "models/heads"; -static const QString SKELETON_MODELS_LOCATION = "models/skeletons/"; +static const QString MODELS_LOCATION = "models/"; static const QString PREFIX_PARAMETER_NAME = "prefix"; static const QString MARKER_PARAMETER_NAME = "marker"; @@ -243,11 +244,7 @@ void ModelHandler::queryNewFiles(QString marker) { // Build query QUrl url(S3_URL); QUrlQuery query; - if (_type == Head) { - query.addQueryItem(PREFIX_PARAMETER_NAME, HEAD_MODELS_LOCATION); - } else if (_type == Skeleton) { - query.addQueryItem(PREFIX_PARAMETER_NAME, SKELETON_MODELS_LOCATION); - } + query.addQueryItem(PREFIX_PARAMETER_NAME, MODELS_LOCATION + MODEL_TYPE_NAMES[_type]); if (!marker.isEmpty()) { query.addQueryItem(MARKER_PARAMETER_NAME, marker); diff --git a/interface/src/ui/ModelsBrowser.h b/interface/src/ui/ModelsBrowser.h index 81f64c6730..ff273a45bc 100644 --- a/interface/src/ui/ModelsBrowser.h +++ b/interface/src/ui/ModelsBrowser.h @@ -16,12 +16,14 @@ #include #include - enum ModelType { - Head, - Skeleton + HEAD_MODEL, + SKELETON_MODEL, + ATTACHMENT_MODEL }; +extern const char* MODEL_TYPE_NAMES[]; + class ModelHandler : public QObject { Q_OBJECT public: diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 7a70b743bd..eed33fda23 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -48,7 +48,7 @@ void PreferencesDialog::openHeadModelBrowser() { setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); - ModelsBrowser modelBrowser(Head); + ModelsBrowser modelBrowser(HEAD_MODEL); connect(&modelBrowser, &ModelsBrowser::selected, this, &PreferencesDialog::setHeadUrl); modelBrowser.browse(); @@ -60,7 +60,7 @@ void PreferencesDialog::openBodyModelBrowser() { setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); - ModelsBrowser modelBrowser(Skeleton); + ModelsBrowser modelBrowser(SKELETON_MODEL); connect(&modelBrowser, &ModelsBrowser::selected, this, &PreferencesDialog::setSkeletonUrl); modelBrowser.browse(); diff --git a/interface/ui/preferencesDialog.ui b/interface/ui/preferencesDialog.ui index 278e3c5dab..8a84956269 100644 --- a/interface/ui/preferencesDialog.ui +++ b/interface/ui/preferencesDialog.ui @@ -198,9 +198,6 @@ color: #0e7077 25 - - - @@ -510,7 +507,7 @@ color: #0e7077 - + 0 @@ -547,7 +544,7 @@ color: #0e7077 - + @@ -564,7 +561,7 @@ color: #0e7077 - + Qt::Horizontal diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index b57d5406d5..bbfff8f025 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -599,8 +599,9 @@ bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray &packet) { QUuid avatarUUID; QUrl faceModelURL, skeletonModelURL; + QVector attachmentData; QString displayName; - packetStream >> avatarUUID >> faceModelURL >> skeletonModelURL >> displayName; + packetStream >> avatarUUID >> faceModelURL >> skeletonModelURL >> attachmentData >> displayName; bool hasIdentityChanged = false; @@ -618,7 +619,12 @@ bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray &packet) { setDisplayName(displayName); hasIdentityChanged = true; } - + + if (attachmentData != _attachmentData) { + setAttachmentData(attachmentData); + hasIdentityChanged = true; + } + return hasIdentityChanged; } @@ -626,7 +632,7 @@ QByteArray AvatarData::identityByteArray() { QByteArray identityData; QDataStream identityStream(&identityData, QIODevice::Append); - identityStream << QUuid() << _faceModelURL << _skeletonModelURL << _displayName; + identityStream << QUuid() << _faceModelURL << _skeletonModelURL << _attachmentData << _displayName; return identityData; } @@ -654,6 +660,10 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { updateJointMappings(); } +void AvatarData::setAttachmentData(const QVector& attachmentData) { + _attachmentData = attachmentData; +} + void AvatarData::setDisplayName(const QString& displayName) { _displayName = displayName; @@ -762,3 +772,23 @@ void AvatarData::updateJointMappings() { connect(networkReply, SIGNAL(finished()), this, SLOT(setJointMappingsFromNetworkReply())); } } + +AttachmentData::AttachmentData() : + scale(1.0f) { +} + +bool AttachmentData::operator==(const AttachmentData& other) const { + return modelURL == other.modelURL && jointName == other.jointName && translation == other.translation && + rotation == other.rotation && scale == other.scale; +} + +QDataStream& operator<<(QDataStream& out, const AttachmentData& attachment) { + return out << attachment.modelURL << attachment.jointName << + attachment.translation << attachment.rotation << attachment.scale; +} + +QDataStream& operator>>(QDataStream& in, AttachmentData& attachment) { + return in >> attachment.modelURL >> attachment.jointName >> + attachment.translation >> attachment.rotation >> attachment.scale; +} + diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index af0b0c57d6..79ca638585 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -45,6 +45,8 @@ typedef unsigned long long quint64; #include #include +#include + #include #include "HeadData.h" @@ -95,8 +97,10 @@ enum KeyState { const glm::vec3 vec3Zero(0.0f); +class QDataStream; class QNetworkAccessManager; +class AttachmentData; class JointData; class AvatarData : public QObject { @@ -226,9 +230,11 @@ public: const QUrl& getFaceModelURL() const { return _faceModelURL; } QString getFaceModelURLString() const { return _faceModelURL.toString(); } const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; } + const QVector& getAttachmentData() const { return _attachmentData; } const QString& getDisplayName() const { return _displayName; } virtual void setFaceModelURL(const QUrl& faceModelURL); virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); + virtual void setAttachmentData(const QVector& attachmentData); virtual void setDisplayName(const QString& displayName); virtual void setBillboard(const QByteArray& billboard); @@ -291,6 +297,7 @@ protected: QUrl _faceModelURL; QUrl _skeletonModelURL; + QVector _attachmentData; QString _displayName; QRect _displayNameBoundingRect; @@ -325,4 +332,20 @@ public: glm::quat rotation; }; +class AttachmentData { +public: + QUrl modelURL; + QString jointName; + glm::vec3 translation; + glm::quat rotation; + float scale; + + AttachmentData(); + + bool operator==(const AttachmentData& other) const; +}; + +QDataStream& operator<<(QDataStream& out, const AttachmentData& attachment); +QDataStream& operator>>(QDataStream& in, AttachmentData& attachment); + #endif // hifi_AvatarData_h diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 6b17a3fab8..8e3797cbc0 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -127,8 +127,9 @@ void AvatarHashMap::processAvatarIdentityPacket(const QByteArray &packet, const while (!identityStream.atEnd()) { QUrl faceMeshURL, skeletonURL; + QVector attachmentData; QString displayName; - identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> displayName; + identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> attachmentData >> displayName; // mesh URL for a UUID, find avatar in our list AvatarSharedPointer matchingAvatar = matchingOrNewAvatar(sessionUUID, mixerWeakPointer); @@ -142,6 +143,10 @@ void AvatarHashMap::processAvatarIdentityPacket(const QByteArray &packet, const matchingAvatar->setSkeletonModelURL(skeletonURL); } + if (matchingAvatar->getAttachmentData() != attachmentData) { + matchingAvatar->setAttachmentData(attachmentData); + } + if (matchingAvatar->getDisplayName() != displayName) { matchingAvatar->setDisplayName(displayName); } @@ -171,4 +176,4 @@ void AvatarHashMap::processKillAvatar(const QByteArray& datagram) { if (matchedAvatar != _avatarHash.end()) { erase(matchedAvatar); } -} \ No newline at end of file +} diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index a21ed2627a..40a7d56c0d 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -817,7 +817,7 @@ ExtractedMesh extractMesh(const FBXNode& object) { while (endIndex < data.polygonIndices.size() && data.polygonIndices.at(endIndex++) >= 0); QPair materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0, - (polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0); + (polygonIndex < textures.size()) ? textures.at(polygonIndex) : -1); int& partIndex = materialTextureParts[materialTexture]; if (partIndex == 0) { data.extracted.partMaterialTextures.append(materialTexture); diff --git a/libraries/models/src/ModelItem.h b/libraries/models/src/ModelItem.h index 317299be2d..76a78122ff 100644 --- a/libraries/models/src/ModelItem.h +++ b/libraries/models/src/ModelItem.h @@ -46,7 +46,7 @@ const uint16_t MODEL_PACKET_CONTAINS_MODEL_ROTATION = 2048; const float MODEL_DEFAULT_RADIUS = 0.1f / TREE_SCALE; const float MINIMUM_MODEL_ELEMENT_SIZE = (1.0f / 100000.0f) / TREE_SCALE; // smallest size container const QString MODEL_DEFAULT_MODEL_URL(""); -const glm::quat MODEL_DEFAULT_MODEL_ROTATION(0, 0, 0, 0); +const glm::quat MODEL_DEFAULT_MODEL_ROTATION; /// A collection of properties of a model item used in the scripting API. Translates between the actual properties of a model /// and a JavaScript style hash/QScriptValue storing a set of properties. Used in scripting to set/get the complete set of diff --git a/libraries/networking/src/PacketHeaders.cpp b/libraries/networking/src/PacketHeaders.cpp index 0955759097..0785b81581 100644 --- a/libraries/networking/src/PacketHeaders.cpp +++ b/libraries/networking/src/PacketHeaders.cpp @@ -49,6 +49,8 @@ PacketVersion versionForPacketType(PacketType type) { switch (type) { case PacketTypeAvatarData: return 3; + case PacketTypeAvatarIdentity: + return 1; case PacketTypeEnvironmentData: return 1; case PacketTypeParticleData: diff --git a/libraries/octree/src/ViewFrustum.h b/libraries/octree/src/ViewFrustum.h index acd5c639f7..4fc3bc2d8b 100644 --- a/libraries/octree/src/ViewFrustum.h +++ b/libraries/octree/src/ViewFrustum.h @@ -24,6 +24,7 @@ const float DEFAULT_KEYHOLE_RADIUS = 3.0f; const float DEFAULT_FIELD_OF_VIEW_DEGREES = 90.0f; +const float DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES = 30.f; const float DEFAULT_ASPECT_RATIO = 16.f/9.f; const float DEFAULT_NEAR_CLIP = 0.08f; const float DEFAULT_FAR_CLIP = 50.0f * TREE_SCALE; diff --git a/libraries/shared/src/StreamUtils.cpp b/libraries/shared/src/StreamUtils.cpp index d7b0c83c1f..5356c45a51 100644 --- a/libraries/shared/src/StreamUtils.cpp +++ b/libraries/shared/src/StreamUtils.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include "StreamUtils.h" @@ -47,6 +49,22 @@ std::ostream& operator<<(std::ostream& s, const glm::mat4& m) { return s; } +QDataStream& operator<<(QDataStream& out, const glm::vec3& vector) { + return out << vector.x << vector.y << vector.z; +} + +QDataStream& operator>>(QDataStream& in, glm::vec3& vector) { + return in >> vector.x >> vector.y >> vector.z; +} + +QDataStream& operator<<(QDataStream& out, const glm::quat& quaternion) { + return out << quaternion.x << quaternion.y << quaternion.z << quaternion.w; +} + +QDataStream& operator>>(QDataStream& in, glm::quat& quaternion) { + return in >> quaternion.x >> quaternion.y >> quaternion.z >> quaternion.w; +} + // less common utils can be enabled with DEBUG #ifdef DEBUG diff --git a/libraries/shared/src/StreamUtils.h b/libraries/shared/src/StreamUtils.h index 2546d49ffc..2a42a3ea7b 100644 --- a/libraries/shared/src/StreamUtils.h +++ b/libraries/shared/src/StreamUtils.h @@ -19,6 +19,7 @@ #include #include +class QDataStream; namespace StreamUtil { // dump the buffer, 32 bytes per row, each byte in hex, separated by whitespace @@ -29,6 +30,12 @@ std::ostream& operator<<(std::ostream& s, const glm::vec3& v); std::ostream& operator<<(std::ostream& s, const glm::quat& q); std::ostream& operator<<(std::ostream& s, const glm::mat4& m); +QDataStream& operator<<(QDataStream& out, const glm::vec3& vector); +QDataStream& operator>>(QDataStream& in, glm::vec3& vector); + +QDataStream& operator<<(QDataStream& out, const glm::quat& quaternion); +QDataStream& operator>>(QDataStream& in, glm::quat& quaternion); + // less common utils can be enabled with DEBUG #ifdef DEBUG #include "CollisionInfo.h"