diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 86054f3fcd..d9cc78304f 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" @@ -3090,6 +3090,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 +3427,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 4af38e8770..0c1c7fd3b5 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, @@ -835,6 +840,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..9cca82f399 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; @@ -171,6 +172,7 @@ public slots: private slots: void aboutApp(); void editPreferences(); + void editAttachments(); void goToDomainDialog(); void goToLocation(); void nameLocation(); @@ -252,6 +254,7 @@ private: SimpleMovingAverage _fastFPSAverage; QAction* _loginAction; QPointer _preferencesDialog; + QPointer _attachmentsDialog; QAction* _chatAction; QString _snapshotsLocation; }; @@ -261,6 +264,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 +366,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..2b86e04829 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; @@ -510,9 +506,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 +527,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 +571,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 +604,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/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index e8ac93234c..e3700b920b 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; @@ -338,6 +339,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 +349,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 * _skeletonModel.getScale()); + 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 +692,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..9828e120c1 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -131,6 +131,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 +161,7 @@ signals: protected: SkeletonModel _skeletonModel; + QVector _attachmentModels; float _bodyYawDelta; glm::vec3 _velocity; float _leanScale; @@ -188,6 +190,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: diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index cd799b7d2e..0ef28a87f8 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()); @@ -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(); @@ -548,7 +589,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 +774,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/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..edd28d461c --- /dev/null +++ b/interface/src/ui/AttachmentsDialog.cpp @@ -0,0 +1,154 @@ +// +// 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); + + 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())); + + setMinimumSize(600, 600); +} + +void AttachmentsDialog::updateAttachmentData() { + QVector data; + for (int i = 0; i < _attachments->count(); i++) { + data.append(static_cast(_attachments->itemAt(i)->widget())->getAttachmentData()); + } + Application::getInstance()->getAvatar()->setAttachmentData(data); +} + +void AttachmentsDialog::addAttachment(const AttachmentData& data) { + _attachments->addWidget(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) { + QFormLayout* layout = new QFormLayout(); + 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())); +} + +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..c23bd2efb8 --- /dev/null +++ b/interface/src/ui/AttachmentsDialog.h @@ -0,0 +1,73 @@ +// +// 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 + +class QComboBox; +class QDoubleSpinner; +class QLineEdit; +class QVBoxLayout; + +/// Allows users to edit the avatar attachments. +class AttachmentsDialog : public QDialog { + Q_OBJECT + +public: + + AttachmentsDialog(); + +public slots: + + void updateAttachmentData(); + +private slots: + + void addAttachment(const AttachmentData& data = AttachmentData()); + +private: + + QVBoxLayout* _attachments; +}; + +/// A panel controlling a single attachment. +class AttachmentPanel : public QWidget { + 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/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/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/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/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"