diff --git a/BUILD.md b/BUILD.md index 30302d611b..4d321146c3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,7 +2,7 @@ - [cmake](https://cmake.org/download/): 3.9 - [Qt](https://www.qt.io/download-open-source): 5.9.1 -- [OpenSSL](https://www.openssl.org/): Use the latest available version of OpenSSL to avoid security vulnerabilities. +- [OpenSSL](https://www.openssl.org/): Use the latest available 1.0 version (**NOT** 1.1) of OpenSSL to avoid security vulnerabilities. - [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) ### CMake External Project Dependencies diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 1a27ddd479..0421195612 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -13,7 +13,7 @@ setup_memory_debugger() link_hifi_libraries( audio avatars octree gpu model fbx entities networking animation recording shared script-engine embedded-webserver - controllers physics plugins midi + controllers physics plugins midi baking image ) if (WIN32) diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 3886ff8d92..9df606c227 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -13,24 +13,32 @@ #include "AssetServer.h" #include +#include #include #include #include #include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include #include #include -#include "NetworkLogging.h" -#include "NodeType.h" +#include "AssetServerLogging.h" +#include "BakeAssetTask.h" #include "SendAssetTask.h" #include "UploadAssetTask.h" -#include + static const uint8_t MIN_CORES_FOR_MULTICORE = 4; static const uint8_t CPU_AFFINITY_COUNT_HIGH = 2; @@ -41,6 +49,151 @@ static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000; const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server"; +static const QStringList BAKEABLE_MODEL_EXTENSIONS = { "fbx" }; +static QStringList BAKEABLE_TEXTURE_EXTENSIONS; +static const QString BAKED_MODEL_SIMPLE_NAME = "asset.fbx"; +static const QString BAKED_TEXTURE_SIMPLE_NAME = "texture.ktx"; + +void AssetServer::bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) { + qDebug() << "Starting bake for: " << assetPath << assetHash; + auto it = _pendingBakes.find(assetHash); + if (it == _pendingBakes.end()) { + auto task = std::make_shared(assetHash, assetPath, filePath); + task->setAutoDelete(false); + _pendingBakes[assetHash] = task; + + connect(task.get(), &BakeAssetTask::bakeComplete, this, &AssetServer::handleCompletedBake); + connect(task.get(), &BakeAssetTask::bakeFailed, this, &AssetServer::handleFailedBake); + connect(task.get(), &BakeAssetTask::bakeAborted, this, &AssetServer::handleAbortedBake); + + _bakingTaskPool.start(task.get()); + } else { + qDebug() << "Already in queue"; + } +} + +QString AssetServer::getPathToAssetHash(const AssetHash& assetHash) { + return _filesDirectory.absoluteFilePath(assetHash); +} + +std::pair AssetServer::getAssetStatus(const AssetPath& path, const AssetHash& hash) { + auto it = _pendingBakes.find(hash); + if (it != _pendingBakes.end()) { + return { (*it)->isBaking() ? Baking : Pending, "" }; + } + + if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + return { Baked, "" }; + } + + auto dotIndex = path.lastIndexOf("."); + if (dotIndex == -1) { + return { Irrelevant, "" }; + } + + auto extension = path.mid(dotIndex + 1); + + QString bakedFilename; + + if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) { + bakedFilename = BAKED_MODEL_SIMPLE_NAME; + } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) { + bakedFilename = BAKED_TEXTURE_SIMPLE_NAME; + } else { + return { Irrelevant, "" }; + } + + auto bakedPath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + bakedFilename; + auto jt = _fileMappings.find(bakedPath); + if (jt != _fileMappings.end()) { + if (jt->second == hash) { + return { NotBaked, "" }; + } else { + return { Baked, "" }; + } + } else { + bool loaded; + AssetMeta meta; + + std::tie(loaded, meta) = readMetaFile(hash); + if (loaded && meta.failedLastBake) { + return { Error, meta.lastBakeErrors }; + } + } + + return { Pending, "" }; +} + +void AssetServer::bakeAssets() { + auto it = _fileMappings.cbegin(); + for (; it != _fileMappings.cend(); ++it) { + auto path = it->first; + auto hash = it->second; + maybeBake(path, hash); + } +} + +void AssetServer::maybeBake(const AssetPath& path, const AssetHash& hash) { + if (needsToBeBaked(path, hash)) { + qDebug() << "Queuing bake of: " << path; + bakeAsset(hash, path, getPathToAssetHash(hash)); + } +} + +void AssetServer::createEmptyMetaFile(const AssetHash& hash) { + QString metaFilePath = "atp:/" + hash + "/meta.json"; + QFile metaFile { metaFilePath }; + + if (!metaFile.exists()) { + qDebug() << "Creating metafile for " << hash; + if (metaFile.open(QFile::WriteOnly)) { + qDebug() << "Created metafile for " << hash; + metaFile.write("{}"); + } + } +} + +bool AssetServer::hasMetaFile(const AssetHash& hash) { + QString metaFilePath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/meta.json"; + + return _fileMappings.find(metaFilePath) != _fileMappings.end(); +} + +bool AssetServer::needsToBeBaked(const AssetPath& path, const AssetHash& assetHash) { + if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + return false; + } + + auto dotIndex = path.lastIndexOf("."); + if (dotIndex == -1) { + return false; + } + + auto extension = path.mid(dotIndex + 1); + + QString bakedFilename; + + bool loaded; + AssetMeta meta; + std::tie(loaded, meta) = readMetaFile(assetHash); + + // TODO: Allow failed bakes that happened on old versions to be re-baked + if (loaded && meta.failedLastBake) { + return false; + } + + if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) { + bakedFilename = BAKED_MODEL_SIMPLE_NAME; + } else if (loaded && BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit())) { + bakedFilename = BAKED_TEXTURE_SIMPLE_NAME; + } else { + return false; + } + + auto bakedPath = HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename; + return _fileMappings.find(bakedPath) == _fileMappings.end(); +} + bool interfaceRunning() { bool result = false; @@ -67,20 +220,36 @@ void updateConsumedCores() { if (isInterfaceRunning) { coreCount = coreCount > MIN_CORES_FOR_MULTICORE ? CPU_AFFINITY_COUNT_HIGH : CPU_AFFINITY_COUNT_LOW; } - qDebug() << "Setting max consumed cores to " << coreCount; + qCDebug(asset_server) << "Setting max consumed cores to " << coreCount; setMaxCores(coreCount); } AssetServer::AssetServer(ReceivedMessage& message) : ThreadedAssignment(message), - _taskPool(this) + _transferTaskPool(this), + _bakingTaskPool(this) { + // store the current state of image compression so we can reset it when this assignment is complete + _wasColorTextureCompressionEnabled = image::isColorTexturesCompressionEnabled(); + _wasGrayscaleTextureCompressionEnabled = image::isGrayscaleTexturesCompressionEnabled(); + _wasNormalTextureCompressionEnabled = image::isNormalTexturesCompressionEnabled(); + _wasCubeTextureCompressionEnabled = image::isCubeTexturesCompressionEnabled(); + + // enable compression in image library + image::setColorTexturesCompressionEnabled(true); + image::setGrayscaleTexturesCompressionEnabled(true); + image::setNormalTexturesCompressionEnabled(true); + image::setCubeTexturesCompressionEnabled(true); + + BAKEABLE_TEXTURE_EXTENSIONS = TextureBaker::getSupportedFormats(); + qDebug() << "Supported baking texture formats:" << BAKEABLE_MODEL_EXTENSIONS; // Most of the work will be I/O bound, reading from disk and constructing packet objects, // so the ideal is greater than the number of cores on the system. static const int TASK_POOL_THREAD_COUNT = 50; - _taskPool.setMaxThreadCount(TASK_POOL_THREAD_COUNT); + _transferTaskPool.setMaxThreadCount(TASK_POOL_THREAD_COUNT); + _bakingTaskPool.setMaxThreadCount(1); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet"); @@ -103,9 +272,39 @@ AssetServer::AssetServer(ReceivedMessage& message) : #endif } +void AssetServer::aboutToFinish() { + + // remove pending transfer tasks + _transferTaskPool.clear(); + + // abort each of our still running bake tasks, remove pending bakes that were never put on the thread pool + auto it = _pendingBakes.begin(); + while (it != _pendingBakes.end()) { + auto pendingRunnable = _bakingTaskPool.tryTake(it->get()); + + if (pendingRunnable) { + it = _pendingBakes.erase(it); + } else { + it.value()->abort(); + ++it; + } + } + + // make sure all bakers are finished or aborted + while (_pendingBakes.size() > 0) { + QCoreApplication::processEvents(); + } + + // re-set defaults in image library + image::setColorTexturesCompressionEnabled(_wasCubeTextureCompressionEnabled); + image::setGrayscaleTexturesCompressionEnabled(_wasGrayscaleTextureCompressionEnabled); + image::setNormalTexturesCompressionEnabled(_wasNormalTextureCompressionEnabled); + image::setCubeTexturesCompressionEnabled(_wasCubeTextureCompressionEnabled); +} + void AssetServer::run() { - qDebug() << "Waiting for connection to domain to request settings from domain-server."; + qCDebug(asset_server) << "Waiting for connection to domain to request settings from domain-server."; // wait until we have the domain-server settings, otherwise we bail DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); @@ -126,7 +325,7 @@ void AssetServer::completeSetup() { static const QString ASSET_SERVER_SETTINGS_KEY = "asset_server"; if (!settingsObject.contains(ASSET_SERVER_SETTINGS_KEY)) { - qCritical() << "Received settings from the domain-server with no asset-server section. Stopping assignment."; + qCCritical(asset_server) << "Received settings from the domain-server with no asset-server section. Stopping assignment."; setFinished(true); return; } @@ -141,7 +340,7 @@ void AssetServer::completeSetup() { const int BITS_PER_MEGABITS = 1000 * 1000; int maxBandwidth = maxBandwidthFloat * BITS_PER_MEGABITS; nodeList->setConnectionMaxBandwidth(maxBandwidth); - qInfo() << "Set maximum bandwith per connection to" << maxBandwidthFloat << "Mb/s." + qCInfo(asset_server) << "Set maximum bandwith per connection to" << maxBandwidthFloat << "Mb/s." " (" << maxBandwidth << "bits/s)"; } @@ -150,7 +349,7 @@ void AssetServer::completeSetup() { auto assetsJSONValue = assetServerObject[ASSETS_PATH_OPTION]; if (!assetsJSONValue.isString()) { - qCritical() << "Received an assets path from the domain-server that could not be parsed. Stopping assignment."; + qCCritical(asset_server) << "Received an assets path from the domain-server that could not be parsed. Stopping assignment."; setFinished(true); return; } @@ -167,19 +366,19 @@ void AssetServer::completeSetup() { _resourcesDirectory = QDir(absoluteFilePath); - qDebug() << "Creating resources directory"; + qCDebug(asset_server) << "Creating resources directory"; _resourcesDirectory.mkpath("."); _filesDirectory = _resourcesDirectory; if (!_resourcesDirectory.mkpath(ASSET_FILES_SUBDIR) || !_filesDirectory.cd(ASSET_FILES_SUBDIR)) { - qCritical() << "Unable to create file directory for asset-server files. Stopping assignment."; + qCCritical(asset_server) << "Unable to create file directory for asset-server files. Stopping assignment."; setFinished(true); return; } // load whatever mappings we currently have from the local file if (loadMappingsFromFile()) { - qInfo() << "Serving files from: " << _filesDirectory.path(); + qCInfo(asset_server) << "Serving files from: " << _filesDirectory.path(); // Check the asset directory to output some information about what we have auto files = _filesDirectory.entryList(QDir::Files); @@ -187,18 +386,19 @@ void AssetServer::completeSetup() { QRegExp hashFileRegex { ASSET_HASH_REGEX_STRING }; auto hashedFiles = files.filter(hashFileRegex); - qInfo() << "There are" << hashedFiles.size() << "asset files in the asset directory."; + qCInfo(asset_server) << "There are" << hashedFiles.size() << "asset files in the asset directory."; - if (_fileMappings.count() > 0) { + if (_fileMappings.size() > 0) { cleanupUnmappedFiles(); } nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer }); + + bakeAssets(); } else { - qCritical() << "Asset Server assignment will not continue because mapping file could not be loaded."; + qCCritical(asset_server) << "Asset Server assignment will not continue because mapping file could not be loaded."; setFinished(true); } - } void AssetServer::cleanupUnmappedFiles() { @@ -206,21 +406,28 @@ void AssetServer::cleanupUnmappedFiles() { auto files = _filesDirectory.entryInfoList(QDir::Files); - // grab the currently mapped hashes - auto mappedHashes = _fileMappings.values(); - - qInfo() << "Performing unmapped asset cleanup."; + qCInfo(asset_server) << "Performing unmapped asset cleanup."; for (const auto& fileInfo : files) { - if (hashFileRegex.exactMatch(fileInfo.fileName())) { - if (!mappedHashes.contains(fileInfo.fileName())) { + auto filename = fileInfo.fileName(); + if (hashFileRegex.exactMatch(filename)) { + bool matched { false }; + for (auto& pair : _fileMappings) { + if (pair.second == filename) { + matched = true; + break; + } + } + if (!matched) { // remove the unmapped file QFile removeableFile { fileInfo.absoluteFilePath() }; if (removeableFile.remove()) { - qDebug() << "\tDeleted" << fileInfo.fileName() << "from asset files directory since it is unmapped."; + qCDebug(asset_server) << "\tDeleted" << filename << "from asset files directory since it is unmapped."; + + removeBakedPathsForDeletedAsset(filename); } else { - qDebug() << "\tAttempt to delete unmapped file" << fileInfo.fileName() << "failed"; + qCDebug(asset_server) << "\tAttempt to delete unmapped file" << filename << "failed"; } } } @@ -238,26 +445,24 @@ void AssetServer::handleAssetMappingOperation(QSharedPointer me replyPacket->writePrimitive(messageID); switch (operationType) { - case AssetMappingOperationType::Get: { + case AssetMappingOperationType::Get: handleGetMappingOperation(*message, senderNode, *replyPacket); break; - } - case AssetMappingOperationType::GetAll: { + case AssetMappingOperationType::GetAll: handleGetAllMappingOperation(*message, senderNode, *replyPacket); break; - } - case AssetMappingOperationType::Set: { + case AssetMappingOperationType::Set: handleSetMappingOperation(*message, senderNode, *replyPacket); break; - } - case AssetMappingOperationType::Delete: { + case AssetMappingOperationType::Delete: handleDeleteMappingsOperation(*message, senderNode, *replyPacket); break; - } - case AssetMappingOperationType::Rename: { + case AssetMappingOperationType::Rename: handleRenameMappingOperation(*message, senderNode, *replyPacket); break; - } + case AssetMappingOperationType::SetBakingEnabled: + handleSetBakingEnabledOperation(*message, senderNode, *replyPacket); + break; } auto nodeList = DependencyManager::get(); @@ -267,11 +472,75 @@ void AssetServer::handleAssetMappingOperation(QSharedPointer me void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { QString assetPath = message.readString(); + QUrl url { assetPath }; + assetPath = url.path(); + auto it = _fileMappings.find(assetPath); if (it != _fileMappings.end()) { - auto assetHash = it->toString(); + + // check if we should re-direct to a baked asset + + // first, figure out from the mapping extension what type of file this is + auto assetPathExtension = assetPath.mid(assetPath.lastIndexOf('.') + 1).toLower(); + QString bakedRootFile; + + if (BAKEABLE_MODEL_EXTENSIONS.contains(assetPathExtension)) { + bakedRootFile = BAKED_MODEL_SIMPLE_NAME; + } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(assetPathExtension.toLocal8Bit())) { + bakedRootFile = BAKED_TEXTURE_SIMPLE_NAME; + } + + auto originalAssetHash = it->second; + QString redirectedAssetHash; + QString bakedAssetPath; + quint8 wasRedirected = false; + bool bakingDisabled = false; + + if (!bakedRootFile.isEmpty()) { + // we ran into an asset for which we could have a baked version, let's check if it's ready + bakedAssetPath = HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + bakedRootFile; + auto bakedIt = _fileMappings.find(bakedAssetPath); + + if (bakedIt != _fileMappings.end()) { + if (bakedIt->second != originalAssetHash) { + qDebug() << "Did find baked version for: " << originalAssetHash << assetPath; + // we found a baked version of the requested asset to serve, redirect to that + redirectedAssetHash = bakedIt->second; + wasRedirected = true; + } else { + qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath << " (disabled)"; + bakingDisabled = true; + } + } else { + qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath; + } + } + replyPacket.writePrimitive(AssetServerError::NoError); - replyPacket.write(QByteArray::fromHex(assetHash.toUtf8())); + + if (wasRedirected) { + qDebug() << "Writing re-directed hash for" << originalAssetHash << "to" << redirectedAssetHash; + replyPacket.write(QByteArray::fromHex(redirectedAssetHash.toUtf8())); + + // add a flag saying that this mapping request was redirect + replyPacket.writePrimitive(wasRedirected); + + // include the re-directed path in case the caller needs to make relative path requests for the baked asset + replyPacket.writeString(bakedAssetPath); + + } else { + replyPacket.write(QByteArray::fromHex(originalAssetHash.toUtf8())); + replyPacket.writePrimitive(wasRedirected); + + auto query = QUrlQuery(url.query()); + bool isSkybox = query.hasQueryItem("skybox"); + if (isSkybox) { + writeMetaFile(originalAssetHash); + if (!bakingDisabled) { + maybeBake(assetPath, originalAssetHash); + } + } + } } else { replyPacket.writePrimitive(AssetServerError::AssetNotFound); } @@ -280,13 +549,23 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { replyPacket.writePrimitive(AssetServerError::NoError); - auto count = _fileMappings.size(); + uint32_t count = (uint32_t)_fileMappings.size(); replyPacket.writePrimitive(count); for (auto it = _fileMappings.cbegin(); it != _fileMappings.cend(); ++ it) { - replyPacket.writeString(it.key()); - replyPacket.write(QByteArray::fromHex(it.value().toString().toUtf8())); + auto mapping = it->first; + auto hash = it->second; + replyPacket.writeString(mapping); + replyPacket.write(QByteArray::fromHex(hash.toUtf8())); + + BakingStatus status; + QString lastBakeErrors; + std::tie(status, lastBakeErrors) = getAssetStatus(mapping, hash); + replyPacket.writePrimitive(status); + if (status == Error) { + replyPacket.writeString(lastBakeErrors); + } } } @@ -296,11 +575,18 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode auto assetHash = message.read(SHA256_HASH_LENGTH).toHex(); - if (setMapping(assetPath, assetHash)) { - replyPacket.writePrimitive(AssetServerError::NoError); + // don't process a set mapping operation that is inside the hidden baked folder + if (assetPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + qCDebug(asset_server) << "Refusing to process a set mapping operation inside" << HIDDEN_BAKED_CONTENT_FOLDER; + replyPacket.writePrimitive(AssetServerError::PermissionDenied); } else { - replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + if (setMapping(assetPath, assetHash)) { + replyPacket.writePrimitive(AssetServerError::NoError); + } else { + replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + } } + } else { replyPacket.writePrimitive(AssetServerError::PermissionDenied); } @@ -314,7 +600,14 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared QStringList mappingsToDelete; for (int i = 0; i < numberOfDeletedMappings; ++i) { - mappingsToDelete << message.readString(); + auto mapping = message.readString(); + + if (!mapping.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + mappingsToDelete << mapping; + } else { + qCDebug(asset_server) << "Refusing to delete mapping" << mapping + << "that is inside" << HIDDEN_BAKED_CONTENT_FOLDER; + } } if (deleteMappings(mappingsToDelete)) { @@ -332,7 +625,38 @@ void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedN QString oldPath = message.readString(); QString newPath = message.readString(); - if (renameMapping(oldPath, newPath)) { + if (oldPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER) || newPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + qCDebug(asset_server) << "Cannot rename" << oldPath << "to" << newPath + << "since one of the paths is inside" << HIDDEN_BAKED_CONTENT_FOLDER; + replyPacket.writePrimitive(AssetServerError::PermissionDenied); + } else { + if (renameMapping(oldPath, newPath)) { + replyPacket.writePrimitive(AssetServerError::NoError); + } else { + replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + } + } + + } else { + replyPacket.writePrimitive(AssetServerError::PermissionDenied); + } +} + +void AssetServer::handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { + if (senderNode->getCanWriteToAssetServer()) { + bool enabled { true }; + message.readPrimitive(&enabled); + + int numberOfMappings{ 0 }; + message.readPrimitive(&numberOfMappings); + + QStringList mappings; + + for (int i = 0; i < numberOfMappings; ++i) { + mappings << message.readString(); + } + + if (setBakingEnabled(mappings, enabled)) { replyPacket.writePrimitive(AssetServerError::NoError); } else { replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); @@ -347,7 +671,7 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh MessageID messageID; if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID))) { - qDebug() << "ERROR bad file request"; + qCDebug(asset_server) << "ERROR bad file request"; return; } @@ -366,11 +690,11 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh QFileInfo fileInfo { _filesDirectory.filePath(fileName) }; if (fileInfo.exists() && fileInfo.isReadable()) { - qDebug() << "Opening file: " << fileInfo.filePath(); + qCDebug(asset_server) << "Opening file: " << fileInfo.filePath(); replyPacket->writePrimitive(AssetServerError::NoError); replyPacket->writePrimitive(fileInfo.size()); } else { - qDebug() << "Asset not found: " << QString(hexHash); + qCDebug(asset_server) << "Asset not found: " << QString(hexHash); replyPacket->writePrimitive(AssetServerError::AssetNotFound); } @@ -383,22 +707,22 @@ void AssetServer::handleAssetGet(QSharedPointer message, Shared auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(DataOffset) + sizeof(DataOffset)); if (message->getSize() < minSize) { - qDebug() << "ERROR bad file request"; + qCDebug(asset_server) << "ERROR bad file request"; return; } // Queue task auto task = new SendAssetTask(message, senderNode, _filesDirectory); - _taskPool.start(task); + _transferTaskPool.start(task); } void AssetServer::handleAssetUpload(QSharedPointer message, SharedNodePointer senderNode) { if (senderNode->getCanWriteToAssetServer()) { - qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID()); + qCDebug(asset_server) << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID()); auto task = new UploadAssetTask(message, senderNode, _filesDirectory); - _taskPool.start(task); + _transferTaskPool.start(task); } else { // this is a node the domain told us is not allowed to rez entities // for now this also means it isn't allowed to add assets @@ -502,39 +826,46 @@ bool AssetServer::loadMappingsFromFile() { auto jsonDocument = QJsonDocument::fromJson(mapFile.readAll(), &error); if (error.error == QJsonParseError::NoError) { - _fileMappings = jsonDocument.object().toVariantHash(); - - // remove any mappings that don't match the expected format - auto it = _fileMappings.begin(); - while (it != _fileMappings.end()) { - bool shouldDrop = false; - - if (!isValidFilePath(it.key())) { - qWarning() << "Will not keep mapping for" << it.key() << "since it is not a valid path."; - shouldDrop = true; - } - - if (!isValidHash(it.value().toString())) { - qWarning() << "Will not keep mapping for" << it.key() << "since it does not have a valid hash."; - shouldDrop = true; - } - - if (shouldDrop) { - it = _fileMappings.erase(it); - } else { - ++it; - } + if (!jsonDocument.isObject()) { + qCWarning(asset_server) << "Failed to read mapping file, root value in" << mapFilePath << "is not an object"; + return false; } - qInfo() << "Loaded" << _fileMappings.count() << "mappings from map file at" << mapFilePath; + //_fileMappings = jsonDocument.object().toVariantHash(); + auto root = jsonDocument.object(); + for (auto it = root.begin(); it != root.end(); ++it) { + auto key = it.key(); + auto value = it.value(); + + if (!value.isString()) { + qCWarning(asset_server) << "Skipping" << key << ":" << value << "because it is not a string"; + continue; + } + + if (!isValidFilePath(key)) { + qCWarning(asset_server) << "Will not keep mapping for" << key << "since it is not a valid path."; + continue; + } + + if (!isValidHash(value.toString())) { + qCWarning(asset_server) << "Will not keep mapping for" << key << "since it does not have a valid hash."; + continue; + } + + + qDebug() << "Added " << key << value.toString(); + _fileMappings[key] = value.toString(); + } + + qCInfo(asset_server) << "Loaded" << _fileMappings.size() << "mappings from map file at" << mapFilePath; return true; } } - qCritical() << "Failed to read mapping file at" << mapFilePath; + qCCritical(asset_server) << "Failed to read mapping file at" << mapFilePath; return false; } else { - qInfo() << "No existing mappings loaded from file since no file was found at" << mapFilePath; + qCInfo(asset_server) << "No existing mappings loaded from file since no file was found at" << mapFilePath; } return true; @@ -545,17 +876,22 @@ bool AssetServer::writeMappingsToFile() { QFile mapFile { mapFilePath }; if (mapFile.open(QIODevice::WriteOnly)) { - auto jsonObject = QJsonObject::fromVariantHash(_fileMappings); - QJsonDocument jsonDocument { jsonObject }; + QJsonObject root; + + for (auto it : _fileMappings) { + root[it.first] = it.second; + } + + QJsonDocument jsonDocument { root }; if (mapFile.write(jsonDocument.toJson()) != -1) { - qDebug() << "Wrote JSON mappings to file at" << mapFilePath; + qCDebug(asset_server) << "Wrote JSON mappings to file at" << mapFilePath; return true; } else { - qWarning() << "Failed to write JSON mappings to file at" << mapFilePath; + qCWarning(asset_server) << "Failed to write JSON mappings to file at" << mapFilePath; } } else { - qWarning() << "Failed to open map file at" << mapFilePath; + qCWarning(asset_server) << "Failed to open map file at" << mapFilePath; } return false; @@ -565,17 +901,18 @@ bool AssetServer::setMapping(AssetPath path, AssetHash hash) { path = path.trimmed(); if (!isValidFilePath(path)) { - qWarning() << "Cannot set a mapping for invalid path:" << path << "=>" << hash; + qCWarning(asset_server) << "Cannot set a mapping for invalid path:" << path << "=>" << hash; return false; } if (!isValidHash(hash)) { - qWarning() << "Cannot set a mapping for invalid hash" << path << "=>" << hash; + qCWarning(asset_server) << "Cannot set a mapping for invalid hash" << path << "=>" << hash; return false; } // remember what the old mapping was in case persistence fails - auto oldMapping = _fileMappings.value(path).toString(); + auto it = _fileMappings.find(path); + auto oldMapping = it != _fileMappings.end() ? it->second : ""; // update the in memory QHash _fileMappings[path] = hash; @@ -583,17 +920,18 @@ bool AssetServer::setMapping(AssetPath path, AssetHash hash) { // attempt to write to file if (writeMappingsToFile()) { // persistence succeeded, we are good to go - qDebug() << "Set mapping:" << path << "=>" << hash; + qCDebug(asset_server) << "Set mapping:" << path << "=>" << hash; + maybeBake(path, hash); return true; } else { // failed to persist this mapping to file - put back the old one in our in-memory representation if (oldMapping.isEmpty()) { - _fileMappings.remove(path); + _fileMappings.erase(_fileMappings.find(path)); } else { _fileMappings[path] = oldMapping; } - qWarning() << "Failed to persist mapping:" << path << "=>" << hash; + qCWarning(asset_server) << "Failed to persist mapping:" << path << "=>" << hash; return false; } @@ -603,16 +941,27 @@ bool pathIsFolder(const AssetPath& path) { return path.endsWith('/'); } -bool AssetServer::deleteMappings(AssetPathList& paths) { +void AssetServer::removeBakedPathsForDeletedAsset(AssetHash hash) { + // we deleted the file with this hash + + // check if we had baked content for that file that should also now be removed + // by calling deleteMappings for the hidden baked content folder for this hash + AssetPathList hiddenBakedFolder { HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" }; + + qCDebug(asset_server) << "Deleting baked content below" << hiddenBakedFolder << "since" << hash << "was deleted"; + + deleteMappings(hiddenBakedFolder); +} + +bool AssetServer::deleteMappings(const AssetPathList& paths) { // take a copy of the current mappings in case persistence of these deletes fails auto oldMappings = _fileMappings; QSet hashesToCheckForDeletion; // enumerate the paths to delete and remove them all - for (auto& path : paths) { - - path = path.trimmed(); + for (const auto& rawPath : paths) { + auto path = rawPath.trimmed(); // figure out if this path will delete a file or folder if (pathIsFolder(path)) { @@ -621,9 +970,9 @@ bool AssetServer::deleteMappings(AssetPathList& paths) { auto sizeBefore = _fileMappings.size(); while (it != _fileMappings.end()) { - if (it.key().startsWith(path)) { + if (it->first.startsWith(path)) { // add this hash to the list we need to check for asset removal from the server - hashesToCheckForDeletion << it.value().toString(); + hashesToCheckForDeletion << it->second; it = _fileMappings.erase(it); } else { @@ -633,20 +982,22 @@ bool AssetServer::deleteMappings(AssetPathList& paths) { auto sizeNow = _fileMappings.size(); if (sizeBefore != sizeNow) { - qDebug() << "Deleted" << sizeBefore - sizeNow << "mappings in folder: " << path; + qCDebug(asset_server) << "Deleted" << sizeBefore - sizeNow << "mappings in folder: " << path; } else { - qDebug() << "Did not find any mappings to delete in folder:" << path; + qCDebug(asset_server) << "Did not find any mappings to delete in folder:" << path; } } else { - auto oldMapping = _fileMappings.take(path); - if (!oldMapping.isNull()) { + auto it = _fileMappings.find(path); + if (it != _fileMappings.end()) { // add this hash to the list we need to check for asset removal from server - hashesToCheckForDeletion << oldMapping.toString(); + hashesToCheckForDeletion << it->second; - qDebug() << "Deleted a mapping:" << path << "=>" << oldMapping.toString(); + qCDebug(asset_server) << "Deleted a mapping:" << path << "=>" << it->second; + + _fileMappings.erase(it); } else { - qDebug() << "Unable to delete a mapping that was not found:" << path; + qCDebug(asset_server) << "Unable to delete a mapping that was not found:" << path; } } } @@ -655,12 +1006,9 @@ bool AssetServer::deleteMappings(AssetPathList& paths) { if (writeMappingsToFile()) { // persistence succeeded we are good to go - // grab the current mapped hashes - auto mappedHashes = _fileMappings.values(); - - // enumerate the mapped hashes and clear the list of hashes to check for anything that's present - for (auto& hashVariant : mappedHashes) { - auto it = hashesToCheckForDeletion.find(hashVariant.toString()); + // TODO iterate through hashesToCheckForDeletion instead + for (auto& pair : _fileMappings) { + auto it = hashesToCheckForDeletion.find(pair.second); if (it != hashesToCheckForDeletion.end()) { hashesToCheckForDeletion.erase(it); } @@ -672,15 +1020,17 @@ bool AssetServer::deleteMappings(AssetPathList& paths) { QFile removeableFile { _filesDirectory.absoluteFilePath(hash) }; if (removeableFile.remove()) { - qDebug() << "\tDeleted" << hash << "from asset files directory since it is now unmapped."; + qCDebug(asset_server) << "\tDeleted" << hash << "from asset files directory since it is now unmapped."; + + removeBakedPathsForDeletedAsset(hash); } else { - qDebug() << "\tAttempt to delete unmapped file" << hash << "failed"; + qCDebug(asset_server) << "\tAttempt to delete unmapped file" << hash << "failed"; } } return true; } else { - qWarning() << "Failed to persist deleted mappings, rolling back"; + qCWarning(asset_server) << "Failed to persist deleted mappings, rolling back"; // we didn't delete the previous mapping, put it back in our in-memory representation _fileMappings = oldMappings; @@ -694,7 +1044,7 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { newPath = newPath.trimmed(); if (!isValidFilePath(oldPath) || !isValidFilePath(newPath)) { - qWarning() << "Cannot perform rename with invalid paths - both should have leading forward and no ending slashes:" + qCWarning(asset_server) << "Cannot perform rename with invalid paths - both should have leading forward and no ending slashes:" << oldPath << "=>" << newPath; return false; @@ -704,7 +1054,7 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { if (pathIsFolder(oldPath)) { if (!pathIsFolder(newPath)) { // we were asked to rename a path to a folder to a path that isn't a folder, this is a fail - qWarning() << "Cannot rename mapping from folder path" << oldPath << "to file path" << newPath; + qCWarning(asset_server) << "Cannot rename mapping from folder path" << oldPath << "to file path" << newPath; return false; } @@ -716,13 +1066,14 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { auto it = oldMappings.begin(); while (it != oldMappings.end()) { - if (it.key().startsWith(oldPath)) { - auto newKey = it.key(); + auto& oldKey = it->first; + if (oldKey.startsWith(oldPath)) { + auto newKey = oldKey; newKey.replace(0, oldPath.size(), newPath); // remove the old version from the in memory file mappings - _fileMappings.remove(it.key()); - _fileMappings.insert(newKey, it.value()); + _fileMappings.erase(_fileMappings.find(oldKey)); + _fileMappings[newKey] = it->second; } ++it; @@ -730,52 +1081,54 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { if (writeMappingsToFile()) { // persisted the changed mappings, return success - qDebug() << "Renamed folder mapping:" << oldPath << "=>" << newPath; + qCDebug(asset_server) << "Renamed folder mapping:" << oldPath << "=>" << newPath; return true; } else { // couldn't persist the renamed paths, rollback and return failure _fileMappings = oldMappings; - qWarning() << "Failed to persist renamed folder mapping:" << oldPath << "=>" << newPath; + qCWarning(asset_server) << "Failed to persist renamed folder mapping:" << oldPath << "=>" << newPath; return false; } } else { if (pathIsFolder(newPath)) { // we were asked to rename a path to a file to a path that is a folder, this is a fail - qWarning() << "Cannot rename mapping from file path" << oldPath << "to folder path" << newPath; + qCWarning(asset_server) << "Cannot rename mapping from file path" << oldPath << "to folder path" << newPath; return false; } // take the old hash to remove the old mapping - auto oldSourceMapping = _fileMappings.take(oldPath).toString(); + auto it = _fileMappings.find(oldPath); + auto oldSourceMapping = it->second; + _fileMappings.erase(it); // in case we're overwriting, keep the current destination mapping for potential rollback - auto oldDestinationMapping = _fileMappings.value(newPath); + auto oldDestinationIt = _fileMappings.find(newPath); if (!oldSourceMapping.isEmpty()) { _fileMappings[newPath] = oldSourceMapping; if (writeMappingsToFile()) { // persisted the renamed mapping, return success - qDebug() << "Renamed mapping:" << oldPath << "=>" << newPath; + qCDebug(asset_server) << "Renamed mapping:" << oldPath << "=>" << newPath; return true; } else { // we couldn't persist the renamed mapping, rollback and return failure _fileMappings[oldPath] = oldSourceMapping; - if (!oldDestinationMapping.isNull()) { + if (oldDestinationIt != _fileMappings.end()) { // put back the overwritten mapping for the destination path - _fileMappings[newPath] = oldDestinationMapping.toString(); + _fileMappings[newPath] = oldDestinationIt->second; } else { // clear the new mapping - _fileMappings.remove(newPath); + _fileMappings.erase(_fileMappings.find(newPath)); } - qDebug() << "Failed to persist renamed mapping:" << oldPath << "=>" << newPath; + qCDebug(asset_server) << "Failed to persist renamed mapping:" << oldPath << "=>" << newPath; return false; } @@ -785,3 +1138,241 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { } } } + +static const QString BAKED_ASSET_SIMPLE_FBX_NAME = "asset.fbx"; +static const QString BAKED_ASSET_SIMPLE_TEXTURE_NAME = "texture.ktx"; + +QString getBakeMapping(const AssetHash& hash, const QString& relativeFilePath) { + return HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath; +} + +void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, QString errors) { + qDebug() << "Failed: " << originalAssetHash << assetPath << errors; + + bool loaded; + AssetMeta meta; + + std::tie(loaded, meta) = readMetaFile(originalAssetHash); + + meta.failedLastBake = true; + meta.lastBakeErrors = errors; + + writeMetaFile(originalAssetHash, meta); + + _pendingBakes.remove(originalAssetHash); +} + +void AssetServer::handleCompletedBake(QString originalAssetHash, QString originalAssetPath, QVector bakedFilePaths) { + bool errorCompletingBake { false }; + QString errorReason; + + qDebug() << "Completing bake for " << originalAssetHash; + + for (auto& filePath : bakedFilePaths) { + // figure out the hash for the contents of this file + QFile file(filePath); + + qDebug() << "File path: " << filePath; + + AssetHash bakedFileHash; + + if (file.open(QIODevice::ReadOnly)) { + QCryptographicHash hasher(QCryptographicHash::Sha256); + + if (hasher.addData(&file)) { + bakedFileHash = hasher.result().toHex(); + } else { + // stop handling this bake, couldn't hash the contents of the file + errorCompletingBake = true; + errorReason = "Failed to finalize bake"; + break; + } + + // first check that we don't already have this bake file in our list + auto bakeFileDestination = _filesDirectory.absoluteFilePath(bakedFileHash); + if (!QFile::exists(bakeFileDestination)) { + // copy each to our files folder (with the hash as their filename) + if (!file.copy(_filesDirectory.absoluteFilePath(bakedFileHash))) { + // stop handling this bake, couldn't copy the bake file into our files directory + errorCompletingBake = true; + errorReason = "Failed to copy baked assets to asset server"; + break; + } + } + + // setup the mapping for this bake file + auto relativeFilePath = QUrl(filePath).fileName(); + qDebug() << "Relative file path is: " << relativeFilePath; + + if (relativeFilePath.endsWith(".fbx", Qt::CaseInsensitive)) { + // for an FBX file, we replace the filename with the simple name + // (to handle the case where two mapped assets have the same hash but different names) + relativeFilePath = BAKED_ASSET_SIMPLE_FBX_NAME; + } else if (!originalAssetPath.endsWith(".fbx", Qt::CaseInsensitive)) { + relativeFilePath = BAKED_ASSET_SIMPLE_TEXTURE_NAME; + + } + + QString bakeMapping = getBakeMapping(originalAssetHash, relativeFilePath); + + // add a mapping (under the hidden baked folder) for this file resulting from the bake + if (setMapping(bakeMapping, bakedFileHash)) { + qDebug() << "Added" << bakeMapping << "for bake file" << bakedFileHash << "from bake of" << originalAssetHash; + } else { + qDebug() << "Failed to set mapping"; + // stop handling this bake, couldn't add a mapping for this bake file + errorCompletingBake = true; + errorReason = "Failed to finalize bake"; + break; + } + } else { + qDebug() << "Failed to open baked file: " << filePath; + // stop handling this bake, we couldn't open one of the files for reading + errorCompletingBake = true; + errorReason = "Failed to finalize bake"; + break; + } + } + + if (!errorCompletingBake) { + // create the meta file to store which version of the baking process we just completed + writeMetaFile(originalAssetHash); + } else { + qWarning() << "Could not complete bake for" << originalAssetHash; + AssetMeta meta; + meta.failedLastBake = true; + meta.lastBakeErrors = errorReason; + writeMetaFile(originalAssetHash, meta); + } + + _pendingBakes.remove(originalAssetHash); +} + +void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath) { + // for an aborted bake we don't do anything but remove the BakeAssetTask from our pending bakes + _pendingBakes.remove(originalAssetHash); +} + +static const QString BAKE_VERSION_KEY = "bake_version"; +static const QString FAILED_LAST_BAKE_KEY = "failed_last_bake"; +static const QString LAST_BAKE_ERRORS_KEY = "last_bake_errors"; + +std::pair AssetServer::readMetaFile(AssetHash hash) { + auto metaFilePath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + "meta.json"; + + auto it = _fileMappings.find(metaFilePath); + if (it == _fileMappings.end()) { + return { false, {} }; + } + + auto metaFileHash = it->second; + + QFile metaFile(_filesDirectory.absoluteFilePath(metaFileHash)); + + if (metaFile.open(QIODevice::ReadOnly)) { + auto data = metaFile.readAll(); + metaFile.close(); + + QJsonParseError error; + auto doc = QJsonDocument::fromJson(data, &error); + + if (error.error == QJsonParseError::NoError && doc.isObject()) { + auto root = doc.object(); + + auto bakeVersion = root[BAKE_VERSION_KEY].toInt(-1); + auto failedLastBake = root[FAILED_LAST_BAKE_KEY]; + auto lastBakeErrors = root[LAST_BAKE_ERRORS_KEY]; + + if (bakeVersion != -1 + && failedLastBake.isBool() + && lastBakeErrors.isString()) { + + AssetMeta meta; + meta.bakeVersion = bakeVersion; + meta.failedLastBake = failedLastBake.toBool(); + meta.lastBakeErrors = lastBakeErrors.toString(); + + return { true, meta }; + } else { + qCWarning(asset_server) << "Metafile for" << hash << "has either missing or malformed data."; + } + } + } + + return { false, {} }; +} + +bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta) { + // construct the JSON that will be in the meta file + QJsonObject metaFileObject; + + metaFileObject[BAKE_VERSION_KEY] = meta.bakeVersion; + metaFileObject[FAILED_LAST_BAKE_KEY] = meta.failedLastBake; + metaFileObject[LAST_BAKE_ERRORS_KEY] = meta.lastBakeErrors; + + QJsonDocument metaFileDoc; + metaFileDoc.setObject(metaFileObject); + + auto metaFileJSON = metaFileDoc.toJson(); + + // get a hash for the contents of the meta-file + AssetHash metaFileHash = QCryptographicHash::hash(metaFileJSON, QCryptographicHash::Sha256).toHex(); + + // create the meta file in our files folder, named by the hash of its contents + QFile metaFile(_filesDirectory.absoluteFilePath(metaFileHash)); + + if (metaFile.open(QIODevice::WriteOnly)) { + metaFile.write(metaFileJSON); + metaFile.close(); + + // add a mapping to the meta file so it doesn't get deleted because it is unmapped + auto metaFileMapping = HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + "meta.json"; + + return setMapping(metaFileMapping, metaFileHash); + } else { + return false; + } +} + +bool AssetServer::setBakingEnabled(const AssetPathList& paths, bool enabled) { + for (const auto& path : paths) { + auto it = _fileMappings.find(path); + if (it != _fileMappings.end()) { + auto hash = it->second; + + auto dotIndex = path.lastIndexOf("."); + if (dotIndex == -1) { + continue; + } + + auto extension = path.mid(dotIndex + 1); + + QString bakedFilename; + + if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) { + bakedFilename = BAKED_MODEL_SIMPLE_NAME; + } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) { + bakedFilename = BAKED_TEXTURE_SIMPLE_NAME; + } else { + continue; + } + + auto bakedMapping = getBakeMapping(hash, bakedFilename); + + auto it = _fileMappings.find(bakedMapping); + bool currentlyDisabled = (it != _fileMappings.end() && it->second == hash); + + if (enabled && currentlyDisabled) { + QStringList bakedMappings{ bakedMapping }; + deleteMappings(bakedMappings); + maybeBake(path, hash); + qDebug() << "Enabled baking for" << path; + } else if (!enabled && !currentlyDisabled) { + removeBakedPathsForDeletedAsset(hash); + setMapping(bakedMapping, hash); + qDebug() << "Disabled baking for" << path; + } + } + } + return true; +} diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index 132fb51433..94be560c9b 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -14,17 +14,39 @@ #include #include +#include #include #include "AssetUtils.h" #include "ReceivedMessage.h" + +namespace std { + template <> + struct hash { + size_t operator()(const QString& v) const { return qHash(v); } + }; +} + +struct AssetMeta { + AssetMeta() { + } + + int bakeVersion { 0 }; + bool failedLastBake { false }; + QString lastBakeErrors; +}; + +class BakeAssetTask; + class AssetServer : public ThreadedAssignment { Q_OBJECT public: AssetServer(ReceivedMessage& message); + void aboutToFinish() override; + public slots: void run() override; @@ -39,13 +61,14 @@ private slots: void sendStatsPacket() override; private: - using Mappings = QVariantHash; + using Mappings = std::unordered_map; void handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket); void handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket); void handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket); void handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket); void handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket); + void handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket); // Mapping file operations must be called from main assignment thread only bool loadMappingsFromFile(); @@ -55,19 +78,54 @@ private: bool setMapping(AssetPath path, AssetHash hash); /// Delete mapping `path`. Returns `true` if deletion of mappings succeeds, else `false`. - bool deleteMappings(AssetPathList& paths); + bool deleteMappings(const AssetPathList& paths); /// Rename mapping from `oldPath` to `newPath`. Returns true if successful bool renameMapping(AssetPath oldPath, AssetPath newPath); - // deletes any unmapped files from the local asset directory + bool setBakingEnabled(const AssetPathList& paths, bool enabled); + + /// Delete any unmapped files from the local asset directory void cleanupUnmappedFiles(); + QString getPathToAssetHash(const AssetHash& assetHash); + + std::pair getAssetStatus(const AssetPath& path, const AssetHash& hash); + + void bakeAssets(); + void maybeBake(const AssetPath& path, const AssetHash& hash); + void createEmptyMetaFile(const AssetHash& hash); + bool hasMetaFile(const AssetHash& hash); + bool needsToBeBaked(const AssetPath& path, const AssetHash& assetHash); + void bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath); + + /// Move baked content for asset to baked directory and update baked status + void handleCompletedBake(QString originalAssetHash, QString assetPath, QVector bakedFilePaths); + void handleFailedBake(QString originalAssetHash, QString assetPath, QString errors); + void handleAbortedBake(QString originalAssetHash, QString assetPath); + + /// Create meta file to describe baked content for original asset + std::pair readMetaFile(AssetHash hash); + bool writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta = AssetMeta()); + + /// Remove baked paths when the original asset is deleteds + void removeBakedPathsForDeletedAsset(AssetHash originalAssetHash); + Mappings _fileMappings; QDir _resourcesDirectory; QDir _filesDirectory; - QThreadPool _taskPool; + + /// Task pool for handling uploads and downloads of assets + QThreadPool _transferTaskPool; + + QHash> _pendingBakes; + QThreadPool _bakingTaskPool; + + bool _wasColorTextureCompressionEnabled { false }; + bool _wasGrayscaleTextureCompressionEnabled { false }; + bool _wasNormalTextureCompressionEnabled { false }; + bool _wasCubeTextureCompressionEnabled { false }; }; #endif diff --git a/assignment-client/src/assets/AssetServerLogging.cpp b/assignment-client/src/assets/AssetServerLogging.cpp new file mode 100644 index 0000000000..39a02107ea --- /dev/null +++ b/assignment-client/src/assets/AssetServerLogging.cpp @@ -0,0 +1,14 @@ +// +// AssetServerLogging.cpp +// assignment-client/src/assets +// +// Created by Clement Brisset on 8/9/17. +// Copyright 2017 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 "AssetServerLogging.h" + +Q_LOGGING_CATEGORY(asset_server, "hifi.asset-server") diff --git a/assignment-client/src/assets/AssetServerLogging.h b/assignment-client/src/assets/AssetServerLogging.h new file mode 100644 index 0000000000..986e01ecc5 --- /dev/null +++ b/assignment-client/src/assets/AssetServerLogging.h @@ -0,0 +1,19 @@ +// +// AssetServerLogging.h +// assignment-client/src/assets +// +// Created by Clement Brisset on 8/9/17. +// Copyright 2017 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_AssetServerLogging_h +#define hifi_AssetServerLogging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(asset_server) + +#endif // hifi_AssetServerLogging_h diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp new file mode 100644 index 0000000000..9073510f79 --- /dev/null +++ b/assignment-client/src/assets/BakeAssetTask.cpp @@ -0,0 +1,79 @@ +// +// BakeAssetTask.cpp +// assignment-client/src/assets +// +// Created by Stephen Birarda on 9/18/17. +// Copyright 2017 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 "BakeAssetTask.h" + +#include + +#include +#include + +BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) : + _assetHash(assetHash), + _assetPath(assetPath), + _filePath(filePath) +{ + +} + +void BakeAssetTask::run() { + _isBaking.store(true); + + qRegisterMetaType >("QVector"); + TextureBakerThreadGetter fn = []() -> QThread* { return QThread::currentThread(); }; + + if (_assetPath.endsWith(".fbx")) { + _baker = std::unique_ptr { + new FBXBaker(QUrl("file:///" + _filePath), fn, PathUtils::generateTemporaryDir()) + }; + } else { + _baker = std::unique_ptr { + new TextureBaker(QUrl("file:///" + _filePath), image::TextureUsage::CUBE_TEXTURE, + PathUtils::generateTemporaryDir()) + }; + } + + QEventLoop loop; + connect(_baker.get(), &Baker::finished, &loop, &QEventLoop::quit); + connect(_baker.get(), &Baker::aborted, &loop, &QEventLoop::quit); + QMetaObject::invokeMethod(_baker.get(), "bake", Qt::QueuedConnection); + loop.exec(); + + if (_baker->wasAborted()) { + qDebug() << "Aborted baking: " << _assetHash << _assetPath; + + _wasAborted.store(true); + + emit bakeAborted(_assetHash, _assetPath); + } else if (_baker->hasErrors()) { + qDebug() << "Failed to bake: " << _assetHash << _assetPath << _baker->getErrors(); + + auto errors = _baker->getErrors().join('\n'); // Join error list into a single string for convenience + + _didFinish.store(true); + + emit bakeFailed(_assetHash, _assetPath, errors); + } else { + auto vectorOutputFiles = QVector::fromStdVector(_baker->getOutputFiles()); + + qDebug() << "Finished baking: " << _assetHash << _assetPath << vectorOutputFiles; + + _didFinish.store(true); + + emit bakeComplete(_assetHash, _assetPath, vectorOutputFiles); + } +} + +void BakeAssetTask::abort() { + if (_baker) { + _baker->abort(); + } +} diff --git a/assignment-client/src/assets/BakeAssetTask.h b/assignment-client/src/assets/BakeAssetTask.h new file mode 100644 index 0000000000..45e7ec8702 --- /dev/null +++ b/assignment-client/src/assets/BakeAssetTask.h @@ -0,0 +1,52 @@ +// +// BakeAssetTask.h +// assignment-client/src/assets +// +// Created by Stephen Birarda on 9/18/17. +// Copyright 2017 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_BakeAssetTask_h +#define hifi_BakeAssetTask_h + +#include + +#include +#include +#include + +#include +#include + +class BakeAssetTask : public QObject, public QRunnable { + Q_OBJECT +public: + BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath); + + bool isBaking() { return _isBaking.load(); } + + void run() override; + + void abort(); + bool wasAborted() const { return _wasAborted.load(); } + bool didFinish() const { return _didFinish.load(); } + +signals: + void bakeComplete(QString assetHash, QString assetPath, QVector outputFiles); + void bakeFailed(QString assetHash, QString assetPath, QString errors); + void bakeAborted(QString assetHash, QString assetPath); + +private: + std::atomic _isBaking { false }; + AssetHash _assetHash; + AssetPath _assetPath; + QString _filePath; + std::unique_ptr _baker; + std::atomic _wasAborted { false }; + std::atomic _didFinish { false }; +}; + +#endif // hifi_BakeAssetTask_h diff --git a/cmake/externals/draco/CMakeLists.txt b/cmake/externals/draco/CMakeLists.txt new file mode 100644 index 0000000000..44ddd6d3de --- /dev/null +++ b/cmake/externals/draco/CMakeLists.txt @@ -0,0 +1,40 @@ +set(EXTERNAL_NAME draco) + +if (ANDROID) + set(ANDROID_CMAKE_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" "-DANDROID_NATIVE_API_LEVEL=19") +endif () + +if (APPLE) + set(EXTRA_CMAKE_FLAGS -DCMAKE_CXX_FLAGS=-stdlib=libc++ -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++) +endif () + +include(ExternalProject) +ExternalProject_Add( + ${EXTERNAL_NAME} + URL http://hifi-public.s3.amazonaws.com/dependencies/draco-1.1.0.zip + URL_MD5 208f8b04c91d5f1c73d731a3ea37c5bb + CONFIGURE_COMMAND CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH= ${EXTRA_CMAKE_FLAGS} + LOG_DOWNLOAD 1 + LOG_CONFIGURE 1 + LOG_BUILD 1 +) + +# Hide this external target (for ide users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + +ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "List of Draco include directories") + +if (UNIX) + set(LIB_PREFIX "lib") + set(LIB_EXT "a") +elseif (WIN32) + set(LIB_EXT "lib") +endif () + +set(${EXTERNAL_NAME_UPPER}_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}draco.${LIB_EXT} CACHE FILEPATH "Path to Draco release library") +set(${EXTERNAL_NAME_UPPER}_ENCODER_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}dracoenc.${LIB_EXT} CACHE FILEPATH "Path to Draco encoder release library") +set(${EXTERNAL_NAME_UPPER}_DECODER_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}dracodec.${LIB_EXT} CACHE FILEPATH "Path to Draco decoder release library") diff --git a/cmake/modules/FindDraco.cmake b/cmake/modules/FindDraco.cmake new file mode 100644 index 0000000000..342797b62e --- /dev/null +++ b/cmake/modules/FindDraco.cmake @@ -0,0 +1,30 @@ +# +# FindDraco.cmake +# +# Try to find Draco libraries and include path. +# Once done this will define +# +# DRACO_FOUND +# DRACO_INCLUDE_DIRS +# DRACO_LIBRARY +# DRACO_ENCODER_LIBRARY +# DRACO_DECODER_LIBRARY +# +# Created on 8/8/2017 by Stephen Birarda +# Copyright 2017 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("${MACRO_DIR}/HifiLibrarySearchHints.cmake") +hifi_library_search_hints("draco") + +find_path(DRACO_INCLUDE_DIRS draco/core/draco_types.h PATH_SUFFIXES include/draco/src include HINTS ${DRACO_SEARCH_DIRS}) + +find_library(DRACO_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS}) +find_library(DRACO_ENCODER_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS}) +find_library(DRACO_DECODER_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(DRACO DEFAULT_MSG DRACO_INCLUDE_DIRS DRACO_LIBRARY DRACO_ENCODER_LIBRARY DRACO_DECODER_LIBRARY) diff --git a/cmake/modules/FindFBX.cmake b/cmake/modules/FindFBX.cmake deleted file mode 100644 index 9a1d08a010..0000000000 --- a/cmake/modules/FindFBX.cmake +++ /dev/null @@ -1,115 +0,0 @@ -# Locate the FBX SDK -# -# Defines the following variables: -# -# FBX_FOUND - Found the FBX SDK -# FBX_VERSION - Version number -# FBX_INCLUDE_DIRS - Include directories -# FBX_LIBRARIES - The libraries to link to -# -# Accepts the following variables as input: -# -# FBX_VERSION - as a CMake variable, e.g. 2017.0.1 -# FBX_ROOT - (as a CMake or environment variable) -# The root directory of the FBX SDK install - -# adapted from https://github.com/ufz-vislab/VtkFbxConverter/blob/master/FindFBX.cmake -# which uses the MIT license (https://github.com/ufz-vislab/VtkFbxConverter/blob/master/LICENSE.txt) - -if (NOT FBX_VERSION) - if (WIN32) - set(FBX_VERSION 2017.1) - else() - set(FBX_VERSION 2017.0.1) - endif() -endif() - -string(REGEX REPLACE "^([0-9]+).*$" "\\1" FBX_VERSION_MAJOR "${FBX_VERSION}") -string(REGEX REPLACE "^[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_MINOR "${FBX_VERSION}") -string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_PATCH "${FBX_VERSION}") - -set(FBX_MAC_LOCATIONS "/Applications/Autodesk/FBX\ SDK/${FBX_VERSION}") -set(FBX_LINUX_LOCATIONS "/usr/local/lib/gcc4/x64/debug/") - -if (WIN32) - string(REGEX REPLACE "\\\\" "/" WIN_PROGRAM_FILES_X64_DIRECTORY $ENV{ProgramW6432}) -endif() - -set(FBX_WIN_LOCATIONS "${WIN_PROGRAM_FILES_X64_DIRECTORY}/Autodesk/FBX/FBX SDK/${FBX_VERSION}") - -set(FBX_SEARCH_LOCATIONS $ENV{FBX_ROOT} ${FBX_ROOT} ${FBX_MAC_LOCATIONS} ${FBX_WIN_LOCATIONS} ${FBX_LINUX_LOCATIONS}) - -function(_fbx_append_debugs _endvar _library) - if (${_library} AND ${_library}_DEBUG) - set(_output optimized ${${_library}} debug ${${_library}_DEBUG}) - else() - set(_output ${${_library}}) - endif() - - set(${_endvar} ${_output} PARENT_SCOPE) -endfunction() - -if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") - set(fbx_compiler clang) -elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") - set(fbx_compiler gcc4) -endif() - -function(_fbx_find_library _name _lib _suffix) - if (MSVC_VERSION EQUAL 1910) - set(VS_PREFIX vs2017) - elseif (MSVC_VERSION EQUAL 1900) - set(VS_PREFIX vs2015) - elseif (MSVC_VERSION EQUAL 1800) - set(VS_PREFIX vs2013) - elseif (MSVC_VERSION EQUAL 1700) - set(VS_PREFIX vs2012) - elseif (MSVC_VERSION EQUAL 1600) - set(VS_PREFIX vs2010) - elseif (MSVC_VERSION EQUAL 1500) - set(VS_PREFIX vs2008) - endif() - - find_library(${_name} - NAMES ${_lib} - HINTS ${FBX_SEARCH_LOCATIONS} - PATH_SUFFIXES lib/${fbx_compiler}/${_suffix} lib/${fbx_compiler}/ub/${_suffix} lib/${VS_PREFIX}/x64/${_suffix} - ) - - mark_as_advanced(${_name}) -endfunction() - -find_path(FBX_INCLUDE_DIR fbxsdk.h - PATHS ${FBX_SEARCH_LOCATIONS} - PATH_SUFFIXES include -) -mark_as_advanced(FBX_INCLUDE_DIR) - -if (WIN32) - _fbx_find_library(FBX_LIBRARY libfbxsdk-md release) - _fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk-md debug) -elseif (APPLE) - find_library(CARBON NAMES Carbon) - find_library(SYSTEM_CONFIGURATION NAMES SystemConfiguration) - _fbx_find_library(FBX_LIBRARY libfbxsdk.a release) - _fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk.a debug) -else () - _fbx_find_library(FBX_LIBRARY libfbxsdk.a release) -endif() - -include(FindPackageHandleStandardArgs) -FIND_PACKAGE_HANDLE_STANDARD_ARGS(FBX DEFAULT_MSG FBX_LIBRARY FBX_INCLUDE_DIR) - -if (FBX_FOUND) - set(FBX_INCLUDE_DIRS ${FBX_INCLUDE_DIR}) - _fbx_append_debugs(FBX_LIBRARIES FBX_LIBRARY) - add_definitions(-DFBXSDK_NEW_API) - - if (WIN32) - add_definitions(-DK_PLUGIN -DK_FBXSDK -DK_NODLL) - set(CMAKE_EXE_LINKER_FLAGS /NODEFAULTLIB:\"LIBCMT\") - set(FBX_LIBRARIES ${FBX_LIBRARIES} Wininet.lib) - elseif (APPLE) - set(FBX_LIBRARIES ${FBX_LIBRARIES} ${CARBON} ${SYSTEM_CONFIGURATION}) - endif() -endif() diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 5eedbb06ed..8abb202bd4 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -836,6 +836,9 @@ Section "-Core installation" Delete "$INSTDIR\ui_resources_200_percent.pak" Delete "$INSTDIR\vccorlib120.dll" Delete "$INSTDIR\version" + Delete "$INSTDIR\msvcr140.dll" + Delete "$INSTDIR\msvcp140.dll" + Delete "$INSTDIR\vcruntime140.dll" Delete "$INSTDIR\xinput1_3.dll" ; Delete old desktop shortcuts before they were renamed during Sandbox rename diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 620d593ebc..dbbcc004ca 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -272,22 +272,22 @@ void DomainGatekeeper::updateNodePermissions() { userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; } else { - // this node is an agent - const QHostAddress& addr = node->getLocalSocket().getAddress(); - bool isLocalUser = (addr == limitedNodeList->getLocalSockAddr().getAddress() || - addr == QHostAddress::LocalHost); - // at this point we don't have a sending socket for packets from this node - assume it is the active socket // or the public socket if we haven't activated a socket for the node yet HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket(); QString hardwareAddress; QUuid machineFingerprint; + bool isLocalUser { false }; DomainServerNodeData* nodeData = static_cast(node->getLinkedData()); if (nodeData) { hardwareAddress = nodeData->getHardwareAddress(); machineFingerprint = nodeData->getMachineFingerprint(); + + auto sendingAddress = nodeData->getSendingSockAddr().getAddress(); + isLocalUser = (sendingAddress == limitedNodeList->getLocalSockAddr().getAddress() || + sendingAddress == QHostAddress::LocalHost); } userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress, machineFingerprint); diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 0d5c095585..28f15605e0 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -25,7 +25,7 @@ }, { "from": "Standard.RX", - "when": [ "Application.InHMD", "Application.SnapTurn" ], + "when": [ "Application.SnapTurn" ], "to": "Actions.StepYaw", "filters": [ @@ -128,4 +128,4 @@ { "from": "Standard.TrackedObject14", "to" : "Actions.TrackedObject14" }, { "from": "Standard.TrackedObject15", "to" : "Actions.TrackedObject15" } ] -} +} \ No newline at end of file diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 8733349227..558562cec5 100644 Binary files a/interface/resources/fonts/hifi-glyphs.ttf and b/interface/resources/fonts/hifi-glyphs.ttf differ diff --git a/interface/resources/fonts/hifi-glyphs/icons-reference.html b/interface/resources/fonts/hifi-glyphs/icons-reference.html new file mode 100644 index 0000000000..99e826e0b9 --- /dev/null +++ b/interface/resources/fonts/hifi-glyphs/icons-reference.html @@ -0,0 +1,1056 @@ + + + + + + + Font Reference - HiFi Glyphs + + + + + +
+

HiFi Glyphs

+

This font was created inHigh Fidelity

+

CSS mapping

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • + + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • + + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
+

Character mapping

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
+
+ + + diff --git a/interface/resources/fonts/hifi-glyphs/styles.css b/interface/resources/fonts/hifi-glyphs/styles.css new file mode 100644 index 0000000000..66277740fc --- /dev/null +++ b/interface/resources/fonts/hifi-glyphs/styles.css @@ -0,0 +1,421 @@ +@charset "UTF-8"; + +@font-face { + font-family: "hifi-glyphs"; + src:url("fonts/hifi-glyphs.eot"); + src:url("fonts/hifi-glyphs.eot?#iefix") format("embedded-opentype"), + url("fonts/hifi-glyphs.woff") format("woff"), + url("fonts/hifi-glyphs.ttf") format("truetype"), + url("fonts/hifi-glyphs.svg#hifi-glyphs") format("svg"); + font-weight: normal; + font-style: normal; + +} + +[data-icon]:before { + font-family: "hifi-glyphs" !important; + content: attr(data-icon); + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + speak: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +[class^="icon-"]:before, +[class*=" icon-"]:before { + font-family: "hifi-glyphs" !important; + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + speak: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-hmd:before { + content: "\62"; +} +.icon-2d-screen:before { + content: "\63"; +} +.icon-keyboard:before { + content: "\64"; +} +.icon-hand-controllers:before { + content: "\65"; +} +.icon-headphones-mic:before { + content: "\66"; +} +.icon-gamepad:before { + content: "\67"; +} +.icon-headphones:before { + content: "\68"; +} +.icon-mic:before { + content: "\69"; +} +.icon-upload:before { + content: "\6a"; +} +.icon-script:before { + content: "\6b"; +} +.icon-text:before { + content: "\6c"; +} +.icon-cube:before { + content: "\6d"; +} +.icon-sphere:before { + content: "\6e"; +} +.icon-zone:before { + content: "\6f"; +} +.icon-light:before { + content: "\70"; +} +.icon-web:before { + content: "\71"; +} +.icon-web-2:before { + content: "\72"; +} +.icon-edit:before { + content: "\73"; +} +.icon-market:before { + content: "\74"; +} +.icon-directory:before { + content: "\75"; +} +.icon-menu:before { + content: "\76"; +} +.icon-close:before { + content: "\77"; +} +.icon-close-inverted:before { + content: "\78"; +} +.icon-pin:before { + content: "\79"; +} +.icon-pin-inverted:before { + content: "\7a"; +} +.icon-resize-handle:before { + content: "\41"; +} +.icon-diclosure-expand:before { + content: "\42"; +} +.icon-reload-small:before { + content: "\61"; +} +.icon-close-small:before { + content: "\43"; +} +.icon-backward:before { + content: "\45"; +} +.icon-reload:before { + content: "\46"; +} +.icon-minimize:before { + content: "\49"; +} +.icon-maximize:before { + content: "\4a"; +} +.icon-maximize-inverted:before { + content: "\4b"; +} +.icon-disclosure-button-expand:before { + content: "\4c"; +} +.icon-disclosure-button-collapse:before { + content: "\4d"; +} +.icon-script-stop:before { + content: "\4e"; +} +.icon-script-reload:before { + content: "\4f"; +} +.icon-script-run:before { + content: "\50"; +} +.icon-script-new:before { + content: "\51"; +} +.icon-hifi-forum:before { + content: "\32"; +} +.icon-hifi-logo-small:before { + content: "\53"; +} +.icon-avatar-1:before { + content: "\54"; +} +.icon-placemark:before { + content: "\55"; +} +.icon-box:before { + content: "\56"; +} +.icon-community:before { + content: "\30"; +} +.icon-grab-handle:before { + content: "\58"; +} +.icon-search:before { + content: "\59"; +} +.icon-disclosure-collapse:before { + content: "\5a"; +} +.icon-script-upload:before { + content: "\52"; +} +.icon-code:before { + content: "\57"; +} +.icon-avatar:before { + content: "\3c"; +} +.icon-arrows-h:before { + content: "\3a"; +} +.icon-arrows-v:before { + content: "\3b"; +} +.icon-arrows:before { + content: "\60"; +} +.icon-compress:before { + content: "\21"; +} +.icon-expand:before { + content: "\22"; +} +.icon-placemark-1:before { + content: "\23"; +} +.icon-circle:before { + content: "\24"; +} +.icon-hand-pointer:before { + content: "\39"; +} +.icon-plus-square-o:before { + content: "\25"; +} +.icon-square:before { + content: "\27"; +} +.icon-align-center:before { + content: "\38"; +} +.icon-align-justify:before { + content: "\29"; +} +.icon-align-left:before { + content: "\2a"; +} +.icon-align-right:before { + content: "\5e"; +} +.icon-bars:before { + content: "\37"; +} +.icon-circle-slash:before { + content: "\2c"; +} +.icon-sync:before { + content: "\28"; +} +.icon-key:before { + content: "\2d"; +} +.icon-link:before { + content: "\2e"; +} +.icon-location:before { + content: "\2f"; +} +.icon-carat-r:before { + content: "\33"; +} +.icon-carat-l:before { + content: "\34"; +} +.icon-folder-lg:before { + content: "\3e"; +} +.icon-folder-sm:before { + content: "\3f"; +} +.icon-level-up:before { + content: "\31"; +} +.icon-info:before { + content: "\5b"; +} +.icon-question:before { + content: "\5d"; +} +.icon-alert:before { + content: "\2b"; +} +.icon-home:before { + content: "\5f"; +} +.icon-error:before { + content: "\3d"; +} +.icon-settings:before { + content: "\40"; +} +.icon-trash:before { + content: "\7b"; +} +.icon-object-group:before { + content: "\e000"; +} +.icon-cm:before { + content: "\7d"; +} +.icon-msvg:before { + content: "\7e"; +} +.icon-deg:before { + content: "\5c"; +} +.icon-px:before { + content: "\7c"; +} +.icon-m-sq:before { + content: "\e001"; +} +.icon-m-cubed:before { + content: "\e002"; +} +.icon-acceleration:before { + content: "\e003"; +} +.icon-particles:before { + content: "\e004"; +} +.icon-voxels:before { + content: "\e005"; +} +.icon-lock:before { + content: "\e006"; +} +.icon-visible:before { + content: "\e007"; +} +.icon-model:before { + content: "\e008"; +} +.icon-forward:before { + content: "\44"; +} +.icon-avatar-2:before { + content: "\e009"; +} +.icon-arrow-dn:before { + content: "\35"; +} +.icon-arrow-up:before { + content: "\36"; +} +.icon-time:before { + content: "\e00a"; +} +.icon-transparency:before { + content: "\e00b"; +} +.icon-unmuted:before { + content: "\47"; +} +.icon-user:before { + content: "\e00c"; +} +.icon-edit-pencil:before { + content: "\e00d"; +} +.icon-muted:before { + content: "\48"; +} +.icon-vol-0:before { + content: "\e00e"; +} +.icon-vol-1:before { + content: "\e00f"; +} +.icon-vol-2:before { + content: "\e010"; +} +.icon-vol-3:before { + content: "\e011"; +} +.icon-vol-4:before { + content: "\e012"; +} +.icon-vol-x-0:before { + content: "\e013"; +} +.icon-vol-x-1:before { + content: "\e014"; +} +.icon-vol-x-2:before { + content: "\e015"; +} +.icon-vol-x-3:before { + content: "\e016"; +} +.icon-vol-x-4:before { + content: "\e017"; +} +.icon-share-ext:before { + content: "\e018"; +} +.icon-ellipsis:before { + content: "\e019"; +} +.icon-check:before { + content: "\e01a"; +} +.icon-sliders:before { + content: "\26"; +} +.icon-polyline:before { + content: "\e01b"; +} +.icon-source:before { + content: "\e01c"; +} +.icon-playback-play:before { + content: "\e01d"; +} +.icon-stop-square:before { + content: "\e01e"; +} +.icon-avatar-t-pose:before { + content: "\e01f"; +} +.icon-check-2-01:before { + content: "\e020"; +} diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index ee37dbd8db..649ea49153 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -10,6 +10,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 @@ -48,8 +49,14 @@ ScrollingWindow { Component.onCompleted: { ApplicationInterface.uploadRequest.connect(uploadClicked); assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); + assetMappingsModel.autoRefreshEnabled = true; + reload(); } + + Component.onDestruction: { + assetMappingsModel.autoRefreshEnabled = false; + } function doDeleteFile(path) { console.log("Deleting " + path); @@ -70,11 +77,11 @@ ScrollingWindow { function doRenameFile(oldPath, newPath) { - if (newPath[0] != "/") { + if (newPath[0] !== "/") { newPath = "/" + newPath; } - if (oldPath[oldPath.length - 1] == '/' && newPath[newPath.length - 1] != '/') { + if (oldPath[oldPath.length - 1] === '/' && newPath[newPath.length - 1] != '/') { // this is a folder rename but the user neglected to add a trailing slash when providing a new path newPath = newPath + "/"; } @@ -144,7 +151,6 @@ ScrollingWindow { function reload() { Assets.mappingModel.refresh(); - treeView.selection.clear(); } function handleGetMappingsError(errorString) { @@ -300,7 +306,7 @@ ScrollingWindow { object.selected.connect(function(destinationPath) { destinationPath = destinationPath.trim(); - if (path == destinationPath) { + if (path === destinationPath) { return; } if (fileExists(destinationPath)) { @@ -361,7 +367,7 @@ ScrollingWindow { running: false } - property var uploadOpen: false; + property bool uploadOpen: false; Timer { id: timer } @@ -464,20 +470,10 @@ ScrollingWindow { HifiControls.VerticalSpacer {} Row { - id: buttonRow anchors.left: parent.left anchors.right: parent.right spacing: hifi.dimensions.contentSpacing.x - HifiControls.GlyphButton { - glyph: hifi.glyphs.reload - color: hifi.buttons.black - colorScheme: root.colorScheme - width: hifi.dimensions.controlLineHeight - - onClicked: root.reload() - } - HifiControls.Button { text: "Add To World" color: hifi.buttons.black @@ -511,7 +507,182 @@ ScrollingWindow { enabled: treeView.selection.hasSelection } } + } + HifiControls.Tree { + id: treeView + anchors.top: assetDirectory.bottom + anchors.bottom: infoRow.top + anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border + anchors.left: parent.left + anchors.right: parent.right + + treeModel: assetProxyModel + selectionMode: SelectionMode.ExtendedSelection + headerVisible: true + sortIndicatorVisible: true + + colorScheme: root.colorScheme + + modifyEl: renameEl + + TableViewColumn { + id: nameColumn + title: "Name:" + role: "name" + width: treeView.width - bakedColumn.width; + } + TableViewColumn { + id: bakedColumn + title: "Use Baked?" + role: "baked" + width: 100 + } + + itemDelegate: Loader { + id: itemDelegateLoader + + anchors { + left: parent ? parent.left : undefined + leftMargin: (styleData.column === 0 ? (2 + styleData.depth) : 1) * hifi.dimensions.tablePadding + right: parent ? parent.right : undefined + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent ? parent.verticalCenter : undefined + } + + function convertToGlyph(text) { + switch (text) { + case "Not Baked": + return hifi.glyphs.circleSlash; + case "Baked": + return hifi.glyphs.check_2_01; + case "Error": + return hifi.glyphs.alert; + default: + return ""; + } + } + + function getComponent() { + if ((styleData.column === 0) && styleData.selected) { + return textFieldComponent; + } else if (convertToGlyph(styleData.value) != "") { + return glyphComponent; + } else { + return labelComponent; + } + + } + sourceComponent: getComponent() + + Component { + id: labelComponent + FiraSansSemiBold { + text: styleData.value + size: hifi.fontSizes.tableText + color: colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + + elide: Text.ElideRight + horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft + } + } + Component { + id: glyphComponent + + HiFiGlyphs { + text: convertToGlyph(styleData.value) + size: hifi.dimensions.frameIconSize + color: colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + + elide: Text.ElideRight + horizontalAlignment: TextInput.AlignHCenter + + HifiControls.ToolTip { + anchors.fill: parent + + visible: styleData.value === "Error" + + toolTip: assetProxyModel.data(styleData.index, 0x106) + } + } + } + Component { + id: textFieldComponent + + TextField { + id: textField + readOnly: !activeFocus + + text: styleData.value + + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + font.family: firaSansSemiBold.name + font.pixelSize: hifi.fontSizes.textFieldInput + height: hifi.dimensions.tableRowHeight + + style: TextFieldStyle { + textColor: readOnly + ? hifi.colors.black + : (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white) + background: Rectangle { + visible: !readOnly + color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black + border.color: hifi.colors.primaryHighlight + border.width: 1 + } + selectedTextColor: hifi.colors.black + selectionColor: hifi.colors.primaryHighlight + padding.left: readOnly ? 0 : hifi.dimensions.textPadding + padding.right: readOnly ? 0 : hifi.dimensions.textPadding + } + + validator: RegExpValidator { + regExp: /[^/]+/ + } + + Keys.onPressed: { + if (event.key == Qt.Key_Escape) { + text = styleData.value; + unfocusHelper.forceActiveFocus(); + event.accepted = true; + } + } + onAccepted: { + if (acceptableInput && styleData.selected) { + if (!treeView.modifyEl(treeView.selection.currentIndex, text)) { + text = styleData.value; + } + unfocusHelper.forceActiveFocus(); + } + } + + onReadOnlyChanged: { + // Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time. + keyboardRaised = activeFocus; + } + } + } + } + + + MouseArea { + propagateComposedEvents: true + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: { + if (!HMD.active) { // Popup only displays properly on desktop + var index = treeView.indexAt(mouse.x, mouse.y); + treeView.selection.setCurrentIndex(index, 0x0002); + contextMenu.currentIndex = index; + contextMenu.popup(); + } + } + } + Menu { id: contextMenu title: "Edit" @@ -541,36 +712,49 @@ ScrollingWindow { } } - HifiControls.Tree { - id: treeView - anchors.top: assetDirectory.bottom + Row { + id: infoRow + anchors.left: treeView.left + anchors.right: treeView.right anchors.bottom: uploadSection.top - anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border - anchors.left: parent.left - anchors.right: parent.right - - treeModel: assetProxyModel - canEdit: true - colorScheme: root.colorScheme - selectionMode: SelectionMode.ExtendedSelection - - modifyEl: renameEl - - MouseArea { - propagateComposedEvents: true - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: { - if (!HMD.active) { // Popup only displays properly on desktop - var index = treeView.indexAt(mouse.x, mouse.y); - treeView.selection.setCurrentIndex(index, 0x0002); - contextMenu.currentIndex = index; - contextMenu.popup(); - } - } + anchors.bottomMargin: hifi.dimensions.contentSpacing.y + spacing: hifi.dimensions.contentSpacing.x + + RalewayRegular { + size: hifi.fontSizes.sectionName + font.capitalization: Font.AllUppercase + text: selectedItems + " items selected" + color: hifi.colors.lightGrayText } + HifiControls.CheckBox { + function isChecked() { + var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + var bakingDisabled = (status === "Not Baked" || status === "--"); + return selectedItems === 1 && !bakingDisabled; + } + + text: "Use baked (optimized) versions" + colorScheme: root.colorScheme + enabled: selectedItems === 1 && assetProxyModel.data(treeView.selection.currentIndex, 0x105) !== "--" + checked: isChecked() + onClicked: { + var mappings = []; + for (var i in treeView.selection.selectedIndexes) { + var index = treeView.selection.selectedIndexes[i]; + var path = assetProxyModel.data(index, 0x100); + mappings.push(path); + } + print("Setting baking enabled:" + mappings + " " + checked); + Assets.setBakingEnabled(mappings, checked, function() { + reload(); + }); + + checked = Qt.binding(isChecked); + } + } } + HifiControls.ContentSection { id: uploadSection name: "Upload A File" diff --git a/interface/resources/qml/controls-uit/CheckBoxQQC2.qml b/interface/resources/qml/controls-uit/CheckBoxQQC2.qml new file mode 100644 index 0000000000..92bad04d01 --- /dev/null +++ b/interface/resources/qml/controls-uit/CheckBoxQQC2.qml @@ -0,0 +1,113 @@ +// +// CheckBox2.qml +// +// Created by Vlad Stelmahovsky on 10 Aug 2017 +// Copyright 2017 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 +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +import "../styles-uit" +import "../controls-uit" as HiFiControls + +CheckBox { + id: checkBox + + HifiConstants { id: hifi; } + + padding: 0 + leftPadding: 0 + property int colorScheme: hifi.colorSchemes.light + property string color: hifi.colors.lightGrayText + readonly property bool isLightColorScheme: colorScheme === hifi.colorSchemes.light + property bool isRedCheck: false + property bool isRound: false + property int boxSize: 14 + property int boxRadius: isRound ? boxSize : 3 + property bool wrap: true; + readonly property int checkSize: Math.max(boxSize - 8, 10) + readonly property int checkRadius: isRound ? checkSize / 2 : 2 + focusPolicy: Qt.ClickFocus + + indicator: Rectangle { + id: box + implicitWidth: boxSize + implicitHeight: boxSize + radius: boxRadius + x: checkBox.leftPadding + y: parent.height / 2 - height / 2 + border.width: 1 + border.color: pressed || hovered + ? hifi.colors.checkboxCheckedBorder + : (checkBox.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish) + + gradient: Gradient { + GradientStop { + position: 0.2 + color: pressed || hovered + ? (checkBox.isLightColorScheme ? hifi.colors.checkboxChecked : hifi.colors.checkboxLightStart) + : (checkBox.isLightColorScheme ? hifi.colors.checkboxLightStart : hifi.colors.checkboxDarkStart) + } + GradientStop { + position: 1.0 + color: pressed || hovered + ? (checkBox.isLightColorScheme ? hifi.colors.checkboxChecked : hifi.colors.checkboxLightFinish) + : (checkBox.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish) + } + } + + Rectangle { + visible: pressed || hovered + anchors.centerIn: parent + id: innerBox + width: checkSize - 4 + height: width + radius: checkRadius + color: hifi.colors.checkboxCheckedBorder + } + + Rectangle { + id: check + width: checkSize + height: checkSize + radius: checkRadius + anchors.centerIn: parent + color: isRedCheck ? hifi.colors.checkboxCheckedRed : hifi.colors.checkboxChecked + border.width: 2 + border.color: isRedCheck? hifi.colors.checkboxCheckedBorderRed : hifi.colors.checkboxCheckedBorder + visible: checked && !pressed || !checked && pressed + } + + Rectangle { + id: disabledOverlay + visible: !enabled + width: boxSize + height: boxSize + radius: boxRadius + border.width: 1 + border.color: hifi.colors.baseGrayHighlight + color: hifi.colors.baseGrayHighlight + opacity: 0.5 + } + } + + contentItem: Text { + id: root + FontLoader { id: ralewaySemiBold; source: pathToFonts + "fonts/Raleway-SemiBold.ttf"; } + font.pixelSize: hifi.fontSizes.inputLabel + font.family: ralewaySemiBold.name + text: checkBox.text + color: checkBox.color + x: 2 + wrapMode: checkBox.wrap ? Text.Wrap : Text.NoWrap + elide: checkBox.wrap ? Text.ElideNone : Text.ElideRight + enabled: checkBox.enabled + verticalAlignment: Text.AlignVCenter + leftPadding: checkBox.indicator.width + checkBox.spacing + } +} + diff --git a/interface/resources/qml/controls-uit/Key.qml b/interface/resources/qml/controls-uit/Key.qml index 2218474936..0c888d1a0a 100644 --- a/interface/resources/qml/controls-uit/Key.qml +++ b/interface/resources/qml/controls-uit/Key.qml @@ -34,7 +34,10 @@ Item { onClicked: { mouse.accepted = true; + webEntity.synthesizeKeyPress(glyph); + webEntity.synthesizeKeyPress(glyph, mirrorText); + if (toggle) { toggled = !toggled; } diff --git a/interface/resources/qml/controls-uit/Keyboard.qml b/interface/resources/qml/controls-uit/Keyboard.qml index 07488fe3e6..4739534fcd 100644 --- a/interface/resources/qml/controls-uit/Keyboard.qml +++ b/interface/resources/qml/controls-uit/Keyboard.qml @@ -9,6 +9,7 @@ // import QtQuick 2.0 +import "." Item { id: keyboardBase @@ -16,9 +17,15 @@ Item { property bool raised: false property bool numeric: false + readonly property int keyboardRowHeight: 50 + readonly property int keyboardWidth: 480 + + readonly property int mirrorTextHeight: keyboardRowHeight + + property bool showMirrorText: true readonly property int raisedHeight: 200 - height: enabled && raised ? raisedHeight : 0 + height: enabled && raised ? raisedHeight + (showMirrorText ? keyboardRowHeight : 0) : 0 visible: enabled && raised property bool shiftMode: false @@ -93,24 +100,35 @@ Item { } Rectangle { - id: leftRect y: 0 - height: 200 + x: 0 + height: showMirrorText ? mirrorTextHeight : 0 + width: keyboardWidth color: "#252525" - anchors.right: keyboardRect.left - anchors.rightMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 + + TextEdit { + id: mirrorText + visible: showMirrorText + size: 13.5 + horizontalAlignment: Text.AlignHCenter + color: "#FFFFFF"; + anchors.fill: parent + wrapMode: Text.WordWrap + readOnly: false // we need to leave this property read-only to allow control to accept QKeyEvent + selectByMouse: false + } + + MouseArea { // ... and we need this mouse area to prevent mirrorText from getting mouse events to ensure it will never get focus + anchors.fill: parent + } } Rectangle { id: keyboardRect - x: 206 - y: 0 - width: 480 - height: 200 + x: 0 + y: showMirrorText ? mirrorTextHeight : 0 + width: keyboardWidth + height: raisedHeight color: "#252525" anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom @@ -118,13 +136,13 @@ Item { Column { id: columnAlpha - width: 480 - height: 200 + width: keyboardWidth + height: raisedHeight visible: !numeric Row { - width: 480 - height: 50 + width: keyboardWidth + height: keyboardRowHeight anchors.left: parent.left anchors.leftMargin: 4 @@ -142,8 +160,8 @@ Item { } Row { - width: 480 - height: 50 + width: keyboardWidth + height: keyboardRowHeight anchors.left: parent.left anchors.leftMargin: 20 @@ -160,8 +178,8 @@ Item { } Row { - width: 480 - height: 50 + width: keyboardWidth + height: keyboardRowHeight anchors.left: parent.left anchors.leftMargin: 4 @@ -185,8 +203,8 @@ Item { } Row { - width: 480 - height: 50 + width: keyboardWidth + height: keyboardRowHeight anchors.left: parent.left anchors.leftMargin: 4 @@ -205,13 +223,13 @@ Item { Column { id: columnNumeric - width: 480 - height: 200 + width: keyboardWidth + height: raisedHeight visible: numeric Row { - width: 480 - height: 50 + width: keyboardWidth + height: keyboardRowHeight anchors.left: parent.left anchors.leftMargin: 4 @@ -229,8 +247,8 @@ Item { } Row { - width: 480 - height: 50 + width: keyboardWidth + height: keyboardRowHeight anchors.left: parent.left anchors.leftMargin: 4 @@ -248,8 +266,8 @@ Item { } Row { - width: 480 - height: 50 + width: keyboardWidth + height: keyboardRowHeight anchors.left: parent.left anchors.leftMargin: 4 @@ -273,8 +291,8 @@ Item { } Row { - width: 480 - height: 50 + width: keyboardWidth + height: keyboardRowHeight anchors.left: parent.left anchors.leftMargin: 4 @@ -291,31 +309,4 @@ Item { } } } - - Rectangle { - id: rightRect - y: 280 - height: 200 - color: "#252525" - border.width: 0 - anchors.left: keyboardRect.right - anchors.leftMargin: 0 - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - } - - Rectangle { - id: rectangle1 - color: "#ffffff" - anchors.bottom: keyboardRect.top - anchors.bottomMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.top: parent.top - anchors.topMargin: 0 - } } diff --git a/interface/resources/qml/controls-uit/ToolTip.qml b/interface/resources/qml/controls-uit/ToolTip.qml new file mode 100644 index 0000000000..4fe36adcd5 --- /dev/null +++ b/interface/resources/qml/controls-uit/ToolTip.qml @@ -0,0 +1,51 @@ +// +// ToolTip.qml +// +// Created by Clement on 9/12/17 +// Copyright 2017 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 +// + +import QtQuick 2.5 + +Item { + property string toolTip + property bool showToolTip: false + + Rectangle { + id: toolTipRectangle + anchors.right: parent.right + + width: toolTipText.width + 4 + height: toolTipText.height + 4 + opacity: (toolTip != "" && showToolTip) ? 1 : 0 + color: "#ffffaa" + border.color: "#0a0a0a" + Text { + id: toolTipText + text: toolTip + color: "black" + anchors.centerIn: parent + } + Behavior on opacity { + PropertyAnimation { + easing.type: Easing.InOutQuad + duration: 250 + } + } + } + MouseArea { + id: mouseArea + anchors.fill: parent + onEntered: showTimer.start() + onExited: { showToolTip = false; showTimer.stop(); } + hoverEnabled: true + } + Timer { + id: showTimer + interval: 250 + onTriggered: { showToolTip = true; } + } +} \ No newline at end of file diff --git a/interface/resources/qml/controls-uit/Tree.qml b/interface/resources/qml/controls-uit/Tree.qml index 53f66fa67c..79d3b958ea 100644 --- a/interface/resources/qml/controls-uit/Tree.qml +++ b/interface/resources/qml/controls-uit/Tree.qml @@ -19,7 +19,7 @@ TreeView { id: treeView property var treeModel: ListModel { } - property var canEdit: false + property bool centerHeaderText: false property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light @@ -31,14 +31,9 @@ TreeView { model: treeModel } - TableViewColumn { - role: "display"; - } - anchors { left: parent.left; right: parent.right } - + headerVisible: false - headerDelegate: Item { } // Fix OSX QML bug that displays scrollbar starting too low. // Use rectangle to draw border with rounded corners. frameVisible: false @@ -61,6 +56,64 @@ TreeView { // Needed in order for rows to keep displaying rows after end of table entries. backgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightEven : hifi.colors.tableRowDarkEven alternateBackgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd + + headerDelegate: Rectangle { + height: hifi.dimensions.tableHeaderHeight + color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark + + RalewayRegular { + id: titleText + text: styleData.value + size: hifi.fontSizes.tableHeading + font.capitalization: Font.AllUppercase + color: hifi.colors.baseGrayHighlight + horizontalAlignment: (centerHeaderText ? Text.AlignHCenter : Text.AlignLeft) + elide: Text.ElideRight + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: sortIndicatorVisible && sortIndicatorColumn === styleData.column ? titleSort.left : parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + } + + HiFiGlyphs { + id: titleSort + text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn + color: isLightColorScheme ? hifi.colors.darkGray : hifi.colors.baseGrayHighlight + opacity: 0.6; + size: hifi.fontSizes.tableHeadingIcon + anchors { + right: parent.right + verticalCenter: titleText.verticalCenter + } + visible: sortIndicatorVisible && sortIndicatorColumn === styleData.column + } + + Rectangle { + width: 1 + anchors { + left: parent.left + top: parent.top + topMargin: 1 + bottom: parent.bottom + bottomMargin: 2 + } + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + visible: styleData.column > 0 + } + + Rectangle { + height: 1 + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + } + } branchDelegate: HiFiGlyphs { text: styleData.isExpanded ? hifi.glyphs.caratDn : hifi.glyphs.caratR @@ -76,28 +129,53 @@ TreeView { handle: Item { id: scrollbarHandle - implicitWidth: 6 + implicitWidth: hifi.dimensions.scrollbarHandleWidth Rectangle { anchors { fill: parent - leftMargin: 2 // Move it right - rightMargin: -2 // "" - topMargin: 3 // Shrink vertically + topMargin: treeView.headerVisible ? hifi.dimensions.tableHeaderHeight + 3 : 3 bottomMargin: 3 // "" + leftMargin: 1 // Move it right + rightMargin: -1 // "" } - radius: 3 - color: hifi.colors.tableScrollHandleDark + radius: hifi.dimensions.scrollbarHandleWidth / 2 + color: treeView.isLightColorScheme ? hifi.colors.tableScrollHandleLight : hifi.colors.tableScrollHandleDark } } scrollBarBackground: Item { - implicitWidth: 9 + implicitWidth: hifi.dimensions.scrollbarBackgroundWidth Rectangle { anchors { fill: parent + topMargin: treeView.headerVisible ? hifi.dimensions.tableHeaderHeight - 1 : -1 margins: -1 // Expand } - color: hifi.colors.tableBackgroundDark + color: treeView.isLightColorScheme ? hifi.colors.tableScrollBackgroundLight : hifi.colors.tableScrollBackgroundDark + + // Extend header color above scrollbar background + Rectangle { + anchors { + top: parent.top + topMargin: -hifi.dimensions.tableHeaderHeight + left: parent.left + right: parent.right + } + height: hifi.dimensions.tableHeaderHeight + color: treeView.isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark + visible: treeView.headerVisible + } + Rectangle { + // Extend header bottom border + anchors { + top: parent.top + left: parent.left + right: parent.right + } + height: 1 + color: treeView.isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + visible: treeView.headerVisible + } } } @@ -119,9 +197,7 @@ TreeView { : (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd) } - itemDelegate: Loader { - id: itemDelegateLoader - + itemDelegate: FiraSansSemiBold { anchors { left: parent ? parent.left : undefined leftMargin: (2 + styleData.depth) * hifi.dimensions.tablePadding @@ -130,83 +206,13 @@ TreeView { verticalCenter: parent ? parent.verticalCenter : undefined } - function getComponent() { - if (treeView.canEdit && styleData.selected) { - return textFieldComponent; - } else { - return labelComponent; - } - - } - sourceComponent: getComponent() - - Component { - id: labelComponent - FiraSansSemiBold { - - text: styleData.value - size: hifi.fontSizes.tableText - color: colorScheme == hifi.colorSchemes.light - ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) - : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) - } - } - Component { - id: textFieldComponent - - TextField { - id: textField - readOnly: !activeFocus - - text: styleData.value - - FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } - font.family: firaSansSemiBold.name - font.pixelSize: hifi.fontSizes.textFieldInput - height: hifi.dimensions.tableRowHeight - - style: TextFieldStyle { - textColor: readOnly - ? hifi.colors.black - : (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white) - background: Rectangle { - visible: !readOnly - color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black - border.color: hifi.colors.primaryHighlight - border.width: 1 - } - selectedTextColor: hifi.colors.black - selectionColor: hifi.colors.primaryHighlight - padding.left: readOnly ? 0 : hifi.dimensions.textPadding - padding.right: readOnly ? 0 : hifi.dimensions.textPadding - } - - validator: RegExpValidator { - regExp: /[^/]+/ - } - - Keys.onPressed: { - if (event.key == Qt.Key_Escape) { - text = styleData.value; - unfocusHelper.forceActiveFocus(); - event.accepted = true; - } - } - onAccepted: { - if (acceptableInput && styleData.selected) { - if (!modifyEl(selection.currentIndex, text)) { - text = styleData.value; - } - unfocusHelper.forceActiveFocus(); - } - } - - onReadOnlyChanged: { - // Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time. - keyboardRaised = activeFocus; - } - } - } + text: styleData.value + size: hifi.fontSizes.tableText + color: colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + + elide: Text.ElideRight } Item { @@ -219,11 +225,4 @@ TreeView { onClicked: { selectionModel.setCurrentIndex(index, ItemSelectionModel.ClearAndSelect); } - - onActivated: { - var path = scriptsModel.data(index, 0x100) - if (path) { - loadScript(path) - } - } } diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index a819e032eb..b1f80ac5e8 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -12,7 +12,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import "../../styles-uit" @@ -36,7 +36,41 @@ Rectangle { return (root.parent !== null) && root.parent.objectName == "loader"; } + + property bool isVR: Audio.context === "VR" + property real rightMostInputLevelPos: 0 + //placeholder for control sizes and paddings + //recalculates dynamically in case of UI size is changed + QtObject { + id: margins + property real paddings: root.width / 20.25 + + property real sizeCheckBox: root.width / 13.5 + property real sizeText: root.width / 2.5 + property real sizeLevel: root.width / 5.8 + property real sizeDesktop: root.width / 5.8 + property real sizeVR: root.width / 13.5 + } + + TabBar { + id: bar + spacing: 0 + width: parent.width + height: 42 + currentIndex: isVR ? 1 : 0 + + AudioControls.AudioTabButton { + height: parent.height + text: qsTr("Desktop") + } + AudioControls.AudioTabButton { + height: parent.height + text: qsTr("VR") + } + } + property bool showPeaks: true; + function enablePeakValues() { Audio.devices.input.peakValuesEnabled = true; Audio.devices.input.peakValuesEnabledChanged.connect(function(enabled) { @@ -45,6 +79,7 @@ Rectangle { } }); } + function disablePeakValues() { root.showPeaks = false; Audio.devices.input.peakValuesEnabled = false; @@ -55,29 +90,32 @@ Rectangle { onVisibleChanged: visible ? enablePeakValues() : disablePeakValues(); Column { - y: 16; // padding does not work - spacing: 16; + spacing: 12; + anchors.top: bar.bottom + anchors.bottom: parent.bottom + anchors.bottomMargin: 5 width: parent.width; + Separator { } + RalewayRegular { - x: 16; // padding does not work + x: margins.paddings + muteMic.boxSize + muteMic.spacing; size: 16; color: "white"; - text: root.title; - - visible: root.showTitle(); + text: qsTr("Input Device Settings") } - Separator { visible: root.showTitle() } - ColumnLayout { - x: 16; // padding does not work + x: margins.paddings; spacing: 16; + width: parent.width; // mute is in its own row RowLayout { AudioControls.CheckBox { + id: muteMic text: qsTr("Mute microphone"); + spacing: margins.sizeCheckBox - boxSize isRedCheck: true; checked: Audio.muted; onClicked: { @@ -88,8 +126,9 @@ Rectangle { } RowLayout { - spacing: 16; + spacing: muteMic.spacing*2; //make it visually distinguish AudioControls.CheckBox { + spacing: muteMic.spacing text: qsTr("Enable noise reduction"); checked: Audio.noiseReduction; onClicked: { @@ -98,24 +137,33 @@ Rectangle { } } AudioControls.CheckBox { + spacing: muteMic.spacing text: qsTr("Show audio level meter"); checked: AvatarInputs.showAudioTools; onClicked: { AvatarInputs.showAudioTools = checked; checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding } + onXChanged: rightMostInputLevelPos = x + width } } } Separator {} - RowLayout { + Item { + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: 36 + HiFiGlyphs { + width: margins.sizeCheckBox text: hifi.glyphs.mic; color: hifi.colors.primaryHighlight; + anchors.left: parent.left + anchors.leftMargin: -size/4 //the glyph has empty space at left about 25% anchors.verticalCenter: parent.verticalCenter; - size: 28; + size: 30; } RalewayRegular { anchors.verticalCenter: parent.verticalCenter; @@ -126,90 +174,114 @@ Rectangle { } ListView { - anchors { left: parent.left; right: parent.right; leftMargin: 70 } - height: 125; - spacing: 0; + id: inputView + width: parent.width - margins.paddings*2 + x: margins.paddings + height: Math.min(150, contentHeight); + spacing: 4; snapMode: ListView.SnapToItem; clip: true; model: Audio.devices.input; delegate: Item { - width: parent.width; - height: 36; - + width: rightMostInputLevelPos + height: margins.sizeCheckBox > checkBoxInput.implicitHeight ? + margins.sizeCheckBox : checkBoxInput.implicitHeight + AudioControls.CheckBox { - id: checkbox - anchors.verticalCenter: parent.verticalCenter + id: checkBoxInput anchors.left: parent.left - text: display; - wrap: false; - checked: selected; - enabled: false; + spacing: margins.sizeCheckBox - boxSize + anchors.verticalCenter: parent.verticalCenter + width: parent.width - inputLevel.width + clip: true + checkable: !checked + checked: bar.currentIndex === 0 ? selectedDesktop : selectedHMD; + boxSize: margins.sizeCheckBox / 2 + isRound: true + text: devicename + onPressed: { + if (!checked) { + Audio.setInputDevice(info, bar.currentIndex === 1); + } + } } - - MouseArea { - anchors.fill: checkbox - onClicked: Audio.setInputDevice(info); - } - InputPeak { - id: inputPeak; - visible: Audio.devices.input.peakValuesAvailable; + id: inputLevel + anchors.right: parent.right peak: model.peak; anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: 30 + visible: (bar.currentIndex === 1 && selectedHMD && isVR) || + (bar.currentIndex === 0 && selectedDesktop && !isVR) && + Audio.devices.input.peakValuesAvailable; } } } Separator {} - RowLayout { - Column { - RowLayout { - HiFiGlyphs { - text: hifi.glyphs.unmuted; - color: hifi.colors.primaryHighlight; - anchors.verticalCenter: parent.verticalCenter; - size: 36; - } - RalewayRegular { - anchors.verticalCenter: parent.verticalCenter; - size: 16; - color: hifi.colors.lightGrayText; - text: qsTr("CHOOSE OUTPUT DEVICE"); - } - } + Item { + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: 36 - PlaySampleSound { anchors { left: parent.left; leftMargin: 60 }} + HiFiGlyphs { + anchors.left: parent.left + anchors.leftMargin: -size/4 //the glyph has empty space at left about 25% + anchors.verticalCenter: parent.verticalCenter; + width: margins.sizeCheckBox + text: hifi.glyphs.unmuted; + color: hifi.colors.primaryHighlight; + size: 36; + } + + RalewayRegular { + width: margins.sizeText + margins.sizeLevel + anchors.left: parent.left + anchors.leftMargin: margins.sizeCheckBox + anchors.verticalCenter: parent.verticalCenter; + size: 16; + color: hifi.colors.lightGrayText; + text: qsTr("CHOOSE OUTPUT DEVICE"); } } ListView { - anchors { left: parent.left; right: parent.right; leftMargin: 70 } - height: Math.min(250, contentHeight); - spacing: 0; + id: outputView + width: parent.width - margins.paddings*2 + x: margins.paddings + height: Math.min(360 - inputView.height, contentHeight); + spacing: 4; snapMode: ListView.SnapToItem; clip: true; model: Audio.devices.output; delegate: Item { - width: parent.width; - height: 36; + width: rightMostInputLevelPos + height: margins.sizeCheckBox > checkBoxOutput.implicitHeight ? + margins.sizeCheckBox : checkBoxOutput.implicitHeight AudioControls.CheckBox { - id: checkbox - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - text: display; - checked: selected; - enabled: false; - } - - MouseArea { - anchors.fill: checkbox - onClicked: Audio.setOutputDevice(info); + id: checkBoxOutput + width: parent.width + spacing: margins.sizeCheckBox - boxSize + boxSize: margins.sizeCheckBox / 2 + isRound: true + checked: bar.currentIndex === 0 ? selectedDesktop : selectedHMD; + checkable: !checked + text: devicename + onPressed: { + if (!checked) { + Audio.setOutputDevice(info, bar.currentIndex === 1); + } + } } } } + PlaySampleSound { + x: margins.paddings + + visible: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR); + anchors { left: parent.left; leftMargin: margins.paddings } + } } } diff --git a/interface/resources/qml/hifi/audio/AudioTabButton.qml b/interface/resources/qml/hifi/audio/AudioTabButton.qml new file mode 100644 index 0000000000..3a3ed90f5e --- /dev/null +++ b/interface/resources/qml/hifi/audio/AudioTabButton.qml @@ -0,0 +1,35 @@ +// +// AudioTabButton.qml +// qml/hifi/audio +// +// Created by Vlad Stelmahovsky on 8/16/2017 +// Copyright 2017 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 +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + +TabButton { + id: control + font.pixelSize: height / 2 + + HifiConstants { id: hifi; } + + contentItem: RalewaySemiBold { + text: control.text + font: control.font + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + color: control.checked ? hifi.colors.baseGray : "black" + } +} diff --git a/interface/resources/qml/hifi/audio/CheckBox.qml b/interface/resources/qml/hifi/audio/CheckBox.qml index 1f632ac479..3a954d4004 100644 --- a/interface/resources/qml/hifi/audio/CheckBox.qml +++ b/interface/resources/qml/hifi/audio/CheckBox.qml @@ -9,10 +9,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 +import QtQuick 2.7 import "../../controls-uit" as HifiControls -HifiControls.CheckBox { +HifiControls.CheckBoxQQC2 { color: "white" } diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index c949be319f..00273171df 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -371,6 +371,17 @@ ScrollingWindow { colorScheme: hifi.colorSchemes.dark anchors.left: parent.left anchors.right: parent.right + + TableViewColumn { + role: "display"; + } + + onActivated: { + var path = scriptsModel.data(index, 0x100) + if (path) { + loadScript(path) + } + } } HifiControls.VerticalSpacer { diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index a7b4ad7a53..3f1fcf6bda 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -10,6 +10,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 @@ -48,9 +49,15 @@ Rectangle { isHMD = HMD.active; ApplicationInterface.uploadRequest.connect(uploadClicked); assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); + assetMappingsModel.autoRefreshEnabled = true; + reload(); } + Component.onDestruction: { + assetMappingsModel.autoRefreshEnabled = false; + } + function doDeleteFile(path) { console.log("Deleting " + path); @@ -105,12 +112,12 @@ Rectangle { function askForOverwrite(path, callback) { var object = tabletRoot.messageBox({ - icon: hifi.icons.question, - buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, - defaultButton: OriginalDialogs.StandardButton.No, - title: "Overwrite File", - text: path + "\n" + "This file already exists. Do you want to overwrite it?" - }); + icon: hifi.icons.question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.No, + title: "Overwrite File", + text: path + "\n" + "This file already exists. Do you want to overwrite it?" + }); object.selected.connect(function(button) { if (button === OriginalDialogs.StandardButton.Yes) { callback(); @@ -144,14 +151,13 @@ Rectangle { function reload() { Assets.mappingModel.refresh(); - treeView.selection.clear(); } function handleGetMappingsError(errorString) { errorMessageBox( - "There was a problem retreiving the list of assets from your Asset Server.\n" - + errorString - ); + "There was a problem retreiving the list of assets from your Asset Server.\n" + + errorString + ); } function addToWorld() { @@ -179,25 +185,25 @@ Rectangle { var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; var DYNAMIC_DEFAULT = false; var prompt = tabletRoot.customInputDialog({ - textInput: { - label: "Model URL", - text: defaultURL - }, - comboBox: { - label: "Automatic Collisions", - index: SHAPE_TYPE_DEFAULT, - items: SHAPE_TYPES - }, - checkBox: { - label: "Dynamic", - checked: DYNAMIC_DEFAULT, - disableForItems: [ - SHAPE_TYPE_STATIC_MESH - ], - checkStateOnDisable: false, - warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors" - } - }); + textInput: { + label: "Model URL", + text: defaultURL + }, + comboBox: { + label: "Automatic Collisions", + index: SHAPE_TYPE_DEFAULT, + items: SHAPE_TYPES + }, + checkBox: { + label: "Dynamic", + checked: DYNAMIC_DEFAULT, + disableForItems: [ + SHAPE_TYPE_STATIC_MESH + ], + checkStateOnDisable: false, + warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors" + } + }); prompt.selected.connect(function (jsonResult) { if (jsonResult) { @@ -205,23 +211,23 @@ Rectangle { var url = result.textInput.trim(); var shapeType; switch (result.comboBox) { - case SHAPE_TYPE_SIMPLE_HULL: - shapeType = "simple-hull"; - break; - case SHAPE_TYPE_SIMPLE_COMPOUND: - shapeType = "simple-compound"; - break; - case SHAPE_TYPE_STATIC_MESH: - shapeType = "static-mesh"; - break; - case SHAPE_TYPE_BOX: - shapeType = "box"; - break; - case SHAPE_TYPE_SPHERE: - shapeType = "sphere"; - break; - default: - shapeType = "none"; + case SHAPE_TYPE_SIMPLE_HULL: + shapeType = "simple-hull"; + break; + case SHAPE_TYPE_SIMPLE_COMPOUND: + shapeType = "simple-compound"; + break; + case SHAPE_TYPE_STATIC_MESH: + shapeType = "static-mesh"; + break; + case SHAPE_TYPE_BOX: + shapeType = "box"; + break; + case SHAPE_TYPE_SPHERE: + shapeType = "sphere"; + break; + default: + shapeType = "none"; } var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT; @@ -230,7 +236,7 @@ Rectangle { print("Error: model cannot be both static mesh and dynamic. This should never happen."); } else if (url) { var name = assetProxyModel.data(treeView.selection.currentIndex); - var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation))); + var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation))); var gravity; if (dynamic) { // Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a @@ -293,10 +299,10 @@ Rectangle { } var object = tabletRoot.inputDialog({ - label: "Enter new path:", - current: path, - placeholderText: "Enter path here" - }); + label: "Enter new path:", + current: path, + placeholderText: "Enter path here" + }); object.selected.connect(function(destinationPath) { destinationPath = destinationPath.trim(); @@ -339,12 +345,12 @@ Rectangle { } var object = tabletRoot.messageBox({ - icon: hifi.icons.question, - buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, - defaultButton: OriginalDialogs.StandardButton.Yes, - title: "Delete", - text: modalMessage - }); + icon: hifi.icons.question, + buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.Yes, + title: "Delete", + text: modalMessage + }); object.selected.connect(function(button) { if (button === OriginalDialogs.StandardButton.Yes) { doDeleteFile(path); @@ -379,38 +385,38 @@ Rectangle { var filename = fileUrl.slice(fileUrl.lastIndexOf('/') + 1); Assets.uploadFile(fileUrl, directory + filename, - function() { - // Upload started - uploadSpinner.visible = true; - uploadButton.enabled = false; - uploadProgressLabel.text = "In progress..."; - }, - function(err, path) { - print(err, path); - if (err === "") { - uploadProgressLabel.text = "Upload Complete"; - timer.interval = 1000; - timer.repeat = false; - timer.triggered.connect(function() { - uploadSpinner.visible = false; - uploadButton.enabled = true; - uploadOpen = false; - }); - timer.start(); - console.log("Asset Browser - finished uploading: ", fileUrl); - reload(); - } else { - uploadSpinner.visible = false; - uploadButton.enabled = true; - uploadOpen = false; + function() { + // Upload started + uploadSpinner.visible = true; + uploadButton.enabled = false; + uploadProgressLabel.text = "In progress..."; + }, + function(err, path) { + print(err, path); + if (err === "") { + uploadProgressLabel.text = "Upload Complete"; + timer.interval = 1000; + timer.repeat = false; + timer.triggered.connect(function() { + uploadSpinner.visible = false; + uploadButton.enabled = true; + uploadOpen = false; + }); + timer.start(); + console.log("Asset Browser - finished uploading: ", fileUrl); + reload(); + } else { + uploadSpinner.visible = false; + uploadButton.enabled = true; + uploadOpen = false; - if (err !== -1) { - console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err); - var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err); - box.selected.connect(reload); - } - } - }, dropping); + if (err !== -1) { + console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err); + var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err); + box.selected.connect(reload); + } + } + }, dropping); } function initiateUpload(url) { @@ -421,9 +427,9 @@ Rectangle { doUpload(fileUrl, true); } else { var browser = tabletRoot.fileDialog({ - selectDirectory: false, - dir: currentDirectory - }); + selectDirectory: false, + dir: currentDirectory + }); browser.canceled.connect(function() { uploadOpen = false; @@ -445,11 +451,11 @@ Rectangle { function errorMessageBox(message) { return tabletRoot.messageBox({ - icon: hifi.icons.warning, - defaultButton: OriginalDialogs.StandardButton.Ok, - title: "Error", - text: message - }); + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); } Column { @@ -469,15 +475,6 @@ Rectangle { height: 30 spacing: hifi.dimensions.contentSpacing.x - HifiControls.GlyphButton { - glyph: hifi.glyphs.reload - color: hifi.buttons.black - colorScheme: root.colorScheme - width: hifi.dimensions.controlLineHeight - - onClicked: root.reload() - } - HifiControls.Button { text: "Add To World" color: hifi.buttons.black @@ -511,7 +508,180 @@ Rectangle { enabled: treeView.selection.hasSelection } } + } + HifiControls.Tree { + id: treeView + anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border + anchors.left: parent.left + anchors.right: parent.right + + treeModel: assetProxyModel + selectionMode: SelectionMode.ExtendedSelection + headerVisible: true + sortIndicatorVisible: true + + colorScheme: root.colorScheme + + modifyEl: renameEl + + TableViewColumn { + id: nameColumn + title: "Name:" + role: "name" + width: treeView.width - bakedColumn.width; + } + TableViewColumn { + id: bakedColumn + title: "Use Baked?" + role: "baked" + width: 100 + } + + itemDelegate: Loader { + id: itemDelegateLoader + + anchors { + left: parent ? parent.left : undefined + leftMargin: (styleData.column === 0 ? (2 + styleData.depth) : 1) * hifi.dimensions.tablePadding + right: parent ? parent.right : undefined + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent ? parent.verticalCenter : undefined + } + + function convertToGlyph(text) { + switch (text) { + case "Not Baked": + return hifi.glyphs.circleSlash; + case "Baked": + return hifi.glyphs.check_2_01; + case "Error": + return hifi.glyphs.alert; + default: + return ""; + } + } + + function getComponent() { + if ((styleData.column === 0) && styleData.selected) { + return textFieldComponent; + } else if (convertToGlyph(styleData.value) != "") { + return glyphComponent; + } else { + return labelComponent; + } + + } + sourceComponent: getComponent() + + Component { + id: labelComponent + FiraSansSemiBold { + text: styleData.value + size: hifi.fontSizes.tableText + color: colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + + elide: Text.ElideRight + horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft + } + } + Component { + id: glyphComponent + + HiFiGlyphs { + text: convertToGlyph(styleData.value) + size: hifi.dimensions.frameIconSize + color: colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + + elide: Text.ElideRight + horizontalAlignment: TextInput.AlignHCenter + + HifiControls.ToolTip { + anchors.fill: parent + + visible: styleData.value === "Error" + + toolTip: assetProxyModel.data(styleData.index, 0x106) + } + } + } + Component { + id: textFieldComponent + + TextField { + id: textField + readOnly: !activeFocus + + text: styleData.value + + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + font.family: firaSansSemiBold.name + font.pixelSize: hifi.fontSizes.textFieldInput + height: hifi.dimensions.tableRowHeight + + style: TextFieldStyle { + textColor: readOnly + ? hifi.colors.black + : (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white) + background: Rectangle { + visible: !readOnly + color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black + border.color: hifi.colors.primaryHighlight + border.width: 1 + } + selectedTextColor: hifi.colors.black + selectionColor: hifi.colors.primaryHighlight + padding.left: readOnly ? 0 : hifi.dimensions.textPadding + padding.right: readOnly ? 0 : hifi.dimensions.textPadding + } + + validator: RegExpValidator { + regExp: /[^/]+/ + } + + Keys.onPressed: { + if (event.key == Qt.Key_Escape) { + text = styleData.value; + unfocusHelper.forceActiveFocus(); + event.accepted = true; + } + } + onAccepted: { + if (acceptableInput && styleData.selected) { + if (!treeView.modifyEl(treeView.selection.currentIndex, text)) { + text = styleData.value; + } + unfocusHelper.forceActiveFocus(); + } + } + + onReadOnlyChanged: { + // Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time. + keyboardRaised = activeFocus; + } + } + } + } + + + MouseArea { + propagateComposedEvents: true + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: { + if (!HMD.active) { // Popup only displays properly on desktop + var index = treeView.indexAt(mouse.x, mouse.y); + treeView.selection.setCurrentIndex(index, 0x0002); + contextMenu.currentIndex = index; + contextMenu.popup(); + } + } + } + Menu { id: contextMenu title: "Edit" @@ -539,39 +709,50 @@ Rectangle { } } } - } - HifiControls.Tree { - id: treeView - height: 290 - anchors.leftMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border - anchors.rightMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border - anchors.left: parent.left - anchors.right: parent.right - treeModel: assetProxyModel - canEdit: true - colorScheme: root.colorScheme - selectionMode: SelectionMode.ExtendedSelection + Row { + id: infoRow + anchors.left: treeView.left + anchors.right: treeView.right + anchors.bottomMargin: hifi.dimensions.contentSpacing.y + spacing: hifi.dimensions.contentSpacing.x + + RalewayRegular { + size: hifi.fontSizes.sectionName + font.capitalization: Font.AllUppercase + text: selectedItems + " items selected" + color: hifi.colors.lightGrayText + } - modifyEl: renameEl + HifiControls.CheckBox { + function isChecked() { + var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + var bakingDisabled = (status === "Not Baked" || status === "--"); + return selectedItems === 1 && !bakingDisabled; + } - MouseArea { - propagateComposedEvents: true - anchors.fill: parent - acceptedButtons: Qt.RightButton + text: "Use baked (optimized) versions" + colorScheme: root.colorScheme + enabled: selectedItems === 1 && assetProxyModel.data(treeView.selection.currentIndex, 0x105) !== "--" + checked: isChecked() onClicked: { - if (!HMD.active) { // Popup only displays properly on desktop - var index = treeView.indexAt(mouse.x, mouse.y); - treeView.selection.setCurrentIndex(index, 0x0002); - contextMenu.currentIndex = index; - contextMenu.popup(); + var mappings = []; + for (var i in treeView.selection.selectedIndexes) { + var index = treeView.selection.selectedIndexes[i]; + var path = assetProxyModel.data(index, 0x100); + mappings.push(path); } + print("Setting baking enabled:" + mappings + " " + checked); + Assets.setBakingEnabled(mappings, checked, function() { + reload(); + }); + + checked = Qt.binding(isChecked); } } } - HifiControls.TabletContentSection { id: uploadSection name: "Upload A File" diff --git a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml index 340d2e9ac7..80c1b58444 100644 --- a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml @@ -400,6 +400,17 @@ Rectangle { colorScheme: hifi.colorSchemes.dark anchors.left: parent.left anchors.right: parent.right + + TableViewColumn { + role: "display"; + } + + onActivated: { + var path = scriptsModel.data(index, 0x100) + if (path) { + loadScript(path) + } + } } HifiControls.VerticalSpacer { diff --git a/interface/resources/qml/styles-uit/HiFiGlyphs.qml b/interface/resources/qml/styles-uit/HiFiGlyphs.qml index cbd6fa1d68..f78d6c6f59 100644 --- a/interface/resources/qml/styles-uit/HiFiGlyphs.qml +++ b/interface/resources/qml/styles-uit/HiFiGlyphs.qml @@ -9,8 +9,6 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 Text { id: root diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 4a26d11128..a091c6d5cf 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -338,5 +338,6 @@ Item { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" + readonly property string check_2_01: "\ue020" } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2bfc7c8853..ce615a4739 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2060,6 +2060,7 @@ void Application::cleanupBeforeQuit() { // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; } @@ -5162,12 +5163,6 @@ void Application::update(float deltaTime) { } } - { - PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("overlays"); - _overlays.update(deltaTime); - } - { PROFILE_RANGE(app, "RayPickManager"); _rayPickManager.update(); @@ -5178,6 +5173,12 @@ void Application::update(float deltaTime) { _laserPointerManager.update(); } + { + PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("overlays"); + _overlays.update(deltaTime); + } + // Update _viewFrustum with latest camera and view frustum data... // NOTE: we get this from the view frustum, to make it simpler, since the // loadViewFrumstum() method will get the correct details from the camera diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f7116a60db..e508c972c6 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2196,6 +2196,14 @@ float MyAvatar::getDomainMaxScale() { return _domainMaximumScale; } +void MyAvatar::setGravity(float gravity) { + _characterController.setGravity(gravity); +} + +float MyAvatar::getGravity() { + return _characterController.getGravity(); +} + void MyAvatar::increaseSize() { // make sure we're starting from an allowable scale clampTargetScaleToDomainLimits(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c9d073cfd9..9620d61a49 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -152,7 +152,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float userHeight READ getUserHeight WRITE setUserHeight) Q_PROPERTY(float userEyeHeight READ getUserEyeHeight) - + const QString DOMINANT_LEFT_HAND = "left"; const QString DOMINANT_RIGHT_HAND = "right"; @@ -551,6 +551,9 @@ public slots: float getDomainMinScale(); float getDomainMaxScale(); + void setGravity(float gravity); + float getGravity(); + void goToLocation(const glm::vec3& newPosition, bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), bool shouldFaceLocation = false); diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp index 9d64bcc476..b696afa8e5 100644 --- a/interface/src/raypick/LaserPointer.cpp +++ b/interface/src/raypick/LaserPointer.cpp @@ -86,7 +86,9 @@ void LaserPointer::editRenderState(const std::string& state, const QVariant& sta void LaserPointer::updateRenderStateOverlay(const OverlayID& id, const QVariant& props) { if (!id.isNull() && props.isValid()) { - qApp->getOverlays().editOverlay(id, props); + QVariantMap propMap = props.toMap(); + propMap.remove("visible"); + qApp->getOverlays().editOverlay(id, propMap); } } diff --git a/interface/src/raypick/LaserPointerScriptingInterface.cpp b/interface/src/raypick/LaserPointerScriptingInterface.cpp index a976a00893..d5e435f490 100644 --- a/interface/src/raypick/LaserPointerScriptingInterface.cpp +++ b/interface/src/raypick/LaserPointerScriptingInterface.cpp @@ -95,6 +95,7 @@ const RenderState LaserPointerScriptingInterface::buildRenderState(const QVarian if (propMap["start"].isValid()) { QVariantMap startMap = propMap["start"].toMap(); if (startMap["type"].isValid()) { + startMap.remove("visible"); startID = qApp->getOverlays().addOverlay(startMap["type"].toString(), startMap); } } @@ -104,6 +105,7 @@ const RenderState LaserPointerScriptingInterface::buildRenderState(const QVarian QVariantMap pathMap = propMap["path"].toMap(); // right now paths must be line3ds if (pathMap["type"].isValid() && pathMap["type"].toString() == "line3d") { + pathMap.remove("visible"); pathID = qApp->getOverlays().addOverlay(pathMap["type"].toString(), pathMap); } } @@ -112,6 +114,7 @@ const RenderState LaserPointerScriptingInterface::buildRenderState(const QVarian if (propMap["end"].isValid()) { QVariantMap endMap = propMap["end"].toMap(); if (endMap["type"].isValid()) { + endMap.remove("visible"); endID = qApp->getOverlays().addOverlay(endMap["type"].toString(), endMap); } } diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index 5a81fe8749..5031016c3f 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -19,20 +19,12 @@ #include #include #include +#include #include -void AssetMappingModel::clear() { - // make sure we are on the same thread before we touch the hash - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "clear"); - return; - } +static const int AUTO_REFRESH_INTERVAL = 1000; - qDebug() << "Clearing loaded asset mappings for Asset Browser"; - - _pathToItemMap.clear(); - QStandardItemModel::clear(); -} +int assetMappingModelMetatypeId = qRegisterMetaType("AssetMappingModel*"); AssetMappingsScriptingInterface::AssetMappingsScriptingInterface() { _proxyModel.setSourceModel(&_assetMappingModel); @@ -154,7 +146,7 @@ void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) { auto map = callback.engine()->newObject(); for (auto& kv : mappings ) { - map.setProperty(kv.first, kv.second); + map.setProperty(kv.first, kv.second.hash); } if (callback.isCallable()) { @@ -174,7 +166,7 @@ void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString new connect(request, &RenameMappingRequest::finished, this, [this, callback](RenameMappingRequest* request) mutable { if (callback.isCallable()) { - QJSValueList args { request->getErrorString() }; + QJSValueList args{ request->getErrorString() }; callback.call(args); } @@ -184,6 +176,49 @@ void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString new request->start(); } +void AssetMappingsScriptingInterface::setBakingEnabled(QStringList paths, bool enabled, QJSValue callback) { + auto assetClient = DependencyManager::get(); + auto request = assetClient->createSetBakingEnabledRequest(paths, enabled); + + connect(request, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable { + if (callback.isCallable()) { + QJSValueList args{ request->getErrorString() }; + callback.call(args); + } + + request->deleteLater(); + }); + + request->start(); +} + +AssetMappingModel::AssetMappingModel() { + setupRoles(); + + connect(&_autoRefreshTimer, &QTimer::timeout, this, [this] { + auto nodeList = DependencyManager::get(); + auto assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); + if (assetServer) { + refresh(); + } + }); + _autoRefreshTimer.setInterval(AUTO_REFRESH_INTERVAL); +} + +bool AssetMappingModel::isAutoRefreshEnabled() { + return _autoRefreshTimer.isActive(); +} + +void AssetMappingModel::setAutoRefreshEnabled(bool enabled) { + if (enabled != _autoRefreshTimer.isActive()) { + if (enabled) { + _autoRefreshTimer.start(); + } else { + _autoRefreshTimer.stop(); + } + } +} + bool AssetMappingModel::isKnownFolder(QString path) const { if (!path.endsWith("/")) { return false; @@ -198,10 +233,7 @@ bool AssetMappingModel::isKnownFolder(QString path) const { return false; } -int assetMappingModelMetatypeId = qRegisterMetaType("AssetMappingModel*"); - void AssetMappingModel::refresh() { - qDebug() << "Refreshing asset mapping model"; auto assetClient = DependencyManager::get(); auto request = assetClient->createGetAllMappingsRequest(); @@ -211,6 +243,12 @@ void AssetMappingModel::refresh() { auto existingPaths = _pathToItemMap.keys(); for (auto& mapping : mappings) { auto& path = mapping.first; + + if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + // Hide baked mappings + continue; + } + auto parts = path.split("/"); auto length = parts.length(); @@ -223,27 +261,32 @@ void AssetMappingModel::refresh() { // start index at 1 to avoid empty string from leading slash for (int i = 1; i < length; ++i) { fullPath += (i == 1 ? "" : "/") + parts[i]; + bool isFolder = i < length - 1; auto it = _pathToItemMap.find(fullPath); if (it == _pathToItemMap.end()) { auto item = new QStandardItem(parts[i]); - bool isFolder = i < length - 1; item->setData(isFolder ? fullPath + "/" : fullPath, Qt::UserRole); item->setData(isFolder, Qt::UserRole + 1); item->setData(parts[i], Qt::UserRole + 2); item->setData("atp:" + fullPath, Qt::UserRole + 3); item->setData(fullPath, Qt::UserRole + 4); + if (lastItem) { - lastItem->setChild(lastItem->rowCount(), 0, item); + lastItem->appendRow(item); } else { appendRow(item); } - lastItem = item; _pathToItemMap[fullPath] = lastItem; } else { lastItem = it.value(); } + + // update status + auto statusString = isFolder ? "--" : bakingStatusToString(mapping.second.status); + lastItem->setData(statusString, Qt::UserRole + 5); + lastItem->setData(mapping.second.bakingErrors, Qt::UserRole + 6); } Q_ASSERT(fullPath == path); @@ -295,8 +338,30 @@ void AssetMappingModel::refresh() { emit errorGettingMappings(request->getErrorString()); } + emit updated(); + request->deleteLater(); }); request->start(); } + +void AssetMappingModel::clear() { + // make sure we are on the same thread before we touch the hash + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "clear"); + return; + } + + qDebug() << "Clearing loaded asset mappings for Asset Browser"; + + _pathToItemMap.clear(); + QStandardItemModel::clear(); +} + +void AssetMappingModel::setupRoles() { + QHash roleNames; + roleNames[Qt::DisplayRole] = "name"; + roleNames[Qt::UserRole + 5] = "baked"; + setItemRoleNames(roleNames); +} \ No newline at end of file diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.h b/interface/src/scripting/AssetMappingsScriptingInterface.h index b7fcea2491..04ab488838 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.h +++ b/interface/src/scripting/AssetMappingsScriptingInterface.h @@ -25,9 +25,16 @@ class AssetMappingModel : public QStandardItemModel { Q_OBJECT + Q_PROPERTY(bool autoRefreshEnabled READ isAutoRefreshEnabled WRITE setAutoRefreshEnabled) + public: + AssetMappingModel(); + Q_INVOKABLE void refresh(); + bool isAutoRefreshEnabled(); + void setAutoRefreshEnabled(bool enabled); + bool isKnownMapping(QString path) const { return _pathToItemMap.contains(path); } bool isKnownFolder(QString path) const; @@ -36,9 +43,13 @@ public slots: signals: void errorGettingMappings(QString errorString); + void updated(); private: + void setupRoles(); + QHash _pathToItemMap; + QTimer _autoRefreshTimer; }; Q_DECLARE_METATYPE(AssetMappingModel*) @@ -61,10 +72,11 @@ public: Q_INVOKABLE void setMapping(QString path, QString hash, QJSValue callback = QJSValue()); Q_INVOKABLE void getMapping(QString path, QJSValue callback = QJSValue()); Q_INVOKABLE void uploadFile(QString path, QString mapping, QJSValue startedCallback = QJSValue(), QJSValue completedCallback = QJSValue(), bool dropEvent = false); - Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback); + Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback = QJSValue()); Q_INVOKABLE void deleteMapping(QString path, QJSValue callback) { deleteMappings(QStringList(path), callback = QJSValue()); } Q_INVOKABLE void getAllMappings(QJSValue callback = QJSValue()); Q_INVOKABLE void renameMapping(QString oldPath, QString newPath, QJSValue callback = QJSValue()); + Q_INVOKABLE void setBakingEnabled(QStringList paths, bool enabled, QJSValue callback = QJSValue()); protected: QSet _pendingRequests; diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 9719c23885..f9c1a95fb5 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -135,10 +135,10 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { DependencyManager::get()->setReverbOptions(options); } -void Audio::setInputDevice(const QAudioDeviceInfo& device) { - _devices.chooseInputDevice(device); +void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { + _devices.chooseInputDevice(device, isHMD); } -void Audio::setOutputDevice(const QAudioDeviceInfo& device) { - _devices.chooseOutputDevice(device); +void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { + _devices.chooseOutputDevice(device, isHMD); } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index bd40de4303..abd2312cf0 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -50,8 +50,8 @@ public: void showMicMeter(bool show); void setInputVolume(float volume); - Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device); - Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device); + Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device, bool isHMD); + Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device, bool isHMD); Q_INVOKABLE void setReverb(bool enable); Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); diff --git a/interface/src/scripting/AudioDevices.cpp b/interface/src/scripting/AudioDevices.cpp index e5cc43f9df..a130b46877 100644 --- a/interface/src/scripting/AudioDevices.cpp +++ b/interface/src/scripting/AudioDevices.cpp @@ -38,15 +38,17 @@ Setting::Handle& getSetting(bool contextIsHMD, QAudio::Mode mode) { } enum AudioDeviceRole { - DisplayRole = Qt::DisplayRole, - CheckStateRole = Qt::CheckStateRole, - PeakRole = Qt::UserRole, - InfoRole = Qt::UserRole + 1 + DeviceNameRole = Qt::UserRole, + SelectedDesktopRole, + SelectedHMDRole, + PeakRole, + InfoRole }; QHash AudioDeviceList::_roles { - { DisplayRole, "display" }, - { CheckStateRole, "selected" }, + { DeviceNameRole, "devicename" }, + { SelectedDesktopRole, "selectedDesktop" }, + { SelectedHMDRole, "selectedHMD" }, { PeakRole, "peak" }, { InfoRole, "info" } }; @@ -68,15 +70,64 @@ static QString getTargetDevice(bool hmd, QAudio::Mode mode) { Qt::ItemFlags AudioDeviceList::_flags { Qt::ItemIsSelectable | Qt::ItemIsEnabled }; +AudioDeviceList::AudioDeviceList(QAudio::Mode mode) : _mode(mode) { + auto& setting1 = getSetting(true, QAudio::AudioInput); + if (setting1.isSet()) { + qDebug() << "Device name in settings for HMD, Input" << setting1.get(); + } else { + qDebug() << "Device name in settings for HMD, Input not set"; + } + + auto& setting2 = getSetting(true, QAudio::AudioOutput); + if (setting2.isSet()) { + qDebug() << "Device name in settings for HMD, Output" << setting2.get(); + } else { + qDebug() << "Device name in settings for HMD, Output not set"; + } + + auto& setting3 = getSetting(false, QAudio::AudioInput); + if (setting3.isSet()) { + qDebug() << "Device name in settings for Desktop, Input" << setting3.get(); + } else { + qDebug() << "Device name in settings for Desktop, Input not set"; + } + + auto& setting4 = getSetting(false, QAudio::AudioOutput); + if (setting4.isSet()) { + qDebug() << "Device name in settings for Desktop, Output" << setting4.get(); + } else { + qDebug() << "Device name in settings for Desktop, Output not set"; + } +} + +AudioDeviceList::~AudioDeviceList() { + //save all selected devices + auto& settingHMD = getSetting(true, _mode); + auto& settingDesktop = getSetting(false, _mode); + // store the selected device + foreach(std::shared_ptr adevice, _devices) { + if (adevice->selectedDesktop) { + qDebug() << "Saving Desktop for" << _mode << "name" << adevice->info.deviceName(); + settingDesktop.set(adevice->info.deviceName()); + } + if (adevice->selectedHMD) { + qDebug() << "Saving HMD for" << _mode << "name" << adevice->info.deviceName(); + settingHMD.set(adevice->info.deviceName()); + } + } +} + QVariant AudioDeviceList::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() >= rowCount()) { return QVariant(); } - if (role == DisplayRole) { + if (role == DeviceNameRole) { return _devices.at(index.row())->display; - } else if (role == CheckStateRole) { - return _devices.at(index.row())->selected; + } else if (role == SelectedDesktopRole) { + return _devices.at(index.row())->selectedDesktop; + } else if (role == SelectedHMDRole) { + return _devices.at(index.row())->selectedHMD; } else if (role == InfoRole) { return QVariant::fromValue(_devices.at(index.row())->info); } else { @@ -130,37 +181,48 @@ void AudioDeviceList::resetDevice(bool contextIsHMD) { #endif } -void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device) { - auto oldDevice = _selectedDevice; - _selectedDevice = device; +void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device, bool isHMD) { + auto oldDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice; + QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice; + selectedDevice = device; - for (auto i = 0; i < rowCount(); ++i) { - AudioDevice& device = *_devices[i]; - - if (device.selected && device.info != _selectedDevice) { - device.selected = false; - } else if (device.info == _selectedDevice) { - device.selected = true; + for (auto i = 0; i < _devices.size(); ++i) { + std::shared_ptr device = _devices[i]; + bool &isSelected = isHMD ? device->selectedHMD : device->selectedDesktop; + if (isSelected && device->info != selectedDevice) { + isSelected = false; + } else if (device->info == selectedDevice) { + isSelected = true; } } - emit deviceChanged(_selectedDevice); + emit deviceChanged(selectedDevice); emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0)); } -void AudioDeviceList::onDevicesChanged(const QList& devices) { +void AudioDeviceList::onDevicesChanged(const QList& devices, bool isHMD) { + QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice; + + const QString& savedDeviceName = isHMD ? _hmdSavedDeviceName : _desktopSavedDeviceName; beginResetModel(); _devices.clear(); foreach(const QAudioDeviceInfo& deviceInfo, devices) { AudioDevice device; + bool &isSelected = isHMD ? device.selectedHMD : device.selectedDesktop; device.info = deviceInfo; device.display = device.info.deviceName() .replace("High Definition", "HD") .remove("Device") .replace(" )", ")"); - device.selected = (device.info == _selectedDevice); + if (!selectedDevice.isNull()) { + isSelected = (device.info == selectedDevice); + } else { + //no selected device for context. fallback to saved + isSelected = (device.info.deviceName() == savedDeviceName); + } + qDebug() << "adding audio device:" << device.display << device.selectedDesktop << device.selectedHMD << _mode; _devices.push_back(newDevice(device)); } @@ -203,22 +265,32 @@ AudioDevices::AudioDevices(bool& contextIsHMD) : _contextIsHMD(contextIsHMD) { connect(client.data(), &AudioClient::devicesChanged, this, &AudioDevices::onDevicesChanged, Qt::QueuedConnection); connect(client.data(), &AudioClient::peakValueListChanged, &_inputs, &AudioInputDeviceList::onPeakValueListChanged, Qt::QueuedConnection); + _inputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioInput), contextIsHMD); + _outputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioOutput), contextIsHMD); + // connections are made after client is initialized, so we must also fetch the devices - _inputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioInput)); - _outputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioOutput)); - _inputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioInput)); - _outputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioOutput)); + const QList& devicesInput = client->getAudioDevices(QAudio::AudioInput); + const QList& devicesOutput = client->getAudioDevices(QAudio::AudioOutput); + //setup HMD devices + _inputs.onDevicesChanged(devicesInput, true); + _outputs.onDevicesChanged(devicesOutput, true); + //setup Desktop devices + _inputs.onDevicesChanged(devicesInput, false); + _outputs.onDevicesChanged(devicesOutput, false); } +AudioDevices::~AudioDevices() {} + void AudioDevices::onContextChanged(const QString& context) { _inputs.resetDevice(_contextIsHMD); _outputs.resetDevice(_contextIsHMD); } -void AudioDevices::onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice) { +void AudioDevices::onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device, + const QAudioDeviceInfo& previousDevice, bool isHMD) { QString deviceName = device.isNull() ? QString() : device.deviceName(); - auto& setting = getSetting(_contextIsHMD, mode); + auto& setting = getSetting(isHMD, mode); // check for a previous device auto wasDefault = setting.get().isNull(); @@ -254,42 +326,94 @@ void AudioDevices::onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& d void AudioDevices::onDeviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& device) { if (mode == QAudio::AudioInput) { if (_requestedInputDevice == device) { - onDeviceSelected(QAudio::AudioInput, device, _inputs._selectedDevice); + onDeviceSelected(QAudio::AudioInput, device, + _contextIsHMD ? _inputs._selectedHMDDevice : _inputs._selectedDesktopDevice, + _contextIsHMD); _requestedInputDevice = QAudioDeviceInfo(); } - _inputs.onDeviceChanged(device); + _inputs.onDeviceChanged(device, _contextIsHMD); } else { // if (mode == QAudio::AudioOutput) if (_requestedOutputDevice == device) { - onDeviceSelected(QAudio::AudioOutput, device, _outputs._selectedDevice); + onDeviceSelected(QAudio::AudioOutput, device, + _contextIsHMD ? _outputs._selectedHMDDevice : _outputs._selectedDesktopDevice, + _contextIsHMD); _requestedOutputDevice = QAudioDeviceInfo(); } - _outputs.onDeviceChanged(device); + _outputs.onDeviceChanged(device, _contextIsHMD); } } void AudioDevices::onDevicesChanged(QAudio::Mode mode, const QList& devices) { static std::once_flag once; + std::call_once(once, [&] { + //readout settings + auto client = DependencyManager::get(); + + _inputs._hmdSavedDeviceName = getTargetDevice(true, QAudio::AudioInput); + _inputs._desktopSavedDeviceName = getTargetDevice(false, QAudio::AudioInput); + + //fallback to default device + if (_inputs._desktopSavedDeviceName.isEmpty()) { + _inputs._desktopSavedDeviceName = client->getActiveAudioDevice(QAudio::AudioInput).deviceName(); + } + //fallback to desktop device + if (_inputs._hmdSavedDeviceName.isEmpty()) { + _inputs._hmdSavedDeviceName = _inputs._desktopSavedDeviceName; + } + + _outputs._hmdSavedDeviceName = getTargetDevice(true, QAudio::AudioOutput); + _outputs._desktopSavedDeviceName = getTargetDevice(false, QAudio::AudioOutput); + + if (_outputs._desktopSavedDeviceName.isEmpty()) { + _outputs._desktopSavedDeviceName = client->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); + } + if (_outputs._hmdSavedDeviceName.isEmpty()) { + _outputs._hmdSavedDeviceName = _outputs._desktopSavedDeviceName; + } + onContextChanged(QString()); + }); + + //set devices for both contexts if (mode == QAudio::AudioInput) { - _inputs.onDevicesChanged(devices); + _inputs.onDevicesChanged(devices, _contextIsHMD); + _inputs.onDevicesChanged(devices, !_contextIsHMD); } else { // if (mode == QAudio::AudioOutput) - _outputs.onDevicesChanged(devices); + _outputs.onDevicesChanged(devices, _contextIsHMD); + _outputs.onDevicesChanged(devices, !_contextIsHMD); } - std::call_once(once, [&] { onContextChanged(QString()); }); } -void AudioDevices::chooseInputDevice(const QAudioDeviceInfo& device) { - auto client = DependencyManager::get(); - _requestedInputDevice = device; - QMetaObject::invokeMethod(client.data(), "switchAudioDevice", - Q_ARG(QAudio::Mode, QAudio::AudioInput), - Q_ARG(const QAudioDeviceInfo&, device)); +void AudioDevices::chooseInputDevice(const QAudioDeviceInfo& device, bool isHMD) { + //check if current context equals device to change + if (_contextIsHMD == isHMD) { + auto client = DependencyManager::get(); + _requestedInputDevice = device; + QMetaObject::invokeMethod(client.data(), "switchAudioDevice", + Q_ARG(QAudio::Mode, QAudio::AudioInput), + Q_ARG(const QAudioDeviceInfo&, device)); + } else { + //context is different. just save device in settings + onDeviceSelected(QAudio::AudioInput, device, + isHMD ? _inputs._selectedHMDDevice : _inputs._selectedDesktopDevice, + isHMD); + _inputs.onDeviceChanged(device, isHMD); + } } -void AudioDevices::chooseOutputDevice(const QAudioDeviceInfo& device) { - auto client = DependencyManager::get(); - _requestedOutputDevice = device; - QMetaObject::invokeMethod(client.data(), "switchAudioDevice", - Q_ARG(QAudio::Mode, QAudio::AudioOutput), - Q_ARG(const QAudioDeviceInfo&, device)); +void AudioDevices::chooseOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { + //check if current context equals device to change + if (_contextIsHMD == isHMD) { + auto client = DependencyManager::get(); + _requestedOutputDevice = device; + QMetaObject::invokeMethod(client.data(), "switchAudioDevice", + Q_ARG(QAudio::Mode, QAudio::AudioOutput), + Q_ARG(const QAudioDeviceInfo&, device)); + } else { + //context is different. just save device in settings + onDeviceSelected(QAudio::AudioOutput, device, + isHMD ? _outputs._selectedHMDDevice : _outputs._selectedDesktopDevice, + isHMD); + _outputs.onDeviceChanged(device, isHMD); + } } diff --git a/interface/src/scripting/AudioDevices.h b/interface/src/scripting/AudioDevices.h index 4c1820e9c8..36f1653e38 100644 --- a/interface/src/scripting/AudioDevices.h +++ b/interface/src/scripting/AudioDevices.h @@ -25,15 +25,16 @@ class AudioDevice { public: QAudioDeviceInfo info; QString display; - bool selected { false }; + bool selectedDesktop { false }; + bool selectedHMD { false }; }; class AudioDeviceList : public QAbstractListModel { Q_OBJECT public: - AudioDeviceList(QAudio::Mode mode = QAudio::AudioOutput) : _mode(mode) {} - ~AudioDeviceList() = default; + AudioDeviceList(QAudio::Mode mode = QAudio::AudioOutput); + virtual ~AudioDeviceList(); virtual std::shared_ptr newDevice(const AudioDevice& device) { return std::make_shared(device); } @@ -52,8 +53,8 @@ signals: void deviceChanged(const QAudioDeviceInfo& device); protected slots: - void onDeviceChanged(const QAudioDeviceInfo& device); - void onDevicesChanged(const QList& devices); + void onDeviceChanged(const QAudioDeviceInfo& device, bool isHMD); + void onDevicesChanged(const QList& devices, bool isHMD); protected: friend class AudioDevices; @@ -61,8 +62,11 @@ protected: static QHash _roles; static Qt::ItemFlags _flags; const QAudio::Mode _mode; - QAudioDeviceInfo _selectedDevice; + QAudioDeviceInfo _selectedDesktopDevice; + QAudioDeviceInfo _selectedHMDDevice; QList> _devices; + QString _hmdSavedDeviceName; + QString _desktopSavedDeviceName; }; class AudioInputDevice : public AudioDevice { @@ -102,7 +106,6 @@ protected: void setPeakValuesEnabled(bool enable); bool _peakValuesEnabled { false }; }; - class Audio; class AudioDevices : public QObject { @@ -112,15 +115,18 @@ class AudioDevices : public QObject { public: AudioDevices(bool& contextIsHMD); - void chooseInputDevice(const QAudioDeviceInfo& device); - void chooseOutputDevice(const QAudioDeviceInfo& device); + virtual ~AudioDevices(); + + void chooseInputDevice(const QAudioDeviceInfo& device, bool isHMD); + void chooseOutputDevice(const QAudioDeviceInfo& device, bool isHMD); signals: void nop(); private slots: void onContextChanged(const QString& context); - void onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice); + void onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device, + const QAudioDeviceInfo& previousDevice, bool isHMD); void onDeviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& device); void onDevicesChanged(QAudio::Mode mode, const QList& devices); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index b8d4a38905..9cd00dfdf2 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -343,30 +343,6 @@ void setupPreferences() { preferences->addPreference(preference); } } - { - auto getter = []()->bool { return image::isColorTexturesCompressionEnabled(); }; - auto setter = [](bool value) { return image::setColorTexturesCompressionEnabled(value); }; - auto preference = new CheckPreference(RENDER, "Compress Color Textures", getter, setter); - preferences->addPreference(preference); - } - { - auto getter = []()->bool { return image::isNormalTexturesCompressionEnabled(); }; - auto setter = [](bool value) { return image::setNormalTexturesCompressionEnabled(value); }; - auto preference = new CheckPreference(RENDER, "Compress Normal Textures", getter, setter); - preferences->addPreference(preference); - } - { - auto getter = []()->bool { return image::isGrayscaleTexturesCompressionEnabled(); }; - auto setter = [](bool value) { return image::setGrayscaleTexturesCompressionEnabled(value); }; - auto preference = new CheckPreference(RENDER, "Compress Grayscale Textures", getter, setter); - preferences->addPreference(preference); - } - { - auto getter = []()->bool { return image::isCubeTexturesCompressionEnabled(); }; - auto setter = [](bool value) { return image::setCubeTexturesCompressionEnabled(value); }; - auto preference = new CheckPreference(RENDER, "Compress Cube Textures", getter, setter); - preferences->addPreference(preference); - } } { static const QString RENDER("Networking"); diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 34aae37175..2366b888e7 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -15,6 +15,10 @@ #include #include +#ifndef MIN +#define MIN(a,b) ((a) < (b) ? (a) : (b)) +#endif + static const float CONTEXT_OVERLAY_TABLET_OFFSET = 30.0f; // Degrees static const float CONTEXT_OVERLAY_TABLET_ORIENTATION = 210.0f; // Degrees static const float CONTEXT_OVERLAY_TABLET_DISTANCE = 0.65F; // Meters @@ -38,11 +42,6 @@ ContextOverlayInterface::ContextOverlayInterface() { _entityPropertyFlags += PROP_DIMENSIONS; _entityPropertyFlags += PROP_REGISTRATION_POINT; - // initially, set _enabled to match the switch. Later we enable/disable via the getter/setters - // if we are in edit or pal (for instance). Note this is temporary, as we expect to enable this all - // the time after getting edge highlighting, etc... - _enabled = _settingSwitch.get(); - auto entityTreeRenderer = DependencyManager::get().data(); connect(entityTreeRenderer, SIGNAL(mousePressOnEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(createOrDestroyContextOverlay(const EntityItemID&, const PointerEvent&))); connect(entityTreeRenderer, SIGNAL(hoverEnterEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(contextOverlays_hoverEnterEntity(const EntityItemID&, const PointerEvent&))); @@ -65,40 +64,36 @@ ContextOverlayInterface::ContextOverlayInterface() { connect(_selectionScriptingInterface.data(), &SelectionScriptingInterface::selectedItemsListChanged, &_selectionToSceneHandler, &SelectionToSceneHandler::selectedItemsListChanged); } +static const uint32_t MOUSE_HW_ID = 0; static const uint32_t LEFT_HAND_HW_ID = 1; static const xColor CONTEXT_OVERLAY_COLOR = { 255, 255, 255 }; static const float CONTEXT_OVERLAY_INSIDE_DISTANCE = 1.0f; // in meters -static const float CONTEXT_OVERLAY_CLOSE_DISTANCE = 1.5f; // in meters -static const float CONTEXT_OVERLAY_CLOSE_SIZE = 0.12f; // in meters, same x and y dims -static const float CONTEXT_OVERLAY_FAR_SIZE = 0.08f; // in meters, same x and y dims -static const float CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE = 20.0f; +static const float CONTEXT_OVERLAY_SIZE = 0.09f; // in meters, same x and y dims +static const float CONTEXT_OVERLAY_OFFSET_DISTANCE = 0.1f; +static const float CONTEXT_OVERLAY_OFFSET_ANGLE = 5.0f; static const float CONTEXT_OVERLAY_UNHOVERED_ALPHA = 0.85f; static const float CONTEXT_OVERLAY_HOVERED_ALPHA = 1.0f; static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMIN = 0.6f; static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMAX = 1.0f; static const float CONTEXT_OVERLAY_UNHOVERED_PULSEPERIOD = 1.0f; static const float CONTEXT_OVERLAY_UNHOVERED_COLORPULSE = 1.0f; -static const float CONTEXT_OVERLAY_FAR_OFFSET = 0.1f; void ContextOverlayInterface::setEnabled(bool enabled) { - // only enable/disable if the setting in 'on'. If it is 'off', - // make sure _enabled is always false. - if (_settingSwitch.get()) { - _enabled = enabled; - } else { - _enabled = false; - } + _enabled = enabled; } bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { if (_enabled && event.getButton() == PointerEvent::SecondaryButton) { if (contextOverlayFilterPassed(entityItemID)) { + if (event.getID() == MOUSE_HW_ID) { + enableEntityHighlight(entityItemID); + } + qCDebug(context_overlay) << "Creating Context Overlay on top of entity with ID: " << entityItemID; // Add all necessary variables to the stack EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags); glm::vec3 cameraPosition = qApp->getCamera().getPosition(); - float distanceFromCameraToEntity = glm::distance(entityProperties.getPosition(), cameraPosition); glm::vec3 entityDimensions = entityProperties.getDimensions(); glm::vec3 entityPosition = entityProperties.getPosition(); glm::vec3 contextOverlayPosition = entityProperties.getPosition(); @@ -131,27 +126,22 @@ bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& // If the camera is inside the box... // ...position the Context Overlay 1 meter in front of the camera. contextOverlayPosition = cameraPosition + CONTEXT_OVERLAY_INSIDE_DISTANCE * (qApp->getCamera().getOrientation() * Vectors::FRONT); - contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); - } else if (distanceFromCameraToEntity < CONTEXT_OVERLAY_CLOSE_DISTANCE) { - // Else if the entity is too close to the camera... - // ...rotate the Context Overlay to the right of the entity. - // This makes it easy to inspect things you're holding. - float offsetAngle = -CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE; - if (event.getID() == LEFT_HAND_HW_ID) { - offsetAngle *= -1; - } - contextOverlayPosition = (glm::quat(glm::radians(glm::vec3(0.0f, offsetAngle, 0.0f))) * (entityPosition - cameraPosition)) + cameraPosition; - contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); + contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_SIZE, CONTEXT_OVERLAY_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); } else { - // Else, place the Context Overlay some offset away from the entity's bounding - // box in the direction of the camera. + // Rotate the Context Overlay some number of degrees offset from the entity + // along the line cast from your head to the entity's bounding box. glm::vec3 direction = glm::normalize(entityPosition - cameraPosition); float distance; BoxFace face; glm::vec3 normal; boundingBox.findRayIntersection(cameraPosition, direction, distance, face, normal); - contextOverlayPosition = (cameraPosition + direction * distance) - direction * CONTEXT_OVERLAY_FAR_OFFSET; - contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_FAR_SIZE, CONTEXT_OVERLAY_FAR_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); + float offsetAngle = -CONTEXT_OVERLAY_OFFSET_ANGLE; + if (event.getID() == LEFT_HAND_HW_ID) { + offsetAngle *= -1; + } + contextOverlayPosition = (glm::quat(glm::radians(glm::vec3(0.0f, offsetAngle, 0.0f)))) * + ((cameraPosition + direction * (distance - CONTEXT_OVERLAY_OFFSET_DISTANCE))); + contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_SIZE, CONTEXT_OVERLAY_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); } // Finally, setup and draw the Context Overlay @@ -176,6 +166,7 @@ bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& } } else { if (!_currentEntityWithContextOverlay.isNull()) { + disableEntityHighlight(_currentEntityWithContextOverlay); return destroyContextOverlay(_currentEntityWithContextOverlay, event); } return false; @@ -237,13 +228,13 @@ void ContextOverlayInterface::contextOverlays_hoverLeaveOverlay(const OverlayID& } void ContextOverlayInterface::contextOverlays_hoverEnterEntity(const EntityItemID& entityID, const PointerEvent& event) { - if (contextOverlayFilterPassed(entityID)) { + if (contextOverlayFilterPassed(entityID) && _enabled && event.getID() != MOUSE_HW_ID) { enableEntityHighlight(entityID); } } void ContextOverlayInterface::contextOverlays_hoverLeaveEntity(const EntityItemID& entityID, const PointerEvent& event) { - if (_currentEntityWithContextOverlay != entityID) { + if (_currentEntityWithContextOverlay != entityID && _enabled && event.getID() != MOUSE_HW_ID) { disableEntityHighlight(entityID); } } diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index c14262029e..fddd1fcdb5 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -76,8 +76,6 @@ private: bool _isInMarketplaceInspectionMode { false }; - Setting::Handle _settingSwitch { "inspectionMode", false }; - void openMarketplace(); void enableEntityHighlight(const EntityItemID& entityItemID); void disableEntityHighlight(const EntityItemID& entityItemID); diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 4e7cc08919..cbfcd473dc 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1725,14 +1725,6 @@ int AudioClient::setOutputBufferSize(int numFrames, bool persist) { if (persist) { _outputBufferSizeFrames.set(numFrames); } - - if (_audioOutput) { - // The buffer size can't be adjusted after QAudioOutput::start() has been called, so - // recreate the device by switching to the default. - QAudioDeviceInfo outputDeviceInfo = defaultAudioDeviceForMode(QAudio::AudioOutput); - qCDebug(audioclient) << __FUNCTION__ << "about to send changeDevice signal outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; - emit changeDevice(outputDeviceInfo); // On correct thread, please, as setOutputBufferSize can be called from main thread. - } } return numFrames; } diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt new file mode 100644 index 0000000000..66cf791776 --- /dev/null +++ b/libraries/baking/CMakeLists.txt @@ -0,0 +1,10 @@ +set(TARGET_NAME baking) +setup_hifi_library(Concurrent) + +link_hifi_libraries(shared model networking ktx image fbx) +include_hifi_library_headers(gpu) + +add_dependency_external_projects(draco) +find_package(Draco REQUIRED) +target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${DRACO_INCLUDE_DIRS}) +target_link_libraries(${TARGET_NAME} ${DRACO_LIBRARY} ${DRACO_ENCODER_LIBRARY}) diff --git a/tools/oven/src/Baker.cpp b/libraries/baking/src/Baker.cpp similarity index 62% rename from tools/oven/src/Baker.cpp rename to libraries/baking/src/Baker.cpp index c0cbd8d124..2adedf08a1 100644 --- a/tools/oven/src/Baker.cpp +++ b/libraries/baking/src/Baker.cpp @@ -13,20 +13,49 @@ #include "Baker.h" +bool Baker::shouldStop() { + if (_shouldAbort) { + setWasAborted(true); + return true; + } + + if (hasErrors()) { + return true; + } + + return false; +} + void Baker::handleError(const QString& error) { qCCritical(model_baking).noquote() << error; _errorList.append(error); - emit finished(); + setIsFinished(true); } void Baker::handleErrors(const QStringList& errors) { // we're appending errors, presumably from a baking operation we called // add those to our list and emit that we are finished _errorList.append(errors); - emit finished(); + setIsFinished(true); } void Baker::handleWarning(const QString& warning) { qCWarning(model_baking).noquote() << warning; _warningList.append(warning); } + +void Baker::setIsFinished(bool isFinished) { + _isFinished.store(isFinished); + + if (isFinished) { + emit finished(); + } +} + +void Baker::setWasAborted(bool wasAborted) { + _wasAborted.store(wasAborted); + + if (wasAborted) { + emit aborted(); + } +} diff --git a/tools/oven/src/Baker.h b/libraries/baking/src/Baker.h similarity index 57% rename from tools/oven/src/Baker.h rename to libraries/baking/src/Baker.h index d7107428bf..2da315c9fc 100644 --- a/tools/oven/src/Baker.h +++ b/libraries/baking/src/Baker.h @@ -18,17 +18,30 @@ class Baker : public QObject { Q_OBJECT public: + bool shouldStop(); + bool hasErrors() const { return !_errorList.isEmpty(); } QStringList getErrors() const { return _errorList; } bool hasWarnings() const { return !_warningList.isEmpty(); } QStringList getWarnings() const { return _warningList; } + std::vector getOutputFiles() const { return _outputFiles; } + + virtual void setIsFinished(bool isFinished); + bool isFinished() const { return _isFinished.load(); } + + virtual void setWasAborted(bool wasAborted); + + bool wasAborted() const { return _wasAborted.load(); } + public slots: virtual void bake() = 0; + virtual void abort() { _shouldAbort.store(true); } signals: void finished(); + void aborted(); protected: void handleError(const QString& error); @@ -36,8 +49,17 @@ protected: void handleErrors(const QStringList& errors); + // List of baked output files. For instance, for an FBX this would + // include the .fbx and all of its texture files. + std::vector _outputFiles; + QStringList _errorList; QStringList _warningList; + + std::atomic _isFinished { false }; + + std::atomic _shouldAbort { false }; + std::atomic _wasAborted { false }; }; #endif // hifi_Baker_h diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp new file mode 100644 index 0000000000..3ef291af22 --- /dev/null +++ b/libraries/baking/src/FBXBaker.cpp @@ -0,0 +1,821 @@ +// +// FBXBaker.cpp +// tools/baking/src +// +// Created by Stephen Birarda on 3/30/17. +// Copyright 2017 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 // need this include so we don't get an error looking for std::isnan + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +#include +#include + +#include "ModelBakingLoggingCategory.h" +#include "TextureBaker.h" + +#include "FBXBaker.h" + +#ifdef _WIN32 +#pragma warning( push ) +#pragma warning( disable : 4267 ) +#endif + +#include +#include + +#ifdef _WIN32 +#pragma warning( pop ) +#endif + + +FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter, + const QString& bakedOutputDir, const QString& originalOutputDir) : + _fbxURL(fbxURL), + _bakedOutputDir(bakedOutputDir), + _originalOutputDir(originalOutputDir), + _textureThreadGetter(textureThreadGetter) +{ + +} + +void FBXBaker::abort() { + Baker::abort(); + + // tell our underlying TextureBaker instances to abort + // the FBXBaker will wait until all are aborted before emitting its own abort signal + for (auto& textureBaker : _bakingTextures) { + textureBaker->abort(); + } +} + +void FBXBaker::bake() { + qDebug() << "FBXBaker" << _fbxURL << "bake starting"; + + auto tempDir = PathUtils::generateTemporaryDir(); + + if (tempDir.isEmpty()) { + handleError("Failed to create a temporary directory."); + return; + } + + _tempDir = tempDir; + + _originalFBXFilePath = _tempDir.filePath(_fbxURL.fileName()); + qDebug() << "Made temporary dir " << _tempDir; + qDebug() << "Origin file path: " << _originalFBXFilePath; + + // setup the output folder for the results of this bake + setupOutputFolder(); + + if (shouldStop()) { + return; + } + + connect(this, &FBXBaker::sourceCopyReadyToLoad, this, &FBXBaker::bakeSourceCopy); + + // make a local copy of the FBX file + loadSourceFBX(); +} + +void FBXBaker::bakeSourceCopy() { + // load the scene from the FBX file + importScene(); + + if (shouldStop()) { + return; + } + + // enumerate the models and textures found in the scene and start a bake for them + rewriteAndBakeSceneTextures(); + + if (shouldStop()) { + return; + } + + rewriteAndBakeSceneModels(); + + if (shouldStop()) { + return; + } + + // export the FBX with re-written texture references + exportScene(); + + if (shouldStop()) { + return; + } + + // check if we're already done with textures (in case we had none to re-write) + checkIfTexturesFinished(); +} + +void FBXBaker::setupOutputFolder() { + // make sure there isn't already an output directory using the same name + if (QDir(_bakedOutputDir).exists()) { + qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; + } else { + qCDebug(model_baking) << "Creating FBX output folder" << _bakedOutputDir; + + // attempt to make the output folder + if (!QDir().mkpath(_bakedOutputDir)) { + handleError("Failed to create FBX output folder " + _bakedOutputDir); + return; + } + // attempt to make the output folder + if (!QDir().mkpath(_originalOutputDir)) { + handleError("Failed to create FBX output folder " + _bakedOutputDir); + return; + } + } +} + +void FBXBaker::loadSourceFBX() { + // check if the FBX is local or first needs to be downloaded + if (_fbxURL.isLocalFile()) { + // load up the local file + QFile localFBX { _fbxURL.toLocalFile() }; + + qDebug() << "Local file url: " << _fbxURL << _fbxURL.toString() << _fbxURL.toLocalFile() << ", copying to: " << _originalFBXFilePath; + + if (!localFBX.exists()) { + //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); + handleError("Could not find " + _fbxURL.toString()); + return; + } + + // make a copy in the output folder + if (!_originalOutputDir.isEmpty()) { + qDebug() << "Copying to: " << _originalOutputDir << "/" << _fbxURL.fileName(); + localFBX.copy(_originalOutputDir + "/" + _fbxURL.fileName()); + } + + localFBX.copy(_originalFBXFilePath); + + // emit our signal to start the import of the FBX source copy + emit sourceCopyReadyToLoad(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + networkRequest.setUrl(_fbxURL); + + qCDebug(model_baking) << "Downloading" << _fbxURL; + auto networkReply = networkAccessManager.get(networkRequest); + + connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); + } +} + +void FBXBaker::handleFBXNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded" << _fbxURL; + + // grab the contents of the reply and make a copy in the output folder + QFile copyOfOriginal(_originalFBXFilePath); + + qDebug(model_baking) << "Writing copy of original FBX to" << _originalFBXFilePath << copyOfOriginal.fileName(); + + if (!copyOfOriginal.open(QIODevice::WriteOnly)) { + // add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made + handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to open " + _originalFBXFilePath + ")"); + return; + } + if (copyOfOriginal.write(requestReply->readAll()) == -1) { + handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to write)"); + return; + } + + // close that file now that we are done writing to it + copyOfOriginal.close(); + + if (!_originalOutputDir.isEmpty()) { + copyOfOriginal.copy(_originalOutputDir + "/" + _fbxURL.fileName()); + } + + // emit our signal to start the import of the FBX source copy + emit sourceCopyReadyToLoad(); + } else { + // add an error to our list stating that the FBX could not be downloaded + handleError("Failed to download " + _fbxURL.toString()); + } +} + +void FBXBaker::importScene() { + qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists(); + + QFile fbxFile(_originalFBXFilePath); + if (!fbxFile.open(QIODevice::ReadOnly)) { + handleError("Error opening " + _originalFBXFilePath + " for reading"); + return; + } + + FBXReader reader; + + qCDebug(model_baking) << "Parsing" << _fbxURL; + _rootNode = reader._rootNode = reader.parseFBX(&fbxFile); + _geometry = reader.extractFBXGeometry({}, _fbxURL.toString()); + _textureContent = reader._textureContent; +} + +QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) { + auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + + if (texturePath.startsWith(fbxPath)) { + // texture path is a child of the FBX path, return the texture path without the fbx path + return texturePath.mid(fbxPath.length()); + } else { + // the texture path was not a child of the FBX path, return the empty string + return ""; + } +} + +QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { + // first make sure we have a unique base name for this texture + // in case another texture referenced by this model has the same base name + auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; + + QString bakedTextureFileName { textureFileInfo.completeBaseName() }; + + if (nameMatches > 0) { + // there are already nameMatches texture with this name + // append - and that number to our baked texture file name so that it is unique + bakedTextureFileName += "-" + QString::number(nameMatches); + } + + bakedTextureFileName += BAKED_TEXTURE_EXT; + + // increment the number of name matches + ++nameMatches; + + return bakedTextureFileName; +} + +QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) { + + QUrl urlToTexture; + + auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); + + if (isEmbedded) { + urlToTexture = _fbxURL.toString() + "/" + apparentRelativePath.filePath(); + } else { + if (textureFileInfo.exists() && textureFileInfo.isFile()) { + // set the texture URL to the local texture that we have confirmed exists + urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); + } else { + // external texture that we'll need to download or find + + // this is a relative file path which will require different handling + // depending on the location of the original FBX + if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { + // the absolute path we ran into for the texture in the FBX exists on this machine + // so use that file + urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath()); + } else { + // we didn't find the texture on this machine at the absolute path + // so assume that it is right beside the FBX to match the behaviour of interface + urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); + } + } + } + + return urlToTexture; +} + +void FBXBaker::rewriteAndBakeSceneModels() { + unsigned int meshIndex = 0; + bool hasDeformers { false }; + for (FBXNode& rootChild : _rootNode.children) { + if (rootChild.name == "Objects") { + for (FBXNode& objectChild : rootChild.children) { + if (objectChild.name == "Deformer") { + hasDeformers = true; + break; + } + } + } + if (hasDeformers) { + break; + } + } + for (FBXNode& rootChild : _rootNode.children) { + if (rootChild.name == "Objects") { + for (FBXNode& objectChild : rootChild.children) { + if (objectChild.name == "Geometry") { + + // TODO Pull this out of _geometry instead so we don't have to reprocess it + auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex, false); + auto& mesh = extractedMesh.mesh; + + if (mesh.wasCompressed) { + handleError("Cannot re-bake a file that contains compressed mesh"); + return; + } + + Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size()); + Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); + Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); + + int64_t numTriangles { 0 }; + for (auto& part : mesh.parts) { + if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) { + handleWarning("Found a mesh part with invalid index data, skipping"); + continue; + } + numTriangles += part.quadTrianglesIndices.size() / 3; + numTriangles += part.triangleIndices.size() / 3; + } + + if (numTriangles == 0) { + continue; + } + + draco::TriangleSoupMeshBuilder meshBuilder; + + meshBuilder.Start(numTriangles); + + bool hasNormals { mesh.normals.size() > 0 }; + bool hasColors { mesh.colors.size() > 0 }; + bool hasTexCoords { mesh.texCoords.size() > 0 }; + bool hasTexCoords1 { mesh.texCoords1.size() > 0 }; + bool hasPerFaceMaterials { mesh.parts.size() > 1 }; + bool needsOriginalIndices { hasDeformers }; + + int normalsAttributeID { -1 }; + int colorsAttributeID { -1 }; + int texCoordsAttributeID { -1 }; + int texCoords1AttributeID { -1 }; + int faceMaterialAttributeID { -1 }; + int originalIndexAttributeID { -1 }; + + const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION, + 3, draco::DT_FLOAT32); + if (needsOriginalIndices) { + originalIndexAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX, + 1, draco::DT_INT32); + } + + if (hasNormals) { + normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL, + 3, draco::DT_FLOAT32); + } + if (hasColors) { + colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR, + 3, draco::DT_FLOAT32); + } + if (hasTexCoords) { + texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD, + 2, draco::DT_FLOAT32); + } + if (hasTexCoords1) { + texCoords1AttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1, + 2, draco::DT_FLOAT32); + } + if (hasPerFaceMaterials) { + faceMaterialAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID, + 1, draco::DT_UINT16); + } + + + auto partIndex = 0; + draco::FaceIndex face; + for (auto& part : mesh.parts) { + const auto& matTex = extractedMesh.partMaterialTextures[partIndex]; + uint16_t materialID = matTex.first; + + auto addFace = [&](QVector& indices, int index, draco::FaceIndex face) { + int32_t idx0 = indices[index]; + int32_t idx1 = indices[index + 1]; + int32_t idx2 = indices[index + 2]; + + if (hasPerFaceMaterials) { + meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID); + } + + meshBuilder.SetAttributeValuesForFace(positionAttributeID, face, + &mesh.vertices[idx0], &mesh.vertices[idx1], + &mesh.vertices[idx2]); + + if (needsOriginalIndices) { + meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face, + &mesh.originalIndices[idx0], + &mesh.originalIndices[idx1], + &mesh.originalIndices[idx2]); + } + if (hasNormals) { + meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face, + &mesh.normals[idx0], &mesh.normals[idx1], + &mesh.normals[idx2]); + } + if (hasColors) { + meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face, + &mesh.colors[idx0], &mesh.colors[idx1], + &mesh.colors[idx2]); + } + if (hasTexCoords) { + meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face, + &mesh.texCoords[idx0], &mesh.texCoords[idx1], + &mesh.texCoords[idx2]); + } + if (hasTexCoords1) { + meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face, + &mesh.texCoords1[idx0], &mesh.texCoords1[idx1], + &mesh.texCoords1[idx2]); + } + }; + + for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) { + addFace(part.quadTrianglesIndices, i, face++); + } + + for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) { + addFace(part.triangleIndices, i, face++); + } + + partIndex++; + } + + auto dracoMesh = meshBuilder.Finalize(); + + if (!dracoMesh) { + handleWarning("Failed to finalize the baking of a draco Geometry node"); + continue; + } + + // we need to modify unique attribute IDs for custom attributes + // so the attributes are easily retrievable on the other side + if (hasPerFaceMaterials) { + dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID); + } + + if (hasTexCoords1) { + dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1); + } + + if (needsOriginalIndices) { + dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); + } + + draco::Encoder encoder; + + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); + encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); + encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); + encoder.SetSpeedOptions(0, 5); + + draco::EncoderBuffer buffer; + encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); + + FBXNode dracoMeshNode; + dracoMeshNode.name = "DracoMesh"; + auto value = QVariant::fromValue(QByteArray(buffer.data(), (int) buffer.size())); + dracoMeshNode.properties.append(value); + + objectChild.children.push_back(dracoMeshNode); + + static const std::vector nodeNamesToDelete { + // Node data that is packed into the draco mesh + "Vertices", + "PolygonVertexIndex", + "LayerElementNormal", + "LayerElementColor", + "LayerElementUV", + "LayerElementMaterial", + "LayerElementTexture", + + // Node data that we don't support + "Edges", + "LayerElementTangent", + "LayerElementBinormal", + "LayerElementSmoothing" + }; + auto& children = objectChild.children; + auto it = children.begin(); + while (it != children.end()) { + auto begin = nodeNamesToDelete.begin(); + auto end = nodeNamesToDelete.end(); + if (find(begin, end, it->name) != end) { + it = children.erase(it); + } else { + ++it; + } + } + } + } + } + } +} + +void FBXBaker::rewriteAndBakeSceneTextures() { + using namespace image::TextureUsage; + QHash textureTypes; + + // enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID + for (const auto& material : _geometry->materials) { + if (material.normalTexture.isBumpmap) { + textureTypes[material.normalTexture.id] = BUMP_TEXTURE; + } else { + textureTypes[material.normalTexture.id] = NORMAL_TEXTURE; + } + + textureTypes[material.albedoTexture.id] = ALBEDO_TEXTURE; + textureTypes[material.glossTexture.id] = GLOSS_TEXTURE; + textureTypes[material.roughnessTexture.id] = ROUGHNESS_TEXTURE; + textureTypes[material.specularTexture.id] = SPECULAR_TEXTURE; + textureTypes[material.metallicTexture.id] = METALLIC_TEXTURE; + textureTypes[material.emissiveTexture.id] = EMISSIVE_TEXTURE; + textureTypes[material.occlusionTexture.id] = OCCLUSION_TEXTURE; + textureTypes[material.lightmapTexture.id] = LIGHTMAP_TEXTURE; + } + + // enumerate the children of the root node + for (FBXNode& rootChild : _rootNode.children) { + + if (rootChild.name == "Objects") { + + // enumerate the objects + auto object = rootChild.children.begin(); + while (object != rootChild.children.end()) { + if (object->name == "Texture") { + + // double check that we didn't get an abort while baking the last texture + if (shouldStop()) { + return; + } + + // enumerate the texture children + for (FBXNode& textureChild : object->children) { + + if (textureChild.name == "RelativeFilename") { + + // use QFileInfo to easily split up the existing texture filename into its components + QString fbxTextureFileName { textureChild.properties.at(0).toByteArray() }; + QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") }; + + if (textureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) { + // re-baking an FBX that already references baked textures is a fail + // so we add an error and return from here + handleError("Cannot re-bake a file that references compressed textures"); + + return; + } + + + // make sure this texture points to something and isn't one we've already re-mapped + if (!textureFileInfo.filePath().isEmpty()) { + // check if this was an embedded texture we have already have in-memory content for + auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit()); + + // figure out the URL to this texture, embedded or external + auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName, + !textureContent.isNull()); + + QString bakedTextureFileName; + if (_remappedTexturePaths.contains(urlToTexture)) { + bakedTextureFileName = _remappedTexturePaths[urlToTexture]; + } else { + // construct the new baked texture file name and file path + // ensuring that the baked texture will have a unique name + // even if there was another texture with the same name at a different path + bakedTextureFileName = createBakedTextureFileName(textureFileInfo); + _remappedTexturePaths[urlToTexture] = bakedTextureFileName; + } + + qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName + << "to" << bakedTextureFileName; + + QString bakedTextureFilePath { + _bakedOutputDir + "/" + bakedTextureFileName + }; + + // write the new filename into the FBX scene + textureChild.properties[0] = bakedTextureFileName.toLocal8Bit(); + + if (!_bakingTextures.contains(urlToTexture)) { + _outputFiles.push_back(bakedTextureFilePath); + + // grab the ID for this texture so we can figure out the + // texture type from the loaded materials + QString textureID { object->properties[0].toByteArray() }; + auto textureType = textureTypes[textureID]; + + // bake this texture asynchronously + bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent); + } + } + } + } + + ++object; + + } else if (object->name == "Video") { + // this is an embedded texture, we need to remove it from the FBX + object = rootChild.children.erase(object); + } else { + ++object; + } + } + } + } +} + +void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, + const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { + // start a bake for this texture and add it to our list to keep track of + QSharedPointer bakingTexture { + new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent), + &TextureBaker::deleteLater + }; + + // make sure we hear when the baking texture is done or aborted + connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture); + connect(bakingTexture.data(), &TextureBaker::aborted, this, &FBXBaker::handleAbortedTexture); + + // keep a shared pointer to the baking texture + _bakingTextures.insert(textureURL, bakingTexture); + + // start baking the texture on one of our available worker threads + bakingTexture->moveToThread(_textureThreadGetter()); + QMetaObject::invokeMethod(bakingTexture.data(), "bake"); +} + +void FBXBaker::handleBakedTexture() { + TextureBaker* bakedTexture = qobject_cast(sender()); + + // make sure we haven't already run into errors, and that this is a valid texture + if (bakedTexture) { + if (!shouldStop()) { + if (!bakedTexture->hasErrors()) { + if (!_originalOutputDir.isEmpty()) { + // we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture + + // use the path to the texture being baked to determine if this was an embedded or a linked texture + + // it is embeddded if the texure being baked was inside a folder with the name of the FBX + // since that is the fake URL we provide when baking external textures + + if (!_fbxURL.isParentOf(bakedTexture->getTextureURL())) { + // for linked textures we want to save a copy of original texture beside the original FBX + + qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); + + // check if we have a relative path to use for the texture + auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL()); + + QFile originalTextureFile { + _originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName() + }; + + if (relativeTexturePath.length() > 0) { + // make the folders needed by the relative path + } + + if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) { + qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName() + << "for" << _fbxURL; + } else { + handleError("Could not save original external texture " + originalTextureFile.fileName() + + " for " + _fbxURL.toString()); + return; + } + } + } + + + // now that this texture has been baked and handled, we can remove that TextureBaker from our hash + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } else { + // there was an error baking this texture - add it to our list of errors + _errorList.append(bakedTexture->getErrors()); + + // we don't emit finished yet so that the other textures can finish baking first + _pendingErrorEmission = true; + + // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list + _bakingTextures.remove(bakedTexture->getTextureURL()); + + // abort any other ongoing texture bakes since we know we'll end up failing + for (auto& bakingTexture : _bakingTextures) { + bakingTexture->abort(); + } + + checkIfTexturesFinished(); + } + } else { + // we have errors to attend to, so we don't do extra processing for this texture + // but we do need to remove that TextureBaker from our list + // and then check if we're done with all textures + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } + } +} + +void FBXBaker::handleAbortedTexture() { + // grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore + TextureBaker* bakedTexture = qobject_cast(sender()); + + if (bakedTexture) { + _bakingTextures.remove(bakedTexture->getTextureURL()); + } + + // since a texture we were baking aborted, our status is also aborted + _shouldAbort.store(true); + + // abort any other ongoing texture bakes since we know we'll end up failing + for (auto& bakingTexture : _bakingTextures) { + bakingTexture->abort(); + } + + checkIfTexturesFinished(); +} + +void FBXBaker::exportScene() { + // save the relative path to this FBX inside our passed output folder + auto fileName = _fbxURL.fileName(); + auto baseName = fileName.left(fileName.lastIndexOf('.')); + auto bakedFilename = baseName + BAKED_FBX_EXTENSION; + + _bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename; + + auto fbxData = FBXWriter::encodeFBX(_rootNode); + + QFile bakedFile(_bakedFBXFilePath); + + if (!bakedFile.open(QIODevice::WriteOnly)) { + handleError("Error opening " + _bakedFBXFilePath + " for writing"); + return; + } + + bakedFile.write(fbxData); + + _outputFiles.push_back(_bakedFBXFilePath); + + qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath; +} + +void FBXBaker::checkIfTexturesFinished() { + // check if we're done everything we need to do for this FBX + // and emit our finished signal if we're done + + if (_bakingTextures.isEmpty()) { + if (shouldStop()) { + // if we're checking for completion but we have errors + // that means one or more of our texture baking operations failed + + if (_pendingErrorEmission) { + setIsFinished(true); + } + + return; + } else { + qCDebug(model_baking) << "Finished baking, emitting finsihed" << _fbxURL; + + setIsFinished(true); + } + } +} + +void FBXBaker::setWasAborted(bool wasAborted) { + if (wasAborted != _wasAborted.load()) { + Baker::setWasAborted(wasAborted); + + if (wasAborted) { + qCDebug(model_baking) << "Aborted baking" << _fbxURL; + } + } +} diff --git a/tools/oven/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h similarity index 57% rename from tools/oven/src/FBXBaker.h rename to libraries/baking/src/FBXBaker.h index bcfebbe2a8..ad8284bfa8 100644 --- a/tools/oven/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -1,6 +1,6 @@ // // FBXBaker.h -// tools/oven/src +// tools/baking/src // // Created by Stephen Birarda on 3/30/17. // Copyright 2017 High Fidelity, Inc. @@ -20,33 +20,30 @@ #include "Baker.h" #include "TextureBaker.h" +#include "ModelBakingLoggingCategory.h" + #include -namespace fbxsdk { - class FbxManager; - class FbxProperty; - class FbxScene; - class FbxFileTexture; -} +#include static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; -using FBXSDKManagerUniquePointer = std::unique_ptr>; using TextureBakerThreadGetter = std::function; class FBXBaker : public Baker { Q_OBJECT public: - FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, - TextureBakerThreadGetter textureThreadGetter, bool copyOriginals = true); + FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter, + const QString& bakedOutputDir, const QString& originalOutputDir = ""); QUrl getFBXUrl() const { return _fbxURL; } - QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } + QString getBakedFBXFilePath() const { return _bakedFBXFilePath; } + + virtual void setWasAborted(bool wasAborted) override; public slots: - // all calls to FBXBaker::bake for FBXBaker instances must be from the same thread - // because the Autodesk SDK will cause a crash if it is called from multiple threads virtual void bake() override; + virtual void abort() override; signals: void sourceCopyReadyToLoad(); @@ -55,46 +52,49 @@ private slots: void bakeSourceCopy(); void handleFBXNetworkReply(); void handleBakedTexture(); + void handleAbortedTexture(); private: void setupOutputFolder(); void loadSourceFBX(); - void bakeCopiedFBX(); - void importScene(); + void rewriteAndBakeSceneModels(); void rewriteAndBakeSceneTextures(); void exportScene(); void removeEmbeddedMediaFolder(); - void possiblyCleanupOriginals(); void checkIfTexturesFinished(); QString createBakedTextureFileName(const QFileInfo& textureFileInfo); - QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); + QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); - void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir); - - QString pathToCopyOfOriginal() const; + void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir, + const QString& bakedFilename, const QByteArray& textureContent = QByteArray()); QUrl _fbxURL; - QString _fbxName; - - QString _baseOutputPath; - QString _uniqueOutputPath; - QString _bakedFBXRelativePath; - static FBXSDKManagerUniquePointer _sdkManager; - fbxsdk::FbxScene* _scene { nullptr }; + FBXNode _rootNode; + FBXGeometry* _geometry; + QHash _textureContent; + + QString _bakedFBXFilePath; + + QString _bakedOutputDir; + + // If set, the original FBX and textures will also be copied here + QString _originalOutputDir; + + QDir _tempDir; + QString _originalFBXFilePath; QMultiHash> _bakingTextures; QHash _textureNameMatchCount; + QHash _remappedTexturePaths; TextureBakerThreadGetter _textureThreadGetter; - bool _copyOriginals { true }; - bool _pendingErrorEmission { false }; }; diff --git a/tools/oven/src/ModelBakingLoggingCategory.cpp b/libraries/baking/src/ModelBakingLoggingCategory.cpp similarity index 100% rename from tools/oven/src/ModelBakingLoggingCategory.cpp rename to libraries/baking/src/ModelBakingLoggingCategory.cpp diff --git a/tools/oven/src/ModelBakingLoggingCategory.h b/libraries/baking/src/ModelBakingLoggingCategory.h similarity index 100% rename from tools/oven/src/ModelBakingLoggingCategory.h rename to libraries/baking/src/ModelBakingLoggingCategory.h diff --git a/tools/oven/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp similarity index 71% rename from tools/oven/src/TextureBaker.cpp rename to libraries/baking/src/TextureBaker.cpp index 70df511d2c..1a320efabc 100644 --- a/tools/oven/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -25,22 +25,48 @@ const QString BAKED_TEXTURE_EXT = ".ktx"; -TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory) : +TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, + const QDir& outputDirectory, const QString& bakedFilename, + const QByteArray& textureContent) : _textureURL(textureURL), + _originalTexture(textureContent), _textureType(textureType), - _outputDirectory(outputDirectory) + _outputDirectory(outputDirectory), + _bakedTextureFileName(bakedFilename) { - // figure out the baked texture filename - auto originalFilename = textureURL.fileName(); - _bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT; + if (bakedFilename.isEmpty()) { + // figure out the baked texture filename + auto originalFilename = textureURL.fileName(); + _bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT; + } } void TextureBaker::bake() { // once our texture is loaded, kick off a the processing connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture); - // first load the texture (either locally or remotely) - loadTexture(); + if (_originalTexture.isEmpty()) { + // first load the texture (either locally or remotely) + loadTexture(); + } else { + // we already have a texture passed to us, use that + emit originalTextureLoaded(); + } +} + +void TextureBaker::abort() { + Baker::abort(); + + // flip our atomic bool so any ongoing texture processing is stopped + _abortProcessing.store(true); +} + +const QStringList TextureBaker::getSupportedFormats() { + auto formats = QImageReader::supportedImageFormats(); + QStringList stringFormats; + std::transform(formats.begin(), formats.end(), std::back_inserter(stringFormats), + [](QByteArray& format) -> QString { return format; }); + return stringFormats; } void TextureBaker::loadTexture() { @@ -96,7 +122,11 @@ void TextureBaker::handleTextureNetworkReply() { void TextureBaker::processTexture() { auto processedTexture = image::processImage(_originalTexture, _textureURL.toString().toStdString(), - ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType); + ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, _abortProcessing); + + if (shouldStop()) { + return; + } if (!processedTexture) { handleError("Could not process texture " + _textureURL.toString()); @@ -120,12 +150,21 @@ void TextureBaker::processTexture() { const size_t length = memKTX->_storage->size(); // attempt to write the baked texture to the destination file path - QFile bakedTextureFile { _outputDirectory.absoluteFilePath(_bakedTextureFileName) }; + auto filePath = _outputDirectory.absoluteFilePath(_bakedTextureFileName); + QFile bakedTextureFile { filePath }; if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) { handleError("Could not write baked texture for " + _textureURL.toString()); + } else { + _outputFiles.push_back(filePath); } qCDebug(model_baking) << "Baked texture" << _textureURL; - emit finished(); + setIsFinished(true); +} + +void TextureBaker::setWasAborted(bool wasAborted) { + Baker::setWasAborted(wasAborted); + + qCDebug(model_baking) << "Aborted baking" << _textureURL; } diff --git a/tools/oven/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h similarity index 77% rename from tools/oven/src/TextureBaker.h rename to libraries/baking/src/TextureBaker.h index ee1e968f20..b2e86b2b5b 100644 --- a/tools/oven/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -15,6 +15,7 @@ #include #include #include +#include #include @@ -26,7 +27,11 @@ class TextureBaker : public Baker { Q_OBJECT public: - TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory); + TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, + const QDir& outputDirectory, const QString& bakedFilename = QString(), + const QByteArray& textureContent = QByteArray()); + + static const QStringList getSupportedFormats(); const QByteArray& getOriginalTexture() const { return _originalTexture; } @@ -35,8 +40,11 @@ public: QString getDestinationFilePath() const { return _outputDirectory.absoluteFilePath(_bakedTextureFileName); } QString getBakedTextureFileName() const { return _bakedTextureFileName; } + virtual void setWasAborted(bool wasAborted) override; + public slots: virtual void bake() override; + virtual void abort() override; signals: void originalTextureLoaded(); @@ -54,6 +62,8 @@ private: QDir _outputDirectory; QString _bakedTextureFileName; + + std::atomic _abortProcessing { false }; }; #endif // hifi_TextureBaker_h diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index e8ad163964..c29d92bae9 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -159,6 +159,88 @@ void EntityTreeRenderer::shutdown() { clear(); // always clear() on shutdown } +void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction) { + // Clear any expired entities + // FIXME should be able to use std::remove_if, but it fails due to some + // weird compilation error related to EntityItemID assignment operators + for (auto itr = _entitiesToAdd.begin(); _entitiesToAdd.end() != itr; ) { + if (itr->second.expired()) { + _entitiesToAdd.erase(itr++); + } else { + ++itr; + } + } + + if (!_entitiesToAdd.empty()) { + std::unordered_set processedIds; + for (const auto& entry : _entitiesToAdd) { + auto entity = entry.second.lock(); + if (!entity) { + continue; + } + + // Path to the parent transforms is not valid, + // don't add to the scene graph yet + if (!entity->isParentPathComplete()) { + continue; + } + + auto entityID = entity->getEntityItemID(); + processedIds.insert(entityID); + auto renderable = EntityRenderer::addToScene(*this, entity, scene, transaction); + if (renderable) { + _entitiesInScene.insert({ entityID, renderable }); + } + } + + + if (!processedIds.empty()) { + for (const auto& processedId : processedIds) { + _entitiesToAdd.erase(processedId); + } + } + } +} + +void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction) { + std::unordered_set changedEntities; + _changedEntitiesGuard.withWriteLock([&] { +#if 0 + // FIXME Weird build failure in latest VC update that fails to compile when using std::swap + changedEntities.swap(_changedEntities); +#else + changedEntities.insert(_changedEntities.begin(), _changedEntities.end()); + _changedEntities.clear(); +#endif + }); + + for (const auto& entityId : changedEntities) { + auto renderable = renderableForEntityId(entityId); + if (!renderable) { + continue; + } + _renderablesToUpdate.insert({ entityId, renderable }); + } + + // NOTE: Looping over all the entity renderers is likely to be a bottleneck in the future + // Currently, this is necessary because the model entity loading logic requires constant polling + // This was working fine because the entity server used to send repeated updates as your view changed, + // but with the improved entity server logic (PR 11141), updateInScene (below) would not be triggered enough + for (const auto& entry : _entitiesInScene) { + const auto& renderable = entry.second; + if (renderable) { + renderable->update(scene, transaction); + } + } + if (!_renderablesToUpdate.empty()) { + for (const auto& entry : _renderablesToUpdate) { + const auto& renderable = entry.second; + renderable->updateInScene(scene, transaction); + } + _renderablesToUpdate.clear(); + } +} + void EntityTreeRenderer::update(bool simulate) { PerformanceTimer perfTimer("ETRupdate"); if (_tree && !_shuttingDown) { @@ -178,30 +260,11 @@ void EntityTreeRenderer::update(bool simulate) { } } - std::unordered_set changedEntities; - // FIXME Weird build failure in latest VC update that fails to compile when using std::swap - _changedEntitiesGuard.withWriteLock([&] { - changedEntities.insert(_changedEntities.begin(), _changedEntities.end()); - _changedEntities.clear(); - }); - - for (const auto& entityId : changedEntities) { - auto renderable = renderableForEntityId(entityId); - if (!renderable) { - continue; - } - - _renderablesToUpdate.insert({ entityId, renderable }); - } - auto scene = _viewState->getMain3DScene(); - if (scene && !_renderablesToUpdate.empty()) { + if (scene) { render::Transaction transaction; - for (const auto& entry : _renderablesToUpdate) { - const auto& renderable = entry.second; - renderable->updateInScene(scene, transaction); - } - _renderablesToUpdate.clear(); + addPendingEntities(scene, transaction); + updateChangedEntities(scene, transaction); scene->enqueueTransaction(transaction); } } @@ -689,8 +752,12 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { } void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { + // If it's in the pending queue, remove it + _entitiesToAdd.erase(entityID); + auto itr = _entitiesInScene.find(entityID); if (_entitiesInScene.end() == itr) { + // Not in the scene, and no longer potentially in the pending queue, we're done return; } @@ -725,25 +792,10 @@ void EntityTreeRenderer::addingEntity(const EntityItemID& entityID) { checkAndCallPreload(entityID); auto entity = std::static_pointer_cast(_tree)->findEntityByID(entityID); if (entity) { - addEntityToScene(entity); + _entitiesToAdd.insert({ entity->getEntityItemID(), entity }); } } -void EntityTreeRenderer::addEntityToScene(const EntityItemPointer& entity) { - // here's where we add the entity payload to the scene - auto scene = _viewState->getMain3DScene(); - if (!scene) { - qCWarning(entitiesrenderer) << "EntityTreeRenderer::addEntityToScene(), Unexpected null scene, possibly during application shutdown"; - return; - } - - auto renderable = EntityRenderer::addToScene(*this, entity, scene); - if (renderable) { - _entitiesInScene[entity->getEntityItemID()] = renderable; - } -} - - void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, bool reload) { checkAndCallPreload(entityID, reload, true); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 05ddaa6b0a..1b1d46d50c 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -36,6 +36,7 @@ namespace render { namespace entities { class EntityRenderer; using EntityRendererPointer = std::shared_ptr; using EntityRendererWeakPointer = std::weak_ptr; + } } // Allow the use of std::unordered_map with QUuid keys @@ -157,12 +158,13 @@ protected: } private: + void addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction); + void updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction); EntityRendererPointer renderableForEntity(const EntityItemPointer& entity) const { return renderableForEntityId(entity->getID()); } render::ItemID renderableIdForEntity(const EntityItemPointer& entity) const { return renderableIdForEntityId(entity->getID()); } void resetEntitiesScriptEngine(); - void addEntityToScene(const EntityItemPointer& entity); bool findBestZoneAndMaybeContainingEntities(QVector* entitiesContainingAvatar = nullptr); bool applyLayeredZones(); @@ -260,6 +262,7 @@ private: std::unordered_map _renderablesToUpdate; std::unordered_map _entitiesInScene; + std::unordered_map _entitiesToAdd; // For Scene.shouldRenderEntities QList _entityIDsLastInScene; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index ceaf073e08..ea514d3181 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -187,7 +187,7 @@ void EntityRenderer::render(RenderArgs* args) { // Methods called by the EntityTreeRenderer // -EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene) { +EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene, Transaction& transaction) { EntityRenderer::Pointer result; if (!entity) { return result; @@ -245,9 +245,7 @@ EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer, } if (result) { - Transaction transaction; result->addToScene(scene, transaction); - scene->enqueueTransaction(transaction); } return result; @@ -293,6 +291,18 @@ void EntityRenderer::updateInScene(const ScenePointer& scene, Transaction& trans }); } +void EntityRenderer::update(const ScenePointer& scene, Transaction& transaction) { + if (!isValidRenderItem()) { + return; + } + + if (!needsUpdate()) { + return; + } + + doUpdate(scene, transaction, _entity); +} + // // Internal methods // @@ -306,6 +316,11 @@ bool EntityRenderer::needsRenderUpdate() const { return needsRenderUpdateFromEntity(_entity); } +// Returns true if the item needs to have update called +bool EntityRenderer::needsUpdate() const { + return needsUpdateFromEntity(_entity); +} + // Returns true if the item in question needs to have updateInScene called because of changes in the entity bool EntityRenderer::needsRenderUpdateFromEntity(const EntityItemPointer& entity) const { bool success = false; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 629841d76e..56cb39252f 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -30,7 +30,7 @@ class EntityRenderer : public QObject, public std::enable_shared_from_thiscopyAnimationJointDataToModel(); } -bool ModelEntityRenderer::needsRenderUpdate() const { +bool ModelEntityRenderer::needsUpdate() const { ModelPointer model; withReadLock([&] { model = _model; }); + if (_modelJustLoaded) { + return true; + } + if (model) { if (_needsJointSimulation || _moving || _animating) { return true; @@ -1057,10 +1061,10 @@ bool ModelEntityRenderer::needsRenderUpdate() const { return true; } } - return Parent::needsRenderUpdate(); + return Parent::needsUpdate(); } -bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { +bool ModelEntityRenderer::needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const { if (resultWithReadLock([&] { if (entity->hasModel() != _hasModel) { return true; @@ -1122,7 +1126,7 @@ bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoin return false; } -void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { +void ModelEntityRenderer::doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { if (_hasModel != entity->hasModel()) { _hasModel = entity->hasModel(); } @@ -1148,9 +1152,11 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce return; } + _modelJustLoaded = false; // Check for addition if (_hasModel && !(bool)_model) { model = std::make_shared(nullptr, entity.get()); + connect(model.get(), &Model::setURLFinished, this, &ModelEntityRenderer::handleModelLoaded); model->setLoadingPriority(EntityTreeRenderer::getEntityLoadingPriority(*entity)); model->init(); entity->setModel(model); @@ -1175,8 +1181,8 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce properties.setLastEdited(usecTimestampNow()); // we must set the edit time since we're editing it auto extents = model->getMeshExtents(); properties.setDimensions(extents.maximum - extents.minimum); - qCDebug(entitiesrenderer) << "Autoresizing" - << (!entity->getName().isEmpty() ? entity->getName() : entity->getModelURL()) + qCDebug(entitiesrenderer) << "Autoresizing" + << (!entity->getName().isEmpty() ? entity->getName() : entity->getModelURL()) << "from mesh extents"; QMetaObject::invokeMethod(DependencyManager::get().data(), "editEntity", @@ -1203,7 +1209,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce entity->updateModelBounds(); } - if (model->isVisible() != _visible) { // FIXME: this seems like it could be optimized if we tracked our last known visible state in // the renderable item. As it stands now the model checks it's visible/invisible state @@ -1234,7 +1239,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce }); } - if (_animating) { if (!jointsMapped()) { mapJoints(entity, model->getJointNames()); @@ -1243,6 +1247,12 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce } } +void ModelEntityRenderer::handleModelLoaded(bool success) { + if (success) { + _modelJustLoaded = true; + } +} + // NOTE: this only renders the "meta" portion of the Model, namely it renders debugging items void ModelEntityRenderer::doRender(RenderArgs* args) { PROFILE_RANGE(render_detail, "MetaModelRender"); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index b9c751761d..ad0afeee0a 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -138,10 +138,10 @@ protected: virtual ItemKey getKey() override; virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override; - virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; - virtual bool needsRenderUpdate() const override; + virtual bool needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; + virtual bool needsUpdate() const override; virtual void doRender(RenderArgs* args) override; - virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; + virtual void doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; private: void animate(const TypedEntityPointer& entity); @@ -151,6 +151,7 @@ private: // Transparency is handled in ModelMeshPartPayload virtual bool isTransparent() const override { return false; } + bool _modelJustLoaded { false }; bool _hasModel { false }; ::ModelPointer _model; GeometryResource::Pointer _compoundShapeResource; @@ -178,6 +179,9 @@ private: bool _animating { false }; uint64_t _lastAnimated { 0 }; float _currentFrame { 0 }; + +private slots: + void handleModelLoaded(bool success); }; } } // namespace diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 8757bcbb0f..f4e4c0ea8f 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -102,7 +102,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { glm::quat orientation = glm::quat(glm::vec3(0.0f, yawRotation, 0.0f)); transformToTopLeft.setRotation(orientation); } - transformToTopLeft.postTranslate(glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left + transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left transformToTopLeft.setScale(1.0f); // Use a scale of one so that the text is not deformed batch.setModelTransform(transformToTopLeft); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 8b5feb15f0..5dc75dad08 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -140,6 +140,11 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene _webSurface->resize(QSize(windowSize.x, windowSize.y)); } +void WebEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { + Parent::doRenderUpdateAsynchronousTyped(entity); + _modelTransform.postScale(entity->getDimensions()); +} + void WebEntityRenderer::doRender(RenderArgs* args) { withWriteLock([&] { _lastRenderTime = usecTimestampNow(); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 4b7e7e25a1..a67eb39670 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -29,6 +29,7 @@ protected: virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; + virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; virtual void doRender(RenderArgs* args) override; virtual bool isTransparent() const override; diff --git a/libraries/fbx/CMakeLists.txt b/libraries/fbx/CMakeLists.txt index d9c073f213..7cead5aa4f 100644 --- a/libraries/fbx/CMakeLists.txt +++ b/libraries/fbx/CMakeLists.txt @@ -1,4 +1,10 @@ set(TARGET_NAME fbx) setup_hifi_library() -link_hifi_libraries(shared model networking) -include_hifi_library_headers(gpu) + +link_hifi_libraries(shared model networking image) +include_hifi_library_headers(gpu image) + +add_dependency_external_projects(draco) +find_package(Draco REQUIRED) +target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${DRACO_INCLUDE_DIRS}) +target_link_libraries(${TARGET_NAME} ${DRACO_LIBRARY} ${DRACO_ENCODER_LIBRARY}) diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h new file mode 100644 index 0000000000..7d3328a2dd --- /dev/null +++ b/libraries/fbx/src/FBX.h @@ -0,0 +1,345 @@ +// +// FBX.h +// libraries/fbx/src +// +// Created by Ryan Huffman on 9/5/17. +// Copyright 2017 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_FBX_h_ +#define hifi_FBX_h_ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include + +static const QByteArray FBX_BINARY_PROLOG = "Kaydara FBX Binary "; +static const int FBX_HEADER_BYTES_BEFORE_VERSION = 23; +static const quint32 FBX_VERSION_2015 = 7400; +static const quint32 FBX_VERSION_2016 = 7500; + +static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; +static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; +static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1; +static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2; + +static const int32_t FBX_PROPERTY_UNCOMPRESSED_FLAG = 0; +static const int32_t FBX_PROPERTY_COMPRESSED_FLAG = 1; + +class FBXNode; +using FBXNodeList = QList; + + +/// A node within an FBX document. +class FBXNode { +public: + QByteArray name; + QVariantList properties; + FBXNodeList children; +}; + + +/// A single blendshape extracted from an FBX document. +class FBXBlendshape { +public: + QVector indices; + QVector vertices; + QVector normals; +}; + +struct FBXJointShapeInfo { + // same units and frame as FBXJoint.translation + glm::vec3 avgPoint; + std::vector dots; + std::vector points; + std::vector debugLines; +}; + +/// A single joint (transformation node) extracted from an FBX document. +class FBXJoint { +public: + + FBXJointShapeInfo shapeInfo; + QVector freeLineage; + bool isFree; + int parentIndex; + float distanceToParent; + + // http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html + + glm::vec3 translation; // T + glm::mat4 preTransform; // Roff * Rp + glm::quat preRotation; // Rpre + glm::quat rotation; // R + glm::quat postRotation; // Rpost + glm::mat4 postTransform; // Rp-1 * Soff * Sp * S * Sp-1 + + // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) + + glm::mat4 transform; + glm::vec3 rotationMin; // radians + glm::vec3 rotationMax; // radians + glm::quat inverseDefaultRotation; + glm::quat inverseBindRotation; + glm::mat4 bindTransform; + QString name; + bool isSkeletonJoint; + bool bindTransformFoundInCluster; + + // geometric offset is applied in local space but does NOT affect children. + bool hasGeometricOffset; + glm::vec3 geometricTranslation; + glm::quat geometricRotation; + glm::vec3 geometricScaling; +}; + + +/// A single binding to a joint in an FBX document. +class FBXCluster { +public: + + int jointIndex; + glm::mat4 inverseBindMatrix; +}; + +const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; + +/// A texture map in an FBX document. +class FBXTexture { +public: + QString id; + QString name; + QByteArray filename; + QByteArray content; + + Transform transform; + int maxNumPixels { MAX_NUM_PIXELS_FOR_FBX_TEXTURE }; + int texcoordSet; + QString texcoordSetName; + + bool isBumpmap{ false }; + + bool isNull() const { return name.isEmpty() && filename.isEmpty() && content.isEmpty(); } +}; + +/// A single part of a mesh (with the same material). +class FBXMeshPart { +public: + + QVector quadIndices; // original indices from the FBX mesh + QVector quadTrianglesIndices; // original indices from the FBX mesh of the quad converted as triangles + QVector triangleIndices; // original indices from the FBX mesh + + QString materialID; +}; + +class FBXMaterial { +public: + FBXMaterial() {}; + FBXMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor, + float shininess, float opacity) : + diffuseColor(diffuseColor), + specularColor(specularColor), + emissiveColor(emissiveColor), + shininess(shininess), + opacity(opacity) {} + + void getTextureNames(QSet& textureList) const; + void setMaxNumPixelsPerTexture(int maxNumPixels); + + glm::vec3 diffuseColor{ 1.0f }; + float diffuseFactor{ 1.0f }; + glm::vec3 specularColor{ 0.02f }; + float specularFactor{ 1.0f }; + + glm::vec3 emissiveColor{ 0.0f }; + float emissiveFactor{ 0.0f }; + + float shininess{ 23.0f }; + float opacity{ 1.0f }; + + float metallic{ 0.0f }; + float roughness{ 1.0f }; + float emissiveIntensity{ 1.0f }; + float ambientFactor{ 1.0f }; + + QString materialID; + QString name; + QString shadingModel; + model::MaterialPointer _material; + + FBXTexture normalTexture; + FBXTexture albedoTexture; + FBXTexture opacityTexture; + FBXTexture glossTexture; + FBXTexture roughnessTexture; + FBXTexture specularTexture; + FBXTexture metallicTexture; + FBXTexture emissiveTexture; + FBXTexture occlusionTexture; + FBXTexture scatteringTexture; + FBXTexture lightmapTexture; + glm::vec2 lightmapParams{ 0.0f, 1.0f }; + + + bool isPBSMaterial{ false }; + // THe use XXXMap are not really used to drive which map are going or not, debug only + bool useNormalMap{ false }; + bool useAlbedoMap{ false }; + bool useOpacityMap{ false }; + bool useRoughnessMap{ false }; + bool useSpecularMap{ false }; + bool useMetallicMap{ false }; + bool useEmissiveMap{ false }; + bool useOcclusionMap{ false }; + + bool needTangentSpace() const; +}; + +/// A single mesh (with optional blendshapes) extracted from an FBX document. +class FBXMesh { +public: + + QVector parts; + + QVector vertices; + QVector normals; + QVector tangents; + QVector colors; + QVector texCoords; + QVector texCoords1; + QVector clusterIndices; + QVector clusterWeights; + QVector originalIndices; + + QVector clusters; + + Extents meshExtents; + glm::mat4 modelTransform; + + QVector blendshapes; + + unsigned int meshIndex; // the order the meshes appeared in the object file + + model::MeshPointer _mesh; + bool wasCompressed { false }; +}; + +class ExtractedMesh { +public: + FBXMesh mesh; + QMultiHash newIndices; + QVector > blendshapeIndexMaps; + QVector > partMaterialTextures; + QHash texcoordSetMap; +}; + +/// A single animation frame extracted from an FBX document. +class FBXAnimationFrame { +public: + QVector rotations; + QVector translations; +}; + +/// A light in an FBX document. +class FBXLight { +public: + QString name; + Transform transform; + float intensity; + float fogValue; + glm::vec3 color; + + FBXLight() : + name(), + transform(), + intensity(1.0f), + fogValue(0.0f), + color(1.0f) + {} +}; + +Q_DECLARE_METATYPE(FBXAnimationFrame) +Q_DECLARE_METATYPE(QVector) + +/// A set of meshes extracted from an FBX document. +class FBXGeometry { +public: + using Pointer = std::shared_ptr; + + QString originalURL; + QString author; + QString applicationName; ///< the name of the application that generated the model + + QVector joints; + QHash jointIndices; ///< 1-based, so as to more easily detect missing indices + bool hasSkeletonJoints; + + QVector meshes; + + QHash materials; + + glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file + + int leftEyeJointIndex = -1; + int rightEyeJointIndex = -1; + int neckJointIndex = -1; + int rootJointIndex = -1; + int leanJointIndex = -1; + int headJointIndex = -1; + int leftHandJointIndex = -1; + int rightHandJointIndex = -1; + int leftToeJointIndex = -1; + int rightToeJointIndex = -1; + + float leftEyeSize = 0.0f; // Maximum mesh extents dimension + float rightEyeSize = 0.0f; + + QVector humanIKJointIndices; + + glm::vec3 palmDirection; + + glm::vec3 neckPivot; + + Extents bindExtents; + Extents meshExtents; + + QVector animationFrames; + + int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; } + QStringList getJointNames() const; + + bool hasBlendedMeshes() const; + + /// Returns the unscaled extents of the model's mesh + Extents getUnscaledMeshExtents() const; + + bool convexHullContains(const glm::vec3& point) const; + + QHash meshIndicesToModelNames; + + /// given a meshIndex this will return the name of the model that mesh belongs to if known + QString getModelNameOfMesh(int meshIndex) const; + + QList blendshapeChannelNames; +}; + +Q_DECLARE_METATYPE(FBXGeometry) +Q_DECLARE_METATYPE(FBXGeometry::Pointer) + +#endif // hifi_FBX_h_ diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index cd313dbd05..e4fea00a34 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -168,7 +168,8 @@ QString getID(const QVariantList& properties, int index = 0) { return processID(properties.at(index).toString()); } -const char* HUMANIK_JOINTS[] = { +/// The names of the joints in the Maya HumanIK rig +static const std::array HUMANIK_JOINTS = {{ "RightHand", "RightForeArm", "RightArm", @@ -184,9 +185,8 @@ const char* HUMANIK_JOINTS[] = { "RightLeg", "LeftLeg", "RightFoot", - "LeftFoot", - "" -}; + "LeftFoot" +}}; class FBXModel { public: @@ -468,7 +468,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { } FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) { - const FBXNode& node = _fbxNode; + const FBXNode& node = _rootNode; QMap meshes; QHash modelIDsToNames; QHash meshIDsToMeshIndices; @@ -512,11 +512,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QVector humanIKJointNames; - for (int i = 0;; i++) { + for (int i = 0; i < (int) HUMANIK_JOINTS.size(); i++) { QByteArray jointName = HUMANIK_JOINTS[i]; - if (jointName.isEmpty()) { - break; - } humanIKJointNames.append(processID(getString(joints.value(jointName, jointName)))); } QVector humanIKJointIDs(humanIKJointNames.size()); @@ -846,12 +843,14 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QByteArray filename = subobject.properties.at(0).toByteArray(); QByteArray filepath = filename.replace('\\', '/'); filename = fileOnUrl(filepath, url); + qDebug() << "Filename" << filepath << filename; _textureFilepaths.insert(getID(object.properties), filepath); _textureFilenames.insert(getID(object.properties), filename); } else if (subobject.name == "TextureName" && subobject.properties.length() >= TEXTURE_NAME_MIN_SIZE) { // trim the name from the timestamp QString name = QString(subobject.properties.at(0).toByteArray()); name = name.left(name.indexOf('[')); + qDebug() << "Filename" << name; _textureNames.insert(getID(object.properties), name); } else if (subobject.name == "Texture_Alpha_Source" && subobject.properties.length() >= TEXTURE_ALPHA_SOURCE_MIN_SIZE) { tex.assign(tex.alphaSource, subobject.properties.at(0).value()); @@ -940,7 +939,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QByteArray content; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "RelativeFilename") { - filepath= subobject.properties.at(0).toByteArray(); + filepath = subobject.properties.at(0).toByteArray(); filepath = filepath.replace('\\', '/'); } else if (subobject.name == "Content" && !subobject.properties.isEmpty()) { @@ -1840,9 +1839,11 @@ FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { FBXReader reader; - reader._fbxNode = FBXReader::parseFBX(device); + reader._rootNode = FBXReader::parseFBX(device); reader._loadLightmaps = loadLightmaps; reader._lightmapLevel = lightmapLevel; + qDebug() << "Reading FBX: " << url; + return reader.extractFBXGeometry(mapping, url); } diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 170bbbf366..843a874a62 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -12,6 +12,8 @@ #ifndef hifi_FBXReader_h #define hifi_FBXReader_h +#include "FBX.h" + #include #include #include @@ -31,305 +33,6 @@ class QIODevice; class FBXNode; -typedef QList FBXNodeList; - -/// The names of the joints in the Maya HumanIK rig, terminated with an empty string. -extern const char* HUMANIK_JOINTS[]; - -/// A node within an FBX document. -class FBXNode { -public: - - QByteArray name; - QVariantList properties; - FBXNodeList children; -}; - -/// A single blendshape extracted from an FBX document. -class FBXBlendshape { -public: - - QVector indices; - QVector vertices; - QVector normals; -}; - -struct FBXJointShapeInfo { - // same units and frame as FBXJoint.translation - glm::vec3 avgPoint; - std::vector dots; - std::vector points; - std::vector debugLines; -}; - -/// A single joint (transformation node) extracted from an FBX document. -class FBXJoint { -public: - - FBXJointShapeInfo shapeInfo; - QVector freeLineage; - bool isFree; - int parentIndex; - float distanceToParent; - - // http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html - - glm::vec3 translation; // T - glm::mat4 preTransform; // Roff * Rp - glm::quat preRotation; // Rpre - glm::quat rotation; // R - glm::quat postRotation; // Rpost - glm::mat4 postTransform; // Rp-1 * Soff * Sp * S * Sp-1 - - // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) - - glm::mat4 transform; - glm::vec3 rotationMin; // radians - glm::vec3 rotationMax; // radians - glm::quat inverseDefaultRotation; - glm::quat inverseBindRotation; - glm::mat4 bindTransform; - QString name; - bool isSkeletonJoint; - bool bindTransformFoundInCluster; - - // geometric offset is applied in local space but does NOT affect children. - bool hasGeometricOffset; - glm::vec3 geometricTranslation; - glm::quat geometricRotation; - glm::vec3 geometricScaling; -}; - - -/// A single binding to a joint in an FBX document. -class FBXCluster { -public: - - int jointIndex; - glm::mat4 inverseBindMatrix; -}; - -const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; - -/// A texture map in an FBX document. -class FBXTexture { -public: - QString name; - QByteArray filename; - QByteArray content; - - Transform transform; - int maxNumPixels { MAX_NUM_PIXELS_FOR_FBX_TEXTURE }; - int texcoordSet; - QString texcoordSetName; - - bool isBumpmap{ false }; - - bool isNull() const { return name.isEmpty() && filename.isEmpty() && content.isEmpty(); } -}; - -/// A single part of a mesh (with the same material). -class FBXMeshPart { -public: - - QVector quadIndices; // original indices from the FBX mesh - QVector quadTrianglesIndices; // original indices from the FBX mesh of the quad converted as triangles - QVector triangleIndices; // original indices from the FBX mesh - - QString materialID; -}; - -class FBXMaterial { -public: - FBXMaterial() {}; - FBXMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor, - float shininess, float opacity) : - diffuseColor(diffuseColor), - specularColor(specularColor), - emissiveColor(emissiveColor), - shininess(shininess), - opacity(opacity) {} - - void getTextureNames(QSet& textureList) const; - void setMaxNumPixelsPerTexture(int maxNumPixels); - - glm::vec3 diffuseColor{ 1.0f }; - float diffuseFactor{ 1.0f }; - glm::vec3 specularColor{ 0.02f }; - float specularFactor{ 1.0f }; - - glm::vec3 emissiveColor{ 0.0f }; - float emissiveFactor{ 0.0f }; - - float shininess{ 23.0f }; - float opacity{ 1.0f }; - - float metallic{ 0.0f }; - float roughness{ 1.0f }; - float emissiveIntensity{ 1.0f }; - float ambientFactor{ 1.0f }; - - QString materialID; - QString name; - QString shadingModel; - model::MaterialPointer _material; - - FBXTexture normalTexture; - FBXTexture albedoTexture; - FBXTexture opacityTexture; - FBXTexture glossTexture; - FBXTexture roughnessTexture; - FBXTexture specularTexture; - FBXTexture metallicTexture; - FBXTexture emissiveTexture; - FBXTexture occlusionTexture; - FBXTexture scatteringTexture; - FBXTexture lightmapTexture; - glm::vec2 lightmapParams{ 0.0f, 1.0f }; - - - bool isPBSMaterial{ false }; - // THe use XXXMap are not really used to drive which map are going or not, debug only - bool useNormalMap{ false }; - bool useAlbedoMap{ false }; - bool useOpacityMap{ false }; - bool useRoughnessMap{ false }; - bool useSpecularMap{ false }; - bool useMetallicMap{ false }; - bool useEmissiveMap{ false }; - bool useOcclusionMap{ false }; - - bool needTangentSpace() const; -}; - -/// A single mesh (with optional blendshapes) extracted from an FBX document. -class FBXMesh { -public: - - QVector parts; - - QVector vertices; - QVector normals; - QVector tangents; - QVector colors; - QVector texCoords; - QVector texCoords1; - QVector clusterIndices; - QVector clusterWeights; - - QVector clusters; - - Extents meshExtents; - glm::mat4 modelTransform; - - QVector blendshapes; - - unsigned int meshIndex; // the order the meshes appeared in the object file - - model::MeshPointer _mesh; -}; - -class ExtractedMesh { -public: - FBXMesh mesh; - QMultiHash newIndices; - QVector > blendshapeIndexMaps; - QVector > partMaterialTextures; - QHash texcoordSetMap; -}; - -/// A single animation frame extracted from an FBX document. -class FBXAnimationFrame { -public: - QVector rotations; - QVector translations; -}; - -/// A light in an FBX document. -class FBXLight { -public: - QString name; - Transform transform; - float intensity; - float fogValue; - glm::vec3 color; - - FBXLight() : - name(), - transform(), - intensity(1.0f), - fogValue(0.0f), - color(1.0f) - {} -}; - -Q_DECLARE_METATYPE(FBXAnimationFrame) -Q_DECLARE_METATYPE(QVector) - -/// A set of meshes extracted from an FBX document. -class FBXGeometry { -public: - using Pointer = std::shared_ptr; - - QString originalURL; - QString author; - QString applicationName; ///< the name of the application that generated the model - - QVector joints; - QHash jointIndices; ///< 1-based, so as to more easily detect missing indices - bool hasSkeletonJoints; - - QVector meshes; - - QHash materials; - - glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file - - int leftEyeJointIndex = -1; - int rightEyeJointIndex = -1; - int neckJointIndex = -1; - int rootJointIndex = -1; - int leanJointIndex = -1; - int headJointIndex = -1; - int leftHandJointIndex = -1; - int rightHandJointIndex = -1; - int leftToeJointIndex = -1; - int rightToeJointIndex = -1; - - float leftEyeSize = 0.0f; // Maximum mesh extents dimension - float rightEyeSize = 0.0f; - - QVector humanIKJointIndices; - - glm::vec3 palmDirection; - - glm::vec3 neckPivot; - - Extents bindExtents; - Extents meshExtents; - - QVector animationFrames; - - int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; } - QStringList getJointNames() const; - - bool hasBlendedMeshes() const; - - /// Returns the unscaled extents of the model's mesh - Extents getUnscaledMeshExtents() const; - - bool convexHullContains(const glm::vec3& point) const; - - QHash meshIndicesToModelNames; - - /// given a meshIndex this will return the name of the model that mesh belongs to if known - QString getModelNameOfMesh(int meshIndex) const; - - QList blendshapeChannelNames; -}; - -Q_DECLARE_METATYPE(FBXGeometry) -Q_DECLARE_METATYPE(FBXGeometry::Pointer) /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing @@ -402,12 +105,12 @@ class FBXReader { public: FBXGeometry* _fbxGeometry; - FBXNode _fbxNode; + FBXNode _rootNode; static FBXNode parseFBX(QIODevice* device); FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QString& url); - ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex); + static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); QHash meshes; static void buildModelMesh(FBXMesh& extractedMesh, const QString& url); diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index ca2ec557b4..ef6496cd10 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -92,6 +92,7 @@ FBXTexture FBXReader::getTexture(const QString& textureID) { texture.filename = filepath; } + texture.id = textureID; texture.name = _textureNames.value(textureID); texture.transform.setIdentity(); texture.texcoordSet = 0; diff --git a/libraries/fbx/src/FBXReader_Mesh.cpp b/libraries/fbx/src/FBXReader_Mesh.cpp index 4e153dfe3a..c64cbcc90d 100644 --- a/libraries/fbx/src/FBXReader_Mesh.cpp +++ b/libraries/fbx/src/FBXReader_Mesh.cpp @@ -9,6 +9,17 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#ifdef _WIN32 +#pragma warning( push ) +#pragma warning( disable : 4267 ) +#endif + +#include + +#ifdef _WIN32 +#pragma warning( pop ) +#endif + #include #include #include @@ -73,7 +84,7 @@ public: }; -void appendIndex(MeshData& data, QVector& indices, int index) { +void appendIndex(MeshData& data, QVector& indices, int index, bool deduplicate) { if (index >= data.polygonIndices.size()) { return; } @@ -145,12 +156,13 @@ void appendIndex(MeshData& data, QVector& indices, int index) { } QHash::const_iterator it = data.indices.find(vertex); - if (it == data.indices.constEnd()) { + if (!deduplicate || it == data.indices.constEnd()) { int newIndex = data.extracted.mesh.vertices.size(); indices.append(newIndex); data.indices.insert(vertex, newIndex); data.extracted.newIndices.insert(vertexIndex, newIndex); data.extracted.mesh.vertices.append(position); + data.extracted.mesh.originalIndices.append(vertexIndex); data.extracted.mesh.normals.append(normal); data.extracted.mesh.texCoords.append(vertex.texCoord); if (hasColors) { @@ -165,14 +177,20 @@ void appendIndex(MeshData& data, QVector& indices, int index) { } } -ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex) { +ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate) { MeshData data; data.extracted.mesh.meshIndex = meshIndex++; + QVector materials; QVector textures; + bool isMaterialPerPolygon = false; + static const QVariant BY_VERTICE = QByteArray("ByVertice"); static const QVariant INDEX_TO_DIRECT = QByteArray("IndexToDirect"); + + bool isDracoMesh = false; + foreach (const FBXNode& child, object.children) { if (child.name == "Vertices") { data.vertices = createVec3Vector(getDoubleVector(child)); @@ -304,8 +322,9 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn if (subdata.name == "Materials") { materials = getIntVector(subdata); } else if (subdata.name == "MappingInformationType") { - if (subdata.properties.at(0) == BY_POLYGON) + if (subdata.properties.at(0) == BY_POLYGON) { isMaterialPerPolygon = true; + } } else { isMaterialPerPolygon = false; } @@ -318,70 +337,206 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn textures = getIntVector(subdata); } } - } - } + } else if (child.name == "DracoMesh") { + isDracoMesh = true; + data.extracted.mesh.wasCompressed = true; - bool isMultiMaterial = false; - if (isMaterialPerPolygon) { - isMultiMaterial = true; - } - // TODO: make excellent use of isMultiMaterial - Q_UNUSED(isMultiMaterial); + // load the draco mesh from the FBX and create a draco::Mesh + draco::Decoder decoder; + draco::DecoderBuffer decodedBuffer; + QByteArray dracoArray = child.properties.at(0).value(); + decodedBuffer.Init(dracoArray.data(), dracoArray.size()); - // convert the polygons to quads and triangles - int polygonIndex = 0; - QHash, int> materialTextureParts; - for (int beginIndex = 0; beginIndex < data.polygonIndices.size(); polygonIndex++) { - int endIndex = beginIndex; - while (endIndex < data.polygonIndices.size() && data.polygonIndices.at(endIndex++) >= 0); + std::unique_ptr dracoMesh(new draco::Mesh()); + decoder.DecodeBufferToGeometry(&decodedBuffer, dracoMesh.get()); - QPair materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0, - (polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0); - int& partIndex = materialTextureParts[materialTexture]; - if (partIndex == 0) { - data.extracted.partMaterialTextures.append(materialTexture); - data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); - partIndex = data.extracted.mesh.parts.size(); - } - FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1]; - - if (endIndex - beginIndex == 4) { - appendIndex(data, part.quadIndices, beginIndex++); - appendIndex(data, part.quadIndices, beginIndex++); - appendIndex(data, part.quadIndices, beginIndex++); - appendIndex(data, part.quadIndices, beginIndex++); + // prepare attributes for this mesh + auto positionAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::POSITION); + auto normalAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::NORMAL); + auto texCoordAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::TEX_COORD); + auto extraTexCoordAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_TEX_COORD_1); + auto colorAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::COLOR); + auto materialIDAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_MATERIAL_ID); + auto originalIndexAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_ORIGINAL_INDEX); - int quadStartIndex = part.quadIndices.size() - 4; - int i0 = part.quadIndices[quadStartIndex + 0]; - int i1 = part.quadIndices[quadStartIndex + 1]; - int i2 = part.quadIndices[quadStartIndex + 2]; - int i3 = part.quadIndices[quadStartIndex + 3]; + // setup extracted mesh data structures given number of points + auto numVertices = dracoMesh->num_points(); - // Sam's recommended triangle slices - // Triangle tri1 = { v0, v1, v3 }; - // Triangle tri2 = { v1, v2, v3 }; - // NOTE: Random guy on the internet's recommended triangle slices - // Triangle tri1 = { v0, v1, v2 }; - // Triangle tri2 = { v2, v3, v0 }; + QHash, int> materialTextureParts; - part.quadTrianglesIndices.append(i0); - part.quadTrianglesIndices.append(i1); - part.quadTrianglesIndices.append(i3); + data.extracted.mesh.vertices.resize(numVertices); - part.quadTrianglesIndices.append(i1); - part.quadTrianglesIndices.append(i2); - part.quadTrianglesIndices.append(i3); + if (normalAttribute) { + data.extracted.mesh.normals.resize(numVertices); + } - } else { - for (int nextIndex = beginIndex + 1;; ) { - appendIndex(data, part.triangleIndices, beginIndex); - appendIndex(data, part.triangleIndices, nextIndex++); - appendIndex(data, part.triangleIndices, nextIndex); - if (nextIndex >= data.polygonIndices.size() || data.polygonIndices.at(nextIndex) < 0) { - break; + if (texCoordAttribute) { + data.extracted.mesh.texCoords.resize(numVertices); + } + + if (extraTexCoordAttribute) { + data.extracted.mesh.texCoords1.resize(numVertices); + } + + if (colorAttribute) { + data.extracted.mesh.colors.resize(numVertices); + } + + // enumerate the vertices and construct the extracted mesh + for (int i = 0; i < numVertices; ++i) { + draco::PointIndex vertexIndex(i); + + if (positionAttribute) { + // read position from draco mesh to extracted mesh + auto mappedIndex = positionAttribute->mapped_index(vertexIndex); + + positionAttribute->ConvertValue(mappedIndex, + reinterpret_cast(&data.extracted.mesh.vertices[i])); + } + + if (normalAttribute) { + // read normals from draco mesh to extracted mesh + auto mappedIndex = normalAttribute->mapped_index(vertexIndex); + + normalAttribute->ConvertValue(mappedIndex, + reinterpret_cast(&data.extracted.mesh.normals[i])); + } + + if (texCoordAttribute) { + // read UVs from draco mesh to extracted mesh + auto mappedIndex = texCoordAttribute->mapped_index(vertexIndex); + + texCoordAttribute->ConvertValue(mappedIndex, + reinterpret_cast(&data.extracted.mesh.texCoords[i])); + } + + if (extraTexCoordAttribute) { + // some meshes have a second set of UVs, read those to extracted mesh + auto mappedIndex = extraTexCoordAttribute->mapped_index(vertexIndex); + + extraTexCoordAttribute->ConvertValue(mappedIndex, + reinterpret_cast(&data.extracted.mesh.texCoords1[i])); + } + + if (colorAttribute) { + // read vertex colors from draco mesh to extracted mesh + auto mappedIndex = colorAttribute->mapped_index(vertexIndex); + + colorAttribute->ConvertValue(mappedIndex, + reinterpret_cast(&data.extracted.mesh.colors[i])); + } + + if (originalIndexAttribute) { + auto mappedIndex = originalIndexAttribute->mapped_index(vertexIndex); + + int32_t originalIndex; + + originalIndexAttribute->ConvertValue(mappedIndex, &originalIndex); + + data.extracted.newIndices.insert(originalIndex, i); + } else { + data.extracted.newIndices.insert(i, i); } } - beginIndex = endIndex; + + for (int i = 0; i < dracoMesh->num_faces(); ++i) { + // grab the material ID and texture ID for this face, if we have it + auto& dracoFace = dracoMesh->face(draco::FaceIndex(i)); + auto& firstCorner = dracoFace[0]; + + uint16_t materialID { 0 }; + + if (materialIDAttribute) { + // read material ID and texture ID mappings into materials and texture vectors + auto mappedIndex = materialIDAttribute->mapped_index(firstCorner); + + materialIDAttribute->ConvertValue(mappedIndex, &materialID); + } + + QPair materialTexture(materialID, 0); + + // grab or setup the FBXMeshPart for the part this face belongs to + int& partIndexPlusOne = materialTextureParts[materialTexture]; + if (partIndexPlusOne == 0) { + data.extracted.partMaterialTextures.append(materialTexture); + data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); + partIndexPlusOne = data.extracted.mesh.parts.size(); + } + + // give the mesh part this index + FBXMeshPart& part = data.extracted.mesh.parts[partIndexPlusOne - 1]; + part.triangleIndices.append(firstCorner.value()); + part.triangleIndices.append(dracoFace[1].value()); + part.triangleIndices.append(dracoFace[2].value()); + } + } + } + + // when we have a draco mesh, we've already built the extracted mesh, so we don't need to do the + // processing we do for normal meshes below + if (!isDracoMesh) { + bool isMultiMaterial = false; + if (isMaterialPerPolygon) { + isMultiMaterial = true; + } + // TODO: make excellent use of isMultiMaterial + Q_UNUSED(isMultiMaterial); + + // convert the polygons to quads and triangles + int polygonIndex = 0; + QHash, int> materialTextureParts; + for (int beginIndex = 0; beginIndex < data.polygonIndices.size(); polygonIndex++) { + int endIndex = beginIndex; + while (endIndex < data.polygonIndices.size() && data.polygonIndices.at(endIndex++) >= 0); + + QPair materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0, + (polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0); + int& partIndex = materialTextureParts[materialTexture]; + if (partIndex == 0) { + data.extracted.partMaterialTextures.append(materialTexture); + data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); + partIndex = data.extracted.mesh.parts.size(); + } + FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1]; + + if (endIndex - beginIndex == 4) { + appendIndex(data, part.quadIndices, beginIndex++, deduplicate); + appendIndex(data, part.quadIndices, beginIndex++, deduplicate); + appendIndex(data, part.quadIndices, beginIndex++, deduplicate); + appendIndex(data, part.quadIndices, beginIndex++, deduplicate); + + int quadStartIndex = part.quadIndices.size() - 4; + int i0 = part.quadIndices[quadStartIndex + 0]; + int i1 = part.quadIndices[quadStartIndex + 1]; + int i2 = part.quadIndices[quadStartIndex + 2]; + int i3 = part.quadIndices[quadStartIndex + 3]; + + // Sam's recommended triangle slices + // Triangle tri1 = { v0, v1, v3 }; + // Triangle tri2 = { v1, v2, v3 }; + // NOTE: Random guy on the internet's recommended triangle slices + // Triangle tri1 = { v0, v1, v2 }; + // Triangle tri2 = { v2, v3, v0 }; + + part.quadTrianglesIndices.append(i0); + part.quadTrianglesIndices.append(i1); + part.quadTrianglesIndices.append(i3); + + part.quadTrianglesIndices.append(i1); + part.quadTrianglesIndices.append(i2); + part.quadTrianglesIndices.append(i3); + + } else { + for (int nextIndex = beginIndex + 1;; ) { + appendIndex(data, part.triangleIndices, beginIndex, deduplicate); + appendIndex(data, part.triangleIndices, nextIndex++, deduplicate); + appendIndex(data, part.triangleIndices, nextIndex, deduplicate); + if (nextIndex >= data.polygonIndices.size() || data.polygonIndices.at(nextIndex) < 0) { + break; + } + } + beginIndex = endIndex; + } } } diff --git a/libraries/fbx/src/FBXReader_Node.cpp b/libraries/fbx/src/FBXReader_Node.cpp index d987f885eb..c4454421b6 100644 --- a/libraries/fbx/src/FBXReader_Node.cpp +++ b/libraries/fbx/src/FBXReader_Node.cpp @@ -24,15 +24,18 @@ #include #include "ModelFormatLogging.h" -template int streamSize() { +template +int streamSize() { return sizeof(T); } -template int streamSize() { +template +int streamSize() { return 1; } -template QVariant readBinaryArray(QDataStream& in, int& position) { +template +QVariant readBinaryArray(QDataStream& in, int& position) { quint32 arrayLength; quint32 encoding; quint32 compressedLength; @@ -45,9 +48,8 @@ template QVariant readBinaryArray(QDataStream& in, int& position) { QVector values; if ((int)QSysInfo::ByteOrder == (int)in.byteOrder()) { values.resize(arrayLength); - const unsigned int DEFLATE_ENCODING = 1; QByteArray arrayData; - if (encoding == DEFLATE_ENCODING) { + if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) { // preface encoded data with uncompressed length QByteArray compressed(sizeof(quint32) + compressedLength, 0); *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); @@ -69,8 +71,7 @@ template QVariant readBinaryArray(QDataStream& in, int& position) { } } else { values.reserve(arrayLength); - const unsigned int DEFLATE_ENCODING = 1; - if (encoding == DEFLATE_ENCODING) { + if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) { // preface encoded data with uncompressed length QByteArray compressed(sizeof(quint32) + compressedLength, 0); *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); @@ -350,8 +351,7 @@ FBXNode parseTextFBXNode(Tokenizer& tokenizer) { FBXNode FBXReader::parseFBX(QIODevice* device) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xff0000ff, device); // verify the prolog - const QByteArray BINARY_PROLOG = "Kaydara FBX Binary "; - if (device->peek(BINARY_PROLOG.size()) != BINARY_PROLOG) { + if (device->peek(FBX_BINARY_PROLOG.size()) != FBX_BINARY_PROLOG) { // parse as a text file FBXNode top; Tokenizer tokenizer(device); @@ -377,15 +377,13 @@ FBXNode FBXReader::parseFBX(QIODevice* device) { // Bytes 0 - 20: Kaydara FBX Binary \x00(file - magic, with 2 spaces at the end, then a NULL terminator). // Bytes 21 - 22: [0x1A, 0x00](unknown but all observed files show these bytes). // Bytes 23 - 26 : unsigned int, the version number. 7300 for version 7.3 for example. - const int HEADER_BEFORE_VERSION = 23; - const quint32 VERSION_FBX2016 = 7500; - in.skipRawData(HEADER_BEFORE_VERSION); - int position = HEADER_BEFORE_VERSION; + in.skipRawData(FBX_HEADER_BYTES_BEFORE_VERSION); + int position = FBX_HEADER_BYTES_BEFORE_VERSION; quint32 fileVersion; in >> fileVersion; position += sizeof(fileVersion); qCDebug(modelformat) << "fileVersion:" << fileVersion; - bool has64BitPositions = (fileVersion >= VERSION_FBX2016); + bool has64BitPositions = (fileVersion >= FBX_VERSION_2016); // parse the top-level node FBXNode top; diff --git a/libraries/fbx/src/FBXWriter.cpp b/libraries/fbx/src/FBXWriter.cpp new file mode 100644 index 0000000000..5029b489bc --- /dev/null +++ b/libraries/fbx/src/FBXWriter.cpp @@ -0,0 +1,200 @@ +// +// FBXWriter.cpp +// libraries/fbx/src +// +// Created by Ryan Huffman on 9/5/17. +// Copyright 2017 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 "FBXWriter.h" + +#include + +#ifdef USE_FBX_2016_FORMAT + using FBXEndOffset = int64_t; + using FBXPropertyCount = uint64_t; + using FBXListLength = uint64_t; +#else + using FBXEndOffset = int32_t; + using FBXPropertyCount = uint32_t; + using FBXListLength = uint32_t; +#endif + +template +void writeVector(QDataStream& out, char ch, QVector vec) { + // Minimum number of bytes to consider compressing + const int ATTEMPT_COMPRESSION_THRESHOLD_BYTES = 2000; + + out.device()->write(&ch, 1); + out << (int32_t)vec.length(); + + auto data { QByteArray::fromRawData((const char*)vec.constData(), vec.length() * sizeof(T)) }; + + if (data.size() >= ATTEMPT_COMPRESSION_THRESHOLD_BYTES) { + auto compressedDataWithLength { qCompress(data) }; + + // qCompress packs a length uint32 at the beginning of the buffer, but the FBX format + // does not expect it. This removes it. + auto compressedData = QByteArray::fromRawData( + compressedDataWithLength.constData() + sizeof(uint32_t), compressedDataWithLength.size() - sizeof(uint32_t)); + + if (compressedData.size() < data.size()) { + out << FBX_PROPERTY_COMPRESSED_FLAG; + out << (int32_t)compressedData.size(); + out.writeRawData(compressedData.constData(), compressedData.size()); + return; + } + } + + out << FBX_PROPERTY_UNCOMPRESSED_FLAG; + out << (int32_t)0; + out.writeRawData(data.constData(), data.size()); +} + + +QByteArray FBXWriter::encodeFBX(const FBXNode& root) { + QByteArray data; + QDataStream out(&data, QIODevice::WriteOnly); + out.setByteOrder(QDataStream::LittleEndian); + out.setVersion(QDataStream::Qt_4_5); + + out.writeRawData(FBX_BINARY_PROLOG, FBX_BINARY_PROLOG.size()); + auto bytes = QByteArray(FBX_HEADER_BYTES_BEFORE_VERSION - FBX_BINARY_PROLOG.size(), '\0'); + out.writeRawData(bytes, bytes.size()); + +#ifdef USE_FBX_2016_FORMAT + out << FBX_VERSION_2016; +#else + out << FBX_VERSION_2015; +#endif + + for (auto& child : root.children) { + encodeNode(out, child); + } + encodeNode(out, FBXNode()); + + return data; +} + +void FBXWriter::encodeNode(QDataStream& out, const FBXNode& node) { + auto device = out.device(); + auto nodeStartPos = device->pos(); + + // endOffset (temporary, updated later) + out << (FBXEndOffset)0; + + // Property count + out << (FBXPropertyCount)node.properties.size(); + + // Property list length (temporary, updated later) + out << (FBXListLength)0; + + out << (quint8)node.name.size(); + out.writeRawData(node.name, node.name.size()); + + auto nodePropertiesStartPos = device->pos(); + + for (const auto& prop : node.properties) { + encodeFBXProperty(out, prop); + } + + // Go back and write property list length + auto nodePropertiesEndPos = device->pos(); + device->seek(nodeStartPos + sizeof(FBXEndOffset) + sizeof(FBXPropertyCount)); + out << (FBXListLength)(nodePropertiesEndPos - nodePropertiesStartPos); + + device->seek(nodePropertiesEndPos); + + for (auto& child : node.children) { + encodeNode(out, child); + } + + if (node.children.length() > 0) { + encodeNode(out, FBXNode()); + } + + // Go back and write actual endOffset + auto nodeEndPos = device->pos(); + device->seek(nodeStartPos); + out << (FBXEndOffset)(nodeEndPos); + + device->seek(nodeEndPos); +} + +void FBXWriter::encodeFBXProperty(QDataStream& out, const QVariant& prop) { + auto type = prop.userType(); + switch (type) { + case QMetaType::Short: + out.device()->write("Y", 1); + out << prop.value(); + break; + + case QVariant::Type::Bool: + out.device()->write("C", 1); + out << prop.toBool(); + break; + + case QMetaType::Int: + out.device()->write("I", 1); + out << prop.toInt(); + break; + + encodeNode(out, FBXNode()); + case QMetaType::Float: + out.device()->write("F", 1); + out << prop.toFloat(); + break; + + case QMetaType::Double: + out.device()->write("D", 1); + out << prop.toDouble(); + break; + + case QMetaType::LongLong: + out.device()->write("L", 1); + out << prop.toLongLong(); + break; + + case QMetaType::QString: + { + auto bytes = prop.toString().toUtf8(); + out << 'S'; + out << bytes.length(); + out << bytes; + out << (int32_t)bytes.size(); + out.writeRawData(bytes, bytes.size()); + break; + } + + case QMetaType::QByteArray: + { + auto bytes = prop.toByteArray(); + out.device()->write("S", 1); + out << (int32_t)bytes.size(); + out.writeRawData(bytes, bytes.size()); + break; + } + + default: + { + if (prop.canConvert>()) { + writeVector(out, 'f', prop.value>()); + } else if (prop.canConvert>()) { + writeVector(out, 'd', prop.value>()); + } else if (prop.canConvert>()) { + writeVector(out, 'l', prop.value>()); + } else if (prop.canConvert>()) { + writeVector(out, 'i', prop.value>()); + } else if (prop.canConvert>()) { + writeVector(out, 'b', prop.value>()); + } else { + qDebug() << "Unsupported property type in FBXWriter::encodeNode: " << type << prop; + throw("Unsupported property type in FBXWriter::encodeNode: " + QString::number(type) + " " + prop.toString()); + } + } + + } +} diff --git a/libraries/fbx/src/FBXWriter.h b/libraries/fbx/src/FBXWriter.h new file mode 100644 index 0000000000..f20d208cb1 --- /dev/null +++ b/libraries/fbx/src/FBXWriter.h @@ -0,0 +1,30 @@ +// +// FBXWriter.h +// libraries/fbx/src +// +// Created by Ryan Huffman on 9/5/17. +// Copyright 2017 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_FBXWriter_h +#define hifi_FBXWriter_h + +#include "FBX.h" + +#include +#include + +//#define USE_FBX_2016_FORMAT + +class FBXWriter { +public: + static QByteArray encodeFBX(const FBXNode& root); + + static void encodeNode(QDataStream& out, const FBXNode& node); + static void encodeFBXProperty(QDataStream& out, const QVariant& property); +}; + +#endif // hifi_FBXWriter_h diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index f274dc54f8..6081355eaa 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include "ImageLogging.h" @@ -30,18 +29,17 @@ using namespace gpu; #define CPU_MIPMAPS 1 -static std::mutex settingsMutex; -static Setting::Handle compressColorTextures("hifi.graphics.compressColorTextures", false); -static Setting::Handle compressNormalTextures("hifi.graphics.compressNormalTextures", false); -static Setting::Handle compressGrayscaleTextures("hifi.graphics.compressGrayscaleTextures", false); -static Setting::Handle compressCubeTextures("hifi.graphics.compressCubeTextures", false); - static const glm::uvec2 SPARSE_PAGE_SIZE(128); static const glm::uvec2 MAX_TEXTURE_SIZE(4096); bool DEV_DECIMATE_TEXTURES = false; std::atomic DECIMATED_TEXTURE_COUNT{ 0 }; std::atomic RECTIFIED_TEXTURE_COUNT{ 0 }; +static std::atomic compressColorTextures { false }; +static std::atomic compressNormalTextures { false }; +static std::atomic compressGrayscaleTextures { false }; +static std::atomic compressCubeTextures { false }; + bool needsSparseRectification(const glm::uvec2& size) { // Don't attempt to rectify small textures (textures less than the sparse page size in any dimension) if (glm::any(glm::lessThan(size, SPARSE_PAGE_SIZE))) { @@ -99,59 +97,70 @@ TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type, con } } -gpu::TexturePointer TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, true); +gpu::TexturePointer TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureColorFromImage(srcImage, srcImageName, true, abortProcessing); } -gpu::TexturePointer TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false); +gpu::TexturePointer TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureColorFromImage(srcImage, srcImageName, false, abortProcessing); } -gpu::TexturePointer TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false); +gpu::TexturePointer TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureColorFromImage(srcImage, srcImageName, false, abortProcessing); } -gpu::TexturePointer TextureUsage::createEmissiveTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false); +gpu::TexturePointer TextureUsage::createEmissiveTextureFromImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureColorFromImage(srcImage, srcImageName, false, abortProcessing); } -gpu::TexturePointer TextureUsage::createLightmapTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false); +gpu::TexturePointer TextureUsage::createLightmapTextureFromImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureColorFromImage(srcImage, srcImageName, false, abortProcessing); } -gpu::TexturePointer TextureUsage::createNormalTextureFromNormalImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureNormalMapFromImage(srcImage, srcImageName, false); +gpu::TexturePointer TextureUsage::createNormalTextureFromNormalImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureNormalMapFromImage(srcImage, srcImageName, false, abortProcessing); } -gpu::TexturePointer TextureUsage::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureNormalMapFromImage(srcImage, srcImageName, true); +gpu::TexturePointer TextureUsage::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureNormalMapFromImage(srcImage, srcImageName, true, abortProcessing); } -gpu::TexturePointer TextureUsage::createRoughnessTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false); +gpu::TexturePointer TextureUsage::createRoughnessTextureFromImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false, abortProcessing); } -gpu::TexturePointer TextureUsage::createRoughnessTextureFromGlossImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureGrayscaleFromImage(srcImage, srcImageName, true); +gpu::TexturePointer TextureUsage::createRoughnessTextureFromGlossImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureGrayscaleFromImage(srcImage, srcImageName, true, abortProcessing); } -gpu::TexturePointer TextureUsage::createMetallicTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false); +gpu::TexturePointer TextureUsage::createMetallicTextureFromImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false, abortProcessing); } -gpu::TexturePointer TextureUsage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return processCubeTextureColorFromImage(srcImage, srcImageName, true); +gpu::TexturePointer TextureUsage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return processCubeTextureColorFromImage(srcImage, srcImageName, true, abortProcessing); } -gpu::TexturePointer TextureUsage::createCubeTextureFromImageWithoutIrradiance(const QImage& srcImage, const std::string& srcImageName) { - return processCubeTextureColorFromImage(srcImage, srcImageName, false); +gpu::TexturePointer TextureUsage::createCubeTextureFromImageWithoutIrradiance(const QImage& srcImage, const std::string& srcImageName, + const std::atomic& abortProcessing) { + return processCubeTextureColorFromImage(srcImage, srcImageName, false, abortProcessing); } bool isColorTexturesCompressionEnabled() { #if CPU_MIPMAPS - std::lock_guard guard(settingsMutex); - return compressColorTextures.get(); + return compressColorTextures.load(); #else return false; #endif @@ -159,8 +168,7 @@ bool isColorTexturesCompressionEnabled() { bool isNormalTexturesCompressionEnabled() { #if CPU_MIPMAPS - std::lock_guard guard(settingsMutex); - return compressNormalTextures.get(); + return compressNormalTextures.load(); #else return false; #endif @@ -168,8 +176,7 @@ bool isNormalTexturesCompressionEnabled() { bool isGrayscaleTexturesCompressionEnabled() { #if CPU_MIPMAPS - std::lock_guard guard(settingsMutex); - return compressGrayscaleTextures.get(); + return compressGrayscaleTextures.load(); #else return false; #endif @@ -177,35 +184,31 @@ bool isGrayscaleTexturesCompressionEnabled() { bool isCubeTexturesCompressionEnabled() { #if CPU_MIPMAPS - std::lock_guard guard(settingsMutex); - return compressCubeTextures.get(); + return compressCubeTextures.load(); #else return false; #endif } void setColorTexturesCompressionEnabled(bool enabled) { - std::lock_guard guard(settingsMutex); - compressColorTextures.set(enabled); + compressColorTextures.store(enabled); } void setNormalTexturesCompressionEnabled(bool enabled) { - std::lock_guard guard(settingsMutex); - compressNormalTextures.set(enabled); + compressNormalTextures.store(enabled); } void setGrayscaleTexturesCompressionEnabled(bool enabled) { - std::lock_guard guard(settingsMutex); - compressGrayscaleTextures.set(enabled); + compressGrayscaleTextures.store(enabled); } void setCubeTexturesCompressionEnabled(bool enabled) { - std::lock_guard guard(settingsMutex); - compressCubeTextures.set(enabled); + compressCubeTextures.store(enabled); } - -gpu::TexturePointer processImage(const QByteArray& content, const std::string& filename, int maxNumPixels, TextureUsage::Type textureType) { +gpu::TexturePointer processImage(const QByteArray& content, const std::string& filename, + int maxNumPixels, TextureUsage::Type textureType, + const std::atomic& abortProcessing) { // Help the QImage loader by extracting the image file format from the url filename ext. // Some tga are not created properly without it. auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); @@ -255,7 +258,7 @@ gpu::TexturePointer processImage(const QByteArray& content, const std::string& f } auto loader = TextureUsage::getTextureLoaderForType(textureType); - auto texture = loader(image, filename); + auto texture = loader(image, filename, abortProcessing); return texture; } @@ -331,7 +334,7 @@ struct MyErrorHandler : public nvtt::ErrorHandler { } }; -void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { +void generateMips(gpu::Texture* texture, QImage& image, const std::atomic& abortProcessing = false, int face = -1) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateMips"); @@ -447,7 +450,26 @@ void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { MyErrorHandler errorHandler; outputOptions.setErrorHandler(&errorHandler); + class SequentialTaskDispatcher : public nvtt::TaskDispatcher { + public: + SequentialTaskDispatcher(const std::atomic& abortProcessing) : _abortProcessing(abortProcessing) {}; + + const std::atomic& _abortProcessing; + + virtual void dispatch(nvtt::Task* task, void* context, int count) override { + for (int i = 0; i < count; i++) { + if (!_abortProcessing.load()) { + task(context, i); + } else { + break; + } + } + } + }; + + SequentialTaskDispatcher dispatcher(abortProcessing); nvtt::Compressor compressor; + compressor.setTaskDispatcher(&dispatcher); compressor.process(inputOptions, compressionOptions, outputOptions); #else texture->autoGenerateMips(-1); @@ -481,7 +503,8 @@ void processTextureAlpha(const QImage& srcImage, bool& validAlpha, bool& alphaAs validAlpha = (numOpaques != NUM_PIXELS); } -gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isStrict) { +gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, + bool isStrict, const std::atomic& abortProcessing) { PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage"); QImage image = processSourceImage(srcImage, false); bool validAlpha = image.hasAlphaChannel(); @@ -529,7 +552,7 @@ gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(const QImage& s } theTexture->setUsage(usage.build()); theTexture->setStoredMipFormat(formatMip); - generateMips(theTexture.get(), image); + generateMips(theTexture.get(), image, abortProcessing); } return theTexture; @@ -605,7 +628,8 @@ QImage processBumpMap(QImage& image) { return result; } -gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName, bool isBumpMap) { +gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName, + bool isBumpMap, const std::atomic& abortProcessing) { PROFILE_RANGE(resource_parse, "process2DTextureNormalMapFromImage"); QImage image = processSourceImage(srcImage, false); @@ -630,13 +654,15 @@ gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(const QImag theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); - generateMips(theTexture.get(), image); + generateMips(theTexture.get(), image, abortProcessing); } return theTexture; } -gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName, bool isInvertedPixels) { +gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName, + bool isInvertedPixels, + const std::atomic& abortProcessing) { PROFILE_RANGE(resource_parse, "process2DTextureGrayscaleFromImage"); QImage image = processSourceImage(srcImage, false); @@ -664,7 +690,7 @@ gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(const QImag theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); - generateMips(theTexture.get(), image); + generateMips(theTexture.get(), image, abortProcessing); } return theTexture; @@ -926,7 +952,9 @@ const CubeLayout CubeLayout::CUBEMAP_LAYOUTS[] = { }; const int CubeLayout::NUM_CUBEMAP_LAYOUTS = sizeof(CubeLayout::CUBEMAP_LAYOUTS) / sizeof(CubeLayout); -gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool generateIrradiance) { +gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, + bool generateIrradiance, + const std::atomic& abortProcessing) { PROFILE_RANGE(resource_parse, "processCubeTextureColorFromImage"); gpu::TexturePointer theTexture = nullptr; @@ -985,7 +1013,7 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(const QImage& theTexture->setStoredMipFormat(formatMip); for (uint8 face = 0; face < faces.size(); ++face) { - generateMips(theTexture.get(), faces[face], face); + generateMips(theTexture.get(), faces[face], abortProcessing, face); } // Generate irradiance while we are at it diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h index 3bf45ace98..856dc009cf 100644 --- a/libraries/image/src/image/Image.h +++ b/libraries/image/src/image/Image.h @@ -41,26 +41,42 @@ enum Type { UNUSED_TEXTURE }; -using TextureLoader = std::function; +using TextureLoader = std::function&)>; TextureLoader getTextureLoaderForType(Type type, const QVariantMap& options = QVariantMap()); -gpu::TexturePointer create2DTextureFromImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createNormalTextureFromBumpImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createRoughnessTextureFromImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createRoughnessTextureFromGlossImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createMetallicTextureFromImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createCubeTextureFromImage(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createCubeTextureFromImageWithoutIrradiance(const QImage& image, const std::string& srcImageName); -gpu::TexturePointer createLightmapTextureFromImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer create2DTextureFromImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createNormalTextureFromBumpImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createRoughnessTextureFromImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createRoughnessTextureFromGlossImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createMetallicTextureFromImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createCubeTextureFromImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createCubeTextureFromImageWithoutIrradiance(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); +gpu::TexturePointer createLightmapTextureFromImage(const QImage& image, const std::string& srcImageName, + const std::atomic& abortProcessing); -gpu::TexturePointer process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isStrict); -gpu::TexturePointer process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName, bool isBumpMap); -gpu::TexturePointer process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName, bool isInvertedPixels); -gpu::TexturePointer processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool generateIrradiance); +gpu::TexturePointer process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isStrict, + const std::atomic& abortProcessing); +gpu::TexturePointer process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName, bool isBumpMap, + const std::atomic& abortProcessing); +gpu::TexturePointer process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName, bool isInvertedPixels, + const std::atomic& abortProcessing); +gpu::TexturePointer processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool generateIrradiance, + const std::atomic& abortProcessing); } // namespace TextureUsage @@ -74,7 +90,9 @@ void setNormalTexturesCompressionEnabled(bool enabled); void setGrayscaleTexturesCompressionEnabled(bool enabled); void setCubeTexturesCompressionEnabled(bool enabled); -gpu::TexturePointer processImage(const QByteArray& content, const std::string& url, int maxNumPixels, TextureUsage::Type textureType); +gpu::TexturePointer processImage(const QByteArray& content, const std::string& url, + int maxNumPixels, TextureUsage::Type textureType, + const std::atomic& abortProcessing = false); } // namespace image diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 704455c981..74c8d06736 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -77,6 +77,8 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { texdir += '/'; } _textureBaseUrl = resolveTextureBaseUrl(url, _url.resolved(texdir)); + } else { + _textureBaseUrl = _effectiveBaseURL; } auto animGraphVariant = mapping.value("animGraphUrl"); @@ -239,7 +241,10 @@ private: }; void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { - QThreadPool::globalInstance()->start(new GeometryReader(_self, _url, _mapping, data, _combineParts)); + qDebug() << "Processing geometry: " << _effectiveBaseURL; + _url = _effectiveBaseURL; + _textureBaseUrl = _effectiveBaseURL; + QThreadPool::globalInstance()->start(new GeometryReader(_self, _effectiveBaseURL, _mapping, data, _combineParts)); } void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxGeometry) { @@ -250,6 +255,7 @@ void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxG QHash materialIDAtlas; for (const FBXMaterial& material : _fbxGeometry->materials) { materialIDAtlas[material.materialID] = _materials.size(); + qDebug() << "setGeometryDefinition() " << _textureBaseUrl; _materials.push_back(std::make_shared(material, _textureBaseUrl)); } @@ -342,6 +348,7 @@ Geometry::Geometry(const Geometry& geometry) { _materials.reserve(geometry._materials.size()); for (const auto& material : geometry._materials) { + qDebug() << "Geometry() no base url..."; _materials.push_back(std::make_shared(*material)); } @@ -427,6 +434,7 @@ void GeometryResource::deleter() { void GeometryResource::setTextures() { if (_fbxGeometry) { for (const FBXMaterial& material : _fbxGeometry->materials) { + qDebug() << "setTextures() " << _textureBaseUrl; _materials.push_back(std::make_shared(material, _textureBaseUrl)); } } @@ -456,7 +464,7 @@ void GeometryResourceWatcher::setResource(GeometryResource::Pointer resource) { _resource = resource; if (_resource) { if (_resource->isLoaded()) { - _geometryRef = std::make_shared(*_resource); + resourceFinished(true); } else { startWatching(); } @@ -528,6 +536,7 @@ model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) : model::Material(*material._material) { + qDebug() << "Created network material with base url: " << textureBaseUrl; _textures = Textures(MapChannel::NUM_MAP_CHANNELS); if (!material.albedoTexture.filename.isEmpty()) { auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 6a1cc4c466..a122e03eb9 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -131,6 +131,7 @@ private: Geometry::Pointer& _geometryRef; }; + /// Stores cached model geometries. class ModelCache : public ResourceCache, public Dependency { Q_OBJECT diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 85bde4c2f1..a4ce892521 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #if DEBUG_DUMP_TEXTURE_LOADS #include @@ -189,8 +190,14 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs if (url.scheme() == RESOURCE_SCHEME) { return getResourceTexture(url); } + auto modifiedUrl = url; + if (type == image::TextureUsage::CUBE_TEXTURE) { + QUrlQuery query { url.query() }; + query.addQueryItem("skybox", ""); + modifiedUrl.setQuery(query.toString()); + } TextureExtra extra = { type, content, maxNumPixels }; - return ResourceCache::getResource(url, QUrl(), &extra).staticCast(); + return ResourceCache::getResource(modifiedUrl, QUrl(), &extra).staticCast(); } gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) { @@ -257,7 +264,7 @@ gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::Type type) { gpu::TexturePointer TextureCache::getImageTexture(const QString& path, image::TextureUsage::Type type, QVariantMap options) { QImage image = QImage(path); auto loader = image::TextureUsage::getTextureLoaderForType(type, options); - return gpu::TexturePointer(loader(image, QUrl::fromLocalFile(path).fileName().toStdString())); + return gpu::TexturePointer(loader(image, QUrl::fromLocalFile(path).fileName().toStdString(), false)); } QSharedPointer TextureCache::createResource(const QUrl& url, const QSharedPointer& fallback, @@ -291,6 +298,8 @@ NetworkTexture::NetworkTexture(const QUrl& url, image::TextureUsage::Type type, _textureSource = std::make_shared(); _lowestRequestedMipLevel = 0; + _shouldFailOnRedirect = !_sourceIsKTX; + if (type == image::TextureUsage::CUBE_TEXTURE) { setLoadPriority(this, SKYBOX_LOAD_PRIORITY); } else if (_sourceIsKTX) { @@ -420,6 +429,21 @@ void NetworkTexture::makeRequest() { } +bool NetworkTexture::handleFailedRequest(ResourceRequest::Result result) { + if (!_sourceIsKTX && result == ResourceRequest::Result::RedirectFail) { + auto newPath = _request->getRelativePathUrl(); + if (newPath.fileName().endsWith(".ktx")) { + qDebug() << "Redirecting to" << newPath << "from" << _url; + _sourceIsKTX = true; + _activeUrl = newPath; + _shouldFailOnRedirect = false; + makeRequest(); + return true; + } + } + return Resource::handleFailedRequest(result); +} + void NetworkTexture::startRequestForNextMipLevel() { auto self = _self.lock(); if (!self) { @@ -519,7 +543,7 @@ void NetworkTexture::ktxInitialDataRequestFinished() { _ktxHighMipData = _ktxMipRequest->getData(); handleFinishedInitialLoad(); } else { - if (handleFailedRequest(result)) { + if (Resource::handleFailedRequest(result)) { _ktxResourceState = PENDING_INITIAL_LOAD; } else { _ktxResourceState = FAILED_TO_LOAD; @@ -608,7 +632,7 @@ void NetworkTexture::ktxMipRequestFinished() { finishedLoading(false); } } else { - if (handleFailedRequest(result)) { + if (Resource::handleFailedRequest(result)) { _ktxResourceState = PENDING_MIP_REQUEST; } else { _ktxResourceState = FAILED_TO_LOAD; diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index f5a0ec5215..5bc5aa7d96 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -75,6 +75,8 @@ protected: virtual void downloadFinished(const QByteArray& data) override; + bool handleFailedRequest(ResourceRequest::Result result) override; + Q_INVOKABLE void loadContent(const QByteArray& content); Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index cb0b620a54..940daf4d19 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -185,6 +185,14 @@ RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& o return request; } +SetBakingEnabledRequest* AssetClient::createSetBakingEnabledRequest(const AssetPathList& path, bool enabled) { + auto bakingEnabledRequest = new SetBakingEnabledRequest(path, enabled); + + bakingEnabledRequest->moveToThread(thread()); + + return bakingEnabledRequest; +} + AssetRequest* AssetClient::createRequest(const AssetHash& hash, const ByteRange& byteRange) { auto request = new AssetRequest(hash, byteRange); @@ -585,6 +593,37 @@ MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetP return INVALID_MESSAGE_ID; } +MessageID AssetClient::setBakingEnabled(const AssetPathList& paths, bool enabled, MappingOperationCallback callback) { + auto nodeList = DependencyManager::get(); + SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); + + if (assetServer) { + auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true); + + auto messageID = ++_currentID; + packetList->writePrimitive(messageID); + + packetList->writePrimitive(AssetMappingOperationType::SetBakingEnabled); + + packetList->writePrimitive(enabled); + + packetList->writePrimitive(int(paths.size())); + + for (auto& path : paths) { + packetList->writeString(path); + } + + if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) { + _pendingMappingRequests[assetServer][messageID] = callback; + + return messageID; + } + } + + callback(false, AssetServerError::NoError, QSharedPointer()); + return INVALID_MESSAGE_ID; +} + bool AssetClient::cancelMappingRequest(MessageID id) { Q_ASSERT(QThread::currentThread() == thread()); diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index 3f6602b76b..8035aa886e 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -32,6 +32,7 @@ class SetMappingRequest; class GetAllMappingsRequest; class DeleteMappingsRequest; class RenameMappingRequest; +class SetBakingEnabledRequest; class AssetRequest; class AssetUpload; @@ -56,6 +57,7 @@ public: Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetPathList& paths); Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetPath& path, const AssetHash& hash); Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath); + Q_INVOKABLE SetBakingEnabledRequest* createSetBakingEnabledRequest(const AssetPathList& path, bool enabled); Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash, const ByteRange& byteRange = ByteRange()); Q_INVOKABLE AssetUpload* createUpload(const QString& filename); Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data); @@ -81,6 +83,7 @@ private: MessageID setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback); MessageID deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback); MessageID renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback); + MessageID setBakingEnabled(const AssetPathList& paths, bool enabled, MappingOperationCallback callback); MessageID getAssetInfo(const QString& hash, GetInfoCallback callback); MessageID getAsset(const QString& hash, DataOffset start, DataOffset end, @@ -119,6 +122,7 @@ private: friend class SetMappingRequest; friend class DeleteMappingsRequest; friend class RenameMappingRequest; + friend class SetBakingEnabledRequest; }; #endif diff --git a/libraries/networking/src/AssetRequest.h b/libraries/networking/src/AssetRequest.h index df5cf80ecd..a7213a90d7 100644 --- a/libraries/networking/src/AssetRequest.h +++ b/libraries/networking/src/AssetRequest.h @@ -21,6 +21,8 @@ #include "ByteRange.h" +const QString ATP_SCHEME { "atp:" }; + class AssetRequest : public QObject { Q_OBJECT public: diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index a41283cc0d..c9ca6ebd43 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -63,7 +63,7 @@ void AssetResourceRequest::doSend() { // This is an ATP path, we'll need to figure out what the mapping is. // This may incur a roundtrip to the asset-server, or it may return immediately from the cache in AssetClient. - auto path = _url.path(); + auto path = _url.path() + (_url.hasQuery() ? "?" + _url.query() : ""); requestMappingForPath(path); } } @@ -82,15 +82,27 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { Q_ASSERT(_state == InProgress); Q_ASSERT(request == _assetMappingRequest); + bool failed = false; + switch (request->getError()) { case MappingRequest::NoError: // we have no error, we should have a resulting hash - use that to send of a request for that asset qCDebug(networking) << "Got mapping for:" << path << "=>" << request->getHash(); - requestHash(request->getHash()); - statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_SUCCESS); + // if we got a redirected path we need to store that with the resource request as relative path URL + if (request->wasRedirected()) { + _relativePathURL = ATP_SCHEME + request->getRedirectedPath(); + } + + if (request->wasRedirected() && _failOnRedirect) { + _result = RedirectFail; + failed = true; + } else { + requestHash(request->getHash()); + } + break; default: { switch (request->getError()) { @@ -107,17 +119,20 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { break; } - // since we've failed we know we are finished - _state = Finished; - emit finished(); - - statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_FAILED); - statTracker->incrementStat(STAT_ATP_REQUEST_FAILED); + failed = true; break; } } + if (failed) { + _state = Finished; + emit finished(); + + statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_FAILED); + statTracker->incrementStat(STAT_ATP_REQUEST_FAILED); + } + _assetMappingRequest->deleteLater(); _assetMappingRequest = nullptr; }); diff --git a/libraries/networking/src/AssetUtils.cpp b/libraries/networking/src/AssetUtils.cpp index 7818c8e5ce..3af0b1df47 100644 --- a/libraries/networking/src/AssetUtils.cpp +++ b/libraries/networking/src/AssetUtils.cpp @@ -84,3 +84,20 @@ bool isValidHash(const AssetHash& hash) { QRegExp hashRegex { ASSET_HASH_REGEX_STRING }; return hashRegex.exactMatch(hash); } + +QString bakingStatusToString(BakingStatus status) { + switch (status) { + case NotBaked: + return "Not Baked"; + case Pending: + return "Pending"; + case Baking: + return "Baking"; + case Baked: + return "Baked"; + case Error: + return "Error"; + default: + return "--"; + } +} diff --git a/libraries/networking/src/AssetUtils.h b/libraries/networking/src/AssetUtils.h index 4137193274..a7c053c3d6 100644 --- a/libraries/networking/src/AssetUtils.h +++ b/libraries/networking/src/AssetUtils.h @@ -23,7 +23,6 @@ using DataOffset = int64_t; using AssetPath = QString; using AssetHash = QString; -using AssetMapping = std::map; using AssetPathList = QStringList; const size_t SHA256_HASH_LENGTH = 32; @@ -34,6 +33,8 @@ const QString ASSET_FILE_PATH_REGEX_STRING = "^(\\/[^\\/\\0]+)+$"; const QString ASSET_PATH_REGEX_STRING = "^\\/([^\\/\\0]+(\\/)?)+$"; const QString ASSET_HASH_REGEX_STRING = QString("^[a-fA-F0-9]{%1}$").arg(SHA256_HASH_HEX_LENGTH); +const QString HIDDEN_BAKED_CONTENT_FOLDER = "/.baked/"; + enum AssetServerError : uint8_t { NoError = 0, AssetNotFound, @@ -49,9 +50,27 @@ enum AssetMappingOperationType : uint8_t { GetAll, Set, Delete, - Rename + Rename, + SetBakingEnabled }; +enum BakingStatus { + Irrelevant, + NotBaked, + Pending, + Baking, + Baked, + Error +}; + +struct MappingInfo { + AssetHash hash; + BakingStatus status; + QString bakingErrors; +}; + +using AssetMapping = std::map; + QUrl getATPUrl(const QString& hash); QByteArray hashData(const QByteArray& data); @@ -63,4 +82,6 @@ bool isValidFilePath(const AssetPath& path); bool isValidPath(const AssetPath& path); bool isValidHash(const QString& hashString); +QString bakingStatusToString(BakingStatus status); + #endif // hifi_AssetUtils_h diff --git a/libraries/networking/src/MappingRequest.cpp b/libraries/networking/src/MappingRequest.cpp index 810b5b376d..a79105e3ab 100644 --- a/libraries/networking/src/MappingRequest.cpp +++ b/libraries/networking/src/MappingRequest.cpp @@ -87,6 +87,20 @@ void GetMappingRequest::doStart() { if (!_error) { _hash = message->read(SHA256_HASH_LENGTH).toHex(); + + // check the boolean to see if this request got re-directed + quint8 wasRedirected; + message->readPrimitive(&wasRedirected); + _wasRedirected = wasRedirected; + + // if it did grab that re-directed path + if (_wasRedirected) { + _redirectedPath = message->readString(); + qDebug() << "Got redirected from " << _path << " to " << _redirectedPath; + } else { + qDebug() << "Not redirected: " << _path; + } + } emit finished(this); }); @@ -117,12 +131,18 @@ void GetAllMappingsRequest::doStart() { if (!_error) { - int numberOfMappings; + uint32_t numberOfMappings; message->readPrimitive(&numberOfMappings); - for (auto i = 0; i < numberOfMappings; ++i) { + for (uint32_t i = 0; i < numberOfMappings; ++i) { auto path = message->readString(); auto hash = message->read(SHA256_HASH_LENGTH).toHex(); - _mappings[path] = hash; + BakingStatus status; + QString lastBakeErrors; + message->readPrimitive(&status); + if (status == BakingStatus::Error) { + lastBakeErrors = message->readString(); + } + _mappings[path] = { hash, status, lastBakeErrors }; } } emit finished(this); @@ -257,3 +277,46 @@ void RenameMappingRequest::doStart() { emit finished(this); }); } + +SetBakingEnabledRequest::SetBakingEnabledRequest(const AssetPathList& paths, bool enabled) : _paths(paths), _enabled(enabled) { + for (auto& path : _paths) { + path = path.trimmed(); + } +}; + +void SetBakingEnabledRequest::doStart() { + + // short circuit the request if any of the paths are invalid + for (auto& path : _paths) { + if (!isValidPath(path)) { + _error = MappingRequest::InvalidPath; + emit finished(this); + return; + } + } + + auto assetClient = DependencyManager::get(); + + _mappingRequestID = assetClient->setBakingEnabled(_paths, _enabled, + [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + + _mappingRequestID = INVALID_MESSAGE_ID; + if (!responseReceived) { + _error = NetworkError; + } else { + switch (error) { + case AssetServerError::NoError: + _error = NoError; + break; + case AssetServerError::PermissionDenied: + _error = PermissionDenied; + break; + default: + _error = UnknownError; + break; + } + } + + emit finished(this); + }); +}; \ No newline at end of file diff --git a/libraries/networking/src/MappingRequest.h b/libraries/networking/src/MappingRequest.h index 85b68e2427..fc43375469 100644 --- a/libraries/networking/src/MappingRequest.h +++ b/libraries/networking/src/MappingRequest.h @@ -53,6 +53,8 @@ public: GetMappingRequest(const AssetPath& path); AssetHash getHash() const { return _hash; } + AssetPath getRedirectedPath() const { return _redirectedPath; } + bool wasRedirected() const { return _wasRedirected; } signals: void finished(GetMappingRequest* thisRequest); @@ -62,6 +64,10 @@ private: AssetPath _path; AssetHash _hash; + + + AssetPath _redirectedPath; + bool _wasRedirected { false }; }; class SetMappingRequest : public MappingRequest { @@ -124,7 +130,22 @@ signals: private: virtual void doStart() override; - std::map _mappings; + AssetMapping _mappings; +}; + +class SetBakingEnabledRequest : public MappingRequest { + Q_OBJECT +public: + SetBakingEnabledRequest(const AssetPathList& path, bool enabled); + +signals: + void finished(SetBakingEnabledRequest* thisRequest); + +private: + virtual void doStart() override; + + AssetPathList _paths; + bool _enabled; }; diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index fbdfa4b87a..a3ac995bcf 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -687,8 +687,9 @@ void Resource::makeRequest() { PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID)); return; } - + _request->setByteRange(_requestByteRange); + _request->setFailOnRedirect(_shouldFailOnRedirect); qCDebug(resourceLog).noquote() << "Starting request for:" << _url.toDisplayString(); emit loading(); @@ -731,6 +732,11 @@ void Resource::handleReplyFinished() { if (result == ResourceRequest::Success) { auto extraInfo = _url == _activeUrl ? "" : QString(", %1").arg(_activeUrl.toDisplayString()); qCDebug(networking).noquote() << QString("Request finished for %1%2").arg(_url.toDisplayString(), extraInfo); + + auto relativePathURL = _request->getRelativePathUrl(); + if (!relativePathURL.isEmpty()) { + _effectiveBaseURL = relativePathURL; + } auto data = _request->getData(); emit loaded(data); diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index f94e1e26d2..17531f45b0 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -449,11 +449,13 @@ protected: Q_INVOKABLE void allReferencesCleared(); /// Return true if the resource will be retried - bool handleFailedRequest(ResourceRequest::Result result); + virtual bool handleFailedRequest(ResourceRequest::Result result); QUrl _url; + QUrl _effectiveBaseURL{ _url }; QUrl _activeUrl; ByteRange _requestByteRange; + bool _shouldFailOnRedirect { false }; // _loaded == true means we are in a loaded and usable state. It is possible that there may still be // active requests/loading while in this state. Example: Progressive KTX downloads, where higher resolution diff --git a/libraries/networking/src/ResourceRequest.cpp b/libraries/networking/src/ResourceRequest.cpp index aeeab2232a..f028535e2f 100644 --- a/libraries/networking/src/ResourceRequest.cpp +++ b/libraries/networking/src/ResourceRequest.cpp @@ -36,6 +36,7 @@ QString ResourceRequest::getResultString() const { case AccessDenied: return "Access Denied"; case InvalidURL: return "Invalid URL"; case NotFound: return "Not Found"; + case RedirectFail: return "Redirect Fail"; default: return "Unspecified Error"; } } diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index 3ee86025a2..cf852c1e1b 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -57,7 +57,8 @@ public: AccessDenied, InvalidByteRange, InvalidURL, - NotFound + NotFound, + RedirectFail }; Q_ENUM(Result) @@ -66,9 +67,11 @@ public: Result getResult() const { return _result; } QString getResultString() const; QUrl getUrl() const { return _url; } + QUrl getRelativePathUrl() const { return _relativePathURL; } bool loadedFromCache() const { return _loadedFromCache; } bool getRangeRequestSuccessful() const { return _rangeRequestSuccessful; } bool getTotalSizeOfResource() const { return _totalSizeOfResource; } + void setFailOnRedirect(bool failOnRedirect) { _failOnRedirect = failOnRedirect; } void setCacheEnabled(bool value) { _cacheEnabled = value; } void setByteRange(ByteRange byteRange) { _byteRange = byteRange; } @@ -84,9 +87,11 @@ protected: virtual void doSend() = 0; QUrl _url; + QUrl _relativePathURL; State _state { NotStarted }; Result _result; QByteArray _data; + bool _failOnRedirect { false }; bool _cacheEnabled { true }; bool _loadedFromCache { false }; ByteRange _byteRange; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 6e899c21df..b95f2ff6a0 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -42,6 +42,9 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(MessageDataVersion::TextOrBinaryData); case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing + case PacketType::AssetMappingOperation: + case PacketType::AssetMappingOperationReply: + return static_cast(AssetServerPacketVersion::RedirectedMappings); case PacketType::AssetGetInfo: case PacketType::AssetGet: case PacketType::AssetUpload: @@ -65,7 +68,6 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::MicrophoneAudioWithEcho: case PacketType::AudioStreamStats: return static_cast(AudioVersion::HighDynamicRangeVolume); - default: return 17; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index a51778d5ff..4fefc6ab3a 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -140,11 +140,16 @@ public: const static QSet getNonVerifiedPackets() { const static QSet NON_VERIFIED_PACKETS = QSet() - << PacketTypeEnum::Value::NodeJsonStats << PacketTypeEnum::Value::EntityQuery - << PacketTypeEnum::Value::OctreeDataNack << PacketTypeEnum::Value::EntityEditNack - << PacketTypeEnum::Value::DomainListRequest << PacketTypeEnum::Value::StopNode - << PacketTypeEnum::Value::DomainDisconnectRequest << PacketTypeEnum::Value::UsernameFromIDRequest - << PacketTypeEnum::Value::NodeKickRequest << PacketTypeEnum::Value::NodeMuteRequest; + << PacketTypeEnum::Value::NodeJsonStats + << PacketTypeEnum::Value::EntityQuery + << PacketTypeEnum::Value::OctreeDataNack + << PacketTypeEnum::Value::EntityEditNack + << PacketTypeEnum::Value::DomainListRequest + << PacketTypeEnum::Value::StopNode + << PacketTypeEnum::Value::DomainDisconnectRequest + << PacketTypeEnum::Value::UsernameFromIDRequest + << PacketTypeEnum::Value::NodeKickRequest + << PacketTypeEnum::Value::NodeMuteRequest; return NON_VERIFIED_PACKETS; } @@ -269,7 +274,8 @@ enum class EntityQueryPacketVersion: PacketVersion { enum class AssetServerPacketVersion: PacketVersion { VegasCongestionControl = 19, - RangeRequestSupport + RangeRequestSupport, + RedirectedMappings }; enum class AvatarMixerPacketVersion : PacketVersion { diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index d2da521768..32e764bd10 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -120,7 +120,7 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR); _dynamicsWorld->addAction(this); // restore gravity settings because adding an object to the world overwrites its gravity setting - _rigidBody->setGravity(_gravity * _currentUp); + _rigidBody->setGravity(_currentGravity * _currentUp); btCollisionShape* shape = _rigidBody->getCollisionShape(); assert(shape && shape->getShapeType() == CONVEX_HULL_SHAPE_PROXYTYPE); _ghost.setCharacterShape(static_cast(shape)); @@ -302,7 +302,7 @@ void CharacterController::playerStep(btCollisionWorld* collisionWorld, btScalar // add minimum velocity to counteract gravity's displacement during one step // Note: the 0.5 factor comes from the fact that we really want the // average velocity contribution from gravity during the step - stepUpSpeed -= 0.5f * _gravity * timeToStep; // remember: _gravity is negative scalar + stepUpSpeed -= 0.5f * _currentGravity * timeToStep; // remember: _gravity is negative scalar btScalar vDotUp = velocity.dot(_currentUp); if (vDotUp < stepUpSpeed) { @@ -351,6 +351,28 @@ static const char* stateToStr(CharacterController::State state) { } #endif // #ifdef DEBUG_STATE_CHANGE +void CharacterController::updateCurrentGravity() { + int16_t collisionGroup = computeCollisionGroup(); + if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _currentGravity = 0.0f; + } else { + _currentGravity = _gravity; + } + if (_rigidBody) { + _rigidBody->setGravity(_currentGravity * _currentUp); + } +} + + +void CharacterController::setGravity(float gravity) { + _gravity = gravity; + updateCurrentGravity(); +} + +float CharacterController::getGravity() { + return _gravity; +} + #ifdef DEBUG_STATE_CHANGE void CharacterController::setState(State desiredState, const char* reason) { #else @@ -365,19 +387,7 @@ void CharacterController::setState(State desiredState) { qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason; #endif _state = desiredState; - updateGravity(); - } -} - -void CharacterController::updateGravity() { - int16_t collisionGroup = computeCollisionGroup(); - if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) { - _gravity = 0.0f; - } else { - _gravity = DEFAULT_AVATAR_GRAVITY; - } - if (_rigidBody) { - _rigidBody->setGravity(_gravity * _currentUp); + updateCurrentGravity(); } } @@ -436,14 +446,14 @@ void CharacterController::handleChangedCollisionGroup() { _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR); } _pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_GROUP; - updateGravity(); + updateCurrentGravity(); } } void CharacterController::updateUpAxis(const glm::quat& rotation) { _currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS); if (_rigidBody) { - _rigidBody->setGravity(_gravity * _currentUp); + _rigidBody->setGravity(_currentGravity * _currentUp); } } diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index 585eb7d3ed..0f97cc7c16 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -24,6 +24,7 @@ #include "BulletUtil.h" #include "CharacterGhostObject.h" +#include "AvatarConstants.h" const uint32_t PENDING_FLAG_ADD_TO_SIMULATION = 1U << 0; const uint32_t PENDING_FLAG_REMOVE_FROM_SIMULATION = 1U << 1; @@ -42,15 +43,18 @@ const btScalar MAX_CHARACTER_MOTOR_TIMESCALE = 60.0f; // one minute const btScalar MIN_CHARACTER_MOTOR_TIMESCALE = 0.05f; class CharacterController : public btCharacterControllerInterface { + public: CharacterController(); virtual ~CharacterController(); - bool needsRemoval() const; bool needsAddition() const; virtual void setDynamicsWorld(btDynamicsWorld* world); btCollisionObject* getCollisionObject() { return _rigidBody; } + void setGravity(float gravity); + float getGravity(); + virtual void updateShapeIfNecessary() = 0; // overrides from btCharacterControllerInterface @@ -131,7 +135,7 @@ protected: #endif virtual void updateMassProperties() = 0; - void updateGravity(); + void updateCurrentGravity(); void updateUpAxis(const glm::quat& rotation); bool checkForSupport(btCollisionWorld* collisionWorld); @@ -184,7 +188,8 @@ protected: bool _stepUpEnabled { true }; bool _hasSupport; - btScalar _gravity { 0.0f }; + btScalar _currentGravity { 0.0f }; + btScalar _gravity { DEFAULT_AVATAR_GRAVITY }; btScalar _followTime; btVector3 _followLinearDisplacement; diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 517fe97dba..3f57a1779a 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -321,17 +321,25 @@ template <> void payloadRender(const ModelMeshPartPayload::Pointer& payload, Ren } -ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int _meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) : - _meshIndex(_meshIndex), +ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) : + _meshIndex(meshIndex), _shapeID(shapeIndex) { assert(model && model->isLoaded()); _model = model; auto& modelMesh = model->getGeometry()->getMeshes().at(_meshIndex); + const Model::MeshState& state = model->getMeshState(_meshIndex); updateMeshPart(modelMesh, partIndex); + computeAdjustedLocalBound(state.clusterMatrices); updateTransform(transform, offsetTransform); + Transform renderTransform = transform; + if (state.clusterMatrices.size() == 1) { + renderTransform = transform.worldTransform(Transform(state.clusterMatrices[0])); + } + updateTransformForSkinnedMesh(renderTransform, transform, state.clusterBuffer); + initCache(); } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 42bb91ce94..9948a8bddd 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -209,11 +209,6 @@ void Model::updateRenderItems() { return; } - glm::vec3 scale = getScale(); - if (_collisionGeometry) { - // _collisionGeometry is already scaled - scale = glm::vec3(1.0f); - } _needsUpdateClusterMatrices = true; _renderItemsNeedUpdate = false; @@ -221,7 +216,7 @@ void Model::updateRenderItems() { // the application will ensure only the last lambda is actually invoked. void* key = (void*)this; std::weak_ptr weakSelf = shared_from_this(); - AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [weakSelf, scale]() { + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [weakSelf]() { // do nothing, if the model has already been destroyed. auto self = weakSelf.lock(); @@ -1219,6 +1214,7 @@ const render::ItemIDs& Model::fetchRenderItemIDs() const { } void Model::createRenderItemSet() { + updateClusterMatrices(); if (_collisionGeometry) { if (_collisionRenderItems.empty()) { createCollisionRenderItemSet(); @@ -1269,7 +1265,6 @@ void Model::createVisibleRenderItemSet() { shapeID++; } } - computeMeshPartLocalBounds(); } void Model::createCollisionRenderItemSet() { diff --git a/libraries/render-utils/src/OutlineEffect.h b/libraries/render-utils/src/OutlineEffect.h index 36dc59f29e..f88092429f 100644 --- a/libraries/render-utils/src/OutlineEffect.h +++ b/libraries/render-utils/src/OutlineEffect.h @@ -81,10 +81,10 @@ public: float getColorB() const { return color.b; } glm::vec3 color{ 1.f, 0.7f, 0.2f }; - float width{ 3.f }; - float intensity{ 1.f }; - float fillOpacityUnoccluded{ 0.35f }; - float fillOpacityOccluded{ 0.1f }; + float width{ 2.0f }; + float intensity{ 0.9f }; + float fillOpacityUnoccluded{ 0.0f }; + float fillOpacityOccluded{ 0.0f }; bool glow{ false }; signals: diff --git a/libraries/render-utils/src/glowLine.slf b/libraries/render-utils/src/glowLine.slf index c0af97930a..580a49dd3e 100644 --- a/libraries/render-utils/src/glowLine.slf +++ b/libraries/render-utils/src/glowLine.slf @@ -9,7 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -layout(location = 0) in vec4 inColor; +in vec4 _color; +in float distanceFromCenter; out vec4 _fragColor; @@ -17,10 +18,10 @@ void main(void) { // The incoming value actually ranges from -1 to 1, so modify it // so that it goes from 0 -> 1 -> 0 with the solid alpha being at // the center of the line - float alpha = 1.0 - abs(inColor.a); + float alpha = 1.0 - abs(distanceFromCenter); // Convert from a linear alpha curve to a sharp peaked one - alpha = pow(alpha, 10); + alpha = _color.a * pow(alpha, 10); // Drop everything where the curve falls off to nearly nothing if (alpha <= 0.05) { @@ -28,6 +29,5 @@ void main(void) { } // Emit the color - _fragColor = vec4(inColor.rgb, alpha); - return; + _fragColor = vec4(_color.rgb, alpha); } diff --git a/libraries/render-utils/src/glowLine.slv b/libraries/render-utils/src/glowLine.slv index e856edc787..fd3a85d254 100644 --- a/libraries/render-utils/src/glowLine.slv +++ b/libraries/render-utils/src/glowLine.slv @@ -18,7 +18,9 @@ layout(std140) uniform lineData { vec4 color; }; -layout(location = 0) out vec4 _color; +out vec4 _color; +// the distance from the center in 'quad space' +out float distanceFromCenter; void main(void) { _color = color; @@ -45,11 +47,10 @@ void main(void) { // Add or subtract the orthogonal vector based on a different vertex ID // calculation if (gl_VertexID < 2) { - // Use the alpha channel to store the distance from the center in 'quad space' - _color.a = -1.0; + distanceFromCenter = -1.0; eye.xyz -= orthogonal; } else { - _color.a = 1.0; + distanceFromCenter = 1.0; eye.xyz += orthogonal; } diff --git a/libraries/render/src/render/Forward.h b/libraries/render/src/render/Forward.h index 1f297d39e1..4fb9251247 100644 --- a/libraries/render/src/render/Forward.h +++ b/libraries/render/src/render/Forward.h @@ -28,6 +28,7 @@ namespace render { class Scene; using ScenePointer = std::shared_ptr; class ShapePipeline; + class Transaction; } using RenderArgs = render::Args; diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index 65259987c4..25e8c0dcf3 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -55,7 +55,6 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { - const QString ATP_SCHEME { "atp:" }; if (!urlString.startsWith(ATP_SCHEME)) { return; @@ -89,6 +88,20 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb assetRequest->start(); } +void AssetScriptingInterface::setBakingEnabled(QString path, bool enabled, QScriptValue callback) { + auto setBakingEnabledRequest = DependencyManager::get()->createSetBakingEnabledRequest({ path }, enabled); + + QObject::connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable { + if (callback.isFunction()) { + QString error = request->getErrorString(); + QScriptValueList args{ error }; + callback.call(_engine->currentContext()->thisObject(), args); + } + request->deleteLater(); + }); + setBakingEnabledRequest->start(); +} + #if (PR_BUILD || DEV_BUILD) void AssetScriptingInterface::sendFakedHandshake() { auto nodeList = DependencyManager::get(); diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 0238329b73..2812be65f9 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -75,6 +75,8 @@ public: * @param {string} error */ Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback); + + Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback); #if (PR_BUILD || DEV_BUILD) Q_INVOKABLE void sendFakedHandshake(); diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index b51e9cd529..55895e31a4 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -54,9 +54,24 @@ float RecordingScriptingInterface::playerLength() const { return _player->length(); } +void RecordingScriptingInterface::playClip(NetworkClipLoaderPointer clipLoader, const QString& url, QScriptValue callback) { + _player->queueClip(clipLoader->getClip()); + + if (callback.isFunction()) { + QScriptValueList args { true, url }; + callback.call(_scriptEngine->globalObject(), args); + } +} + void RecordingScriptingInterface::loadRecording(const QString& url, QScriptValue callback) { auto clipLoader = DependencyManager::get()->getClipLoader(url); + if (clipLoader->isLoaded()) { + qCDebug(scriptengine) << "Recording already loaded from" << url; + playClip(clipLoader, url, callback); + return; + } + // hold a strong pointer to the loading clip so that it has a chance to load _clipLoaders.insert(clipLoader); @@ -69,12 +84,7 @@ void RecordingScriptingInterface::loadRecording(const QString& url, QScriptValue if (auto clipLoader = weakClipLoader.toStrongRef()) { qCDebug(scriptengine) << "Loaded recording from" << url; - _player->queueClip(clipLoader->getClip()); - - if (callback.isFunction()) { - QScriptValueList args { true, url }; - callback.call(_scriptEngine->globalObject(), args); - } + playClip(clipLoader, url, callback); // drop our strong pointer to this clip so it is cleaned up _clipLoaders.remove(clipLoader); diff --git a/libraries/script-engine/src/RecordingScriptingInterface.h b/libraries/script-engine/src/RecordingScriptingInterface.h index bc0b019251..22e4d30830 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.h +++ b/libraries/script-engine/src/RecordingScriptingInterface.h @@ -88,6 +88,9 @@ protected: QSharedPointer _scriptEngine; QSet _clipLoaders; + +private: + void playClip(recording::NetworkClipLoaderPointer clipLoader, const QString& url, QScriptValue callback); }; #endif // hifi_RecordingScriptingInterface_h diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 0636411f51..20e30e15e8 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -54,6 +54,19 @@ QString PathUtils::getAppLocalDataFilePath(const QString& filename) { return QDir(getAppLocalDataPath()).absoluteFilePath(filename); } +QString PathUtils::generateTemporaryDir() { + QDir rootTempDir = QDir::tempPath(); + QString appName = qApp->applicationName(); + for (auto i = 0; i < 64; ++i) { + auto now = std::chrono::system_clock::now().time_since_epoch().count(); + QDir tempDir = rootTempDir.filePath(appName + "-" + QString::number(now)); + if (tempDir.mkpath(".")) { + return tempDir.absolutePath(); + } + } + return ""; +} + QString fileNameWithoutExtension(const QString& fileName, const QVector possibleExtensions) { QString fileNameLowered = fileName.toLower(); foreach (const QString possibleExtension, possibleExtensions) { diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 3cb3cd3b63..8c4bcf2394 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -14,6 +14,7 @@ #include #include +#include #include "DependencyManager.h" @@ -36,6 +37,8 @@ public: static QString getAppDataFilePath(const QString& filename); static QString getAppLocalDataFilePath(const QString& filename); + static QString generateTemporaryDir(); + static Qt::CaseSensitivity getFSCaseSensitivity(); static QString stripFilename(const QUrl& url); // note: this is FS-case-sensitive version of parentURL.isParentOf(childURL) diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index db78144100..8ea38f5f13 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -1182,3 +1182,19 @@ void SpatiallyNestable::dump(const QString& prefix) const { parent->dump(prefix + " "); } } + +bool SpatiallyNestable::isParentPathComplete() const { + static const QUuid IDENTITY; + QUuid parentID = getParentID(); + if (parentID.isNull() || parentID == IDENTITY) { + return true; + } + + bool success = false; + SpatiallyNestablePointer parent = getParentPointer(success); + if (!success || !parent) { + return false; + } + + return parent->isParentPathComplete(); +} diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index 90d2e33016..b6be4dc056 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -66,6 +66,10 @@ public: static QString nestableTypeToString(NestableType nestableType); + + virtual bool isParentPathComplete() const; + + // world frame virtual const Transform getTransform(bool& success, int depth = 0) const; virtual const Transform getTransform() const; diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index d03842d45a..071ccd46b1 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -211,12 +211,12 @@ class UrlHandler : public QObject { public: Q_INVOKABLE bool canHandleUrl(const QString& url) { static auto handler = dynamic_cast(qApp); - return handler->canAcceptURL(url); + return handler && handler->canAcceptURL(url); } Q_INVOKABLE bool handleUrl(const QString& url) { static auto handler = dynamic_cast(qApp); - return handler->acceptURL(url); + return handler && handler->acceptURL(url); } }; @@ -987,8 +987,8 @@ static bool equals(const QByteArray& byteArray, const uint8_t* ptr) { return ptr[i] == 0x00; } -void OffscreenQmlSurface::synthesizeKeyPress(QString key) { - auto eventHandler = getEventHandler(); +void OffscreenQmlSurface::synthesizeKeyPress(QString key, QObject* targetOverride) { + auto eventHandler = targetOverride ? targetOverride : getEventHandler(); if (eventHandler) { auto utf8Key = key.toUtf8(); diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index 95dabdef0f..990f81848d 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -79,7 +79,7 @@ public: bool eventFilter(QObject* originalDestination, QEvent* event) override; void setKeyboardRaised(QObject* object, bool raised, bool numeric = false); - Q_INVOKABLE void synthesizeKeyPress(QString key); + Q_INVOKABLE void synthesizeKeyPress(QString key, QObject* targetOverride = nullptr); using TextureAndFence = std::pair; // Checks to see if a new texture is available. If one is, the function returns true and diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 6ef3effa4c..233a9458fe 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -61,7 +61,7 @@ void RequestFilters::interceptHFWebEngineRequest(QWebEngineUrlRequestInfo& info) // During the period in which we have HFC commerce in the system, but not applied everywhere: const QString tokenStringCommerce{ "Chrome/48.0 (HighFidelityInterface WithHFC)" }; - static Setting::Handle _settingSwitch{ "inspectionMode", false }; + static Setting::Handle _settingSwitch{ "commerce", false }; bool isMoney = _settingSwitch.get(); const QString tokenString = !isAuthable ? tokenStringMobile : (isMoney ? tokenStringCommerce : tokenStringMetaverse); diff --git a/scripts/developer/tests/gravityScript.js b/scripts/developer/tests/gravityScript.js new file mode 100644 index 0000000000..3468de72c3 --- /dev/null +++ b/scripts/developer/tests/gravityScript.js @@ -0,0 +1,45 @@ +// +// Gravity Script 1.0 +// ************ +// +// Created by Cain Kilgore on 9/14/2017 + +// Javascript for the Gravity Modifier Implementation to test +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +function menuParameters(menuNameSelection, menuItemNameSelection) { + Menu.addMenuItem({ + menuName: menuNameSelection, + menuItemName: menuItemNameSelection, + isCheckable: false + }); +} + +function setupMenu() { + if (!Menu.menuExists("Gravity")) { + Menu.addMenu("Gravity"); + for (var i = -5; i <= 5; i++) { + menuParameters("Gravity", i); + } + } +} + +function menuItemEvent(menuItem) { + for (var i = -5; i <= 5; i++) { + if (menuItem == i) { + MyAvatar.setGravity(i); + } + } +} + +function onScriptEnding() { + Menu.removeMenu("Gravity"); +} + +setupMenu(); +Menu.menuItemEvent.connect(menuItemEvent); +Script.scriptEnding.connect(onScriptEnding); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 5a668a3d6e..107160154a 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -131,7 +131,7 @@ var button; var buttonName = "WALLET"; var tablet = null; - var walletEnabled = Settings.getValue("inspectionMode", false); + var walletEnabled = Settings.getValue("commerce", false); function startup() { if (walletEnabled) { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 63657e9b6f..22987245a4 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -140,10 +140,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }; this.setIgnoreTablet = function() { - if (HMD.tabletID !== _this.tabletID) { - RayPick.setIgnoreOverlays(_this.leftControllerRayPick, [HMD.tabletID]); - RayPick.setIgnoreOverlays(_this.rightControllerRayPick, [HMD.tabletID]); - } + RayPick.setIgnoreOverlays(_this.leftControllerRayPick, [HMD.tabletID]); + RayPick.setIgnoreOverlays(_this.rightControllerRayPick, [HMD.tabletID]); }; this.update = function () { diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index eb73b0f908..d2b5f92fde 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -148,7 +148,9 @@ Script.include("/~/system/libraries/controllers.js"); if (mode === "full") { var fullEndToEdit = PICK_WITH_HAND_RAY ? this.fullEnd : fullEnd; fullEndToEdit.dimensions = dim; - LaserPointers.editRenderState(laserPointerID, mode, {path: fullPath, end: fullEndToEdit}); + LaserPointers.editRenderState(laserPointerID, mode, { path: fullPath, end: fullEndToEdit }); + this.contextOverlayTimer = false; + this.destroyContextOverlay(); } else if (mode === "half") { var halfEndToEdit = PICK_WITH_HAND_RAY ? this.halfEnd : halfEnd; halfEndToEdit.dimensions = dim; diff --git a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js index 4ad17e6de9..8d093afe2c 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js @@ -26,6 +26,7 @@ Script.include("/~/system/libraries/utils.js"); this.previousParentID = {}; this.previousParentJointIndex = {}; this.previouslyUnhooked = {}; + this.robbed = false; this.parameters = makeDispatcherModuleParameters( 90, @@ -111,9 +112,9 @@ Script.include("/~/system/libraries/utils.js"); } else if (this.otherHandIsParent(grabbedProperties)) { // the other hand is parent. Steal the object and information var otherModule = this.getOtherModule(); - this.previousParentID[this.grabbedThingID] = otherModule.previousParentID[this.garbbedThingID]; + this.previousParentID[this.grabbedThingID] = otherModule.previousParentID[this.grabbedThingID]; this.previousParentJointIndex[this.grabbedThingID] = otherModule.previousParentJointIndex[this.grabbedThingID]; - + otherModule.robbed = true; } else { this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID; this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; @@ -134,12 +135,12 @@ Script.include("/~/system/libraries/utils.js"); this.endNearParentingGrabOverlay = function () { var previousParentID = this.previousParentID[this.grabbedThingID]; - if (previousParentID === NULL_UUID || previousParentID === null || previousParentID === undefined) { + if ((previousParentID === NULL_UUID || previousParentID === null) && !this.robbed) { Overlays.editOverlay(this.grabbedThingID, { parentID: NULL_UUID, parentJointIndex: -1 }); - } else { + } else if (!this.robbed){ // before we grabbed it, overlay was a child of something; put it back. Overlays.editOverlay(this.grabbedThingID, { parentID: this.previousParentID[this.grabbedThingID], @@ -170,7 +171,9 @@ Script.include("/~/system/libraries/utils.js"); this.isReady = function (controllerData) { - if (controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) { + if ((controllerData.triggerClicks[this.hand] === 0 && + controllerData.secondaryValues[this.hand] === 0)) { + this.robbed = false; return makeRunningValues(false, [], []); } @@ -182,7 +185,7 @@ Script.include("/~/system/libraries/utils.js"); }); var targetID = this.getTargetID(grabbableOverlays, controllerData); - if (targetID) { + if (targetID && !this.robbed) { this.grabbedThingID = targetID; this.startNearParentingGrabOverlay(controllerData); return makeRunningValues(true, [this.grabbedThingID], []); @@ -194,6 +197,7 @@ Script.include("/~/system/libraries/utils.js"); this.run = function (controllerData) { if (controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) { this.endNearParentingGrabOverlay(); + this.robbed = false; return makeRunningValues(false, [], []); } else { // check if someone stole the target from us diff --git a/scripts/system/controllers/controllerModules/overlayLaserInput.js b/scripts/system/controllers/controllerModules/overlayLaserInput.js index 218122e876..7dace85ec4 100644 --- a/scripts/system/controllers/controllerModules/overlayLaserInput.js +++ b/scripts/system/controllers/controllerModules/overlayLaserInput.js @@ -16,6 +16,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.include("/~/system/libraries/controllers.js"); (function() { + var TouchEventUtils = Script.require("/~/system/libraries/touchEventUtils.js"); var halfPath = { type: "line3d", color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, @@ -88,186 +89,6 @@ Script.include("/~/system/libraries/controllers.js"); var HAPTIC_STYLUS_STRENGTH = 1.0; var HAPTIC_STYLUS_DURATION = 20.0; - function laserTargetHasKeyboardFocus(laserTarget) { - if (laserTarget && laserTarget !== NULL_UUID) { - return Overlays.keyboardFocusOverlay === laserTarget; - } - } - - function setKeyboardFocusOnLaserTarget(laserTarget) { - if (laserTarget && laserTarget !== NULL_UUID) { - Overlays.keyboardFocusOverlay = laserTarget; - Entities.keyboardFocusEntity = NULL_UUID; - } - } - - function sendHoverEnterEventToLaserTarget(hand, laserTarget) { - if (!laserTarget) { - return; - } - var pointerEvent = { - type: "Move", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: laserTarget.position2D, - pos3D: laserTarget.position, - normal: laserTarget.normal, - direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), - button: "None" - }; - - if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { - Overlays.sendHoverEnterOverlay(laserTarget.overlayID, pointerEvent); - } - } - - function sendHoverOverEventToLaserTarget(hand, laserTarget) { - - if (!laserTarget) { - return; - } - var pointerEvent = { - type: "Move", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: laserTarget.position2D, - pos3D: laserTarget.position, - normal: laserTarget.normal, - direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), - button: "None" - }; - - if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { - Overlays.sendMouseMoveOnOverlay(laserTarget.overlayID, pointerEvent); - Overlays.sendHoverOverOverlay(laserTarget.overlayID, pointerEvent); - } - } - - function sendTouchStartEventToLaserTarget(hand, laserTarget) { - if (!laserTarget) { - return; - } - - var pointerEvent = { - type: "Press", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: laserTarget.position2D, - pos3D: laserTarget.position, - normal: laserTarget.normal, - direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), - button: "Primary", - isPrimaryHeld: true - }; - - if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { - Overlays.sendMousePressOnOverlay(laserTarget.overlayID, pointerEvent); - } - } - - function sendTouchEndEventToLaserTarget(hand, laserTarget) { - if (!laserTarget) { - return; - } - var pointerEvent = { - type: "Release", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: laserTarget.position2D, - pos3D: laserTarget.position, - normal: laserTarget.normal, - direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), - button: "Primary" - }; - - if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { - Overlays.sendMouseReleaseOnOverlay(laserTarget.overlayID, pointerEvent); - Overlays.sendMouseReleaseOnOverlay(laserTarget.overlayID, pointerEvent); - } - } - - function sendTouchMoveEventToLaserTarget(hand, laserTarget) { - if (!laserTarget) { - return; - } - var pointerEvent = { - type: "Move", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: laserTarget.position2D, - pos3D: laserTarget.position, - normal: laserTarget.normal, - direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), - button: "Primary", - isPrimaryHeld: true - }; - - if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { - Overlays.sendMouseReleaseOnOverlay(laserTarget.overlayID, pointerEvent); - } - } - - // will return undefined if overlayID does not exist. - function calculateLaserTargetFromOverlay(worldPos, overlayID) { - var overlayPosition = Overlays.getProperty(overlayID, "position"); - if (overlayPosition === undefined) { - return null; - } - - // project stylusTip onto overlay plane. - var overlayRotation = Overlays.getProperty(overlayID, "rotation"); - if (overlayRotation === undefined) { - return null; - } - var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1}); - var distance = Vec3.dot(Vec3.subtract(worldPos, overlayPosition), normal); - - // calclulate normalized position - var invRot = Quat.inverse(overlayRotation); - var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, overlayPosition)); - var dpi = Overlays.getProperty(overlayID, "dpi"); - - var dimensions; - if (dpi) { - // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property - // is used as a scale. - var resolution = Overlays.getProperty(overlayID, "resolution"); - if (resolution === undefined) { - return null; - } - resolution.z = 1;// Circumvent divide-by-zero. - var scale = Overlays.getProperty(overlayID, "dimensions"); - if (scale === undefined) { - return null; - } - scale.z = 0.01;// overlay dimensions are 2D, not 3D. - dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); - } else { - dimensions = Overlays.getProperty(overlayID, "dimensions"); - if (dimensions === undefined) { - return null; - } - if (!dimensions.z) { - dimensions.z = 0.01;// sometimes overlay dimensions are 2D, not 3D. - } - } - var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z }; - var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT); - - // 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner. - var position2D = { - x: normalizedPosition.x * dimensions.x, - y: (1 - normalizedPosition.y) * dimensions.y // flip y-axis - }; - - return { - entityID: null, - overlayID: overlayID, - distance: distance, - position: worldPos, - position2D: position2D, - normal: normal, - normalizedPosition: normalizedPosition, - dimensions: dimensions, - valid: true - }; - } - function distance2D(a, b) { var dx = (a.x - b.x); var dy = (a.y - b.y); @@ -277,16 +98,11 @@ Script.include("/~/system/libraries/controllers.js"); function OverlayLaserInput(hand) { this.hand = hand; this.active = false; - this.previousLaserClikcedTarget = false; + this.previousLaserClickedTarget = false; this.laserPressingTarget = false; - this.tabletScreenID = null; this.mode = "none"; - this.laserTargetID = null; this.laserTarget = null; this.pressEnterLaserTarget = null; - this.hover = false; - this.target = null; - this.lastValidTargetID = this.tabletTargetID; this.parameters = makeDispatcherModuleParameters( @@ -307,23 +123,51 @@ Script.include("/~/system/libraries/controllers.js"); return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; }; - this.stealTouchFocus = function(laserTarget) { - this.requestTouchFocus(laserTarget); + this.hasTouchFocus = function(laserTarget) { + return (laserTarget.overlayID === this.hoverOverlay); }; this.requestTouchFocus = function(laserTarget) { - if (laserTarget !== null || laserTarget !== undefined) { - sendHoverEnterEventToLaserTarget(this.hand, this.laserTarget); - this.lastValidTargetID = laserTarget; + if (laserTarget.overlayID && + laserTarget.overlayID !== this.hoverOverlay) { + this.hoverOverlay = laserTarget.overlayID; + TouchEventUtils.sendHoverEnterEventToTouchTarget(this.hand, laserTarget); } }; this.relinquishTouchFocus = function() { // send hover leave event. - var pointerEvent = { type: "Move", id: this.hand + 1 }; - Overlays.sendMouseMoveOnOverlay(this.lastValidTargetID, pointerEvent); - Overlays.sendHoverOverOverlay(this.lastValidTargetID, pointerEvent); - Overlays.sendHoverLeaveOverlay(this.lastValidID, pointerEvent); + if (this.hoverOverlay) { + var pointerEvent = { type: "Move", id: this.hand + 1 }; + Overlays.sendMouseMoveOnOverlay(this.hoverOverlay, pointerEvent); + Overlays.sendHoverOverOverlay(this.hoverOverlay, pointerEvent); + Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); + this.hoverOverlay = null; + } + }; + + this.relinquishStylusTargetTouchFocus = function(laserTarget) { + var stylusModuleNames = ["LeftTabletStylusInput", "RightTabletStylusError"]; + for (var i = 0; i < stylusModuleNames.length; i++) { + var stylusModule = getEnabledModuleByName(stylusModuleNames[i]); + if (stylusModule) { + if (stylusModule.hoverOverlay === laserTarget.overlayID) { + stylusModule.relinquishTouchFocus(); + } + } + } + }; + + this.stealTouchFocus = function(laserTarget) { + if (laserTarget.overlayID === this.getOtherModule().hoverOverlay) { + this.getOtherModule().relinquishTouchFocus(); + } + + // If the focus target we want to request is the same of one of the stylus + // tell the stylus to relinquish it focus on our target + this.relinquishStylusTargetTouchFocus(laserTarget); + + this.requestTouchFocus(laserTarget); }; this.updateLaserPointer = function(controllerData) { @@ -345,38 +189,23 @@ Script.include("/~/system/libraries/controllers.js"); this.processControllerTriggers = function(controllerData) { if (controllerData.triggerClicks[this.hand]) { this.mode = "full"; - this.laserPressingTarget = true; - this.hover = false; } else if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { this.mode = "half"; - this.laserPressingTarget = false; - this.hover = true; - this.requestTouchFocus(this.laserTargetID); } else { this.mode = "none"; - this.laserPressingTarget = false; - this.hover = false; - this.relinquishTouchFocus(); - } }; - this.hovering = function() { - if (!laserTargetHasKeyboardFocus(this.laserTagetID)) { - setKeyboardFocusOnLaserTarget(this.laserTargetID); - } - sendHoverOverEventToLaserTarget(this.hand, this.laserTarget); - }; - this.laserPressEnter = function () { - sendTouchStartEventToLaserTarget(this.hand, this.laserTarget); + this.stealTouchFocus(this.laserTarget); + TouchEventUtils.sendTouchStartEventToTouchTarget(this.hand, this.laserTarget); Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); this.touchingEnterTimer = 0; this.pressEnterLaserTarget = this.laserTarget; this.deadspotExpired = false; - var LASER_PRESS_TO_MOVE_DEADSPOT = 0.026; + var LASER_PRESS_TO_MOVE_DEADSPOT = 0.094; this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT) * this.laserTarget.distance; }; @@ -386,15 +215,15 @@ Script.include("/~/system/libraries/controllers.js"); } // special case to handle home button. - if (this.laserTargetID === HMD.homeButtonID) { - Messages.sendLocalMessage("home", this.laserTargetID); + if (this.laserTarget.overlayID === HMD.homeButtonID) { + Messages.sendLocalMessage("home", this.laserTarget.overlayID); } // send press event if (this.deadspotExpired) { - sendTouchEndEventToLaserTarget(this.hand, this.laserTarget); + TouchEventUtils.sendTouchEndEventToTouchTarget(this.hand, this.laserTarget); } else { - sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget); + TouchEventUtils.sendTouchEndEventToTouchTarget(this.hand, this.pressEnterLaserTarget); } }; @@ -402,41 +231,86 @@ Script.include("/~/system/libraries/controllers.js"); this.touchingEnterTimer += dt; if (this.laserTarget) { - var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds - if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || - distance2D( this.laserTarget.position2D, - this.pressEnterLaserTarget.position2D) > this.deadspotRadius) { - sendTouchMoveEventToLaserTarget(this.hand, this.laserTarget); - this.deadspotExpired = true; + if (controllerData.triggerClicks[this.hand]) { + var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds + if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || + distance2D(this.laserTarget.position2D, + this.pressEnterLaserTarget.position2D) > this.deadspotRadius) { + TouchEventUtils.sendTouchMoveEventToTouchTarget(this.hand, this.laserTarget); + this.deadspotExpired = true; + } + } else { + this.laserPressingTarget = false; } } else { this.laserPressingTarget = false; } }; - this.releaseTouchEvent = function() { - sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget); + this.processLaser = function(controllerData) { + if (this.shouldExit(controllerData) || this.getOtherModule().active) { + this.exitModule(); + return false; + } + var intersection = controllerData.rayPicks[this.hand]; + var laserTarget = TouchEventUtils.composeTouchTargetFromIntersection(intersection); + + if (controllerData.triggerClicks[this.hand]) { + this.laserTarget = laserTarget; + this.laserPressingTarget = true; + } else { + this.requestTouchFocus(laserTarget); + + if (!TouchEventUtils.touchTargetHasKeyboardFocus(laserTarget)) { + TouchEventUtils.setKeyboardFocusOnTouchTarget(laserTarget); + } + + if (this.hasTouchFocus(laserTarget) && !this.laserPressingTarget) { + TouchEventUtils.sendHoverOverEventToTouchTarget(this.hand, laserTarget); + } + } + + this.processControllerTriggers(controllerData); + this.updateLaserPointer(controllerData); + this.active = true; + return true; }; - - this.updateLaserTargets = function(controllerData) { - var intersection = controllerData.rayPicks[this.hand]; - this.laserTargetID = intersection.objectID; - this.laserTarget = calculateLaserTargetFromOverlay(intersection.intersection, intersection.objectID); + this.grabModuleWantsNearbyOverlay = function(controllerData) { + if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { + var nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; + var nearGrabModule = getEnabledModuleByName(nearGrabName); + if (nearGrabModule) { + var candidateOverlays = controllerData.nearbyOverlayIDs[this.hand]; + var grabbableOverlays = candidateOverlays.filter(function(overlayID) { + return Overlays.getProperty(overlayID, "grabbable"); + }); + var target = nearGrabModule.getTargetID(grabbableOverlays, controllerData); + if (target) { + return true; + } + } + } + return false; }; this.shouldExit = function(controllerData) { var intersection = controllerData.rayPicks[this.hand]; - var nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; - var nearGrabModule = getEnabledModuleByName(nearGrabName); - var status = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); var offOverlay = (intersection.type !== RayPick.INTERSECTED_OVERLAY); var triggerOff = (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE); - return offOverlay || status.active || triggerOff; + if (triggerOff) { + this.deleteContextOverlay(); + } + var grabbingOverlay = this.grabModuleWantsNearbyOverlay(controllerData); + return offOverlay || grabbingOverlay || triggerOff; }; this.exitModule = function() { - this.releaseTouchEvent(); + if (this.laserPressingTarget) { + this.deadspotExpired = true; + this.laserPressExit(); + this.laserPressingTarget = false; + } this.relinquishTouchFocus(); this.reset(); this.updateLaserPointer(); @@ -444,12 +318,6 @@ Script.include("/~/system/libraries/controllers.js"); }; this.reset = function() { - this.hover = false; - this.pressEnterLaserTarget = null; - this.laserTarget = null; - this.laserTargetID = null; - this.laserPressingTarget = false; - this.previousLaserClickedTarget = null; this.mode = "none"; this.active = false; }; @@ -467,35 +335,13 @@ Script.include("/~/system/libraries/controllers.js"); }; this.isReady = function (controllerData) { - this.target = null; - var intersection = controllerData.rayPicks[this.hand]; - if (intersection.type === RayPick.INTERSECTED_OVERLAY) { - if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE && !this.getOtherModule().active) { - this.target = intersection.objectID; - this.active = true; - return makeRunningValues(true, [], []); - } else { - this.deleteContextOverlay(); - } + if (this.processLaser(controllerData)) { + return makeRunningValues(true, [], []); } - this.reset(); return makeRunningValues(false, [], []); }; this.run = function (controllerData, deltaTime) { - if (this.shouldExit(controllerData)) { - this.exitModule(); - return makeRunningValues(false, [], []); - } - - if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) { - this.deleteContextOverlay(); - } - - this.updateLaserTargets(controllerData); - this.processControllerTriggers(controllerData); - this.updateLaserPointer(controllerData); - if (!this.previousLaserClickedTarget && this.laserPressingTarget) { this.laserPressEnter(); } @@ -508,11 +354,11 @@ Script.include("/~/system/libraries/controllers.js"); this.laserPressing(controllerData, deltaTime); } - if (this.hover) { - this.hovering(); + if (this.processLaser(controllerData)) { + return makeRunningValues(true, [], []); + } else { + return makeRunningValues(false, [], []); } - - return makeRunningValues(true, [], []); }; this.cleanup = function () { diff --git a/scripts/system/controllers/controllerModules/tabletStylusInput.js b/scripts/system/controllers/controllerModules/tabletStylusInput.js index 230038adb5..9d01ceef65 100644 --- a/scripts/system/controllers/controllerModules/tabletStylusInput.js +++ b/scripts/system/controllers/controllerModules/tabletStylusInput.js @@ -16,238 +16,14 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.include("/~/system/libraries/controllers.js"); (function() { - + var TouchEventUtils = Script.require("/~/system/libraries/touchEventUtils.js"); // triggered when stylus presses a web overlay/entity var HAPTIC_STYLUS_STRENGTH = 1.0; var HAPTIC_STYLUS_DURATION = 20.0; var WEB_DISPLAY_STYLUS_DISTANCE = 0.5; var WEB_STYLUS_LENGTH = 0.2; - var WEB_TOUCH_Y_OFFSET = 0.05; // how far forward (or back with a negative number) to slide stylus in hand - - - function stylusTargetHasKeyboardFocus(stylusTarget) { - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - return Entities.keyboardFocusEntity === stylusTarget.entityID; - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - return Overlays.keyboardFocusOverlay === stylusTarget.overlayID; - } - } - - function setKeyboardFocusOnStylusTarget(stylusTarget) { - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID && - Entities.wantsHandControllerPointerEvents(stylusTarget.entityID)) { - Overlays.keyboardFocusOverlay = NULL_UUID; - Entities.keyboardFocusEntity = stylusTarget.entityID; - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.keyboardFocusOverlay = stylusTarget.overlayID; - Entities.keyboardFocusEntity = NULL_UUID; - } - } - - function sendHoverEnterEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Move", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "None" - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendHoverEnterEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendHoverEnterOverlay(stylusTarget.overlayID, pointerEvent); - } - } - - function sendHoverOverEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Move", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "None" - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendHoverOverEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); - Overlays.sendHoverOverOverlay(stylusTarget.overlayID, pointerEvent); - } - } - - function sendTouchStartEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Press", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "Primary", - isPrimaryHeld: true - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendMousePressOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendClickDownOnEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendMousePressOnOverlay(stylusTarget.overlayID, pointerEvent); - } - } - - function sendTouchEndEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Release", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "Primary" - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendMouseReleaseOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendClickReleaseOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendHoverLeaveEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendMouseReleaseOnOverlay(stylusTarget.overlayID, pointerEvent); - } - } - - function sendTouchMoveEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Move", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "Primary", - isPrimaryHeld: true - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendHoldingClickOnEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); - } - } - - // will return undefined if overlayID does not exist. - function calculateStylusTargetFromOverlay(stylusTip, overlayID) { - var overlayPosition = Overlays.getProperty(overlayID, "position"); - if (overlayPosition === undefined) { - return; - } - - // project stylusTip onto overlay plane. - var overlayRotation = Overlays.getProperty(overlayID, "rotation"); - if (overlayRotation === undefined) { - return; - } - var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1}); - var distance = Vec3.dot(Vec3.subtract(stylusTip.position, overlayPosition), normal); - var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); - - // calclulate normalized position - var invRot = Quat.inverse(overlayRotation); - var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, overlayPosition)); - var dpi = Overlays.getProperty(overlayID, "dpi"); - - var dimensions; - if (dpi) { - // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property - // is used as a scale. - var resolution = Overlays.getProperty(overlayID, "resolution"); - if (resolution === undefined) { - return; - } - resolution.z = 1; // Circumvent divide-by-zero. - var scale = Overlays.getProperty(overlayID, "dimensions"); - if (scale === undefined) { - return; - } - scale.z = 0.01; // overlay dimensions are 2D, not 3D. - dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); - } else { - dimensions = Overlays.getProperty(overlayID, "dimensions"); - if (dimensions === undefined) { - return; - } - if (!dimensions.z) { - dimensions.z = 0.01; // sometimes overlay dimensions are 2D, not 3D. - } - } - var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z }; - var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT); - - // 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner. - var position2D = { - x: normalizedPosition.x * dimensions.x, - y: (1 - normalizedPosition.y) * dimensions.y // flip y-axis - }; - - return { - entityID: null, - overlayID: overlayID, - distance: distance, - position: position, - position2D: position2D, - normal: normal, - normalizedPosition: normalizedPosition, - dimensions: dimensions, - valid: true - }; - } - - // will return undefined if entity does not exist. - function calculateStylusTargetFromEntity(stylusTip, props) { - if (props.rotation === undefined) { - // if rotation is missing from props object, then this entity has probably been deleted. - return; - } - - // project stylus tip onto entity plane. - var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1}); - Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0}); - var distance = Vec3.dot(Vec3.subtract(stylusTip.position, props.position), normal); - var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); - - // generate normalized coordinates - var invRot = Quat.inverse(props.rotation); - var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position)); - var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z }; - var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint); - - // 2D position on entity plane in meters, relative to the bounding box upper-left hand corner. - var position2D = { - x: normalizedPosition.x * props.dimensions.x, - y: (1 - normalizedPosition.y) * props.dimensions.y // flip y-axis - }; - - return { - entityID: props.id, - entityProps: props, - overlayID: null, - distance: distance, - position: position, - position2D: position2D, - normal: normal, - normalizedPosition: normalizedPosition, - dimensions: props.dimensions, - valid: true - }; - } + var WEB_TOUCH_Y_OFFSET = 0.105; // how far forward (or back with a negative number) to slide stylus in hand function isNearStylusTarget(stylusTargets, edgeBorder, minNormalDistance, maxNormalDistance) { for (var i = 0; i < stylusTargets.length; i++) { @@ -330,7 +106,7 @@ Script.include("/~/system/libraries/controllers.js"); 100); this.getOtherHandController = function() { - return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; + return (this.hand === RIGHT_HAND) ? leftTabletStylusInput : rightTabletStylusInput; }; this.handToController = function() { @@ -430,12 +206,12 @@ Script.include("/~/system/libraries/controllers.js"); stylusTarget.entityID !== this.hoverEntity && stylusTarget.entityID !== this.getOtherHandController().hoverEntity) { this.hoverEntity = stylusTarget.entityID; - sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); + TouchEventUtils.sendHoverEnterEventToTouchTarget(this.hand, stylusTarget); } else if (stylusTarget.overlayID && stylusTarget.overlayID !== this.hoverOverlay && stylusTarget.overlayID !== this.getOtherHandController().hoverOverlay) { this.hoverOverlay = stylusTarget.overlayID; - sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); + TouchEventUtils.sendHoverEnterEventToTouchTarget(this.hand, stylusTarget); } }; @@ -492,7 +268,7 @@ Script.include("/~/system/libraries/controllers.js"); for (i = 0; i < candidateEntities.length; i++) { props = candidateEntities[i]; if (props && props.type === "Web") { - stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, candidateEntities[i]); + stylusTarget = TouchEventUtils.calculateTouchTargetFromEntity(this.stylusTip, candidateEntities[i]); if (stylusTarget) { stylusTargets.push(stylusTarget); } @@ -502,7 +278,7 @@ Script.include("/~/system/libraries/controllers.js"); // add the tabletScreen, if it is valid if (HMD.tabletScreenID && HMD.tabletScreenID !== NULL_UUID && Overlays.getProperty(HMD.tabletScreenID, "visible")) { - stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.tabletScreenID); + stylusTarget = TouchEventUtils.calculateTouchTargetFromOverlay(this.stylusTip, HMD.tabletScreenID); if (stylusTarget) { stylusTargets.push(stylusTarget); } @@ -511,7 +287,7 @@ Script.include("/~/system/libraries/controllers.js"); // add the tablet home button. if (HMD.homeButtonID && HMD.homeButtonID !== NULL_UUID && Overlays.getProperty(HMD.homeButtonID, "visible")) { - stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.homeButtonID); + stylusTarget = TouchEventUtils.calculateTouchTargetFromOverlay(this.stylusTip, HMD.homeButtonID); if (stylusTarget) { stylusTargets.push(stylusTarget); } @@ -530,9 +306,9 @@ Script.include("/~/system/libraries/controllers.js"); var sensorScaleFactor = MyAvatar.sensorToWorldScale; this.isNearStylusTarget = isNearStylusTarget(stylusTargets, - (EDGE_BORDER + hysteresisOffset) * sensorScaleFactor, - (TABLET_MIN_TOUCH_DISTANCE - hysteresisOffset) * sensorScaleFactor, - (WEB_DISPLAY_STYLUS_DISTANCE + hysteresisOffset) * sensorScaleFactor); + (EDGE_BORDER + hysteresisOffset) * sensorScaleFactor, + (TABLET_MIN_TOUCH_DISTANCE - hysteresisOffset) * sensorScaleFactor, + (WEB_DISPLAY_STYLUS_DISTANCE + hysteresisOffset) * sensorScaleFactor); if (this.isNearStylusTarget) { if (!this.useFingerInsteadOfStylus) { @@ -556,12 +332,12 @@ Script.include("/~/system/libraries/controllers.js"); this.requestTouchFocus(nearestStylusTarget); - if (!stylusTargetHasKeyboardFocus(nearestStylusTarget)) { - setKeyboardFocusOnStylusTarget(nearestStylusTarget); + if (!TouchEventUtils.touchTargetHasKeyboardFocus(nearestStylusTarget)) { + TouchEventUtils.setKeyboardFocusOnTouchTarget(nearestStylusTarget); } - if (this.hasTouchFocus(nearestStylusTarget)) { - sendHoverOverEventToStylusTarget(this.hand, nearestStylusTarget); + if (this.hasTouchFocus(nearestStylusTarget) && !this.stylusTouchingTarget) { + TouchEventUtils.sendHoverOverEventToTouchTarget(this.hand, nearestStylusTarget); } // filter out presses when tip is moving away from tablet. @@ -592,14 +368,14 @@ Script.include("/~/system/libraries/controllers.js"); this.stylusTouchingEnter = function () { this.stealTouchFocus(this.stylusTarget); - sendTouchStartEventToStylusTarget(this.hand, this.stylusTarget); + TouchEventUtils.sendTouchStartEventToTouchTarget(this.hand, this.stylusTarget); Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); this.touchingEnterTimer = 0; this.touchingEnterStylusTarget = this.stylusTarget; this.deadspotExpired = false; - var TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0381; + var TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0481; this.deadspotRadius = TOUCH_PRESS_TO_MOVE_DEADSPOT; }; @@ -616,9 +392,9 @@ Script.include("/~/system/libraries/controllers.js"); // send press event if (this.deadspotExpired) { - sendTouchEndEventToStylusTarget(this.hand, this.stylusTarget); + TouchEventUtils.sendTouchEndEventToTouchTarget(this.hand, this.stylusTarget); } else { - sendTouchEndEventToStylusTarget(this.hand, this.touchingEnterStylusTarget); + TouchEventUtils.sendTouchEndEventToTouchTarget(this.hand, this.touchingEnterStylusTarget); } }; @@ -627,9 +403,9 @@ Script.include("/~/system/libraries/controllers.js"); this.touchingEnterTimer += dt; if (this.stylusTarget.entityID) { - this.stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, this.stylusTarget.entityProps); + this.stylusTarget = TouchEventUtils.calculateTouchTargetFromEntity(this.stylusTip, this.stylusTarget.entityProps); } else if (this.stylusTarget.overlayID) { - this.stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, this.stylusTarget.overlayID); + this.stylusTarget = TouchEventUtils.calculateTouchTargetFromOverlay(this.stylusTip, this.stylusTarget.overlayID); } var TABLET_MIN_TOUCH_DISTANCE = -0.1; @@ -642,7 +418,7 @@ Script.include("/~/system/libraries/controllers.js"); if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || distance2D(this.stylusTarget.position2D, this.touchingEnterStylusTarget.position2D) > this.deadspotRadius) { - sendTouchMoveEventToStylusTarget(this.hand, this.stylusTarget); + TouchEventUtils.sendTouchMoveEventToTouchTarget(this.hand, this.stylusTarget); this.deadspotExpired = true; } } else { @@ -654,12 +430,11 @@ Script.include("/~/system/libraries/controllers.js"); }; this.overlayLaserActive = function(controllerData) { - var overlayLaserModule = - getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightOverlayLaserInput" : "LeftOverlayLaserInput"); - if (overlayLaserModule) { - return overlayLaserModule.isReady(controllerData).active; - } - return false; + var rightOverlayLaserModule = getEnabledModuleByName("RightOverlayLaserInput"); + var leftOverlayLaserModule = getEnabledModuleByName("LeftOverlayLaserInput"); + var rightModuleRunning = rightOverlayLaserModule ? !rightOverlayLaserModule.shouldExit(controllerData) : false; + var leftModuleRunning = leftOverlayLaserModule ? !leftOverlayLaserModule.shouldExit(controllerData) : false; + return leftModuleRunning || rightModuleRunning; }; this.isReady = function (controllerData) { diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 84c26d482b..138e3a3956 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -450,7 +450,7 @@ var parsedJsonMessage = JSON.parse(message); if (parsedJsonMessage.type === "marketplaces") { - if (parsedJsonMessage.action === "inspectionModeSetting") { + if (parsedJsonMessage.action === "commerceSetting") { confirmAllPurchases = !!parsedJsonMessage.data; injectCode(); } @@ -458,7 +458,7 @@ } }); - // Request inspection mode setting + // Request commerce setting // Code is injected into the webpage after the setting comes back. EventBridge.emitWebEvent(JSON.stringify({ type: "REQUEST_SETTING" diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index d1182f197c..c5f8168c30 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -49,9 +49,9 @@ function calcSpawnInfo(hand, landscape) { var headRot = (HMD.active && Camera.mode === "first person") ? HMD.orientation : Camera.orientation; var forward = Quat.getForward(headRot); - var FORWARD_OFFSET = 0.6 * MyAvatar.sensorToWorldScale; + var FORWARD_OFFSET = 0.5 * MyAvatar.sensorToWorldScale; finalPosition = Vec3.sum(headPos, Vec3.multiply(FORWARD_OFFSET, forward)); - var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, forward, {x: 0, y: 1, z: 0}); + var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, forward, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.UNIT_Y)); return { position: finalPosition, rotation: landscape ? Quat.multiply(orientation, ROT_LANDSCAPE) : Quat.multiply(orientation, ROT_Y_180) diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 33eec74111..10931e4e93 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -318,6 +318,8 @@ if (typeof module !== 'undefined') { makeRunningValues: makeRunningValues, LEFT_HAND: LEFT_HAND, RIGHT_HAND: RIGHT_HAND, - BUMPER_ON_VALUE: BUMPER_ON_VALUE + BUMPER_ON_VALUE: BUMPER_ON_VALUE, + projectOntoOverlayXYPlane: projectOntoOverlayXYPlane, + projectOntoEntityXYPlane: projectOntoEntityXYPlane }; } diff --git a/scripts/system/libraries/touchEventUtils.js b/scripts/system/libraries/touchEventUtils.js new file mode 100644 index 0000000000..fbd56e16ae --- /dev/null +++ b/scripts/system/libraries/touchEventUtils.js @@ -0,0 +1,270 @@ +"use strict"; + +// touchEventUtils.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, + controllerDispatcher.NULL_UUID, enableDispatcherModule, disableDispatcherModule, makeRunningValues, + Messages, Quat, Vec3, getControllerWorldLocation, makeDispatcherModuleParameters, Overlays, controllerDispatcher.ZERO_VEC, + AVATAR_SELF_ID, HMD, INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, Settings, getGrabPointSphereOffset +*/ + +var controllerDispatcher = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); +function touchTargetHasKeyboardFocus(touchTarget) { + if (touchTarget.entityID && touchTarget.entityID !== controllerDispatcher.NULL_UUID) { + return Entities.keyboardFocusEntity === touchTarget.entityID; + } else if (touchTarget.overlayID && touchTarget.overlayID !== controllerDispatcher.NULL_UUID) { + return Overlays.keyboardFocusOverlay === touchTarget.overlayID; + } +} + +function setKeyboardFocusOnTouchTarget(touchTarget) { + if (touchTarget.entityID && touchTarget.entityID !== controllerDispatcher.NULL_UUID && + Entities.wantsHandControllerPointerEvents(touchTarget.entityID)) { + Overlays.keyboardFocusOverlay = controllerDispatcher.NULL_UUID; + Entities.keyboardFocusEntity = touchTarget.entityID; + } else if (touchTarget.overlayID && touchTarget.overlayID !== controllerDispatcher.NULL_UUID) { + Overlays.keyboardFocusOverlay = touchTarget.overlayID; + Entities.keyboardFocusEntity = controllerDispatcher.NULL_UUID; + } +} + +function sendHoverEnterEventToTouchTarget(hand, touchTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: touchTarget.position2D, + pos3D: touchTarget.position, + normal: touchTarget.normal, + direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal), + button: "None" + }; + + if (touchTarget.entityID && touchTarget.entityID !== controllerDispatcher.NULL_UUID) { + Entities.sendHoverEnterEntity(touchTarget.entityID, pointerEvent); + } else if (touchTarget.overlayID && touchTarget.overlayID !== controllerDispatcher.NULL_UUID) { + Overlays.sendHoverEnterOverlay(touchTarget.overlayID, pointerEvent); + } +} + +function sendHoverOverEventToTouchTarget(hand, touchTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: touchTarget.position2D, + pos3D: touchTarget.position, + normal: touchTarget.normal, + direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal), + button: "None" + }; + + if (touchTarget.entityID && touchTarget.entityID !== controllerDispatcher.NULL_UUID) { + Entities.sendMouseMoveOnEntity(touchTarget.entityID, pointerEvent); + Entities.sendHoverOverEntity(touchTarget.entityID, pointerEvent); + } else if (touchTarget.overlayID && touchTarget.overlayID !== controllerDispatcher.NULL_UUID) { + Overlays.sendMouseMoveOnOverlay(touchTarget.overlayID, pointerEvent); + Overlays.sendHoverOverOverlay(touchTarget.overlayID, pointerEvent); + } +} + +function sendTouchStartEventToTouchTarget(hand, touchTarget) { + var pointerEvent = { + type: "Press", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: touchTarget.position2D, + pos3D: touchTarget.position, + normal: touchTarget.normal, + direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (touchTarget.entityID && touchTarget.entityID !== controllerDispatcher.NULL_UUID) { + Entities.sendMousePressOnEntity(touchTarget.entityID, pointerEvent); + Entities.sendClickDownOnEntity(touchTarget.entityID, pointerEvent); + } else if (touchTarget.overlayID && touchTarget.overlayID !== controllerDispatcher.NULL_UUID) { + Overlays.sendMousePressOnOverlay(touchTarget.overlayID, pointerEvent); + } +} + +function sendTouchEndEventToTouchTarget(hand, touchTarget) { + var pointerEvent = { + type: "Release", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: touchTarget.position2D, + pos3D: touchTarget.position, + normal: touchTarget.normal, + direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal), + button: "Primary" + }; + + if (touchTarget.entityID && touchTarget.entityID !== controllerDispatcher.NULL_UUID) { + Entities.sendMouseReleaseOnEntity(touchTarget.entityID, pointerEvent); + Entities.sendClickReleaseOnEntity(touchTarget.entityID, pointerEvent); + Entities.sendHoverLeaveEntity(touchTarget.entityID, pointerEvent); + } else if (touchTarget.overlayID && touchTarget.overlayID !== controllerDispatcher.NULL_UUID) { + Overlays.sendMouseReleaseOnOverlay(touchTarget.overlayID, pointerEvent); + } +} + +function sendTouchMoveEventToTouchTarget(hand, touchTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: touchTarget.position2D, + pos3D: touchTarget.position, + normal: touchTarget.normal, + direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (touchTarget.entityID && touchTarget.entityID !== controllerDispatcher.NULL_UUID) { + Entities.sendMouseMoveOnEntity(touchTarget.entityID, pointerEvent); + Entities.sendHoldingClickOnEntity(touchTarget.entityID, pointerEvent); + } else if (touchTarget.overlayID && touchTarget.overlayID !== controllerDispatcher.NULL_UUID) { + Overlays.sendMouseMoveOnOverlay(touchTarget.overlayID, pointerEvent); + } +} + +function composeTouchTargetFromIntersection(intersection) { + var isEntity = (intersection.type === RayPick.INTERSECTED_ENTITY); + var objectID = intersection.objectID; + var worldPos = intersection.intersection; + var props = null; + if (isEntity) { + props = Entities.getProperties(intersection.objectID); + } + + var position2D =(isEntity ? controllerDispatcher.projectOntoEntityXYPlane(objectID, worldPos, props) : + controllerDispatcher.projectOntoOverlayXYPlane(objectID, worldPos)); + return { + entityID: isEntity ? objectID : null, + overlayID: isEntity ? null : objectID, + distance: intersection.distance, + position: worldPos, + position2D: position2D, + normal: intersection.surfaceNormal + }; +} + +// will return undefined if overlayID does not exist. +function calculateTouchTargetFromOverlay(touchTip, overlayID) { + var overlayPosition = Overlays.getProperty(overlayID, "position"); + if (overlayPosition === undefined) { + return; + } + + // project touchTip onto overlay plane. + var overlayRotation = Overlays.getProperty(overlayID, "rotation"); + if (overlayRotation === undefined) { + return; + } + var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1}); + var distance = Vec3.dot(Vec3.subtract(touchTip.position, overlayPosition), normal); + var position = Vec3.subtract(touchTip.position, Vec3.multiply(normal, distance)); + + // calclulate normalized position + var invRot = Quat.inverse(overlayRotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, overlayPosition)); + var dpi = Overlays.getProperty(overlayID, "dpi"); + + var dimensions; + if (dpi) { + // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property + // is used as a scale. + var resolution = Overlays.getProperty(overlayID, "resolution"); + if (resolution === undefined) { + return; + } + resolution.z = 1; // Circumvent divide-by-zero. + var scale = Overlays.getProperty(overlayID, "dimensions"); + if (scale === undefined) { + return; + } + scale.z = 0.01; // overlay dimensions are 2D, not 3D. + dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); + } else { + dimensions = Overlays.getProperty(overlayID, "dimensions"); + if (dimensions === undefined) { + return; + } + if (!dimensions.z) { + dimensions.z = 0.01; // sometimes overlay dimensions are 2D, not 3D. + } + } + var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT); + + // 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { + x: normalizedPosition.x * dimensions.x, + y: (1 - normalizedPosition.y) * dimensions.y // flip y-axis + }; + + return { + entityID: null, + overlayID: overlayID, + distance: distance, + position: position, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: dimensions, + valid: true + }; +} + +// will return undefined if entity does not exist. +function calculateTouchTargetFromEntity(touchTip, props) { + if (props.rotation === undefined) { + // if rotation is missing from props object, then this entity has probably been deleted. + return; + } + + // project touch tip onto entity plane. + var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1}); + Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0}); + var distance = Vec3.dot(Vec3.subtract(touchTip.position, props.position), normal); + var position = Vec3.subtract(touchTip.position, Vec3.multiply(normal, distance)); + + // generate normalized coordinates + var invRot = Quat.inverse(props.rotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position)); + var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint); + + // 2D position on entity plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { + x: normalizedPosition.x * props.dimensions.x, + y: (1 - normalizedPosition.y) * props.dimensions.y // flip y-axis + }; + + return { + entityID: props.id, + entityProps: props, + overlayID: null, + distance: distance, + position: position, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: props.dimensions, + valid: true + }; +} + +module.exports = { + calculateTouchTargetFromEntity: calculateTouchTargetFromEntity, + calculateTouchTargetFromOverlay: calculateTouchTargetFromOverlay, + touchTargetHasKeyboardFocus: touchTargetHasKeyboardFocus, + setKeyboardFocusOnTouchTarget: setKeyboardFocusOnTouchTarget, + sendHoverEnterEventToTouchTarget: sendHoverEnterEventToTouchTarget, + sendHoverOverEventToTouchTarget: sendHoverOverEventToTouchTarget, + sendTouchStartEventToTouchTarget: sendTouchStartEventToTouchTarget, + sendTouchEndEventToTouchTarget: sendTouchEndEventToTouchTarget, + sendTouchMoveEventToTouchTarget: sendTouchMoveEventToTouchTarget, + composeTouchTargetFromIntersection: composeTouchTargetFromIntersection +}; diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index d95ad919b6..bfad959ffc 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -16,6 +16,7 @@ var request = Script.require('request').request; + var WANT_DEBUG = Settings.getValue('MAKE_USER_CONNECTION_DEBUG', false); var LABEL = "makeUserConnection"; var MAX_AVATAR_DISTANCE = 0.2; // m var GRIP_MIN = 0.75; // goes from 0-1, so 75% pressed is pressed @@ -120,6 +121,9 @@ var successfulHandshakeSound; function debug() { + if (!WANT_DEBUG) { + return; + } var stateString = "<" + STATE_STRINGS[state] + ">"; var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]"; var current = "[" + currentHand + "/" + currentHandJointIndex + "]" @@ -372,7 +376,7 @@ var myHeadIndex = MyAvatar.getJointIndex("Head"); var otherHeadIndex = avatar.getJointIndex("Head"); var diff = (avatar.getJointPosition(otherHeadIndex).y - MyAvatar.getJointPosition(myHeadIndex).y) / 2; - print("head height difference: " + diff); + debug("head height difference: " + diff); updateAnimationData(diff); } } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 2eaefe7565..7ae0aa3390 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -138,8 +138,8 @@ } else if (parsedJsonMessage.type === "REQUEST_SETTING") { tablet.emitScriptEvent(JSON.stringify({ type: "marketplaces", - action: "inspectionModeSetting", - data: Settings.getValue("inspectionMode", false) + action: "commerceSetting", + data: Settings.getValue("commerce", false) })); } else if (parsedJsonMessage.type === "PURCHASES") { tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index 77c8ab2177..5b83ff313b 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -12,7 +12,7 @@ setup_hifi_project(Quick Gui OpenGL) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(shared networking model fbx ktx image octree gl gpu gpu-gl render model-networking networking render-utils entities entities-renderer animation audio avatars script-engine physics procedural midi) +link_hifi_libraries(shared networking model fbx ktx image octree gl gpu gpu-gl render model-networking networking render-utils entities entities-renderer animation audio avatars script-engine physics procedural midi ui) package_libraries_for_deployment() diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 3be6ba2f6c..58eb4d16f9 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -42,11 +42,15 @@ #include #include +#include + #include #include #include #include +#include + #include #include #include @@ -427,6 +431,10 @@ namespace render { } } +OffscreenGLCanvas* _chromiumShareContext{ nullptr }; +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); + + // Create a simple OpenGL window that renders text in various ways class QTestWindow : public QWindow, public AbstractViewStateInterface { @@ -506,8 +514,6 @@ public: AbstractViewStateInterface::setInstance(this); _octree = DependencyManager::set(false, this, nullptr); _octree->init(); - // Prevent web entities from rendering - REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, WebEntityItem::factory); DependencyManager::set(_octree->getTree()); auto nodeList = DependencyManager::get(); @@ -535,6 +541,23 @@ public: _renderThread.initialize(this, _initContext); _initContext.makeCurrent(); + if (nsightActive()) { + // Prevent web entities from rendering + REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, WebEntityItem::factory); + } else { + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->setObjectName("ChromiumShareContext"); + _chromiumShareContext->create(_initContext.qglContext()); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + + // Make sure all QML surfaces share the main thread GL context + OffscreenQmlSurface::setSharedContext(_initContext.qglContext()); + + _initContext.makeCurrent(); + } + + // FIXME use a wait condition QThread::msleep(1000); _renderThread.submitFrame(gpu::FramePointer()); @@ -679,6 +702,7 @@ private: _renderCount = _renderThread._presentCount.load(); update(); + _initContext.makeCurrent(); RenderArgs renderArgs(_renderThread._gpuContext, DEFAULT_OCTREE_SIZE_SCALE, 0, RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); diff --git a/tools/atp-client/src/ATPClientApp.cpp b/tools/atp-client/src/ATPClientApp.cpp index 7d091aec74..c5edf27b67 100644 --- a/tools/atp-client/src/ATPClientApp.cpp +++ b/tools/atp-client/src/ATPClientApp.cpp @@ -332,7 +332,7 @@ void ATPClientApp::listAssets() { } else if (result == GetAllMappingsRequest::NoError) { auto mappings = request->getMappings(); for (auto& kv : mappings ) { - qDebug() << kv.first << kv.second; + qDebug() << kv.first << kv.second.hash; } } else { qDebug() << "error -- " << request->getError() << " -- " << request->getErrorString(); diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index f4fca3304c..1022c204c5 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui Concurrent) -link_hifi_libraries(networking shared image gpu ktx) +link_hifi_libraries(networking shared image gpu ktx fbx baking model) setup_memory_debugger() @@ -17,16 +17,4 @@ if (UNIX) endif() endif () -# try to find the FBX SDK but fail silently if we don't -# because this tool is not built by default -find_package(FBX) -if (FBX_FOUND) - if (CMAKE_THREAD_LIBS_INIT) - target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES} "${CMAKE_THREAD_LIBS_INIT}") - else () - target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES}) - endif () - target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR}) -endif () - set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index 14eb9de150..5ab995be95 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -41,8 +41,8 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) { // create our appropiate baker if (isFBX) { - _baker = std::unique_ptr { new FBXBaker(inputUrl, outputPath, []() -> QThread* { return qApp->getNextWorkerThread(); }) }; - _baker->moveToThread(qApp->getFBXBakerThread()); + _baker = std::unique_ptr { new FBXBaker(inputUrl, []() -> QThread* { return qApp->getNextWorkerThread(); }, outputPath) }; + _baker->moveToThread(qApp->getNextWorkerThread()); } else if (isSupportedImage) { _baker = std::unique_ptr { new TextureBaker(inputUrl, image::TextureUsage::CUBE_TEXTURE, outputPath) }; _baker->moveToThread(qApp->getNextWorkerThread()); @@ -61,4 +61,4 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) { void BakerCLI::handleFinishedBaker() { qCDebug(model_baking) << "Finished baking file."; QApplication::exit(_baker.get()->hasErrors()); -} \ No newline at end of file +} diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 03bc350f42..535d9a49a9 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -192,10 +192,18 @@ void DomainBaker::enumerateEntities() { // setup an FBXBaker for this URL, as long as we don't already have one if (!_modelBakers.contains(modelURL)) { + auto filename = modelURL.fileName(); + auto baseName = filename.left(filename.lastIndexOf('.')); + auto subDirName = "/" + baseName; + int i = 0; + while (QDir(_contentOutputPath + subDirName).exists()) { + subDirName = "/" + baseName + "-" + i++; + } QSharedPointer baker { - new FBXBaker(modelURL, _contentOutputPath, []() -> QThread* { + new FBXBaker(modelURL, []() -> QThread* { return qApp->getNextWorkerThread(); - }), &FBXBaker::deleteLater + }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), + &FBXBaker::deleteLater }; // make sure our handler is called when the baker is done @@ -206,7 +214,7 @@ void DomainBaker::enumerateEntities() { // move the baker to the baker thread // and kickoff the bake - baker->moveToThread(qApp->getFBXBakerThread()); + baker->moveToThread(qApp->getNextWorkerThread()); QMetaObject::invokeMethod(baker.data(), "bake"); // keep track of the total number of baking entities @@ -309,7 +317,11 @@ void DomainBaker::handleFinishedModelBaker() { QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; // setup a new URL using the prefix we were passed - QUrl newModelURL = _destinationPath.resolved(baker->getBakedFBXRelativePath()); + auto relativeFBXFilePath = baker->getBakedFBXFilePath().remove(_contentOutputPath); + if (relativeFBXFilePath.startsWith("/")) { + relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1); + } + QUrl newModelURL = _destinationPath.resolved(relativeFBXFilePath); // copy the fragment and query, and user info from the old model URL newModelURL.setQuery(oldModelURL.query()); @@ -335,7 +347,7 @@ void DomainBaker::handleFinishedModelBaker() { if (oldAnimationURL.matches(oldModelURL, QUrl::RemoveQuery | QUrl::RemoveFragment)) { // the animation URL matched the old model URL, so make the animation URL point to the baked FBX // with its original query and fragment - auto newAnimationURL = _destinationPath.resolved(baker->getBakedFBXRelativePath()); + auto newAnimationURL = _destinationPath.resolved(relativeFBXFilePath); newAnimationURL.setQuery(oldAnimationURL.query()); newAnimationURL.setFragment(oldAnimationURL.fragment()); newAnimationURL.setUserInfo(oldAnimationURL.userInfo()); diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 34c5e11e63..6426af0710 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -55,6 +55,8 @@ private: QString _baseOutputPath; QString _uniqueOutputPath; QString _contentOutputPath; + QString _bakedOutputPath; + QString _originalOutputPath; QUrl _destinationPath; QJsonArray _entities; diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp deleted file mode 100644 index 0259a6baf8..0000000000 --- a/tools/oven/src/FBXBaker.cpp +++ /dev/null @@ -1,554 +0,0 @@ -// -// FBXBaker.cpp -// tools/oven/src -// -// Created by Stephen Birarda on 3/30/17. -// Copyright 2017 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 // need this include so we don't get an error looking for std::isnan - -#include - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include - -#include "ModelBakingLoggingCategory.h" -#include "TextureBaker.h" - -#include "FBXBaker.h" - -std::once_flag onceFlag; -FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr }; - -FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, - TextureBakerThreadGetter textureThreadGetter, bool copyOriginals) : - _fbxURL(fbxURL), - _baseOutputPath(baseOutputPath), - _textureThreadGetter(textureThreadGetter), - _copyOriginals(copyOriginals) -{ - std::call_once(onceFlag, [](){ - // create the static FBX SDK manager - _sdkManager = FBXSDKManagerUniquePointer(FbxManager::Create(), [](FbxManager* manager){ - manager->Destroy(); - }); - }); - - // grab the name of the FBX from the URL, this is used for folder output names - auto fileName = fbxURL.fileName(); - _fbxName = fileName.left(fileName.lastIndexOf('.')); -} - -static const QString BAKED_OUTPUT_SUBFOLDER = "baked/"; -static const QString ORIGINAL_OUTPUT_SUBFOLDER = "original/"; - -QString FBXBaker::pathToCopyOfOriginal() const { - return _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + _fbxURL.fileName(); -} - -void FBXBaker::bake() { - qCDebug(model_baking) << "Baking" << _fbxURL; - - // setup the output folder for the results of this bake - setupOutputFolder(); - - if (hasErrors()) { - return; - } - - connect(this, &FBXBaker::sourceCopyReadyToLoad, this, &FBXBaker::bakeSourceCopy); - - // make a local copy of the FBX file - loadSourceFBX(); -} - -void FBXBaker::bakeSourceCopy() { - // load the scene from the FBX file - importScene(); - - if (hasErrors()) { - return; - } - - // enumerate the textures found in the scene and start a bake for them - rewriteAndBakeSceneTextures(); - - if (hasErrors()) { - return; - } - - // export the FBX with re-written texture references - exportScene(); - - if (hasErrors()) { - return; - } - - // check if we're already done with textures (in case we had none to re-write) - checkIfTexturesFinished(); -} - -void FBXBaker::setupOutputFolder() { - // construct the output path using the name of the fbx and the base output path - _uniqueOutputPath = _baseOutputPath + "/" + _fbxName + "/"; - - // make sure there isn't already an output directory using the same name - int iteration = 0; - - while (QDir(_uniqueOutputPath).exists()) { - _uniqueOutputPath = _baseOutputPath + "/" + _fbxName + "-" + QString::number(++iteration) + "/"; - } - - qCDebug(model_baking) << "Creating FBX output folder" << _uniqueOutputPath; - - // attempt to make the output folder - if (!QDir().mkdir(_uniqueOutputPath)) { - handleError("Failed to create FBX output folder " + _uniqueOutputPath); - return; - } - - // make the baked and original sub-folders used during export - QDir uniqueOutputDir = _uniqueOutputPath; - if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(ORIGINAL_OUTPUT_SUBFOLDER)) { - handleError("Failed to create baked/original subfolders in " + _uniqueOutputPath); - return; - } -} - -void FBXBaker::loadSourceFBX() { - // check if the FBX is local or first needs to be downloaded - if (_fbxURL.isLocalFile()) { - // load up the local file - QFile localFBX { _fbxURL.toLocalFile() }; - - // make a copy in the output folder - localFBX.copy(pathToCopyOfOriginal()); - - // emit our signal to start the import of the FBX source copy - emit sourceCopyReadyToLoad(); - } else { - // remote file, kick off a download - auto& networkAccessManager = NetworkAccessManager::getInstance(); - - QNetworkRequest networkRequest; - - // setup the request to follow re-directs and always hit the network - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - - - networkRequest.setUrl(_fbxURL); - - qCDebug(model_baking) << "Downloading" << _fbxURL; - auto networkReply = networkAccessManager.get(networkRequest); - - connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); - } -} - -void FBXBaker::handleFBXNetworkReply() { - auto requestReply = qobject_cast(sender()); - - if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(model_baking) << "Downloaded" << _fbxURL; - - // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(pathToCopyOfOriginal()); - - qDebug(model_baking) << "Writing copy of original FBX to" << copyOfOriginal.fileName(); - - if (!copyOfOriginal.open(QIODevice::WriteOnly) || (copyOfOriginal.write(requestReply->readAll()) == -1)) { - // add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made - handleError("Could not create copy of " + _fbxURL.toString()); - return; - } - - // close that file now that we are done writing to it - copyOfOriginal.close(); - - // emit our signal to start the import of the FBX source copy - emit sourceCopyReadyToLoad(); - } else { - // add an error to our list stating that the FBX could not be downloaded - handleError("Failed to download " + _fbxURL.toString()); - } -} - -void FBXBaker::importScene() { - // create an FBX SDK importer - FbxImporter* importer = FbxImporter::Create(_sdkManager.get(), ""); - - // import the copy of the original FBX file - QString originalCopyPath = pathToCopyOfOriginal(); - bool importStatus = importer->Initialize(originalCopyPath.toLocal8Bit().data()); - - if (!importStatus) { - // failed to initialize importer, print an error and return - handleError("Failed to import " + _fbxURL.toString() + " - " + importer->GetStatus().GetErrorString()); - return; - } else { - qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene"; - } - - // setup a new scene to hold the imported file - _scene = FbxScene::Create(_sdkManager.get(), "bakeScene"); - - // import the file to the created scene - importer->Import(_scene); - - // destroy the importer that is no longer needed - importer->Destroy(); -} - -QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) { - auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); - auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); - - if (texturePath.startsWith(fbxPath)) { - // texture path is a child of the FBX path, return the texture path without the fbx path - return texturePath.mid(fbxPath.length()); - } else { - // the texture path was not a child of the FBX path, return the empty string - return ""; - } -} - -QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { - // first make sure we have a unique base name for this texture - // in case another texture referenced by this model has the same base name - auto nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; - - QString bakedTextureFileName { textureFileInfo.completeBaseName() }; - - if (nameMatches > 0) { - // there are already nameMatches texture with this name - // append - and that number to our baked texture file name so that it is unique - bakedTextureFileName += "-" + QString::number(nameMatches); - } - - bakedTextureFileName += BAKED_TEXTURE_EXT; - - // increment the number of name matches - ++nameMatches; - - return bakedTextureFileName; -} - -QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) { - QUrl urlToTexture; - - if (textureFileInfo.exists() && textureFileInfo.isFile()) { - // set the texture URL to the local texture that we have confirmed exists - urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); - } else { - // external texture that we'll need to download or find - - // first check if it the RelativePath to the texture in the FBX was relative - QString relativeFileName = fileTexture->GetRelativeFileName(); - auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); - - // this is a relative file path which will require different handling - // depending on the location of the original FBX - if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { - // the absolute path we ran into for the texture in the FBX exists on this machine - // so use that file - urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath()); - } else { - // we didn't find the texture on this machine at the absolute path - // so assume that it is right beside the FBX to match the behaviour of interface - urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); - } - } - - return urlToTexture; -} - -image::TextureUsage::Type textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) { - using namespace image::TextureUsage; - - // this is a property we know has a texture, we need to match it to a High Fidelity known texture type - // since that information is passed to the baking process - - // grab the hierarchical name for this property and lowercase it for case-insensitive compare - auto propertyName = QString(property.GetHierarchicalName()).toLower(); - - // figure out the type of the property based on what known value string it matches - if ((propertyName.contains("diffuse") && !propertyName.contains("tex_global_diffuse")) - || propertyName.contains("tex_color_map")) { - return ALBEDO_TEXTURE; - } else if (propertyName.contains("transparentcolor") || propertyName.contains("transparencyfactor")) { - return ALBEDO_TEXTURE; - } else if (propertyName.contains("bump")) { - return BUMP_TEXTURE; - } else if (propertyName.contains("normal")) { - return NORMAL_TEXTURE; - } else if ((propertyName.contains("specular") && !propertyName.contains("tex_global_specular")) - || propertyName.contains("reflection")) { - return SPECULAR_TEXTURE; - } else if (propertyName.contains("tex_metallic_map")) { - return METALLIC_TEXTURE; - } else if (propertyName.contains("shininess")) { - return GLOSS_TEXTURE; - } else if (propertyName.contains("tex_roughness_map")) { - return ROUGHNESS_TEXTURE; - } else if (propertyName.contains("emissive")) { - return EMISSIVE_TEXTURE; - } else if (propertyName.contains("ambientcolor")) { - return LIGHTMAP_TEXTURE; - } else if (propertyName.contains("ambientfactor")) { - // we need to check what the ambient factor is, since that tells Interface to process this texture - // either as an occlusion texture or a light map - auto lambertMaterial = FbxCast(material); - - if (lambertMaterial->AmbientFactor == 0) { - return LIGHTMAP_TEXTURE; - } else if (lambertMaterial->AmbientFactor > 0) { - return OCCLUSION_TEXTURE; - } else { - return UNUSED_TEXTURE; - } - - } else if (propertyName.contains("tex_ao_map")) { - return OCCLUSION_TEXTURE; - } - - return UNUSED_TEXTURE; -} - -void FBXBaker::rewriteAndBakeSceneTextures() { - - // enumerate the surface materials to find the textures used in the scene - int numMaterials = _scene->GetMaterialCount(); - for (int i = 0; i < numMaterials; i++) { - FbxSurfaceMaterial* material = _scene->GetMaterial(i); - - if (material) { - // enumerate the properties of this material to see what texture channels it might have - FbxProperty property = material->GetFirstProperty(); - - while (property.IsValid()) { - // first check if this property has connected textures, if not we don't need to bother with it here - if (property.GetSrcObjectCount() > 0) { - - // figure out the type of texture from the material property - auto textureType = textureTypeForMaterialProperty(property, material); - - if (textureType != image::TextureUsage::UNUSED_TEXTURE) { - int numTextures = property.GetSrcObjectCount(); - - for (int j = 0; j < numTextures; j++) { - FbxFileTexture* fileTexture = property.GetSrcObject(j); - - // use QFileInfo to easily split up the existing texture filename into its components - QString fbxTextureFileName { fileTexture->GetFileName() }; - QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") }; - - // make sure this texture points to something and isn't one we've already re-mapped - if (!textureFileInfo.filePath().isEmpty() - && textureFileInfo.suffix() != BAKED_TEXTURE_EXT.mid(1)) { - - // construct the new baked texture file name and file path - // ensuring that the baked texture will have a unique name - // even if there was another texture with the same name at a different path - auto bakedTextureFileName = createBakedTextureFileName(textureFileInfo); - QString bakedTextureFilePath { - _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + bakedTextureFileName - }; - - qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() - << "to" << bakedTextureFilePath; - - // figure out the URL to this texture, embedded or external - auto urlToTexture = getTextureURL(textureFileInfo, fileTexture); - - // write the new filename into the FBX scene - fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit()); - - // write the relative filename to be the baked texture file name since it will - // be right beside the FBX - fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit().constData()); - - if (!_bakingTextures.contains(urlToTexture)) { - // bake this texture asynchronously - bakeTexture(urlToTexture, textureType, _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER); - } - } - } - } - } - - property = material->GetNextProperty(property); - } - } - } -} - -void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir) { - // start a bake for this texture and add it to our list to keep track of - QSharedPointer bakingTexture { - new TextureBaker(textureURL, textureType, outputDir), - &TextureBaker::deleteLater - }; - - // make sure we hear when the baking texture is done - connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture); - - // keep a shared pointer to the baking texture - _bakingTextures.insert(textureURL, bakingTexture); - - // start baking the texture on one of our available worker threads - bakingTexture->moveToThread(_textureThreadGetter()); - QMetaObject::invokeMethod(bakingTexture.data(), "bake"); -} - -void FBXBaker::handleBakedTexture() { - TextureBaker* bakedTexture = qobject_cast(sender()); - - // make sure we haven't already run into errors, and that this is a valid texture - if (bakedTexture) { - if (!hasErrors()) { - if (!bakedTexture->hasErrors()) { - if (_copyOriginals) { - // we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture - - // use the path to the texture being baked to determine if this was an embedded or a linked texture - - // it is embeddded if the texure being baked was inside the original output folder - // since that is where the FBX SDK places the .fbm folder it generates when importing the FBX - - auto originalOutputFolder = QUrl::fromLocalFile(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER); - - if (!originalOutputFolder.isParentOf(bakedTexture->getTextureURL())) { - // for linked textures we want to save a copy of original texture beside the original FBX - - qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); - - // check if we have a relative path to use for the texture - auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL()); - - QFile originalTextureFile { - _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + relativeTexturePath + bakedTexture->getTextureURL().fileName() - }; - - if (relativeTexturePath.length() > 0) { - // make the folders needed by the relative path - } - - if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) { - qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName() - << "for" << _fbxURL; - } else { - handleError("Could not save original external texture " + originalTextureFile.fileName() - + " for " + _fbxURL.toString()); - return; - } - } - } - - - // now that this texture has been baked and handled, we can remove that TextureBaker from our hash - _bakingTextures.remove(bakedTexture->getTextureURL()); - - checkIfTexturesFinished(); - } else { - // there was an error baking this texture - add it to our list of errors - _errorList.append(bakedTexture->getErrors()); - - // we don't emit finished yet so that the other textures can finish baking first - _pendingErrorEmission = true; - - // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list - _bakingTextures.remove(bakedTexture->getTextureURL()); - - checkIfTexturesFinished(); - } - } else { - // we have errors to attend to, so we don't do extra processing for this texture - // but we do need to remove that TextureBaker from our list - // and then check if we're done with all textures - _bakingTextures.remove(bakedTexture->getTextureURL()); - - checkIfTexturesFinished(); - } - } -} - -void FBXBaker::exportScene() { - // setup the exporter - FbxExporter* exporter = FbxExporter::Create(_sdkManager.get(), ""); - - auto rewrittenFBXPath = _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + _fbxName + BAKED_FBX_EXTENSION; - - // save the relative path to this FBX inside our passed output folder - _bakedFBXRelativePath = rewrittenFBXPath; - _bakedFBXRelativePath.remove(_baseOutputPath + "/"); - - bool exportStatus = exporter->Initialize(rewrittenFBXPath.toLocal8Bit().data()); - - if (!exportStatus) { - // failed to initialize exporter, print an error and return - handleError("Failed to export FBX file at " + _fbxURL.toString() + " to " + rewrittenFBXPath - + "- error: " + exporter->GetStatus().GetErrorString()); - } - - // export the scene - exporter->Export(_scene); - - qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << rewrittenFBXPath; -} - - -void FBXBaker::removeEmbeddedMediaFolder() { - // now that the bake is complete, remove the embedded media folder produced by the FBX SDK when it imports an FBX - auto embeddedMediaFolderName = _fbxURL.fileName().replace(".fbx", ".fbm"); - QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + embeddedMediaFolderName).removeRecursively(); -} - -void FBXBaker::possiblyCleanupOriginals() { - if (!_copyOriginals) { - // caller did not ask us to keep the original around, so delete the original output folder now - QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER).removeRecursively(); - } -} - -void FBXBaker::checkIfTexturesFinished() { - // check if we're done everything we need to do for this FBX - // and emit our finished signal if we're done - - if (_bakingTextures.isEmpty()) { - // remove the embedded media folder that the FBX SDK produces when reading the original - removeEmbeddedMediaFolder(); - - // cleanup the originals if we weren't asked to keep them around - possiblyCleanupOriginals(); - - if (hasErrors()) { - // if we're checking for completion but we have errors - // that means one or more of our texture baking operations failed - - if (_pendingErrorEmission) { - emit finished(); - } - - return; - } else { - qCDebug(model_baking) << "Finished baking" << _fbxURL; - - emit finished(); - } - } -} diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index d0b8c3cd65..d91206a592 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -44,18 +44,14 @@ Oven::Oven(int argc, char* argv[]) : parser.addHelpOption(); parser.process(*this); - // enable compression in image library, except for cube maps + // enable compression in image library image::setColorTexturesCompressionEnabled(true); image::setGrayscaleTexturesCompressionEnabled(true); image::setNormalTexturesCompressionEnabled(true); image::setCubeTexturesCompressionEnabled(true); // setup our worker threads - setupWorkerThreads(QThread::idealThreadCount() - 1); - - // Autodesk's SDK means that we need a single thread for all FBX importing/exporting in the same process - // setup the FBX Baker thread - setupFBXBakerThread(); + setupWorkerThreads(QThread::idealThreadCount()); // check if we were passed any command line arguments that would tell us just to run without the GUI if (parser.isSet(CLI_INPUT_PARAMETER) || parser.isSet(CLI_OUTPUT_PARAMETER)) { @@ -81,10 +77,6 @@ Oven::~Oven() { _workerThreads[i]->quit(); _workerThreads[i]->wait(); } - - // cleanup the FBX Baker thread - _fbxBakerThread->quit(); - _fbxBakerThread->wait(); } void Oven::setupWorkerThreads(int numWorkerThreads) { @@ -97,22 +89,6 @@ void Oven::setupWorkerThreads(int numWorkerThreads) { } } -void Oven::setupFBXBakerThread() { - // we're being asked for the FBX baker thread, but we don't have one yet - // so set that up now - _fbxBakerThread = new QThread(this); - _fbxBakerThread->setObjectName("Oven FBX Baker Thread"); -} - -QThread* Oven::getFBXBakerThread() { - if (!_fbxBakerThread->isRunning()) { - // start the FBX baker thread if it isn't running yet - _fbxBakerThread->start(); - } - - return _fbxBakerThread; -} - QThread* Oven::getNextWorkerThread() { // Here we replicate some of the functionality of QThreadPool by giving callers an available worker thread to use. // We can't use QThreadPool because we want to put QObjects with signals/slots on these threads. diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h index 569b73a3e2..928ba4eb11 100644 --- a/tools/oven/src/Oven.h +++ b/tools/oven/src/Oven.h @@ -34,7 +34,6 @@ public: OvenMainWindow* getMainWindow() const { return _mainWindow; } - QThread* getFBXBakerThread(); QThread* getNextWorkerThread(); private: @@ -42,7 +41,6 @@ private: void setupFBXBakerThread(); OvenMainWindow* _mainWindow; - QThread* _fbxBakerThread; QList _workerThreads; std::atomic _nextWorkerThreadIndex; diff --git a/tools/oven/src/ui/BakeWidget.h b/tools/oven/src/ui/BakeWidget.h index e7ab8d1840..00996128ed 100644 --- a/tools/oven/src/ui/BakeWidget.h +++ b/tools/oven/src/ui/BakeWidget.h @@ -14,7 +14,7 @@ #include -#include "../Baker.h" +#include class BakeWidget : public QWidget { Q_OBJECT diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index c696fbad26..7963b3f3c4 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -155,15 +156,10 @@ void ModelBakeWidget::outputDirectoryChanged(const QString& newDirectory) { } void ModelBakeWidget::bakeButtonClicked() { - // make sure we have a valid output directory - QDir outputDirectory(_outputDirLineEdit->text()); - - if (!outputDirectory.exists()) { - return; - } // make sure we have a non empty URL to a model to bake if (_modelLineEdit->text().isEmpty()) { + QMessageBox::warning(this, "Model URL unspecified", "A model file is required."); return; } @@ -175,18 +171,49 @@ void ModelBakeWidget::bakeButtonClicked() { // if the URL doesn't have a scheme, assume it is a local file if (modelToBakeURL.scheme() != "http" && modelToBakeURL.scheme() != "https" && modelToBakeURL.scheme() != "ftp") { - modelToBakeURL.setScheme("file"); + qDebug() << modelToBakeURL.toString(); + qDebug() << modelToBakeURL.scheme(); + modelToBakeURL = QUrl::fromLocalFile(fileURLString); + qDebug() << "New url: " << modelToBakeURL; } + auto modelName = modelToBakeURL.fileName().left(modelToBakeURL.fileName().lastIndexOf('.')); + + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + QString subFolderName = modelName + "/"; + + // output in a sub-folder with the name of the fbx, potentially suffixed by a number to make it unique + int iteration = 0; + + while (outputDirectory.exists(subFolderName)) { + subFolderName = modelName + "-" + QString::number(++iteration) + "/"; + } + + outputDirectory.mkdir(subFolderName); + + if (!outputDirectory.exists()) { + QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); + return; + } + + outputDirectory.cd(subFolderName); + + QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked"); + QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original"); + + bakedOutputDirectory.mkdir("."); + originalOutputDirectory.mkdir("."); + // everything seems to be in place, kick off a bake for this model now auto baker = std::unique_ptr { - new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), []() -> QThread* { + new FBXBaker(modelToBakeURL, []() -> QThread* { return qApp->getNextWorkerThread(); - }, false) + }, bakedOutputDirectory.absolutePath(), originalOutputDirectory.absolutePath()) }; // move the baker to the FBX baker thread - baker->moveToThread(qApp->getFBXBakerThread()); + baker->moveToThread(qApp->getNextWorkerThread()); // invoke the bake method on the baker thread QMetaObject::invokeMethod(baker.get(), "bake"); @@ -211,6 +238,10 @@ void ModelBakeWidget::handleFinishedBaker() { return value.first.get() == baker; }); + for (auto& file : baker->getOutputFiles()) { + qDebug() << "Baked file: " << file; + } + if (it != _bakers.end()) { auto resultRow = it->second; auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index ed08990ba5..b42b8725f6 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -16,7 +16,7 @@ #include -#include "../FBXBaker.h" +#include #include "BakeWidget.h" diff --git a/tools/oven/src/ui/ResultsWindow.cpp b/tools/oven/src/ui/ResultsWindow.cpp index 35b5160f9b..3a37a328de 100644 --- a/tools/oven/src/ui/ResultsWindow.cpp +++ b/tools/oven/src/ui/ResultsWindow.cpp @@ -50,6 +50,7 @@ void ResultsWindow::setupUI() { // strech the last column of the table (that holds the results) to fill up the remaining available size _resultsTable->horizontalHeader()->resizeSection(0, 0.25 * FIXED_WINDOW_WIDTH); _resultsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + _resultsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // make sure we hear about cell clicks so that we can show the output directory for the given row connect(_resultsTable, &QTableWidget::cellClicked, this, &ResultsWindow::handleCellClicked); diff --git a/tools/oven/src/ui/SkyboxBakeWidget.cpp b/tools/oven/src/ui/SkyboxBakeWidget.cpp index d5c280aebd..cbaaa5ec0a 100644 --- a/tools/oven/src/ui/SkyboxBakeWidget.cpp +++ b/tools/oven/src/ui/SkyboxBakeWidget.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -155,7 +156,9 @@ void SkyboxBakeWidget::bakeButtonClicked() { // make sure we have a valid output directory QDir outputDirectory(_outputDirLineEdit->text()); + outputDirectory.mkdir("."); if (!outputDirectory.exists()) { + QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); return; } diff --git a/tools/oven/src/ui/SkyboxBakeWidget.h b/tools/oven/src/ui/SkyboxBakeWidget.h index 4063a5459b..f00ab07f33 100644 --- a/tools/oven/src/ui/SkyboxBakeWidget.h +++ b/tools/oven/src/ui/SkyboxBakeWidget.h @@ -16,7 +16,7 @@ #include -#include "../TextureBaker.h" +#include #include "BakeWidget.h" diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js b/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js index c2cf2a2353..bc5368ba5b 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js @@ -599,6 +599,20 @@ Doppleganger.prototype = { return this.active; }, + // @private @method - get an avatar's "absolute joint translations in object frame" as local translations + // @param {AvatarData} - avatar to read translations from + // @return {glm::vec3[]} - the adapted translations + _getLocalAvatarJointTranslations: function(avatar) { + // NOTE: avatar.getJointTranslations() seems to return meters and avatar.getJointTranslation(jointIndex) centimeters... + // adapting meters -> centimeters on this side seems to fix the "scrunching into ball" problem (~Beta 54) + // and perform slightly faster than calling getJointTranslation(jointIndex) N times. + const CENTIMETERS_PER_METER = 100.0; + function scaleToMeters(v) { + return Vec3.multiply(CENTIMETERS_PER_METER, v); + } + return avatar.getJointTranslations().map(scaleToMeters); + }, + // @public @method - synchronize the joint data between Avatar / doppleganger update: function() { this.frame++; @@ -612,7 +626,7 @@ Doppleganger.prototype = { } var rotations = this.avatar.getJointRotations(); - var translations = this.avatar.getJointTranslations(); + var translations = this._getLocalAvatarJointTranslations(this.avatar); var size = rotations.length; // note: this mismatch can happen when the avatar's model is actively changing @@ -1488,13 +1502,13 @@ DebugControls.prototype = { /* 6 */ /***/ (function(module, exports) { -module.exports = "data:image/svg+xml;xml,\n\n\nimage/svg+xml\n\t.st0{fill:#FFFFFF;}\n"; +module.exports = "data:image/svg+xml;xml,\n\n\nimage/svg+xml\n\t.st0{fill:#FFFFFF;}\n" /***/ }), /* 7 */ /***/ (function(module, exports) { -module.exports = "data:image/svg+xml;xml,\n\n\nimage/svg+xml\n\t.st0{fill:#FFFFFF;}\n"; +module.exports = "data:image/svg+xml;xml,\n\n\nimage/svg+xml\n\t.st0{fill:#FFFFFF;}\n" /***/ }) /******/ ]); \ No newline at end of file diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js index 190a8aa69e..9e35d791d6 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js @@ -78,6 +78,20 @@ Doppleganger.prototype = { return this.active; }, + // @private @method - get an avatar's "absolute joint translations in object frame" as local translations + // @param {AvatarData} - avatar to read translations from + // @return {glm::vec3[]} - the adapted translations + _getLocalAvatarJointTranslations: function(avatar) { + // NOTE: avatar.getJointTranslations() seems to return meters and avatar.getJointTranslation(jointIndex) centimeters... + // adapting meters -> centimeters on this side seems to fix the "scrunching into ball" problem (~Beta 54) + // and perform slightly faster than calling getJointTranslation(jointIndex) N times. + const CENTIMETERS_PER_METER = 100.0; + function scaleToMeters(v) { + return Vec3.multiply(CENTIMETERS_PER_METER, v); + } + return avatar.getJointTranslations().map(scaleToMeters); + }, + // @public @method - synchronize the joint data between Avatar / doppleganger update: function() { this.frame++; @@ -91,7 +105,7 @@ Doppleganger.prototype = { } var rotations = this.avatar.getJointRotations(); - var translations = this.avatar.getJointTranslations(); + var translations = this._getLocalAvatarJointTranslations(this.avatar); var size = rotations.length; // note: this mismatch can happen when the avatar's model is actively changing diff --git a/unpublishedScripts/marketplace/record/record.js b/unpublishedScripts/marketplace/record/record.js index 5439d68c9a..6110721ff9 100644 --- a/unpublishedScripts/marketplace/record/record.js +++ b/unpublishedScripts/marketplace/record/record.js @@ -571,7 +571,7 @@ function onTabletScreenChanged(type, url) { // Opened/closed dialog in tablet or window. - var RECORD_URL = "/scripts/system/html/record.html"; + var RECORD_URL = "/html/record.html"; if (type === "Web" && url.slice(-RECORD_URL.length) === RECORD_URL) { if (Dialog.finishOnOpen()) {