diff --git a/examples/dice.js b/examples/dice.js index b0021ecebb..2aefcf90fe 100644 --- a/examples/dice.js +++ b/examples/dice.js @@ -45,6 +45,9 @@ var diceButton = Overlays.addOverlay("image", { var GRAVITY = -3.5; var LIFETIME = 300; +// NOTE: angularVelocity is in radians/sec +var MAX_ANGULAR_SPEED = Math.PI; + function shootDice(position, velocity) { for (var i = 0; i < NUMBER_OF_DICE; i++) { dice.push(Entities.addEntity( @@ -53,9 +56,7 @@ function shootDice(position, velocity) { position: position, velocity: velocity, rotation: Quat.fromPitchYawRollDegrees(Math.random() * 360, Math.random() * 360, Math.random() * 360), - // NOTE: angularVelocity is in radians/sec - var maxAngularSpeed = Math.PI; - angularVelocity: { x: Math.random() * maxAngularSpeed, y: Math.random() * maxAngularSpeed, z: Math.random() * maxAngularSpeed }, + angularVelocity: { x: Math.random() * MAX_ANGULAR_SPEED, y: Math.random() * MAX_ANGULAR_SPEED, z: Math.random() * MAX_ANGULAR_SPEED }, lifetime: LIFETIME, gravity: { x: 0, y: GRAVITY, z: 0 }, shapeType: "box", diff --git a/examples/example/entities/makeHouses.js b/examples/example/entities/makeHouses.js index 37bc1d5a8e..d49f737880 100644 --- a/examples/example/entities/makeHouses.js +++ b/examples/example/entities/makeHouses.js @@ -70,6 +70,7 @@ var rotOdd = Quat.fromPitchYawRollDegrees(0, 90.0 + MyAvatar.bodyYaw, 0.0); var housePos = Vec3.sum(MyAvatar.position, Quat.getFront(Camera.getOrientation())); + var housePositions = [] for (var j = 0; j < measures.rows; j++) { var posX1 = 0 - (xRange / 2); @@ -87,11 +88,8 @@ y: 0, z: dd }; - - print("House nr.:" + (houses.length + 1)); - houses.push( - addHouseAt(Vec3.sum(housePos, posShift), (j % 2 == 0) ? rotEven : rotOdd) - ); + + housePositions.push(Vec3.sum(housePos, posShift)); posX1 += measures.parcelWidth; } } @@ -144,14 +142,21 @@ }; } - function cleanup() { - while (houses.length > 0) { - if (!houses[0].isKnownID) { - houses[0] = Entities.identifyEntity(houses[0]); - } - Entities.deleteEntity(houses.shift()); + var addHouses = function() { + if (housePositions.length > 0) { + position = housePositions.pop(); + print("House nr.:" + (houses.length + 1)); + houses.push( + addHouseAt(position, (housePositions.length % 2 == 0) ? rotEven : rotOdd) + ); + + // max 20 per second + Script.setTimeout(addHouses, 50); + } else { + Script.stop(); } - } - - Script.scriptEnding.connect(cleanup); + }; + + addHouses(); + })(); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 549f05a905..eb427737e1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -76,7 +76,7 @@ #include #include #include -//#include +#include #include #include #include @@ -221,7 +221,7 @@ bool setupEssentials(int& argc, char** argv) { auto addressManager = DependencyManager::set(); auto nodeList = DependencyManager::set(NodeType::Agent, listenPort); auto geometryCache = DependencyManager::set(); - //auto scriptCache = DependencyManager::set(); + auto scriptCache = DependencyManager::set(); auto soundCache = DependencyManager::set(); auto glowEffect = DependencyManager::set(); auto faceshift = DependencyManager::set(); @@ -620,7 +620,7 @@ Application::~Application() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - //DependencyManager::destroy(); + DependencyManager::destroy(); DependencyManager::destroy(); QThread* nodeThread = DependencyManager::get()->thread(); @@ -1524,7 +1524,7 @@ void Application::idle() { } // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing - // details if we're in ExtraDebugging mode. However, the ::update() and it's subcomponents will show their timing + // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing // details normally. bool showWarnings = getLogger()->extraDebugging(); PerformanceWarning warn(showWarnings, "idle()"); @@ -2540,7 +2540,7 @@ bool Application::isHMDMode() const { QRect Application::getDesirableApplicationGeometry() { QRect applicationGeometry = getWindow()->geometry(); - // If our parent window is on the HMD, then don't use it's geometry, instead use + // If our parent window is on the HMD, then don't use its geometry, instead use // the "main screen" geometry. HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); if (hmdTools && hmdTools->hasHMDScreen()) { @@ -3376,7 +3376,7 @@ void Application::nodeKilled(SharedNodePointer node) { void Application::trackIncomingOctreePacket(const QByteArray& packet, const SharedNodePointer& sendingNode, bool wasStatsPacket) { - // Attempt to identify the sender from it's address. + // Attempt to identify the sender from its address. if (sendingNode) { QUuid nodeUUID = sendingNode->getUUID(); @@ -3445,7 +3445,7 @@ int Application::parseOctreeStats(const QByteArray& packet, const SharedNodePoin } // store jurisdiction details for later use // This is bit of fiddling is because JurisdictionMap assumes it is the owner of the values used to construct it - // but OctreeSceneStats thinks it's just returning a reference to it's contents. So we need to make a copy of the + // but OctreeSceneStats thinks it's just returning a reference to its contents. So we need to make a copy of the // details from the OctreeSceneStats to construct the JurisdictionMap JurisdictionMap jurisdictionMap; jurisdictionMap.copyContents(temp.getJurisdictionRoot(), temp.getJurisdictionEndNodes()); @@ -3665,17 +3665,56 @@ bool Application::askToSetAvatarUrl(const QString& url) { msgBox.exec(); return false; } - - QString message = "Would you like to use this model for part of avatar:\n" + url; + + // Download the FST file, to attempt to determine its model type + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest = QNetworkRequest(url); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + qDebug() << "Downloading avatar file at " << url; + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + QByteArray fstContents = reply->readAll(); + delete reply; + QVariantHash fstMapping = FSTReader::readMapping(fstContents); + + FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); + QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Question); msgBox.setWindowTitle("Set Avatar"); - msgBox.setText(message); + QPushButton* headButton = NULL; + QPushButton* bodyButton = NULL; + QPushButton* bodyAndHeadButton = NULL; + + QString message; + QString typeInfo; + switch (modelType) { + case FSTReader::HEAD_MODEL: + message = QString("Would you like to use '") + fstMapping["name"].toString() + QString("' for your avatar head?"); + headButton = msgBox.addButton(tr("Yes"), QMessageBox::ActionRole); + break; - QPushButton* headButton = msgBox.addButton(tr("Head"), QMessageBox::ActionRole); - QPushButton* bodyButton = msgBox.addButton(tr("Body"), QMessageBox::ActionRole); - QPushButton* bodyAndHeadButton = msgBox.addButton(tr("Body + Head"), QMessageBox::ActionRole); + case FSTReader::BODY_ONLY_MODEL: + message = QString("Would you like to use '") + fstMapping["name"].toString() + QString("' for your avatar body?"); + bodyButton = msgBox.addButton(tr("Yes"), QMessageBox::ActionRole); + break; + + case FSTReader::HEAD_AND_BODY_MODEL: + message = QString("Would you like to use '") + fstMapping["name"].toString() + QString("' for your avatar?"); + bodyAndHeadButton = msgBox.addButton(tr("Yes"), QMessageBox::ActionRole); + break; + + default: + message = QString("Would you like to use '") + fstMapping["name"].toString() + QString("' for some part of your avatar head?"); + headButton = msgBox.addButton(tr("Use for Head"), QMessageBox::ActionRole); + bodyButton = msgBox.addButton(tr("Use for Body"), QMessageBox::ActionRole); + bodyAndHeadButton = msgBox.addButton(tr("Use for Body and Head"), QMessageBox::ActionRole); + break; + } + + msgBox.setText(message); msgBox.addButton(QMessageBox::Cancel); msgBox.exec(); @@ -3688,6 +3727,11 @@ bool Application::askToSetAvatarUrl(const QString& url) { } else if (msgBox.clickedButton() == bodyButton) { qDebug() << "Chose to use for body: " << url; _myAvatar->setSkeletonModelURL(url); + // if the head is empty, reset it to the default head. + if (_myAvatar->getFaceModelURLString().isEmpty()) { + _myAvatar->setFaceModelURL(DEFAULT_HEAD_MODEL_URL); + UserActivityLogger::getInstance().changedModel("head", DEFAULT_HEAD_MODEL_URL.toString()); + } UserActivityLogger::getInstance().changedModel("skeleton", url); _myAvatar->sendIdentityPacket(); } else if (msgBox.clickedButton() == bodyAndHeadButton) { diff --git a/interface/src/Menu.h b/interface/src/Menu.h index a4e644f20f..63dab63711 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -246,7 +246,7 @@ namespace MenuOption { const QString ToolWindow = "Tool Window"; const QString TransmitterDrive = "Transmitter Drive"; const QString TurnWithHead = "Turn using Head"; - const QString PackageModel = "Package Model"; + const QString PackageModel = "Package Model..."; const QString Visage = "Visage"; const QString Wireframe = "Wireframe"; } diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index f552d67a98..787c21a2ef 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -85,7 +85,7 @@ bool ModelPackager::loadModel() { return false; } qDebug() << "Reading FST file : " << _modelFile.filePath(); - _mapping = readMapping(fst.readAll()); + _mapping = FSTReader::readMapping(fst.readAll()); fst.close(); _fbxInfo = QFileInfo(_modelFile.path() + "/" + _mapping.value(FILENAME_FIELD).toString()); @@ -119,21 +119,23 @@ bool ModelPackager::editProperties() { return false; } _mapping = properties.getMapping(); - - // Make sure that a mapping for the root joint has been specified - QVariantHash joints = _mapping.value(JOINT_FIELD).toHash(); - if (!joints.contains("jointRoot")) { - qWarning() << QString("%1 root joint not configured for skeleton.").arg(_modelFile.fileName()); + + if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { + // Make sure that a mapping for the root joint has been specified + QVariantHash joints = _mapping.value(JOINT_FIELD).toHash(); + if (!joints.contains("jointRoot")) { + qWarning() << QString("%1 root joint not configured for skeleton.").arg(_modelFile.fileName()); - QString message = "Your did not configure a root joint for your skeleton model.\n\nThe upload will be canceled."; - QMessageBox msgBox; - msgBox.setWindowTitle("Model Upload"); - msgBox.setText(message); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Warning); - msgBox.exec(); + QString message = "Your did not configure a root joint for your skeleton model.\n\nPackaging will be canceled."; + QMessageBox msgBox; + msgBox.setWindowTitle("Model Packager"); + msgBox.setText(message); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Warning); + msgBox.exec(); - return false; + return false; + } } return true; @@ -183,7 +185,7 @@ bool ModelPackager::zipModel() { // Copy FST QFile fst(tempDir.path() + "/" + nameField + ".fst"); if (fst.open(QIODevice::WriteOnly)) { - fst.write(writeMapping(_mapping)); + fst.write(FSTReader::writeMapping(_mapping)); fst.close(); } else { qDebug() << "Couldn't write FST file" << fst.fileName(); @@ -204,6 +206,18 @@ bool ModelPackager::zipModel() { } void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry) { + + bool isBodyType = _modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL; + + // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will + // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file + bool likelyMixamoFile = geometry.applicationName == "mixamo.com" || + (geometry.blendshapeChannelNames.contains("BrowsDown_Right") && + geometry.blendshapeChannelNames.contains("MouthOpen") && + geometry.blendshapeChannelNames.contains("Blink_Left") && + geometry.blendshapeChannelNames.contains("Blink_Right") && + geometry.blendshapeChannelNames.contains("Squint_Right")); + if (!mapping.contains(NAME_FIELD)) { mapping.insert(NAME_FIELD, QFileInfo(filename).baseName()); } @@ -232,39 +246,40 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename if (!joints.contains("jointNeck")) { joints.insert("jointNeck", geometry.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); } - if (!joints.contains("jointRoot")) { - joints.insert("jointRoot", "Hips"); - } - if (!joints.contains("jointLean")) { - joints.insert("jointLean", "Spine"); + + if (isBodyType) { + if (!joints.contains("jointRoot")) { + joints.insert("jointRoot", "Hips"); + } + if (!joints.contains("jointLean")) { + joints.insert("jointLean", "Spine"); + } + if (!joints.contains("jointLeftHand")) { + joints.insert("jointLeftHand", "LeftHand"); + } + if (!joints.contains("jointRightHand")) { + joints.insert("jointRightHand", "RightHand"); + } } + if (!joints.contains("jointHead")) { - const char* topName = (geometry.applicationName == "mixamo.com") ? "HeadTop_End" : "HeadEnd"; + const char* topName = likelyMixamoFile ? "HeadTop_End" : "HeadEnd"; joints.insert("jointHead", geometry.jointIndices.contains(topName) ? topName : "Head"); } - if (!joints.contains("jointLeftHand")) { - joints.insert("jointLeftHand", "LeftHand"); - } - if (!joints.contains("jointRightHand")) { - joints.insert("jointRightHand", "RightHand"); - } + mapping.insert(JOINT_FIELD, joints); - if (!mapping.contains(FREE_JOINT_FIELD)) { - mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); + + if (isBodyType) { + if (!mapping.contains(FREE_JOINT_FIELD)) { + mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); + } } - // mixamo blendshapes - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will - // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file - bool likelyMixamoFile = geometry.applicationName == "mixamo.com" || - (geometry.blendshapeChannelNames.contains("BrowsDown_Right") && - geometry.blendshapeChannelNames.contains("MouthOpen") && - geometry.blendshapeChannelNames.contains("Blink_Left") && - geometry.blendshapeChannelNames.contains("Blink_Right") && - geometry.blendshapeChannelNames.contains("Squint_Right")); - + // If there are no blendshape mappings, and we detect that this is likely a mixamo file, + // then we can add the default mixamo to "faceshift" mappings if (!mapping.contains(BLENDSHAPE_FIELD) && likelyMixamoFile) { QVariantHash blendshapes; blendshapes.insertMulti("BrowsD_L", QVariantList() << "BrowsDown_Left" << 1.0); diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h index 2c90395e56..c681ae436f 100644 --- a/interface/src/ModelPackager.h +++ b/interface/src/ModelPackager.h @@ -35,7 +35,7 @@ private: QFileInfo _modelFile; QFileInfo _fbxInfo; - ModelType _modelType; + FSTReader::ModelType _modelType; QString _texDir; QVariantHash _mapping; diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index 81fe9ce7fd..28a50205c9 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -25,7 +25,7 @@ #include "ModelPropertiesDialog.h" -ModelPropertiesDialog::ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, +ModelPropertiesDialog::ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, const QString& basePath, const FBXGeometry& geometry) : _modelType(modelType), _originalMapping(originalMapping), @@ -46,8 +46,8 @@ _geometry(geometry) _scale->setMaximum(FLT_MAX); _scale->setSingleStep(0.01); - if (_modelType != ENTITY_MODEL) { - if (_modelType == ATTACHMENT_MODEL) { + if (_modelType != FSTReader::ENTITY_MODEL) { + if (_modelType == FSTReader::ATTACHMENT_MODEL) { QHBoxLayout* translation = new QHBoxLayout(); form->addRow("Translation:", translation); translation->addWidget(_translationX = createTranslationBox()); @@ -63,7 +63,7 @@ _geometry(geometry) form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); form->addRow("Neck Joint:", _neckJoint = createJointBox()); } - if (_modelType == SKELETON_MODEL) { + if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { form->addRow("Root Joint:", _rootJoint = createJointBox()); form->addRow("Lean Joint:", _leanJoint = createJointBox()); form->addRow("Head Joint:", _headJoint = createJointBox()); @@ -89,8 +89,14 @@ _geometry(geometry) reset(); } + +QString ModelPropertiesDialog::getType() const { + return FSTReader::getNameFromType(_modelType); +} + QVariantHash ModelPropertiesDialog::getMapping() const { QVariantHash mapping = _originalMapping; + mapping.insert(TYPE_FIELD, getType()); mapping.insert(NAME_FIELD, _name->text()); mapping.insert(TEXDIR_FIELD, _textureDirectory->text()); mapping.insert(SCALE_FIELD, QString::number(_scale->value())); @@ -102,9 +108,9 @@ QVariantHash ModelPropertiesDialog::getMapping() const { } mapping.insert(JOINT_INDEX_FIELD, jointIndices); - if (_modelType != ENTITY_MODEL) { + if (_modelType != FSTReader::ENTITY_MODEL) { QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); - if (_modelType == ATTACHMENT_MODEL) { + if (_modelType == FSTReader::ATTACHMENT_MODEL) { glm::vec3 pivot; if (_pivotAboutCenter->isChecked()) { pivot = (_geometry.meshExtents.minimum + _geometry.meshExtents.maximum) * 0.5f; @@ -121,7 +127,9 @@ QVariantHash ModelPropertiesDialog::getMapping() const { insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); } - if (_modelType == SKELETON_MODEL) { + + + if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { insertJointMapping(joints, "jointRoot", _rootJoint->currentText()); insertJointMapping(joints, "jointLean", _leanJoint->currentText()); insertJointMapping(joints, "jointHead", _headJoint->currentText()); @@ -151,8 +159,8 @@ void ModelPropertiesDialog::reset() { QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash(); - if (_modelType != ENTITY_MODEL) { - if (_modelType == ATTACHMENT_MODEL) { + if (_modelType != FSTReader::ENTITY_MODEL) { + if (_modelType == FSTReader::ATTACHMENT_MODEL) { _translationX->setValue(_originalMapping.value(TRANSLATION_X_FIELD).toDouble()); _translationY->setValue(_originalMapping.value(TRANSLATION_Y_FIELD).toDouble()); _translationZ->setValue(_originalMapping.value(TRANSLATION_Z_FIELD).toDouble()); @@ -164,7 +172,8 @@ void ModelPropertiesDialog::reset() { setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString()); setJointText(_neckJoint, jointHash.value("jointNeck").toString()); } - if (_modelType == SKELETON_MODEL) { + + if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { setJointText(_rootJoint, jointHash.value("jointRoot").toString()); setJointText(_leanJoint, jointHash.value("jointLean").toString()); setJointText(_headJoint, jointHash.value("jointHead").toString()); diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index 5af4d173f1..11abc5ab54 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -15,6 +15,7 @@ #include #include +#include #include "ui/ModelsBrowser.h" @@ -28,7 +29,7 @@ class ModelPropertiesDialog : public QDialog { Q_OBJECT public: - ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, + ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, const QString& basePath, const FBXGeometry& geometry); QVariantHash getMapping() const; @@ -43,8 +44,9 @@ private: QComboBox* createJointBox(bool withNone = true) const; QDoubleSpinBox* createTranslationBox() const; void insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const; + QString getType() const; - ModelType _modelType; + FSTReader::ModelType _modelType; QVariantHash _originalMapping; QString _basePath; FBXGeometry _geometry; diff --git a/interface/src/ModelSelector.cpp b/interface/src/ModelSelector.cpp index c55d77dc00..8e130cec1a 100644 --- a/interface/src/ModelSelector.cpp +++ b/interface/src/ModelSelector.cpp @@ -18,8 +18,9 @@ #include "ModelSelector.h" -static const QString AVATAR_HEAD_STRING = "Avatar Head"; -static const QString AVATAR_BODY_STRING = "Avatar Body"; +static const QString AVATAR_HEAD_STRING = "Avatar Head Only"; +static const QString AVATAR_BODY_STRING = "Avatar Body Only"; +static const QString AVATAR_HEAD_AND_BODY_STRING = "Avatar Body with Head"; static const QString AVATAR_ATTACHEMENT_STRING = "Avatar Attachment"; static const QString ENTITY_MODEL_STRING = "Entity Model"; @@ -36,6 +37,7 @@ ModelSelector::ModelSelector() { _modelType = new QComboBox(this); _modelType->addItem(AVATAR_HEAD_STRING); _modelType->addItem(AVATAR_BODY_STRING); + _modelType->addItem(AVATAR_HEAD_AND_BODY_STRING); _modelType->addItem(AVATAR_ATTACHEMENT_STRING); _modelType->addItem(ENTITY_MODEL_STRING); form->addRow("Model Type:", _modelType); @@ -50,17 +52,19 @@ QFileInfo ModelSelector::getFileInfo() const { return _modelFile; } -ModelType ModelSelector::getModelType() const { +FSTReader::ModelType ModelSelector::getModelType() const { QString text = _modelType->currentText(); if (text == AVATAR_HEAD_STRING) { - return HEAD_MODEL; + return FSTReader::HEAD_MODEL; } else if (text == AVATAR_BODY_STRING) { - return SKELETON_MODEL; + return FSTReader::BODY_ONLY_MODEL; + } else if (text == AVATAR_HEAD_AND_BODY_STRING) { + return FSTReader::HEAD_AND_BODY_MODEL; } else if (text == AVATAR_ATTACHEMENT_STRING) { - return ATTACHMENT_MODEL; + return FSTReader::ATTACHMENT_MODEL; } else if (text == ENTITY_MODEL_STRING) { - return ENTITY_MODEL; + return FSTReader::ENTITY_MODEL; } else { Q_UNREACHABLE(); } diff --git a/interface/src/ModelSelector.h b/interface/src/ModelSelector.h index aaa35e01c3..0ac3df5963 100644 --- a/interface/src/ModelSelector.h +++ b/interface/src/ModelSelector.h @@ -29,7 +29,7 @@ public: ModelSelector(); QFileInfo getFileInfo() const; - ModelType getModelType() const; + FSTReader::ModelType getModelType() const; public slots: virtual void accept(); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index a5b8128d1e..6ed3e48274 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -638,7 +638,7 @@ QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QS /// \param const QString& nameFilter filter to filter filenames /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::showS3Browse(const QString& nameFilter) { - ModelsBrowser browser(ENTITY_MODEL); + ModelsBrowser browser(FSTReader::ENTITY_MODEL); if (nameFilter != "") { browser.setNameFilter(nameFilter); } diff --git a/interface/src/ui/AttachmentsDialog.cpp b/interface/src/ui/AttachmentsDialog.cpp index 58b46e5bbf..2a6ff2b2b1 100644 --- a/interface/src/ui/AttachmentsDialog.cpp +++ b/interface/src/ui/AttachmentsDialog.cpp @@ -164,7 +164,7 @@ AttachmentData AttachmentPanel::getAttachmentData() const { } void AttachmentPanel::chooseModelURL() { - ModelsBrowser modelBrowser(ATTACHMENT_MODEL, this); + ModelsBrowser modelBrowser(FSTReader::ATTACHMENT_MODEL, this); connect(&modelBrowser, SIGNAL(selected(QString)), SLOT(setModelURL(const QString&))); modelBrowser.browse(); } diff --git a/interface/src/ui/ModelsBrowser.cpp b/interface/src/ui/ModelsBrowser.cpp index 070c6a85cc..91de4e36ac 100644 --- a/interface/src/ui/ModelsBrowser.cpp +++ b/interface/src/ui/ModelsBrowser.cpp @@ -28,7 +28,7 @@ #include "ModelsBrowser.h" -const char* MODEL_TYPE_NAMES[] = { "entities", "heads", "skeletons", "attachments" }; +const char* MODEL_TYPE_NAMES[] = { "entities", "heads", "skeletons", "skeletons", "attachments" }; static const QString S3_URL = "http://s3.amazonaws.com/hifi-public"; static const QString PUBLIC_URL = "http://public.highfidelity.io"; @@ -71,7 +71,7 @@ static const QString propertiesIds[MODEL_METADATA_COUNT] = { "Tags" }; -ModelsBrowser::ModelsBrowser(ModelType modelsType, QWidget* parent) : +ModelsBrowser::ModelsBrowser(FSTReader::ModelType modelsType, QWidget* parent) : QWidget(parent, Qt::WindowStaysOnTopHint), _handler(new ModelHandler(modelsType)) { @@ -184,7 +184,7 @@ void ModelsBrowser::browse() { } -ModelHandler::ModelHandler(ModelType modelsType, QWidget* parent) : +ModelHandler::ModelHandler(FSTReader::ModelType modelsType, QWidget* parent) : QObject(parent), _initiateExit(false), _type(modelsType), diff --git a/interface/src/ui/ModelsBrowser.h b/interface/src/ui/ModelsBrowser.h index 0c8bb59c85..2cb3c67991 100644 --- a/interface/src/ui/ModelsBrowser.h +++ b/interface/src/ui/ModelsBrowser.h @@ -16,21 +16,16 @@ #include #include -class QNetworkReply; +#include -enum ModelType { - ENTITY_MODEL, - HEAD_MODEL, - SKELETON_MODEL, - ATTACHMENT_MODEL -}; +class QNetworkReply; extern const char* MODEL_TYPE_NAMES[]; class ModelHandler : public QObject { Q_OBJECT public: - ModelHandler(ModelType modelsType, QWidget* parent = NULL); + ModelHandler(FSTReader::ModelType modelsType, QWidget* parent = NULL); void lockModel() { _lock.lockForRead(); } QStandardItemModel* getModel() { return &_model; } @@ -51,7 +46,7 @@ private slots: private: bool _initiateExit; - ModelType _type; + FSTReader::ModelType _type; QReadWriteLock _lock; QStandardItemModel _model; QString _nameFilter; @@ -66,7 +61,7 @@ class ModelsBrowser : public QWidget { Q_OBJECT public: - ModelsBrowser(ModelType modelsType, QWidget* parent = NULL); + ModelsBrowser(FSTReader::ModelType modelsType, QWidget* parent = NULL); QString getSelectedFile() { return _selectedFile; } signals: diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index a07de371a2..4a9165ae44 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -67,13 +67,13 @@ void PreferencesDialog::setSkeletonUrl(QString modelUrl) { } void PreferencesDialog::openHeadModelBrowser() { - ModelsBrowser modelBrowser(HEAD_MODEL); + ModelsBrowser modelBrowser(FSTReader::HEAD_MODEL); connect(&modelBrowser, &ModelsBrowser::selected, this, &PreferencesDialog::setHeadUrl); modelBrowser.browse(); } void PreferencesDialog::openBodyModelBrowser() { - ModelsBrowser modelBrowser(SKELETON_MODEL); + ModelsBrowser modelBrowser(FSTReader::HEAD_AND_BODY_MODEL); connect(&modelBrowser, &ModelsBrowser::selected, this, &PreferencesDialog::setSkeletonUrl); modelBrowser.browse(); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 3faa06fc53..058e02b507 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -110,14 +110,30 @@ void EntityTreeRenderer::shutdown() { _shuttingDown = true; } +void EntityTreeRenderer::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) { + if (_waitingOnPreload.contains(url)) { + QList entityIDs = _waitingOnPreload.values(url); + _waitingOnPreload.remove(url); + foreach(EntityItemID entityID, entityIDs) { + checkAndCallPreload(entityID); + } + } +} -QScriptValue EntityTreeRenderer::loadEntityScript(const EntityItemID& entityItemID) { +void EntityTreeRenderer::errorInLoadingScript(const QUrl& url) { + if (_waitingOnPreload.contains(url)) { + _waitingOnPreload.remove(url); + } +} + +QScriptValue EntityTreeRenderer::loadEntityScript(const EntityItemID& entityItemID, bool isPreload) { EntityItem* entity = static_cast(_tree)->findEntityByEntityItemID(entityItemID); - return loadEntityScript(entity); + return loadEntityScript(entity, isPreload); } -QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL) { +QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL, bool& isPending, QUrl& urlOut) { + isPending = false; QUrl url(scriptMaybeURLorText); // If the url is not valid, this must be script text... @@ -126,6 +142,7 @@ QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorTe return scriptMaybeURLorText; } isURL = true; + urlOut = url; QString scriptContents; // assume empty @@ -148,20 +165,11 @@ QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorTe qDebug() << "ERROR Loading file:" << fileName; } } else { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - qDebug() << "Downloading script at" << url; - QEventLoop loop; - QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 200) { - scriptContents = reply->readAll(); - } else { - qDebug() << "ERROR Loading file:" << url.toString(); + auto scriptCache = DependencyManager::get(); + + if (!scriptCache->isInBadScriptList(url)) { + scriptContents = scriptCache->getScript(url, this, isPending); } - delete reply; } } @@ -169,7 +177,7 @@ QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorTe } -QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { +QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity, bool isPreload) { if (_shuttingDown) { return QScriptValue(); // since we're shutting down, we don't load any more scripts } @@ -203,7 +211,24 @@ QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { } bool isURL = false; // loadScriptContents() will tell us if this is a URL or just text. - QString scriptContents = loadScriptContents(entityScript, isURL); + bool isPending = false; + QUrl url; + QString scriptContents = loadScriptContents(entityScript, isURL, isPending, url); + + if (isPending && isPreload && isURL) { + _waitingOnPreload.insert(url, entityID); + + } + + auto scriptCache = DependencyManager::get(); + + if (isURL && scriptCache->isInBadScriptList(url)) { + return QScriptValue(); // no script contents... + } + + if (scriptContents.isEmpty()) { + return QScriptValue(); // no script contents... + } QScriptSyntaxCheckResult syntaxCheck = QScriptEngine::checkSyntax(scriptContents); if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { @@ -211,6 +236,9 @@ QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { qDebug() << " " << syntaxCheck.errorMessage() << ":" << syntaxCheck.errorLineNumber() << syntaxCheck.errorColumnNumber(); qDebug() << " SCRIPT:" << entityScript; + + scriptCache->addScriptToBadScriptList(url); + return QScriptValue(); // invalid script } @@ -223,6 +251,9 @@ QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { qDebug() << "EntityTreeRenderer::loadEntityScript() entity:" << entityID; qDebug() << " NOT CONSTRUCTOR"; qDebug() << " SCRIPT:" << entityScript; + + scriptCache->addScriptToBadScriptList(url); + return QScriptValue(); // invalid script } else { entityScriptConstructor = _entitiesScriptEngine->evaluate(scriptContents); @@ -920,7 +951,7 @@ void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID) { void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID) { if (_tree && !_shuttingDown) { // load the entity script if needed... - QScriptValue entityScript = loadEntityScript(entityID); + QScriptValue entityScript = loadEntityScript(entityID, true); // is preload! if (entityScript.property("preload").isValid()) { QScriptValueList entityArgs = createEntityArgs(entityID); entityScript.property("preload").call(entityScript, entityArgs); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index c4aa5a42d0..12d5f6e166 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -16,6 +16,7 @@ #include // for RayToEntityIntersectionResult #include #include +#include class Model; class ScriptEngine; @@ -31,7 +32,7 @@ public: }; // Generic client side Octree renderer class. -class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService { +class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService, public ScriptUser { Q_OBJECT public: EntityTreeRenderer(bool wantScripts, AbstractViewStateInterface* viewState, @@ -84,6 +85,9 @@ public: /// hovering over, and entering entities void connectSignalsToSlots(EntityScriptingInterface* entityScriptingInterface); + virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents); + virtual void errorInLoadingScript(const QUrl& url); + signals: void mousePressOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); void mouseMoveOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); @@ -138,10 +142,10 @@ private: ScriptEngine* _entitiesScriptEngine; ScriptEngine* _sandboxScriptEngine; - QScriptValue loadEntityScript(EntityItem* entity); - QScriptValue loadEntityScript(const EntityItemID& entityItemID); + QScriptValue loadEntityScript(EntityItem* entity, bool isPreload = false); + QScriptValue loadEntityScript(const EntityItemID& entityItemID, bool isPreload = false); QScriptValue getPreviouslyLoadedEntityScript(const EntityItemID& entityItemID); - QString loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL); + QString loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL, bool& isPending, QUrl& url); QScriptValueList createMouseEventArgs(const EntityItemID& entityID, QMouseEvent* event, unsigned int deviceID); QScriptValueList createMouseEventArgs(const EntityItemID& entityID, const MouseEvent& mouseEvent); @@ -157,6 +161,8 @@ private: bool _dontDoPrecisionPicking; bool _shuttingDown = false; + + QMultiMap _waitingOnPreload; }; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 826df45294..2b65081185 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -317,29 +317,30 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { AABox aaBox; _points.clear(); unsigned int i = 0; - foreach (const FBXMesh& mesh, fbxGeometry.meshes) { + // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect + // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. + foreach (const FBXMesh& mesh, fbxGeometry.meshes) { + // each meshPart is a convex hull foreach (const FBXMeshPart &meshPart, mesh.parts) { QVector pointsInPart; + + // run through all the triangles and (uniquely) add each point to the hull unsigned int triangleCount = meshPart.triangleIndices.size() / 3; assert((unsigned int)meshPart.triangleIndices.size() == triangleCount*3); for (unsigned int j = 0; j < triangleCount; j++) { unsigned int p0Index = meshPart.triangleIndices[j*3]; unsigned int p1Index = meshPart.triangleIndices[j*3+1]; unsigned int p2Index = meshPart.triangleIndices[j*3+2]; - assert(p0Index < (unsigned int)mesh.vertices.size()); assert(p1Index < (unsigned int)mesh.vertices.size()); assert(p2Index < (unsigned int)mesh.vertices.size()); - glm::vec3 p0 = mesh.vertices[p0Index]; glm::vec3 p1 = mesh.vertices[p1Index]; glm::vec3 p2 = mesh.vertices[p2Index]; - aaBox += p0; aaBox += p1; aaBox += p2; - if (!pointsInPart.contains(p0)) { pointsInPart << p0; } @@ -351,8 +352,44 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } } + // run through all the quads and (uniquely) add each point to the hull + unsigned int quadCount = meshPart.quadIndices.size() / 4; + assert((unsigned int)meshPart.quadIndices.size() == quadCount*4); + for (unsigned int j = 0; j < quadCount; j++) { + unsigned int p0Index = meshPart.quadIndices[j*4]; + unsigned int p1Index = meshPart.quadIndices[j*4+1]; + unsigned int p2Index = meshPart.quadIndices[j*4+2]; + unsigned int p3Index = meshPart.quadIndices[j*4+3]; + assert(p0Index < (unsigned int)mesh.vertices.size()); + assert(p1Index < (unsigned int)mesh.vertices.size()); + assert(p2Index < (unsigned int)mesh.vertices.size()); + assert(p3Index < (unsigned int)mesh.vertices.size()); + glm::vec3 p0 = mesh.vertices[p0Index]; + glm::vec3 p1 = mesh.vertices[p1Index]; + glm::vec3 p2 = mesh.vertices[p2Index]; + glm::vec3 p3 = mesh.vertices[p3Index]; + aaBox += p0; + aaBox += p1; + aaBox += p2; + aaBox += p3; + if (!pointsInPart.contains(p0)) { + pointsInPart << p0; + } + if (!pointsInPart.contains(p1)) { + pointsInPart << p1; + } + if (!pointsInPart.contains(p2)) { + pointsInPart << p2; + } + if (!pointsInPart.contains(p3)) { + pointsInPart << p3; + } + } + + // add next convex hull QVector newMeshPoints; _points << newMeshPoints; + // add points to the new convex hull _points[i++] << pointsInPart; } } @@ -362,7 +399,6 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { aaBoxDim = glm::clamp(aaBoxDim, glm::vec3(FLT_EPSILON), aaBoxDim); glm::vec3 scale = _dimensions / aaBoxDim; - // multiply each point by scale before handing the point-set off to the physics engine for (int i = 0; i < _points.size(); i++) { for (int j = 0; j < _points[i].size(); j++) { diff --git a/libraries/fbx/src/FSTReader.cpp b/libraries/fbx/src/FSTReader.cpp index f1ffe11996..6a5cc2bd53 100644 --- a/libraries/fbx/src/FSTReader.cpp +++ b/libraries/fbx/src/FSTReader.cpp @@ -13,7 +13,7 @@ #include "FSTReader.h" -QVariantHash parseMapping(QIODevice* device) { +QVariantHash FSTReader::parseMapping(QIODevice* device) { QVariantHash properties; QByteArray line; @@ -48,13 +48,13 @@ QVariantHash parseMapping(QIODevice* device) { return properties; } -QVariantHash readMapping(const QByteArray& data) { +QVariantHash FSTReader::readMapping(const QByteArray& data) { QBuffer buffer(const_cast(&data)); buffer.open(QIODevice::ReadOnly); - return parseMapping(&buffer); + return FSTReader::parseMapping(&buffer); } -void writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) { +void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) { QByteArray key = it.key().toUtf8() + " = "; QVariantHash hashValue = it.value().toHash(); if (hashValue.isEmpty()) { @@ -76,8 +76,8 @@ void writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) { } } -QByteArray writeMapping(const QVariantHash& mapping) { - static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << SCALE_FIELD << FILENAME_FIELD +QByteArray FSTReader::writeMapping(const QVariantHash& mapping) { + static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << TYPE_FIELD << SCALE_FIELD << FILENAME_FIELD << TEXDIR_FIELD << JOINT_FIELD << FREE_JOINT_FIELD << BLENDSHAPE_FIELD << JOINT_INDEX_FIELD; QBuffer buffer; @@ -96,4 +96,76 @@ QByteArray writeMapping(const QVariantHash& mapping) { } } return buffer.data(); -} \ No newline at end of file +} + +QHash FSTReader::_typesToNames; +QString FSTReader::getNameFromType(ModelType modelType) { + if (_typesToNames.size() == 0) { + _typesToNames[ENTITY_MODEL] = "entity"; + _typesToNames[HEAD_MODEL] = "head"; + _typesToNames[BODY_ONLY_MODEL] = "body"; + _typesToNames[HEAD_AND_BODY_MODEL] = "body+head"; + _typesToNames[ATTACHMENT_MODEL] = "attachment"; + } + return _typesToNames[modelType]; +} + +QHash FSTReader::_namesToTypes; +FSTReader::ModelType FSTReader::getTypeFromName(const QString& name) { + if (_namesToTypes.size() == 0) { + _namesToTypes["entity"] = ENTITY_MODEL; + _namesToTypes["head"] = HEAD_MODEL ; + _namesToTypes["body"] = BODY_ONLY_MODEL; + _namesToTypes["body+head"] = HEAD_AND_BODY_MODEL; + _namesToTypes["attachment"] = ATTACHMENT_MODEL; + } + return _namesToTypes[name]; +} + +FSTReader::ModelType FSTReader::predictModelType(const QVariantHash& mapping) { + + QVariantHash joints; + + if (mapping.contains("joint") && mapping["joint"].type() == QVariant::Hash) { + joints = mapping["joint"].toHash(); + } + + // if the mapping includes the type hint... then we trust the mapping + if (mapping.contains(TYPE_FIELD)) { + return FSTReader::getTypeFromName(mapping[TYPE_FIELD].toString()); + } + + // check for blendshapes + bool hasBlendshapes = mapping.contains(BLENDSHAPE_FIELD); + + // a Head needs to have these minimum fields... + //joint = jointEyeLeft = EyeL = 1 + //joint = jointEyeRight = EyeR = 1 + //joint = jointNeck = Head = 1 + bool hasHeadMinimum = joints.contains("jointNeck") && joints.contains("jointEyeLeft") && joints.contains("jointEyeRight"); + + // a Body needs to have these minimum fields... + //joint = jointRoot = Hips + //joint = jointLean = Spine + //joint = jointNeck = Neck + //joint = jointHead = HeadTop_End + + bool hasBodyMinimumJoints = joints.contains("jointRoot") && joints.contains("jointLean") && joints.contains("jointNeck") + && joints.contains("jointHead"); + + bool isLikelyHead = hasBlendshapes || hasHeadMinimum; + + if (isLikelyHead && hasBodyMinimumJoints) { + return HEAD_AND_BODY_MODEL; + } + + if (isLikelyHead) { + return HEAD_MODEL; + } + + if (hasBodyMinimumJoints) { + return BODY_ONLY_MODEL; + } + + return ENTITY_MODEL; +} diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index 59559dea74..5752a224c6 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -12,9 +12,11 @@ #ifndef hifi_FSTReader_h #define hifi_FSTReader_h +#include #include static const QString NAME_FIELD = "name"; +static const QString TYPE_FIELD = "type"; static const QString FILENAME_FIELD = "filename"; static const QString TEXDIR_FIELD = "texdir"; static const QString LOD_FIELD = "lod"; @@ -27,10 +29,35 @@ static const QString JOINT_FIELD = "joint"; static const QString FREE_JOINT_FIELD = "freeJoint"; static const QString BLENDSHAPE_FIELD = "bs"; -/// Reads an FST mapping from the supplied data. -QVariantHash readMapping(const QByteArray& data); +class FSTReader { +public: -/// Writes an FST mapping to a byte array. -QByteArray writeMapping(const QVariantHash& mapping); + enum ModelType { + ENTITY_MODEL, + HEAD_MODEL, + BODY_ONLY_MODEL, + HEAD_AND_BODY_MODEL, + ATTACHMENT_MODEL + }; + + /// Reads an FST mapping from the supplied data. + static QVariantHash readMapping(const QByteArray& data); + + /// Writes an FST mapping to a byte array. + static QByteArray writeMapping(const QVariantHash& mapping); + + /// Predicts the type of model by examining the mapping + static ModelType predictModelType(const QVariantHash& mapping); + + static QString getNameFromType(ModelType modelType); + static FSTReader::ModelType getTypeFromName(const QString& name); + +private: + static void writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it); + static QVariantHash parseMapping(QIODevice* device); + + static QHash _typesToNames; + static QHash _namesToTypes; +}; #endif // hifi_FSTReader_h \ No newline at end of file diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index f8fc4633cc..c74f714e8d 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2132,7 +2132,7 @@ void NetworkGeometry::downloadFinished(QNetworkReply* reply) { QUrl url = reply->url(); if (url.path().toLower().endsWith(".fst")) { // it's a mapping file; parse it and get the mesh filename - _mapping = readMapping(reply->readAll()); + _mapping = FSTReader::readMapping(reply->readAll()); reply->deleteLater(); QString filename = _mapping.value("filename").toString(); if (filename.isNull()) { diff --git a/libraries/render-utils/src/TextRenderer.cpp b/libraries/render-utils/src/TextRenderer.cpp index 9238db91e3..75448f90cc 100644 --- a/libraries/render-utils/src/TextRenderer.cpp +++ b/libraries/render-utils/src/TextRenderer.cpp @@ -359,8 +359,12 @@ void Font::setupGL() { // FIXME there has to be a cleaner way of doing this QStringList Font::tokenizeForWrapping(const QString & str) const { QStringList result; - foreach(const QString & token1, str.split(" ", QString::SkipEmptyParts)) { + foreach(const QString & token1, str.split(" ")) { bool lineFeed = false; + if (token1.isEmpty()) { + result << token1; + continue; + } foreach(const QString & token2, token1.split("\n")) { if (lineFeed) { result << "\n"; diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp new file mode 100644 index 0000000000..c0ffc7b4e7 --- /dev/null +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -0,0 +1,76 @@ +// +// ScriptCache.cpp +// libraries/script-engine/src +// +// Created by Brad Hefta-Gaub on 2015-03-30 +// Copyright 2015 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 "ScriptCache.h" + +ScriptCache::ScriptCache(QObject* parent) { + // nothing to do here... +} + +QString ScriptCache::getScript(const QUrl& url, ScriptUser* scriptUser, bool& isPending) { + QString scriptContents; + if (_scriptCache.contains(url)) { + qDebug() << "Found script in cache:" << url.toString(); + scriptContents = _scriptCache[url]; + scriptUser->scriptContentsAvailable(url, scriptContents); + isPending = false; + } else { + isPending = true; + bool alreadyWaiting = _scriptUsers.contains(url); + _scriptUsers.insert(url, scriptUser); + + if (alreadyWaiting) { + qDebug() << "Already downloading script at:" << url.toString(); + } else { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest = QNetworkRequest(url); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + qDebug() << "Downloading script at:" << url.toString(); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + connect(reply, &QNetworkReply::finished, this, &ScriptCache::scriptDownloaded); + } + } + return scriptContents; +} + +void ScriptCache::scriptDownloaded() { + QNetworkReply* reply = qobject_cast(sender()); + QUrl url = reply->url(); + QList scriptUsers = _scriptUsers.values(url); + _scriptUsers.remove(url); + + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 200) { + _scriptCache[url] = reply->readAll(); + qDebug() << "Done downloading script at:" << url.toString(); + + foreach(ScriptUser* user, scriptUsers) { + user->scriptContentsAvailable(url, _scriptCache[url]); + } + } else { + qDebug() << "ERROR Loading file:" << reply->url().toString(); + foreach(ScriptUser* user, scriptUsers) { + user->errorInLoadingScript(url); + } + } + reply->deleteLater(); +} + + diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h new file mode 100644 index 0000000000..16bf04c53e --- /dev/null +++ b/libraries/script-engine/src/ScriptCache.h @@ -0,0 +1,44 @@ +// +// ScriptCache.h +// libraries/script-engine/src +// +// Created by Brad Hefta-Gaub on 2015-03-30 +// Copyright 2015 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_ScriptCache_h +#define hifi_ScriptCache_h + +#include + +class ScriptUser { +public: + virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents) = 0; + virtual void errorInLoadingScript(const QUrl& url) = 0; +}; + +/// Interface for loading scripts +class ScriptCache : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + QString getScript(const QUrl& url, ScriptUser* scriptUser, bool& isPending); + void addScriptToBadScriptList(const QUrl& url) { _badScripts.insert(url); } + bool isInBadScriptList(const QUrl& url) { return _badScripts.contains(url); } + +private slots: + void scriptDownloaded(); + +private: + ScriptCache(QObject* parent = NULL); + + QHash _scriptCache; + QMultiMap _scriptUsers; + QSet _badScripts; +}; + +#endif // hifi_ScriptCache_h \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 831db73a0a..c31f5be46a 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -34,6 +34,7 @@ #include "EventTypes.h" #include "MenuItemProperties.h" #include "ScriptAudioInjector.h" +#include "ScriptCache.h" #include "ScriptEngine.h" #include "TypedArrays.h" #include "XMLHttpRequestClass.h" @@ -275,31 +276,26 @@ void ScriptEngine::loadURL(const QUrl& scriptURL) { _scriptContents = in.readAll(); emit scriptLoaded(_fileNameString); } else { - qDebug() << "ERROR Loading file:" << _fileNameString; + qDebug() << "ERROR Loading file:" << _fileNameString << "line:" << __LINE__; emit errorLoadingScript(_fileNameString); } } else { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - connect(reply, &QNetworkReply::finished, this, &ScriptEngine::handleScriptDownload); + bool isPending; + auto scriptCache = DependencyManager::get(); + scriptCache->getScript(url, this, isPending); + } } } -void ScriptEngine::handleScriptDownload() { - QNetworkReply* reply = qobject_cast(sender()); - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 200) { - _scriptContents = reply->readAll(); - emit scriptLoaded(_fileNameString); - } else { - qDebug() << "ERROR Loading file:" << reply->url().toString(); - emit errorLoadingScript(_fileNameString); - } - - reply->deleteLater(); +void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) { + _scriptContents = scriptContents; + emit scriptLoaded(_fileNameString); +} + +void ScriptEngine::errorInLoadingScript(const QUrl& url) { + qDebug() << "ERROR Loading file:" << url.toString() << "line:" << __LINE__; + emit errorLoadingScript(_fileNameString); // ?? } void ScriptEngine::init() { @@ -765,7 +761,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { - qDebug() << "Error loading file: " << url; + qDebug() << "Error loading file: " << url << "line:" << __LINE__; } else { QScriptValue result = evaluate(contents, url.toString()); } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 84d2527bd0..45e56850a7 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -28,6 +28,7 @@ #include "ArrayBufferClass.h" #include "AudioScriptingInterface.h" #include "Quat.h" +#include "ScriptCache.h" #include "ScriptUUID.h" #include "Vec3.h" @@ -35,7 +36,7 @@ const QString NO_SCRIPT(""); const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0 / 60.0f) * 1000 * 1000) + 0.5); -class ScriptEngine : public QScriptEngine { +class ScriptEngine : public QScriptEngine, public ScriptUser { Q_OBJECT public: ScriptEngine(const QString& scriptContents = NO_SCRIPT, @@ -94,6 +95,9 @@ public: void waitTillDoneRunning(); + virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents); + virtual void errorInLoadingScript(const QUrl& url); + public slots: void loadURL(const QUrl& scriptURL); void stop(); @@ -160,8 +164,6 @@ private: ArrayBufferClass* _arrayBufferClass; QHash _outgoingScriptAudioSequenceNumbers; -private slots: - void handleScriptDownload(); private: static QSet _allKnownScriptEngines; diff --git a/libraries/shared/src/Extents.h b/libraries/shared/src/Extents.h index 95f242c30b..66b33114f7 100644 --- a/libraries/shared/src/Extents.h +++ b/libraries/shared/src/Extents.h @@ -14,6 +14,7 @@ #define hifi_Extents_h #include +#include #include #include "StreamUtils.h" @@ -46,6 +47,9 @@ public: /// rotate the extents around orign by rotation void rotate(const glm::quat& rotation); + glm::vec3 size() const { return maximum - minimum; } + float largestDimension () const {glm::vec3 s = size(); return glm::max(s[0], s[1], s[2]); } + /// \return new Extents which is original rotated around orign by rotation Extents getRotated(const glm::quat& rotation) const { Extents temp = { minimum, maximum }; @@ -68,4 +72,4 @@ inline QDebug operator<<(QDebug debug, const Extents& extents) { } -#endif // hifi_Extents_h \ No newline at end of file +#endif // hifi_Extents_h diff --git a/libraries/shared/src/StreamUtils.cpp b/libraries/shared/src/StreamUtils.cpp index 96967648ed..f58115d288 100644 --- a/libraries/shared/src/StreamUtils.cpp +++ b/libraries/shared/src/StreamUtils.cpp @@ -143,4 +143,12 @@ QDebug& operator<<(QDebug& dbg, const glm::mat4& m) { return dbg << " ]}"; } +QDebug& operator<<(QDebug& dbg, const QVariantHash& v) { + dbg.nospace() << "["; + for (QVariantHash::const_iterator it = v.constBegin(); it != v.constEnd(); it++) { + dbg << it.key() << ":" << it.value(); + } + return dbg << " ]"; +} + #endif // QT_NO_DEBUG_STREAM diff --git a/libraries/shared/src/StreamUtils.h b/libraries/shared/src/StreamUtils.h index b9823a6743..fd22f7c068 100644 --- a/libraries/shared/src/StreamUtils.h +++ b/libraries/shared/src/StreamUtils.h @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -54,6 +55,7 @@ QDebug& operator<<(QDebug& s, const glm::vec3& v); QDebug& operator<<(QDebug& s, const glm::vec4& v); QDebug& operator<<(QDebug& s, const glm::quat& q); QDebug& operator<<(QDebug& s, const glm::mat4& m); +QDebug& operator<<(QDebug& dbg, const QVariantHash& v); #endif // QT_NO_DEBUG_STREAM #endif // hifi_StreamUtils_h diff --git a/tools/vhacd/src/VHACDUtil.cpp b/tools/vhacd/src/VHACDUtil.cpp index bc9ad09bde..4f2cb0e2b8 100644 --- a/tools/vhacd/src/VHACDUtil.cpp +++ b/tools/vhacd/src/VHACDUtil.cpp @@ -44,7 +44,13 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, vhacd::LoadFBXResults *re int count = 0; foreach(FBXMesh mesh, geometry.meshes) { //get vertices for each mesh - QVector vertices = mesh.vertices; + // QVector vertices = mesh.vertices; + + + QVector vertices; + foreach (glm::vec3 vertex, mesh.vertices) { + vertices.append(glm::vec3(mesh.modelTransform * glm::vec4(vertex, 1.0f))); + } //get the triangle indices for each mesh QVector triangles; @@ -57,8 +63,15 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, vhacd::LoadFBXResults *re if (triangles.count() <= 0){ continue; } + + AABox aaBox; + foreach (glm::vec3 p, vertices) { + aaBox += p; + } + results->perMeshVertices.append(vertices); results->perMeshTriangleIndices.append(triangles); + results->perMeshLargestDimension.append(aaBox.getLargestDimension()); count++; } @@ -66,23 +79,117 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, vhacd::LoadFBXResults *re return true; } -bool vhacd::VHACDUtil::computeVHACD(vhacd::LoadFBXResults *meshes, VHACD::IVHACD::Parameters params, vhacd::ComputeResults *results)const{ + +void vhacd::VHACDUtil::combineMeshes(vhacd::LoadFBXResults *meshes, vhacd::LoadFBXResults *results) const { + float largestDimension = 0; + int indexStart = 0; + + QVector emptyVertices; + QVector emptyTriangles; + results->perMeshVertices.append(emptyVertices); + results->perMeshTriangleIndices.append(emptyTriangles); + results->perMeshLargestDimension.append(largestDimension); + + for (int i = 0; i < meshes->meshCount; i++) { + QVector vertices = meshes->perMeshVertices.at(i); + QVector triangles = meshes->perMeshTriangleIndices.at(i); + const float largestDimension = meshes->perMeshLargestDimension.at(i); + + for (int j = 0; j < triangles.size(); j++) { + triangles[ j ] += indexStart; + } + indexStart += vertices.size(); + + results->perMeshVertices[0] << vertices; + results->perMeshTriangleIndices[0] << triangles; + if (results->perMeshLargestDimension[0] < largestDimension) { + results->perMeshLargestDimension[0] = largestDimension; + } + } + + results->meshCount = 1; +} + + +void vhacd::VHACDUtil::fattenMeshes(vhacd::LoadFBXResults *meshes, vhacd::LoadFBXResults *results) const { + + for (int i = 0; i < meshes->meshCount; i++) { + QVector vertices = meshes->perMeshVertices.at(i); + QVector triangles = meshes->perMeshTriangleIndices.at(i); + const float largestDimension = meshes->perMeshLargestDimension.at(i); + + results->perMeshVertices.append(vertices); + results->perMeshTriangleIndices.append(triangles); + results->perMeshLargestDimension.append(largestDimension); + + for (int j = 0; j < triangles.size(); j += 3) { + auto p0 = vertices[triangles[j]]; + auto p1 = vertices[triangles[j+1]]; + auto p2 = vertices[triangles[j+2]]; + + auto d0 = p1 - p0; + auto d1 = p2 - p0; + + auto cp = glm::cross(d0, d1); + cp = 5.0f * glm::normalize(cp); + + auto p3 = p0 + cp; + auto p4 = p1 + cp; + auto p5 = p2 + cp; + + auto n = results->perMeshVertices.size(); + results->perMeshVertices[i] << p3 << p4 << p5; + results->perMeshTriangleIndices[i] << n << n+1 << n+2; + } + + results->meshCount++; + } +} + + + +bool vhacd::VHACDUtil::computeVHACD(vhacd::LoadFBXResults *meshes, VHACD::IVHACD::Parameters params, + vhacd::ComputeResults *results, + int startMeshIndex, int endMeshIndex, float minimumMeshSize) const { + + // vhacd::LoadFBXResults *meshes = new vhacd::LoadFBXResults; + // combineMeshes(inMeshes, meshes); + + // vhacd::LoadFBXResults *meshes = new vhacd::LoadFBXResults; + // fattenMeshes(inMeshes, meshes); + + VHACD::IVHACD * interfaceVHACD = VHACD::CreateVHACD(); int meshCount = meshes->meshCount; int count = 0; std::cout << "Performing V-HACD computation on " << meshCount << " meshes ..... " << std::endl; - for (int i = 0; i < meshCount; i++){ + if (startMeshIndex < 0) { + startMeshIndex = 0; + } + if (endMeshIndex < 0) { + endMeshIndex = meshCount; + } + for (int i = startMeshIndex; i < endMeshIndex; i++){ + qDebug() << "--------------------"; std::vector vertices = meshes->perMeshVertices.at(i).toStdVector(); std::vector triangles = meshes->perMeshTriangleIndices.at(i).toStdVector(); int nPoints = (unsigned int)vertices.size(); int nTriangles = (unsigned int)triangles.size() / 3; - std::cout << "Mesh " << i + 1 << " : "; + const float largestDimension = meshes->perMeshLargestDimension.at(i); + + qDebug() << "Mesh " << i << " -- " << nPoints << " points, " << nTriangles << " triangles, " + << "size =" << largestDimension; + + if (largestDimension < minimumMeshSize /* || largestDimension > 1000 */) { + qDebug() << " Skipping..."; + continue; + } // compute approximate convex decomposition bool res = interfaceVHACD->Compute(&vertices[0].x, 3, nPoints, &triangles[0], 3, nTriangles, params); if (!res){ - std::cout << "V-HACD computation failed for Mesh : " << i + 1 << std::endl; + qDebug() << "V-HACD computation failed for Mesh : " << i; continue; } count++; //For counting number of successfull computations @@ -111,8 +218,6 @@ bool vhacd::VHACDUtil::computeVHACD(vhacd::LoadFBXResults *meshes, VHACD::IVHACD m_triangles_copy[ i ] = hull.m_triangles[ i ]; } hull.m_triangles = m_triangles_copy; - - convexHulls.append(hull); } results->convexHullList.append(convexHulls); diff --git a/tools/vhacd/src/VHACDUtil.h b/tools/vhacd/src/VHACDUtil.h index b0b9da9720..fd5d253334 100644 --- a/tools/vhacd/src/VHACDUtil.h +++ b/tools/vhacd/src/VHACDUtil.h @@ -34,12 +34,16 @@ namespace vhacd { int meshCount; QVector> perMeshVertices; QVector> perMeshTriangleIndices; + QVector perMeshLargestDimension; } LoadFBXResults; class VHACDUtil { public: bool loadFBX(const QString filename, vhacd::LoadFBXResults *results); - bool computeVHACD(vhacd::LoadFBXResults *meshes, VHACD::IVHACD::Parameters params, vhacd::ComputeResults *results)const; + void combineMeshes(vhacd::LoadFBXResults *meshes, vhacd::LoadFBXResults *results) const; + void fattenMeshes(vhacd::LoadFBXResults *meshes, vhacd::LoadFBXResults *results) const; + bool computeVHACD(vhacd::LoadFBXResults *meshes, VHACD::IVHACD::Parameters params, + vhacd::ComputeResults *results, int startMeshIndex, int endMeshIndex, float minimumMeshSize) const; ~VHACDUtil(); }; diff --git a/tools/vhacd/src/VHACDUtilApp.cpp b/tools/vhacd/src/VHACDUtilApp.cpp index b4c141acae..a1a04fd127 100644 --- a/tools/vhacd/src/VHACDUtilApp.cpp +++ b/tools/vhacd/src/VHACDUtilApp.cpp @@ -98,6 +98,15 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : const QCommandLineOption outputFilenameOption("o", "output file", "filename.obj"); parser.addOption(outputFilenameOption); + const QCommandLineOption startMeshIndexOption("s", "start-mesh index", "0"); + parser.addOption(startMeshIndexOption); + + const QCommandLineOption endMeshIndexOption("e", "end-mesh index", "0"); + parser.addOption(endMeshIndexOption); + + const QCommandLineOption minimumMeshSizeOption("m", "minimum mesh size to consider", "0"); + parser.addOption(minimumMeshSizeOption); + if (!parser.parse(QCoreApplication::arguments())) { qCritical() << parser.errorText() << endl; @@ -138,13 +147,31 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : Q_UNREACHABLE(); } + int startMeshIndex = -1; + // check for an assignment pool passed on the command line or in the config + if (parser.isSet(startMeshIndexOption)) { + startMeshIndex = parser.value(startMeshIndexOption).toInt(); + } + + int endMeshIndex = -1; + // check for an assignment pool passed on the command line or in the config + if (parser.isSet(endMeshIndexOption)) { + endMeshIndex = parser.value(endMeshIndexOption).toInt(); + } + + float minimumMeshSize = 0.0f; + // check for an assignment pool passed on the command line or in the config + if (parser.isSet(minimumMeshSizeOption)) { + minimumMeshSize = parser.value(minimumMeshSizeOption).toFloat(); + } + //set parameters for V-HACD params.m_callback = &pCallBack; //progress callback params.m_resolution = 100000; // 100000 params.m_depth = 20; // 20 params.m_concavity = 0.001; // 0.001 - params.m_delta = 0.01; // 0.05 + params.m_delta = 0.05; // 0.05 params.m_planeDownsampling = 4; // 4 params.m_convexhullDownsampling = 4; // 4 params.m_alpha = 0.05; // 0.05 // controls the bias toward clipping along symmetry planes @@ -153,7 +180,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : params.m_pca = 0; // 0 enable/disable normalizing the mesh before applying the convex decomposition params.m_mode = 0; // 0: voxel-based (recommended), 1: tetrahedron-based params.m_maxNumVerticesPerCH = 64; // 64 - params.m_minVolumePerCH = 0.00001; // 0.0001 + params.m_minVolumePerCH = 0.0001; // 0.0001 params.m_callback = 0; // 0 params.m_logger = 0; // 0 params.m_convexhullApproximation = true; // true @@ -172,7 +199,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : begin = std::chrono::high_resolution_clock::now(); - if (!vUtil.computeVHACD(&fbx, params, &results)){ + if (!vUtil.computeVHACD(&fbx, params, &results, startMeshIndex, endMeshIndex, minimumMeshSize)) { cout << "Compute Failed..."; } end = std::chrono::high_resolution_clock::now();