Merge branch 'master' into M22196

This commit is contained in:
David Rowe 2019-04-27 10:26:10 +12:00
commit 72ef36db02
275 changed files with 10557 additions and 3205 deletions
CMakeLists.txtCODING_STANDARD.mdCONTRIBUTING.md
assignment-client/src
cmake
domain-server/src
interface
libraries

View file

@ -52,6 +52,9 @@ else()
set(MOBILE 0)
endif()
# Use default time server if none defined in environment
set_from_env(TIMESERVER_URL TIMESERVER_URL "http://sha256timestamp.ws.symantec.com/sha256/timestamp")
set(HIFI_USE_OPTIMIZED_IK OFF)
set(BUILD_CLIENT_OPTION ON)
set(BUILD_SERVER_OPTION ON)

1008
CODING_STANDARD.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ Contributing
git checkout -b new_branch_name
```
4. Code
* Follow the [coding standard](https://docs.highfidelity.com/build-guide/coding-standards)
* Follow the [coding standard](CODING_STANDARD.md)
5. Commit
* Use [well formed commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
6. Update your branch

View file

@ -107,6 +107,10 @@ BakeVersion currentBakeVersionForAssetType(BakedAssetType type) {
}
}
QString getBakeMapping(const AssetUtils::AssetHash& hash, const QString& relativeFilePath) {
return AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath;
}
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
void AssetServer::bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath) {
@ -141,26 +145,27 @@ std::pair<AssetUtils::BakingStatus, QString> AssetServer::getAssetStatus(const A
return { AssetUtils::Baked, "" };
}
auto dotIndex = path.lastIndexOf(".");
if (dotIndex == -1) {
BakedAssetType type = assetTypeForFilename(path);
if (type == BakedAssetType::Undefined) {
return { AssetUtils::Irrelevant, "" };
}
auto extension = path.mid(dotIndex + 1);
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(hash);
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 if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) {
bakedFilename = BAKED_SCRIPT_SIMPLE_NAME;
} else {
// We create a meta file for Skyboxes at runtime when they get requested
// Otherwise, textures don't get baked by themselves.
if (type == BakedAssetType::Texture && !loaded) {
return { AssetUtils::Irrelevant, "" };
}
auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + bakedFilename;
QString bakedFilename = bakedFilenameForAssetType(type);
auto bakedPath = getBakeMapping(hash, bakedFilename);
if (loaded && !meta.redirectTarget.isEmpty()) {
bakedPath = meta.redirectTarget;
}
auto jt = _fileMappings.find(bakedPath);
if (jt != _fileMappings.end()) {
if (jt->second == hash) {
@ -168,14 +173,8 @@ std::pair<AssetUtils::BakingStatus, QString> AssetServer::getAssetStatus(const A
} else {
return { AssetUtils::Baked, "" };
}
} else {
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(hash);
if (loaded && meta.failedLastBake) {
return { AssetUtils::Error, meta.lastBakeErrors };
}
} else if (loaded && meta.failedLastBake) {
return { AssetUtils::Error, meta.lastBakeErrors };
}
return { AssetUtils::Pending, "" };
@ -227,8 +226,16 @@ bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetU
return false;
}
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(assetHash);
QString bakedFilename = bakedFilenameForAssetType(type);
auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename;
auto bakedPath = getBakeMapping(assetHash, bakedFilename);
if (loaded && !meta.redirectTarget.isEmpty()) {
bakedPath = meta.redirectTarget;
}
auto mappingIt = _fileMappings.find(bakedPath);
bool bakedMappingExists = mappingIt != _fileMappings.end();
@ -238,10 +245,8 @@ bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetU
return false;
}
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(assetHash);
// We create a meta file for Skyboxes at runtime when they get requested
// Otherwise, textures don't get baked by themselves.
if (type == BakedAssetType::Texture && !loaded) {
return false;
}
@ -633,36 +638,33 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi
if (it != _fileMappings.end()) {
// 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();
auto type = assetTypeForFilename(assetPath);
QString bakedRootFile = bakedFilenameForAssetType(type);
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 = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + bakedRootFile;
auto bakedIt = _fileMappings.find(bakedAssetPath);
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(originalAssetHash);
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;
}
auto type = assetTypeForFilename(assetPath);
QString bakedRootFile = bakedFilenameForAssetType(type);
QString bakedAssetPath = getBakeMapping(originalAssetHash, bakedRootFile);
if (loaded && !meta.redirectTarget.isEmpty()) {
bakedAssetPath = meta.redirectTarget;
}
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;
qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath << " (disabled)";
bakingDisabled = true;
}
}
@ -684,20 +686,13 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi
auto query = QUrlQuery(url.query());
bool isSkybox = query.hasQueryItem("skybox");
if (isSkybox) {
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(originalAssetHash);
if (!loaded) {
AssetMeta needsBakingMeta;
needsBakingMeta.bakeVersion = NEEDS_BAKING_BAKE_VERSION;
writeMetaFile(originalAssetHash, needsBakingMeta);
if (!bakingDisabled) {
maybeBake(assetPath, originalAssetHash);
}
if (isSkybox && !loaded) {
AssetMeta needsBakingMeta;
needsBakingMeta.bakeVersion = NEEDS_BAKING_BAKE_VERSION;
writeMetaFile(originalAssetHash, needsBakingMeta);
if (!bakingDisabled) {
maybeBake(assetPath, originalAssetHash);
}
}
}
@ -1297,14 +1292,6 @@ bool AssetServer::renameMapping(AssetUtils::AssetPath oldPath, AssetUtils::Asset
}
}
static const QString BAKED_ASSET_SIMPLE_FBX_NAME = "asset.fbx";
static const QString BAKED_ASSET_SIMPLE_TEXTURE_NAME = "texture.ktx";
static const QString BAKED_ASSET_SIMPLE_JS_NAME = "asset.js";
QString getBakeMapping(const AssetUtils::AssetHash& hash, const QString& relativeFilePath) {
return AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath;
}
void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, QString errors) {
qDebug() << "Failed to bake: " << originalAssetHash << assetPath << "(" << errors << ")";
@ -1326,12 +1313,78 @@ void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath,
}
void AssetServer::handleCompletedBake(QString originalAssetHash, QString originalAssetPath,
QString bakedTempOutputDir, QVector<QString> bakedFilePaths) {
QString bakedTempOutputDir) {
auto reportCompletion = [this, originalAssetPath, originalAssetHash](bool errorCompletingBake,
QString errorReason,
QString redirectTarget) {
auto type = assetTypeForFilename(originalAssetPath);
auto currentTypeVersion = currentBakeVersionForAssetType(type);
AssetMeta meta;
meta.bakeVersion = currentTypeVersion;
meta.failedLastBake = errorCompletingBake;
meta.redirectTarget = redirectTarget;
if (errorCompletingBake) {
qWarning() << "Could not complete bake for" << originalAssetHash;
meta.lastBakeErrors = errorReason;
}
writeMetaFile(originalAssetHash, meta);
_pendingBakes.remove(originalAssetHash);
};
bool errorCompletingBake { false };
QString errorReason;
QString redirectTarget;
qDebug() << "Completing bake for " << originalAssetHash;
// Find the directory containing the baked content
QDir outputDir(bakedTempOutputDir);
QString outputDirName = outputDir.dirName();
auto directories = outputDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
QString bakedDirectoryPath;
for (const auto& dirName : directories) {
outputDir.cd(dirName);
if (outputDir.exists("baked") && outputDir.exists("original")) {
bakedDirectoryPath = outputDir.filePath("baked");
break;
}
outputDir.cdUp();
}
if (bakedDirectoryPath.isEmpty()) {
errorCompletingBake = true;
errorReason = "Failed to find baking output";
// Cleanup temporary output directory
PathUtils::deleteMyTemporaryDir(outputDirName);
reportCompletion(errorCompletingBake, errorReason, redirectTarget);
return;
}
// Compile list of all the baked files
QDirIterator it(bakedDirectoryPath, QDirIterator::Subdirectories);
QVector<QString> bakedFilePaths;
while (it.hasNext()) {
it.next();
if (it.fileInfo().isFile()) {
bakedFilePaths.push_back(it.filePath());
}
}
if (bakedFilePaths.isEmpty()) {
errorCompletingBake = true;
errorReason = "Baking output has no files";
// Cleanup temporary output directory
PathUtils::deleteMyTemporaryDir(outputDirName);
reportCompletion(errorCompletingBake, errorReason, redirectTarget);
return;
}
QDir bakedDirectory(bakedDirectoryPath);
for (auto& filePath : bakedFilePaths) {
// figure out the hash for the contents of this file
QFile file(filePath);
@ -1340,89 +1393,72 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina
AssetUtils::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 (relativeFilePath.endsWith(".js", Qt::CaseInsensitive)) {
relativeFilePath = BAKED_ASSET_SIMPLE_JS_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 {
if (!file.open(QIODevice::ReadOnly)) {
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";
errorReason = "Could not open baked file " + file.fileName();
break;
}
}
for (auto& filePath : bakedFilePaths) {
QFile file(filePath);
if (!file.remove()) {
qWarning() << "Failed to remove temporary file:" << filePath;
QCryptographicHash hasher(QCryptographicHash::Sha256);
if (!hasher.addData(&file)) {
// stop handling this bake, couldn't hash the contents of the file
errorCompletingBake = true;
errorReason = "Could not hash data for " + file.fileName();
break;
}
}
if (!QDir(bakedTempOutputDir).rmdir(".")) {
qWarning() << "Failed to remove temporary directory:" << bakedTempOutputDir;
bakedFileHash = hasher.result().toHex();
// 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 = bakedDirectory.relativeFilePath(filePath);
QString bakeMapping = getBakeMapping(originalAssetHash, relativeFilePath);
// Check if this is the file we should redirect to when someone asks for the original asset
if ((relativeFilePath.endsWith(".baked.fst", Qt::CaseInsensitive) && originalAssetPath.endsWith(".fbx")) ||
(relativeFilePath.endsWith(".texmeta.json", Qt::CaseInsensitive) && !originalAssetPath.endsWith(".fbx"))) {
if (!redirectTarget.isEmpty()) {
qWarning() << "Found multiple baked redirect target for" << originalAssetPath;
}
redirectTarget = bakeMapping;
}
// add a mapping (under the hidden baked folder) for this file resulting from the bake
if (!setMapping(bakeMapping, bakedFileHash)) {
qDebug() << "Failed to set mapping";
// stop handling this bake, couldn't add a mapping for this bake file
errorCompletingBake = true;
errorReason = "Failed to set mapping for baked file " + file.fileName();
break;
}
qDebug() << "Added" << bakeMapping << "for bake file" << bakedFileHash << "from bake of" << originalAssetHash;
}
auto type = assetTypeForFilename(originalAssetPath);
auto currentTypeVersion = currentBakeVersionForAssetType(type);
AssetMeta meta;
meta.bakeVersion = currentTypeVersion;
meta.failedLastBake = errorCompletingBake;
if (errorCompletingBake) {
qWarning() << "Could not complete bake for" << originalAssetHash;
meta.lastBakeErrors = errorReason;
if (redirectTarget.isEmpty()) {
errorCompletingBake = true;
errorReason = "Could not find root file for baked output";
}
writeMetaFile(originalAssetHash, meta);
_pendingBakes.remove(originalAssetHash);
// Cleanup temporary output directory
PathUtils::deleteMyTemporaryDir(outputDirName);
reportCompletion(errorCompletingBake, errorReason, redirectTarget);
}
void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath) {
@ -1435,6 +1471,7 @@ void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath
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";
static const QString REDIRECT_TARGET_KEY = "redirect_target";
std::pair<bool, AssetMeta> AssetServer::readMetaFile(AssetUtils::AssetHash hash) {
auto metaFilePath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + "meta.json";
@ -1461,6 +1498,7 @@ std::pair<bool, AssetMeta> AssetServer::readMetaFile(AssetUtils::AssetHash hash)
auto bakeVersion = root[BAKE_VERSION_KEY];
auto failedLastBake = root[FAILED_LAST_BAKE_KEY];
auto lastBakeErrors = root[LAST_BAKE_ERRORS_KEY];
auto redirectTarget = root[REDIRECT_TARGET_KEY];
if (bakeVersion.isDouble()
&& failedLastBake.isBool()
@ -1470,6 +1508,7 @@ std::pair<bool, AssetMeta> AssetServer::readMetaFile(AssetUtils::AssetHash hash)
meta.bakeVersion = bakeVersion.toInt();
meta.failedLastBake = failedLastBake.toBool();
meta.lastBakeErrors = lastBakeErrors.toString();
meta.redirectTarget = redirectTarget.toString();
return { true, meta };
} else {
@ -1488,6 +1527,7 @@ bool AssetServer::writeMetaFile(AssetUtils::AssetHash originalAssetHash, const A
metaFileObject[BAKE_VERSION_KEY] = (int)meta.bakeVersion;
metaFileObject[FAILED_LAST_BAKE_KEY] = meta.failedLastBake;
metaFileObject[LAST_BAKE_ERRORS_KEY] = meta.lastBakeErrors;
metaFileObject[REDIRECT_TARGET_KEY] = meta.redirectTarget;
QJsonDocument metaFileDoc;
metaFileDoc.setObject(metaFileObject);
@ -1521,10 +1561,18 @@ bool AssetServer::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool
if (type == BakedAssetType::Undefined) {
continue;
}
QString bakedFilename = bakedFilenameForAssetType(type);
auto hash = it->second;
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(hash);
QString bakedFilename = bakedFilenameForAssetType(type);
auto bakedMapping = getBakeMapping(hash, bakedFilename);
if (loaded && !meta.redirectTarget.isEmpty()) {
bakedMapping = meta.redirectTarget;
}
auto it = _fileMappings.find(bakedMapping);
bool currentlyDisabled = (it != _fileMappings.end() && it->second == hash);

View file

@ -62,12 +62,10 @@ enum class ScriptBakeVersion : BakeVersion {
};
struct AssetMeta {
AssetMeta() {
}
BakeVersion bakeVersion { INITIAL_BAKE_VERSION };
bool failedLastBake { false };
QString lastBakeErrors;
QString redirectTarget;
};
class BakeAssetTask;
@ -139,8 +137,7 @@ private:
void bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath);
/// Move baked content for asset to baked directory and update baked status
void handleCompletedBake(QString originalAssetHash, QString assetPath, QString bakedTempOutputDir,
QVector<QString> bakedFilePaths);
void handleCompletedBake(QString originalAssetHash, QString assetPath, QString bakedTempOutputDir);
void handleFailedBake(QString originalAssetHash, QString assetPath, QString errors);
void handleAbortedBake(QString originalAssetHash, QString assetPath);

View file

@ -36,33 +36,38 @@ BakeAssetTask::BakeAssetTask(const AssetUtils::AssetHash& assetHash, const Asset
});
}
void cleanupTempFiles(QString tempOutputDir, std::vector<QString> files) {
for (const auto& filename : files) {
QFile f { filename };
if (!f.remove()) {
qDebug() << "Failed to remove:" << filename;
}
}
if (!tempOutputDir.isEmpty()) {
QDir dir { tempOutputDir };
if (!dir.rmdir(".")) {
qDebug() << "Failed to remove temporary directory:" << tempOutputDir;
}
}
};
void BakeAssetTask::run() {
if (_isBaking.exchange(true)) {
qWarning() << "Tried to start bake asset task while already baking";
return;
}
// Make a new temporary directory for the Oven to work in
QString tempOutputDir = PathUtils::generateTemporaryDir();
QString tempOutputDirName = QDir(tempOutputDir).dirName();
if (tempOutputDir.isEmpty()) {
QString errors = "Could not create temporary working directory";
emit bakeFailed(_assetHash, _assetPath, errors);
PathUtils::deleteMyTemporaryDir(tempOutputDirName);
return;
}
// Copy file to bake the temporary dir and give a name the oven can work with
auto assetName = _assetPath.split("/").last();
auto tempAssetPath = tempOutputDir + "/" + assetName;
auto success = QFile::copy(_filePath, tempAssetPath);
if (!success) {
QString errors = "Couldn't copy file to bake to temporary directory";
emit bakeFailed(_assetHash, _assetPath, errors);
PathUtils::deleteMyTemporaryDir(tempOutputDirName);
return;
}
auto base = QFileInfo(QCoreApplication::applicationFilePath()).absoluteDir();
QString path = base.absolutePath() + "/oven";
QString extension = _assetPath.mid(_assetPath.lastIndexOf('.') + 1);
QStringList args {
"-i", _filePath,
"-i", tempAssetPath,
"-o", tempOutputDir,
"-t", extension,
};
@ -72,10 +77,11 @@ void BakeAssetTask::run() {
QEventLoop loop;
connect(_ovenProcess.get(), static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
this, [&loop, this, tempOutputDir](int exitCode, QProcess::ExitStatus exitStatus) {
this, [&loop, this, tempOutputDir, tempAssetPath, tempOutputDirName](int exitCode, QProcess::ExitStatus exitStatus) {
qDebug() << "Baking process finished: " << exitCode << exitStatus;
if (exitStatus == QProcess::CrashExit) {
PathUtils::deleteMyTemporaryDir(tempOutputDirName);
if (_wasAborted) {
emit bakeAborted(_assetHash, _assetPath);
} else {
@ -83,16 +89,10 @@ void BakeAssetTask::run() {
emit bakeFailed(_assetHash, _assetPath, errors);
}
} else if (exitCode == OVEN_STATUS_CODE_SUCCESS) {
QDir outputDir = tempOutputDir;
auto files = outputDir.entryInfoList(QDir::Files);
QVector<QString> outputFiles;
for (auto& file : files) {
outputFiles.push_back(file.absoluteFilePath());
}
emit bakeComplete(_assetHash, _assetPath, tempOutputDir, outputFiles);
emit bakeComplete(_assetHash, _assetPath, tempOutputDir);
} else if (exitStatus == QProcess::NormalExit && exitCode == OVEN_STATUS_CODE_ABORT) {
_wasAborted.store(true);
PathUtils::deleteMyTemporaryDir(tempOutputDirName);
emit bakeAborted(_assetHash, _assetPath);
} else {
QString errors;
@ -107,6 +107,7 @@ void BakeAssetTask::run() {
errors = "Unknown error occurred while baking";
}
}
PathUtils::deleteMyTemporaryDir(tempOutputDirName);
emit bakeFailed(_assetHash, _assetPath, errors);
}
@ -115,7 +116,10 @@ void BakeAssetTask::run() {
qDebug() << "Starting oven for " << _assetPath;
_ovenProcess->start(path, args, QIODevice::ReadOnly);
if (!_ovenProcess->waitForStarted(-1)) {
qDebug() << "Running:" << path << args;
if (!_ovenProcess->waitForStarted()) {
PathUtils::deleteMyTemporaryDir(tempOutputDirName);
QString errors = "Oven process failed to start";
emit bakeFailed(_assetHash, _assetPath, errors);
return;

View file

@ -37,7 +37,7 @@ public slots:
void abort();
signals:
void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir, QVector<QString> outputFiles);
void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir);
void bakeFailed(QString assetHash, QString assetPath, QString errors);
void bakeAborted(QString assetHash, QString assetPath);

View file

@ -155,6 +155,12 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared
void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
const SlaveSharedData& slaveSharedData,
Node& sendingNode) {
// Trying to read more bytes than available, bail
if (message.getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitVersion))) {
qWarning() << "Refusing to process malformed traits packet from" << message.getSenderSockAddr();
return;
}
// pull the trait version from the message
AvatarTraits::TraitVersion packetTraitVersion;
message.readPrimitive(&packetTraitVersion);
@ -164,10 +170,22 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
while (message.getBytesLeftToRead() > 0) {
// for each trait in the packet, apply it if the trait version is newer than what we have
// Trying to read more bytes than available, bail
if (message.getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitType))) {
qWarning() << "Refusing to process malformed traits packet from" << message.getSenderSockAddr();
return;
}
AvatarTraits::TraitType traitType;
message.readPrimitive(&traitType);
if (AvatarTraits::isSimpleTrait(traitType)) {
// Trying to read more bytes than available, bail
if (message.getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitWireSize))) {
qWarning() << "Refusing to process malformed traits packet from" << message.getSenderSockAddr();
return;
}
AvatarTraits::TraitWireSize traitSize;
message.readPrimitive(&traitSize);
@ -179,7 +197,6 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
if (packetTraitVersion > _lastReceivedTraitVersions[traitType]) {
_avatar->processTrait(traitType, message.read(traitSize));
_lastReceivedTraitVersions[traitType] = packetTraitVersion;
if (traitType == AvatarTraits::SkeletonModelURL) {
// special handling for skeleton model URL, since we need to make sure it is in the whitelist
checkSkeletonURLAgainstWhitelist(slaveSharedData, sendingNode, packetTraitVersion);
@ -190,13 +207,15 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
message.seek(message.getPosition() + traitSize);
}
} else {
AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
if (message.getBytesLeftToRead() == 0) {
qWarning() << "Received an instanced trait with no size from" << message.getSenderSockAddr();
break;
// Trying to read more bytes than available, bail
if (message.getBytesLeftToRead() < qint64(NUM_BYTES_RFC4122_UUID +
sizeof(AvatarTraits::TraitWireSize))) {
qWarning() << "Refusing to process malformed traits packet from" << message.getSenderSockAddr();
return;
}
AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
AvatarTraits::TraitWireSize traitSize;
message.readPrimitive(&traitSize);

View file

@ -62,8 +62,8 @@
* @property {boolean} lookAtSnappingEnabled=true - <code>true</code> if the avatar's eyes snap to look at another avatar's
* eyes when the other avatar is in the line of sight and also has <code>lookAtSnappingEnabled == true</code>.
* @property {string} skeletonModelURL - The avatar's FST file.
* @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.<br />
* <strong>Deprecated:</strong> Use avatar entities instead.
* @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* <p class="important">Deprecated: This property is deprecated and will be removed. Use avatar entities instead.</p>
* @property {string[]} jointNames - The list of joints in the current avatar model. <em>Read-only.</em>
* @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. <em>Read-only.</em>
* @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the

View file

@ -56,7 +56,8 @@ public slots:
/**jsdoc
* @function EntityViewer.setKeyholeRadius
* @param {number} radius
* @deprecated Use {@link EntityViewer.setCenterRadius|setCenterRadius} instead.
* @deprecated This function is deprecated and will be removed. Use {@link EntityViewer.setCenterRadius|setCenterRadius}
* instead.
*/
void setKeyholeRadius(float radius) { _hasViewFrustum = true; _viewFrustum.setCenterRadius(radius); } // TODO: remove this legacy support

View file

@ -17,8 +17,8 @@ if (WIN32)
ExternalProject_Add(
${EXTERNAL_NAME}
URL https://public.highfidelity.com/dependencies/ovr_sdk_win_1.26.0_public.zip
URL_MD5 06804ff9727b910dcd04a37c800053b5
URL https://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.35.0.zip
URL_MD5 1e3e8b2101387af07ff9c841d0ea285e
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
PATCH_COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/LibOVRCMakeLists.txt" <SOURCE_DIR>/CMakeLists.txt
LOG_DOWNLOAD 1

View file

@ -22,7 +22,7 @@ macro(optional_win_executable_signing)
# setup a post build command to sign the executable
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND ${SIGNTOOL_EXECUTABLE} sign /fd sha256 /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /td SHA256 ${EXECUTABLE_PATH}
COMMAND ${SIGNTOOL_EXECUTABLE} sign /fd sha256 /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr ${TIMESERVER_URL} /td SHA256 ${EXECUTABLE_PATH}
)
else ()
message(FATAL_ERROR "HF_PFX_PASSPHRASE must be set for executables to be signed.")

View file

@ -1010,7 +1010,7 @@ void DomainGatekeeper::refreshGroupsCache() {
nodeList->eachNode([this](const SharedNodePointer& node) {
if (!node->getPermissions().isAssignment) {
// this node is an agent
const QString& verifiedUserName = node->getPermissions().getVerifiedUserName();
QString verifiedUserName = node->getPermissions().getVerifiedUserName();
if (!verifiedUserName.isEmpty()) {
getGroupMemberships(verifiedUserName);
}

View file

@ -115,7 +115,6 @@ void DomainMetadata::securityChanged(bool send) {
auto& state = *static_cast<QVariantMap*>(_metadata[DESCRIPTORS].data());
const QString RESTRICTION_OPEN = "open";
const QString RESTRICTION_ANON = "anon";
const QString RESTRICTION_HIFI = "hifi";
const QString RESTRICTION_ACL = "acl";
@ -127,7 +126,7 @@ void DomainMetadata::securityChanged(bool send) {
bool hasHifiAccess = settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn).can(
NodePermissions::Permission::canConnectToDomain);
if (hasAnonymousAccess) {
restriction = hasHifiAccess ? RESTRICTION_OPEN : RESTRICTION_ANON;
restriction = RESTRICTION_OPEN;
} else if (hasHifiAccess) {
restriction = RESTRICTION_HIFI;
} else {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -903,52 +903,557 @@
"children": [
{
"id": "idle",
"type": "stateMachine",
"type": "overlay",
"data": {
"currentState": "idleStand",
"states": [
{
"id": "idleStand",
"interpTarget": 6,
"interpDuration": 10,
"transitions": [
{ "var": "isTalking", "state": "idleTalk" }
]
},
{
"id": "idleTalk",
"interpTarget": 6,
"interpDuration": 10,
"transitions": [
{ "var": "notIsTalking", "state": "idleStand" }
]
}
]
"alpha": 1.0,
"alphaVar": "idleOverlayAlpha",
"boneSet": "upperBody"
},
"children": [
{
"id": "idleTalk",
"type": "randomSwitchStateMachine",
"data": {
"currentState": "idleTalk1",
"triggerRandomSwitch": "idleTalkSwitch",
"randomSwitchTimeMin": 5.0,
"randomSwitchTimeMax": 10.0,
"states": [
{
"id": "idleTalk1",
"interpTarget": 6,
"interpDuration": 15,
"priority": 0.33,
"resume": true,
"transitions": []
},
{
"id": "idleTalk2",
"interpTarget": 6,
"interpDuration": 15,
"priority": 0.33,
"resume": true,
"transitions": []
},
{
"id": "idleTalk3",
"interpTarget": 6,
"interpDuration": 15,
"priority": 0.33,
"resume": true,
"transitions": []
}
]
},
"children": [
{
"id": "idleTalk1",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/talk.fbx",
"startFrame": 1.0,
"endFrame": 800.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "idleTalk2",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/talk_righthand.fbx",
"startFrame": 1.0,
"endFrame": 501.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "idleTalk3",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/talk04.fbx",
"startFrame": 1.0,
"endFrame": 499.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
}
]
},
{
"id": "idleStand",
"type": "clip",
"type": "randomSwitchStateMachine",
"data": {
"url": "qrc:///avatar/animations/idle.fbx",
"startFrame": 0.0,
"endFrame": 300.0,
"timeScale": 1.0,
"loopFlag": true
"currentState": "masterIdle",
"triggerTimeMin": 10.0,
"triggerTimeMax": 60.0,
"transitionVar": "timeToFidget",
"states": [
{
"id": "masterIdle",
"interpTarget": 21,
"interpDuration": 20,
"priority": 1.0,
"resume": false,
"transitions": [
{ "var": "timeToFidget", "randomSwitchState": "fidget" }
]
},
{
"id": "fidget",
"interpTarget": 21,
"interpDuration": 20,
"priority": -1.0,
"resume": false,
"transitions": [
{ "var": "movement1OnDone", "randomSwitchState": "masterIdle" },
{ "var": "movement2OnDone", "randomSwitchState": "masterIdle" },
{ "var": "movement3OnDone", "randomSwitchState": "masterIdle" },
{ "var": "movement4OnDone", "randomSwitchState": "masterIdle" },
{ "var": "movement5OnDone", "randomSwitchState": "masterIdle" },
{ "var": "movement6OnDone", "randomSwitchState": "masterIdle" },
{ "var": "movement7OnDone", "randomSwitchState": "masterIdle" },
{ "var": "movement8OnDone", "randomSwitchState": "masterIdle" },
{ "var": "alt1ToMasterIdleOnDone", "randomSwitchState": "masterIdle" },
{ "var": "alt2ToMasterIdleOnDone", "randomSwitchState": "masterIdle" }
]
}
]
},
"children": []
},
{
"id": "idleTalk",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/talk.fbx",
"startFrame": 0.0,
"endFrame": 800.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
"children": [
{
"id": "masterIdle",
"type": "randomSwitchStateMachine",
"data": {
"currentState": "masterIdle1",
"triggerRandomSwitch": "masterIdleSwitch",
"randomSwitchTimeMin": 10.0,
"randomSwitchTimeMax": 60.0,
"states": [
{
"id": "masterIdle1",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.33,
"resume": true,
"transitions": []
},
{
"id": "masterIdle2",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.33,
"resume": true,
"transitions": []
},
{
"id": "masterIdle3",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.33,
"resume": true,
"transitions": []
}
]
},
"children": [
{
"id": "masterIdle1",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle.fbx",
"startFrame": 1.0,
"endFrame": 300.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "masterIdle2",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idleWS_all.fbx",
"startFrame": 1.0,
"endFrame": 1620.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "masterIdle3",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_lookaround01.fbx",
"startFrame": 1.0,
"endFrame": 901.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
}
]
},
{
"id": "fidget",
"type": "randomSwitchStateMachine",
"data": {
"currentState": "movement",
"states": [
{
"id": "movement",
"interpTarget": 17,
"interpDuration": 15,
"priority": 0.8,
"resume": false,
"transitions": []
},
{
"id": "alternateIdle",
"interpTarget": 17,
"interpDuration": 15,
"priority": 0.2,
"resume": false,
"transitions": []
}
]
},
"children": [
{
"id": "movement",
"type": "randomSwitchStateMachine",
"data": {
"currentState": "movement1",
"states": [
{
"id": "movement1",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.2,
"resume": false,
"transitions": []
},
{
"id": "movement2",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.2,
"resume": false,
"transitions": []
},
{
"id": "movement3",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.2,
"resume": false,
"transitions": []
},
{
"id": "movement4",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.2,
"resume": false,
"transitions": []
},
{
"id": "movement5",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.2,
"resume": false,
"transitions": []
},
{
"id": "movement6",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.2,
"resume": false,
"transitions": []
},
{
"id": "movement7",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.2,
"resume": false,
"transitions": []
},
{
"id": "movement8",
"interpTarget": 21,
"interpDuration": 20,
"priority": 0.2,
"resume": false,
"transitions": []
}
]
},
"children": [
{
"id": "movement1",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_once_slownod.fbx",
"startFrame": 1,
"endFrame": 91.0,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "movement2",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_once_headtilt.fbx",
"startFrame": 1,
"endFrame": 154,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "movement3",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_once_headtilt.fbx",
"startFrame": 1,
"endFrame": 154,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "movement4",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idleWS_all.fbx",
"startFrame": 1,
"endFrame": 1620,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "movement5",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_once_lookaround.fbx",
"startFrame": 1,
"endFrame": 324,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "movement6",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_once_neckstretch.fbx",
"startFrame": 1,
"endFrame": 169,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "movement7",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idleWS_all.fbx",
"startFrame": 1,
"endFrame": 1620,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "movement8",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_once_lookaround.fbx",
"startFrame": 1,
"endFrame": 324,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
}
]
},
{
"id": "alternateIdle",
"type": "randomSwitchStateMachine",
"data": {
"currentState": "transitionToAltIdle1",
"triggerTimeMin": 10.0,
"triggerTimeMax": 60.0,
"transitionVar": "finishAltIdle2",
"states": [
{
"id": "transitionToAltIdle1",
"interpTarget": 11,
"interpDuration": 10,
"priority": 0.5,
"resume": false,
"transitions": [
{
"var": "transitionToAltIdle1OnDone",
"randomSwitchState": "altIdle1"
}
]
},
{
"id": "transitionToAltIdle2",
"interpTarget": 11,
"interpDuration": 10,
"priority": 0.5,
"resume": false,
"transitions": [
{
"var": "transitionToAltIdle2OnDone",
"randomSwitchState": "altIdle2"
}
]
},
{
"id": "altIdle1",
"interpTarget": 11,
"interpDuration": 10,
"priority": -1.0,
"resume": false,
"transitions": [
{
"var": "finishAltIdle2",
"randomSwitchState": "alt1ToMasterIdle"
}
]
},
{
"id": "altIdle2",
"interpTarget": 11,
"interpDuration": 10,
"priority": -1.0,
"resume": false,
"transitions": [
{
"var": "finishAltIdle2",
"randomSwitchState": "alt2ToMasterIdle"
}
]
},
{
"id": "alt1ToMasterIdle",
"interpTarget": 11,
"interpDuration": 10,
"priority": -1.0,
"resume": false,
"transitions": []
},
{
"id": "alt2ToMasterIdle",
"interpTarget": 11,
"interpDuration": 10,
"priority": -1.0,
"resume": false,
"transitions": []
}
]
},
"children": [
{
"id": "transitionToAltIdle1",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_LFF_all.fbx",
"startFrame": 1,
"endFrame": 55,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "transitionToAltIdle2",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_RFF_all.fbx",
"startFrame": 1,
"endFrame": 56,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "altIdle1",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_LFF_all.fbx",
"startFrame": 55,
"endFrame": 389,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "altIdle2",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_RFF_all.fbx",
"startFrame": 56,
"endFrame": 390,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "alt1ToMasterIdle",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_LFF_all.fbx",
"startFrame": 389,
"endFrame": 472,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "alt2ToMasterIdle",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle_RFF_all.fbx",
"startFrame": 390,
"endFrame": 453,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
}
]
}
]
}
]
}
]
},
@ -1594,4 +2099,4 @@
}
]
}
}
}

View file

@ -23,15 +23,15 @@ Rectangle {
property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled;
function updateOpacity() {
if (ignoreRadiusEnabled) {
bubbleRect.opacity = 1.0;
} else {
bubbleRect.opacity = 0.7;
}
var rectOpacity = ignoreRadiusEnabled ? 1.0 : (mouseArea.containsMouse ? 1.0 : 0.7);
bubbleRect.opacity = rectOpacity;
}
Component.onCompleted: {
updateOpacity();
AvatarInputs.ignoreRadiusEnabledChanged.connect(function() {
ignoreRadiusEnabled = AvatarInputs.ignoreRadiusEnabled;
});
}
onIgnoreRadiusEnabledChanged: {
@ -74,10 +74,10 @@ Rectangle {
}
drag.target: dragTarget;
onContainsMouseChanged: {
var rectOpacity = (ignoreRadiusEnabled && containsMouse) ? 1.0 : (containsMouse ? 1.0 : 0.7);
if (containsMouse) {
Tablet.playSound(TabletEnums.ButtonHover);
}
var rectOpacity = ignoreRadiusEnabled ? 1.0 : (mouseArea.containsMouse ? 1.0 : 0.7);
bubbleRect.opacity = rectOpacity;
}
}

View file

@ -398,7 +398,7 @@ Item {
lineHeight: 1
lineHeightMode: Text.ProportionalHeight
onLinkActivated: loginDialog.openUrl(link);
onLinkActivated: Window.openUrl(link);
Component.onCompleted: {
if (termsTextMetrics.width > root.bannerWidth) {

View file

@ -363,7 +363,7 @@ Item {
linkColor: hifi.colors.blueAccent
onLinkActivated: {
Tablet.playSound(TabletEnums.ButtonClick);
loginDialog.openUrl(link);
Window.openUrl(link);
lightboxPopup.titleText = "Can't Access Account";
lightboxPopup.bodyText = lightboxPopup.cantAccessBodyText;
lightboxPopup.button2text = "CLOSE";

View file

@ -411,7 +411,7 @@ Item {
lineHeight: 1
lineHeightMode: Text.ProportionalHeight
onLinkActivated: loginDialog.openUrl(link);
onLinkActivated: Window.openUrl(link);
Component.onCompleted: {
if (termsTextMetrics.width > root.bannerWidth) {

View file

@ -234,7 +234,7 @@ Item {
lineHeight: 1
lineHeightMode: Text.ProportionalHeight
onLinkActivated: loginDialog.openUrl(link);
onLinkActivated: Window.openUrl(link);
Component.onCompleted: {
if (termsTextMetrics.width > root.bannerWidth) {

View file

@ -57,6 +57,14 @@ Item {
StatText {
text: "Avatars: " + root.avatarCount
}
StatText {
visible: true
text: "Refresh: " + root.refreshRateRegime + " - " + root.refreshRateTarget
}
StatText {
visible: root.expanded
text:" " + root.refreshRateMode + " - " + root.uxMode;
}
StatText {
text: "Game Rate: " + root.gameLoopRate
}

View file

@ -254,7 +254,7 @@ Rectangle {
switchWidth: root.switchWidth;
anchors.top: parent.top
anchors.left: parent.left
labelTextOn: qsTr("Warn when muted in HMD");
labelTextOn: qsTr("HMD Mute Warning");
labelTextSize: 16;
backgroundOnColor: "#E3E3E3";
checked: AudioScriptingInterface.warnWhenMuted;

View file

@ -952,7 +952,7 @@ Rectangle {
text: "LOG IN"
onClicked: {
sendToScript({method: 'needsLogIn_loginClicked'});
sendToScript({method: 'marketplace_loginClicked'});
}
}

View file

@ -137,7 +137,7 @@ Item {
width: parent.width/2 - anchors.leftMargin*2;
text: "Cancel"
onClicked: {
sendToScript({method: 'needsLogIn_cancelClicked'});
sendToScript({method: 'passphrasePopup_cancelClicked'});
}
}
@ -155,7 +155,7 @@ Item {
width: parent.width/2 - anchors.rightMargin*2;
text: "Log In"
onClicked: {
sendToScript({method: 'needsLogIn_loginClicked'});
sendToScript({method: 'marketplace_loginClicked'});
}
}
}

View file

@ -150,6 +150,7 @@
#include <Preferences.h>
#include <display-plugins/CompositorHelper.h>
#include <display-plugins/hmd/HmdDisplayPlugin.h>
#include <display-plugins/RefreshRateController.h>
#include <trackers/EyeTracker.h>
#include <avatars-renderer/ScriptAvatar.h>
#include <RenderableEntityItem.h>
@ -192,6 +193,7 @@
#include "scripting/WalletScriptingInterface.h"
#include "scripting/TTSScriptingInterface.h"
#include "scripting/KeyboardScriptingInterface.h"
#include "scripting/RefreshRateScriptingInterface.h"
@ -820,7 +822,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
audioDLLPath += "/audioWin7";
}
QCoreApplication::addLibraryPath(audioDLLPath);
#endif
#endif
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
DependencyManager::registerInheritance<AvatarHashMap, AvatarManager>();
@ -1813,6 +1815,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
});
getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::STARTUP);
// Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings
userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice());
// if the _touchscreenDevice is not supported it will not be registered
@ -1861,12 +1865,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) {
getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person.
} else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) {
Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true);
cameraMenuChanged();
} else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) {
Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true);
cameraMenuChanged();
}
{
@ -2337,7 +2335,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
DependencyManager::get<PickManager>()->setPrecisionPicking(rayPickID, value);
});
EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) {
EntityItem::setBillboardRotationOperator([](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) {
if (billboardMode == BillboardMode::YAW) {
//rotate about vertical to face the camera
glm::vec3 dPosition = frustumPos - position;
@ -2365,7 +2363,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
DependencyManager::get<UsersScriptingInterface>()->setKickConfirmationOperator([this] (const QUuid& nodeID) { userKickConfirmation(nodeID); });
render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer<OffscreenQmlSurface>& webSurface, bool& cachedWebSurface) {
render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([=](const QString& url, bool htmlContent, QSharedPointer<OffscreenQmlSurface>& webSurface, bool& cachedWebSurface) {
bool isTablet = url == TabletScriptingInterface::QML;
if (htmlContent) {
webSurface = DependencyManager::get<OffscreenQmlSurfaceCache>()->acquire(render::entities::WebEntityRenderer::QML);
@ -2405,7 +2403,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
const uint8_t TABLET_FPS = 90;
webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS);
});
render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer<OffscreenQmlSurface>& webSurface, bool& cachedWebSurface, std::vector<QMetaObject::Connection>& connections) {
render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([=](QSharedPointer<OffscreenQmlSurface>& webSurface, bool& cachedWebSurface, std::vector<QMetaObject::Connection>& connections) {
QQuickItem* rootItem = webSurface->getRootItem();
// Fix for crash in QtWebEngineCore when rapidly switching domains
@ -2443,6 +2441,14 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
DependencyManager::get<TabletScriptingInterface>()->preloadSounds();
DependencyManager::get<Keyboard>()->createKeyboard();
FileDialogHelper::setOpenDirectoryOperator([this](const QString& path) { openDirectory(path); });
QDesktopServices::setUrlHandler("file", this, "showUrlHandler");
QDesktopServices::setUrlHandler("", this, "showUrlHandler");
auto drives = QDir::drives();
for (auto drive : drives) {
QDesktopServices::setUrlHandler(QUrl(drive.absolutePath()).scheme(), this, "showUrlHandler");
}
_pendingIdleEvent = false;
_graphicsEngine.startup();
@ -2619,6 +2625,8 @@ void Application::onAboutToQuit() {
_aboutToQuit = true;
cleanupBeforeQuit();
getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::SHUTDOWN);
}
void Application::cleanupBeforeQuit() {
@ -3228,6 +3236,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
surfaceContext->setContextProperty("Controller", DependencyManager::get<controller::ScriptingInterface>().data());
surfaceContext->setContextProperty("Entities", DependencyManager::get<EntityScriptingInterface>().data());
surfaceContext->setContextProperty("RefreshRate", new RefreshRateScriptingInterface());
_fileDownload = new FileScriptingInterface(engine);
surfaceContext->setContextProperty("File", _fileDownload);
connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip);
@ -3376,6 +3385,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona
surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance());
surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance());
surfaceContext->setContextProperty("RefreshRate", new RefreshRateScriptingInterface());
surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED
surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED
@ -4047,6 +4057,9 @@ bool Application::event(QEvent* event) {
case QEvent::KeyRelease:
keyReleaseEvent(static_cast<QKeyEvent*>(event));
return true;
case QEvent::FocusIn:
focusInEvent(static_cast<QFocusEvent*>(event));
return true;
case QEvent::FocusOut:
focusOutEvent(static_cast<QFocusEvent*>(event));
return true;
@ -4108,6 +4121,12 @@ bool Application::eventFilter(QObject* object, QEvent* event) {
}
}
if (event->type() == QEvent::WindowStateChange) {
if (getWindow()->windowState() == Qt::WindowMinimized) {
getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::MINIMIZED);
}
}
return false;
}
@ -4394,6 +4413,13 @@ void Application::keyReleaseEvent(QKeyEvent* event) {
}
void Application::focusInEvent(QFocusEvent* event) {
if (!_aboutToQuit && _startUpFinished) {
getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::RUNNING);
}
}
void Application::focusOutEvent(QFocusEvent* event) {
auto inputPlugins = PluginManager::getInstance()->getInputPlugins();
foreach(auto inputPlugin, inputPlugins) {
@ -4402,6 +4428,9 @@ void Application::focusOutEvent(QFocusEvent* event) {
}
}
if (!_aboutToQuit && _startUpFinished) {
getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::UNFOCUS);
}
// FIXME spacemouse code still needs cleanup
#if 0
//SpacemouseDevice::getInstance().focusOutEvent();
@ -5569,6 +5598,8 @@ void Application::resumeAfterLoginDialogActionTaken() {
menu->getMenu("Developer")->setVisible(_developerMenuVisible);
_myCamera.setMode(_previousCameraMode);
cameraModeChanged();
_startUpFinished = true;
getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::RUNNING);
}
void Application::loadAvatarScripts(const QVector<QString>& urls) {
@ -5757,9 +5788,6 @@ void Application::cycleCamera() {
menu->setIsOptionChecked(MenuOption::ThirdPerson, false);
menu->setIsOptionChecked(MenuOption::FullscreenMirror, true);
} else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) {
// do nothing if in independent or camera entity modes
return;
}
cameraMenuChanged(); // handle the menu change
}
@ -5775,12 +5803,6 @@ void Application::cameraModeChanged() {
case CAMERA_MODE_MIRROR:
Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, true);
break;
case CAMERA_MODE_INDEPENDENT:
Menu::getInstance()->setIsOptionChecked(MenuOption::IndependentMode, true);
break;
case CAMERA_MODE_ENTITY:
Menu::getInstance()->setIsOptionChecked(MenuOption::CameraEntityMode, true);
break;
default:
break;
}
@ -5824,14 +5846,6 @@ void Application::cameraMenuChanged() {
getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT);
}
}
} else if (menu->isOptionChecked(MenuOption::IndependentMode)) {
if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) {
_myCamera.setMode(CAMERA_MODE_INDEPENDENT);
}
} else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) {
if (_myCamera.getMode() != CAMERA_MODE_ENTITY) {
_myCamera.setMode(CAMERA_MODE_ENTITY);
}
}
}
@ -6746,6 +6760,11 @@ void Application::updateRenderArgs(float deltaTime) {
}
}
appRenderArgs._renderArgs._stencilMaskMode = getActiveDisplayPlugin()->getStencilMaskMode();
if (appRenderArgs._renderArgs._stencilMaskMode == StencilMaskMode::MESH) {
appRenderArgs._renderArgs._stencilMaskOperator = getActiveDisplayPlugin()->getStencilMaskMeshOperator();
}
{
QMutexLocker viewLocker(&_viewMutex);
_myCamera.loadViewFrustum(_displayViewFrustum);
@ -7363,6 +7382,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe
scriptEngine->registerGlobalObject("LODManager", DependencyManager::get<LODManager>().data());
scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get<KeyboardScriptingInterface>().data());
scriptEngine->registerGlobalObject("RefreshRate", new RefreshRateScriptingInterface);
scriptEngine->registerGlobalObject("Paths", DependencyManager::get<PathUtils>().data());
@ -8280,19 +8300,6 @@ void Application::packageModel() {
ModelPackager::package();
}
void Application::openUrl(const QUrl& url) const {
if (!url.isEmpty()) {
if (url.scheme() == URL_SCHEME_HIFI) {
DependencyManager::get<AddressManager>()->handleLookupString(url.toString());
} else if (url.scheme() == URL_SCHEME_HIFIAPP) {
DependencyManager::get<QmlCommerce>()->openSystemApp(url.path());
} else {
// address manager did not handle - ask QDesktopServices to handle
QDesktopServices::openUrl(url);
}
}
}
void Application::loadDialog() {
ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"),
getPreviousScriptLocation(),
@ -8433,11 +8440,23 @@ void Application::loadAvatarBrowser() const {
DependencyManager::get<HMDScriptingInterface>()->openTablet();
}
void Application::addSnapshotOperator(const SnapshotOperator& snapshotOperator) {
std::lock_guard<std::mutex> lock(_snapshotMutex);
_snapshotOperators.push(snapshotOperator);
_hasPrimarySnapshot = _hasPrimarySnapshot || std::get<2>(snapshotOperator);
}
bool Application::takeSnapshotOperators(std::queue<SnapshotOperator>& snapshotOperators) {
std::lock_guard<std::mutex> lock(_snapshotMutex);
bool hasPrimarySnapshot = _hasPrimarySnapshot;
_hasPrimarySnapshot = false;
_snapshotOperators.swap(snapshotOperators);
return hasPrimarySnapshot;
}
void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) {
postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] {
// Get a screenshot and save it
QString path = DependencyManager::get<Snapshot>()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename,
TestScriptingInterface::getInstance()->getTestResultsLocation());
addSnapshotOperator(std::make_tuple([notify, includeAnimated, aspectRatio, filename](const QImage& snapshot) {
QString path = DependencyManager::get<Snapshot>()->saveSnapshot(snapshot, filename, TestScriptingInterface::getInstance()->getTestResultsLocation());
// If we're not doing an animated snapshot as well...
if (!includeAnimated) {
@ -8446,19 +8465,20 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa
emit DependencyManager::get<WindowScriptingInterface>()->stillSnapshotTaken(path, notify);
}
} else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) {
// Get an animated GIF snapshot and save it
SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get<WindowScriptingInterface>());
qApp->postLambdaEvent([path, aspectRatio] {
// Get an animated GIF snapshot and save it
SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, DependencyManager::get<WindowScriptingInterface>());
});
}
});
}, aspectRatio, true));
}
void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) {
postLambdaEvent([notify, filename, this] {
QString snapshotPath = DependencyManager::get<Snapshot>()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename,
TestScriptingInterface::getInstance()->getTestResultsLocation());
addSnapshotOperator(std::make_tuple([notify, filename](const QImage& snapshot) {
QString snapshotPath = DependencyManager::get<Snapshot>()->saveSnapshot(snapshot, filename, TestScriptingInterface::getInstance()->getTestResultsLocation());
emit DependencyManager::get<WindowScriptingInterface>()->stillSnapshotTaken(snapshotPath, notify);
});
}, 0.0f, false));
}
void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) {
@ -8763,6 +8783,7 @@ void Application::updateDisplayMode() {
auto displayPlugins = getDisplayPlugins();
// Default to the first item on the list, in case none of the menu items match
DisplayPluginPointer newDisplayPlugin = displayPlugins.at(0);
auto menu = getPrimaryMenu();
if (menu) {
@ -8852,6 +8873,14 @@ void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) {
if (desktop) {
desktop->setProperty("repositionLocked", wasRepositionLocked);
}
RefreshRateManager& refreshRateManager = getRefreshRateManager();
refreshRateManager.setRefreshRateOperator(OpenGLDisplayPlugin::getRefreshRateOperator());
bool isHmd = newDisplayPlugin->isHmd();
RefreshRateManager::UXMode uxMode = isHmd ? RefreshRateManager::UXMode::HMD :
RefreshRateManager::UXMode::DESKTOP;
refreshRateManager.setUXMode(uxMode);
}
bool isHmd = _displayPlugin->isHmd();
@ -9161,7 +9190,7 @@ void Application::readArgumentsFromLocalSocket() const {
// If we received a message, try to open it as a URL
if (message.length() > 0) {
qApp->openUrl(QString::fromUtf8(message));
DependencyManager::get<WindowScriptingInterface>()->openUrl(QString::fromUtf8(message));
}
}
@ -9278,6 +9307,44 @@ QString Application::getGraphicsCardType() {
return GPUIdent::getInstance()->getName();
}
void Application::openDirectory(const QString& path) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "openDirectory", Q_ARG(const QString&, path));
return;
}
QString dirPath = path;
const QString FILE_SCHEME = "file:///";
if (dirPath.startsWith(FILE_SCHEME)) {
dirPath.remove(0, FILE_SCHEME.length());
}
QFileInfo fileInfo(dirPath);
if (fileInfo.isDir()) {
auto scheme = QUrl(path).scheme();
QDesktopServices::unsetUrlHandler(scheme);
QDesktopServices::openUrl(path);
QDesktopServices::setUrlHandler(scheme, this, "showUrlHandler");
}
}
void Application::showUrlHandler(const QUrl& url) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "showUrlHandler", Q_ARG(const QUrl&, url));
return;
}
ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Confirm openUrl", "Do you recognize this path or code and want to open or execute it: " + url.toDisplayString());
QObject::connect(dlg, &ModalDialogListener::response, this, [=](QVariant answer) {
QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr);
if (QMessageBox::Yes == static_cast<QMessageBox::StandardButton>(answer.toInt())) {
// Unset the handler, open the URL, and the reset the handler
QDesktopServices::unsetUrlHandler(url.scheme());
QDesktopServices::openUrl(url);
QDesktopServices::setUrlHandler(url.scheme(), this, "showUrlHandler");
}
});
}
#if defined(Q_OS_ANDROID)
void Application::beforeEnterBackground() {
auto nodeList = DependencyManager::get<NodeList>();
@ -9320,6 +9387,3 @@ void Application::toggleAwayMode(){
#endif
#include "Application.moc"

View file

@ -58,6 +58,7 @@
#include "gpu/Context.h"
#include "LoginStateManager.h"
#include "Menu.h"
#include "RefreshRateManager.h"
#include "octree/OctreePacketProcessor.h"
#include "render/Engine.h"
#include "scripting/ControllerScriptingInterface.h"
@ -203,6 +204,7 @@ public:
CompositorHelper& getApplicationCompositor() const;
Overlays& getOverlays() { return _overlays; }
RefreshRateManager& getRefreshRateManager() { return _refreshRateManager; }
size_t getRenderFrameCount() const { return _graphicsEngine.getRenderFrameCount(); }
float getRenderLoopRate() const { return _graphicsEngine.getRenderLoopRate(); }
@ -345,6 +347,12 @@ public:
void toggleAwayMode();
#endif
using SnapshotOperator = std::tuple<std::function<void(const QImage&)>, float, bool>;
void addSnapshotOperator(const SnapshotOperator& snapshotOperator);
bool takeSnapshotOperators(std::queue<SnapshotOperator>& snapshotOperators);
void openDirectory(const QString& path);
signals:
void svoImportRequested(const QString& url);
@ -404,8 +412,6 @@ public slots:
static void packageModel();
void openUrl(const QUrl& url) const;
void resetSensors(bool andReload = false);
void setActiveFaceTracker() const;
@ -472,6 +478,9 @@ public slots:
QString getGraphicsCardType();
bool gpuTextureMemSizeStable();
void showUrlHandler(const QUrl& url);
private slots:
void onDesktopRootItemCreated(QQuickItem* qmlContext);
void onDesktopRootContextCreated(QQmlContext* qmlContext);
@ -566,7 +575,6 @@ private:
bool importFromZIP(const QString& filePath);
bool importImage(const QString& urlString);
bool gpuTextureMemSizeStable();
int processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode);
void trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket);
@ -717,6 +725,7 @@ private:
QUuid _loginDialogID;
QUuid _avatarInputsBarID;
LoginStateManager _loginStateManager;
RefreshRateManager _refreshRateManager;
quint64 _lastFaceTrackerUpdate;
@ -789,6 +798,9 @@ private:
AudioInjectorPointer _snapshotSoundInjector;
SharedSoundPointer _snapshotSound;
SharedSoundPointer _sampleSound;
std::mutex _snapshotMutex;
std::queue<SnapshotOperator> _snapshotOperators;
bool _hasPrimarySnapshot { false };
DisplayPluginPointer _autoSwitchDisplayModeSupportedHMDPlugin;
QString _autoSwitchDisplayModeSupportedHMDPluginName;
@ -811,5 +823,6 @@ private:
bool _resumeAfterLoginDialogActionTaken_WasPostponed { false };
bool _resumeAfterLoginDialogActionTaken_SafeToRun { false };
bool _startUpFinished { false };
};
#endif // hifi_Application_h

View file

@ -194,20 +194,6 @@ Menu::Menu() {
viewMirrorAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
// View > Independent
auto viewIndependentAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu,
MenuOption::IndependentMode, 0,
false, qApp, SLOT(cameraMenuChanged())));
viewIndependentAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
// View > Entity Camera
auto viewEntityCameraAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu,
MenuOption::CameraEntityMode, 0,
false, qApp, SLOT(cameraMenuChanged())));
viewEntityCameraAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
viewMenu->addSeparator();
// View > Center Player In View
@ -434,9 +420,21 @@ Menu::Menu() {
MenuWrapper* resolutionMenu = renderOptionsMenu->addMenu(MenuOption::RenderResolution);
QActionGroup* resolutionGroup = new QActionGroup(resolutionMenu);
resolutionGroup->setExclusive(true);
#if defined(Q_OS_MAC)
resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionOne, 0, false));
#else
resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionOne, 0, true));
#endif
resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionTwoThird, 0, false));
#if defined(Q_OS_MAC)
resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionHalf, 0, true));
#else
resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionHalf, 0, false));
#endif
resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionThird, 0, false));
resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionQuarter, 0, false));
@ -627,6 +625,8 @@ Menu::Menu() {
avatar.get(), SLOT(setEnableDebugDrawAnimPose(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawPosition, 0, false,
avatar.get(), SLOT(setEnableDebugDrawPosition(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawOtherSkeletons, 0, false,
avatarManager.data(), SLOT(setEnableDebugDrawOtherSkeletons(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::MeshVisible, 0, true,
avatar.get(), SLOT(setEnableMeshVisible(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::DisableEyelidAdjustment, 0, false);
@ -707,8 +707,7 @@ Menu::Menu() {
// Developer > Timing >>>
MenuWrapper* timingMenu = developerMenu->addMenu("Timing");
MenuWrapper* perfTimerMenu = timingMenu->addMenu("Performance Timer");
addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayDebugTimingDetails, 0, false,
qApp, SLOT(enablePerfStats(bool)));
addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayDebugTimingDetails);
addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::OnlyDisplayTopTen, 0, true);
addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandUpdateTiming, 0, false);
addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandSimulationTiming, 0, false);

View file

@ -33,6 +33,7 @@ namespace MenuOption {
const QString AnimDebugDrawBaseOfSupport = "Debug Draw Base of Support";
const QString AnimDebugDrawDefaultPose = "Debug Draw Default Pose";
const QString AnimDebugDrawPosition= "Debug Draw Position";
const QString AnimDebugDrawOtherSkeletons = "Debug Draw Other Skeletons";
const QString AskToResetSettings = "Ask To Reset Settings on Start";
const QString AssetMigration = "ATP Asset Migration";
const QString AssetServer = "Asset Browser";
@ -53,7 +54,6 @@ namespace MenuOption {
const QString BookmarkAvatarEntities = "Bookmark Avatar Entities";
const QString BookmarkLocation = "Bookmark Location";
const QString CalibrateCamera = "Calibrate Camera";
const QString CameraEntityMode = "Entity Mode";
const QString CenterPlayerInView = "Center Player In View";
const QString Chat = "Chat...";
const QString ClearDiskCache = "Clear Disk Cache";
@ -120,7 +120,6 @@ namespace MenuOption {
const QString Help = "Help...";
const QString HomeLocation = "Home ";
const QString IncreaseAvatarSize = "Increase Avatar Size";
const QString IndependentMode = "Independent Mode";
const QString ActionMotorControl = "Enable Default Motor Control";
const QString LastLocation = "Last Location";
const QString LoadScript = "Open and Run Script File...";

View file

@ -18,9 +18,6 @@
#include <QPushButton>
#include <QStandardPaths>
static const QString AVATAR_HEAD_AND_BODY_STRING = "Avatar Body with Head";
static const QString ENTITY_MODEL_STRING = "Entity Model";
ModelSelector::ModelSelector() {
QFormLayout* form = new QFormLayout(this);

View file

@ -0,0 +1,149 @@
//
// RefreshRateManager.cpp
// interface/src/
//
// Created by Dante Ruiz on 2019-04-15.
// Copyright 2019 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 "RefreshRateManager.h"
#include <array>
#include <map>
#include <Application.h>
#include <display-plugins/hmd/HmdDisplayPlugin.h>
static const int HMD_TARGET_RATE = 90;
static const std::array<std::string, RefreshRateManager::RefreshRateProfile::PROFILE_NUM> REFRESH_RATE_PROFILE_TO_STRING =
{ { "Eco", "Interactive", "Realtime" } };
static const std::array<std::string, RefreshRateManager::RefreshRateRegime::REGIME_NUM> REFRESH_RATE_REGIME_TO_STRING =
{ { "Running", "Unfocus", "Minimized", "StartUp", "ShutDown" } };
static const std::array<std::string, RefreshRateManager::UXMode::UX_NUM> UX_MODE_TO_STRING =
{ { "Desktop", "HMD" } };
static const std::map<std::string, RefreshRateManager::RefreshRateProfile> REFRESH_RATE_PROFILE_FROM_STRING =
{ { "Eco", RefreshRateManager::RefreshRateProfile::ECO },
{ "Interactive", RefreshRateManager::RefreshRateProfile::INTERACTIVE },
{ "Realtime", RefreshRateManager::RefreshRateProfile::REALTIME } };
static const std::array<int, RefreshRateManager::RefreshRateProfile::PROFILE_NUM> RUNNING_REGIME_PROFILES =
{ { 5, 20, 60 } };
static const std::array<int, RefreshRateManager::RefreshRateProfile::PROFILE_NUM> UNFOCUS_REGIME_PROFILES =
{ { 5, 5, 10 } };
static const std::array<int, RefreshRateManager::RefreshRateProfile::PROFILE_NUM> MINIMIZED_REGIME_PROFILE =
{ { 2, 2, 2 } };
static const std::array<int, RefreshRateManager::RefreshRateProfile::PROFILE_NUM> START_AND_SHUTDOWN_REGIME_PROFILES =
{ { 30, 30, 30 } };
static const std::array<std::array<int, RefreshRateManager::RefreshRateProfile::PROFILE_NUM>, RefreshRateManager::RefreshRateRegime::REGIME_NUM> REFRESH_RATE_REGIMES =
{ { RUNNING_REGIME_PROFILES, UNFOCUS_REGIME_PROFILES, MINIMIZED_REGIME_PROFILE,
START_AND_SHUTDOWN_REGIME_PROFILES, START_AND_SHUTDOWN_REGIME_PROFILES } };
std::string RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile refreshRateProfile) {
return REFRESH_RATE_PROFILE_TO_STRING.at(refreshRateProfile);
}
RefreshRateManager::RefreshRateProfile RefreshRateManager::refreshRateProfileFromString(std::string refreshRateProfile) {
return REFRESH_RATE_PROFILE_FROM_STRING.at(refreshRateProfile);
}
std::string RefreshRateManager::refreshRateRegimeToString(RefreshRateManager::RefreshRateRegime refreshRateRegime) {
return REFRESH_RATE_REGIME_TO_STRING.at(refreshRateRegime);
}
std::string RefreshRateManager::uxModeToString(RefreshRateManager::RefreshRateManager::UXMode uxMode) {
return UX_MODE_TO_STRING.at(uxMode);
}
RefreshRateManager::RefreshRateManager() {
_refreshRateProfile = (RefreshRateManager::RefreshRateProfile) _refreshRateMode.get();
}
void RefreshRateManager::setRefreshRateProfile(RefreshRateManager::RefreshRateProfile refreshRateProfile) {
if (_refreshRateProfile != refreshRateProfile) {
_refreshRateModeLock.withWriteLock([&] {
_refreshRateProfile = refreshRateProfile;
_refreshRateMode.set((int) refreshRateProfile);
});
updateRefreshRateController();
}
}
RefreshRateManager::RefreshRateProfile RefreshRateManager::getRefreshRateProfile() const {
RefreshRateManager::RefreshRateProfile profile = RefreshRateManager::RefreshRateProfile::REALTIME;
if (getUXMode() != RefreshRateManager::UXMode::HMD) {
profile =(RefreshRateManager::RefreshRateProfile) _refreshRateModeLock.resultWithReadLock<int>([&] {
return _refreshRateMode.get();
});
}
return profile;
}
RefreshRateManager::RefreshRateRegime RefreshRateManager::getRefreshRateRegime() const {
return getUXMode() == RefreshRateManager::UXMode::HMD ? RefreshRateManager::RefreshRateRegime::RUNNING :
_refreshRateRegime;
}
void RefreshRateManager::setRefreshRateRegime(RefreshRateManager::RefreshRateRegime refreshRateRegime) {
if (_refreshRateRegime != refreshRateRegime) {
_refreshRateRegime = refreshRateRegime;
updateRefreshRateController();
}
}
void RefreshRateManager::setUXMode(RefreshRateManager::UXMode uxMode) {
if (_uxMode != uxMode) {
_uxMode = uxMode;
updateRefreshRateController();
}
}
void RefreshRateManager::updateRefreshRateController() const {
if (_refreshRateOperator) {
int targetRefreshRate;
if (_uxMode == RefreshRateManager::UXMode::DESKTOP) {
if (_refreshRateRegime == RefreshRateManager::RefreshRateRegime::RUNNING &&
_refreshRateProfile == RefreshRateManager::RefreshRateProfile::INTERACTIVE) {
targetRefreshRate = getInteractiveRefreshRate();
} else {
targetRefreshRate = REFRESH_RATE_REGIMES[_refreshRateRegime][_refreshRateProfile];
}
} else {
targetRefreshRate = HMD_TARGET_RATE;
}
_refreshRateOperator(targetRefreshRate);
_activeRefreshRate = targetRefreshRate;
}
}
void RefreshRateManager::setInteractiveRefreshRate(int refreshRate) {
_refreshRateLock.withWriteLock([&] {
_interactiveRefreshRate.set(refreshRate);
});
updateRefreshRateController();
}
int RefreshRateManager::getInteractiveRefreshRate() const {
return _refreshRateLock.resultWithReadLock<int>([&] {
return _interactiveRefreshRate.get();
});
}

View file

@ -0,0 +1,83 @@
//
// RefreshRateManager.h
// interface/src/
//
// Created by Dante Ruiz on 2019-04-15.
// Copyright 2019 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_RefreshRateManager_h
#define hifi_RefreshRateManager_h
#include <map>
#include <string>
#include <SettingHandle.h>
#include <shared/ReadWriteLockable.h>
class RefreshRateManager {
public:
enum RefreshRateProfile {
ECO = 0,
INTERACTIVE,
REALTIME,
PROFILE_NUM
};
enum RefreshRateRegime {
RUNNING = 0,
UNFOCUS,
MINIMIZED,
STARTUP,
SHUTDOWN,
REGIME_NUM
};
enum UXMode {
DESKTOP = 0,
HMD,
UX_NUM
};
RefreshRateManager();
~RefreshRateManager() = default;
void setRefreshRateProfile(RefreshRateProfile refreshRateProfile);
RefreshRateProfile getRefreshRateProfile() const;
void setRefreshRateRegime(RefreshRateRegime refreshRateRegime);
RefreshRateRegime getRefreshRateRegime() const;
void setUXMode(UXMode uxMode);
UXMode getUXMode() const { return _uxMode; }
void setRefreshRateOperator(std::function<void(int)> refreshRateOperator) { _refreshRateOperator = refreshRateOperator; }
int getActiveRefreshRate() const { return _activeRefreshRate; }
void updateRefreshRateController() const;
void setInteractiveRefreshRate(int refreshRate);
int getInteractiveRefreshRate() const;
static std::string refreshRateProfileToString(RefreshRateProfile refreshRateProfile);
static RefreshRateProfile refreshRateProfileFromString(std::string refreshRateProfile);
static std::string uxModeToString(UXMode uxMode);
static std::string refreshRateRegimeToString(RefreshRateRegime refreshRateRegime);
private:
mutable ReadWriteLockable _refreshRateLock;
mutable ReadWriteLockable _refreshRateModeLock;
mutable int _activeRefreshRate { 20 };
RefreshRateProfile _refreshRateProfile { RefreshRateProfile::INTERACTIVE};
RefreshRateRegime _refreshRateRegime { RefreshRateRegime::STARTUP };
UXMode _uxMode;
Setting::Handle<int> _interactiveRefreshRate { "interactiveRefreshRate", 20};
Setting::Handle<int> _refreshRateMode { "refreshRateProfile", INTERACTIVE };
std::function<void(int)> _refreshRateOperator { nullptr };
};
#endif

View file

@ -152,10 +152,12 @@ public:
_cachedArgsPointer->_viewport = args->_viewport;
_cachedArgsPointer->_displayMode = args->_displayMode;
_cachedArgsPointer->_renderMode = args->_renderMode;
_cachedArgsPointer->_stencilMaskMode = args->_stencilMaskMode;
args->_blitFramebuffer = destFramebuffer;
args->_viewport = glm::ivec4(0, 0, destFramebuffer->getWidth(), destFramebuffer->getHeight());
args->_displayMode = RenderArgs::MONO;
args->_renderMode = RenderArgs::RenderMode::SECONDARY_CAMERA_RENDER_MODE;
args->_stencilMaskMode = StencilMaskMode::NONE;
gpu::doInBatch("SecondaryCameraJob::run", args->_context, [&](gpu::Batch& batch) {
batch.disableContextStereo();
@ -255,10 +257,11 @@ public:
void run(const render::RenderContextPointer& renderContext, const RenderArgsPointer& cachedArgs) {
auto args = renderContext->args;
if (cachedArgs) {
args->_blitFramebuffer = cachedArgs->_blitFramebuffer;
args->_viewport = cachedArgs->_viewport;
args->_displayMode = cachedArgs->_displayMode;
args->_renderMode = cachedArgs->_renderMode;
args->_blitFramebuffer = cachedArgs->_blitFramebuffer;
args->_viewport = cachedArgs->_viewport;
args->_displayMode = cachedArgs->_displayMode;
args->_renderMode = cachedArgs->_renderMode;
args->_stencilMaskMode = cachedArgs->_stencilMaskMode;
}
args->popViewFrustum();

View file

@ -120,6 +120,8 @@ void AvatarManager::init() {
_myAvatar->addToScene(_myAvatar, scene, transaction);
scene->enqueueTransaction(transaction);
}
setEnableDebugDrawOtherSkeletons(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawOtherSkeletons));
}
void AvatarManager::setSpace(workload::SpacePointer& space ) {
@ -334,9 +336,14 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
if (avatar->getSkeletonModel()->isLoaded() && avatar->getWorkloadRegion() == workload::Region::R1) {
_myAvatar->addAvatarHandsToFlow(avatar);
}
if (_drawOtherAvatarSkeletons) {
avatar->debugJointData();
}
avatar->setEnableMeshVisible(!_drawOtherAvatarSkeletons);
avatar->updateRenderItem(renderTransaction);
avatar->updateSpaceProxy(workloadTransaction);
avatar->setLastRenderUpdateTime(startTime);
} else {
// we've spent our time budget for this priority bucket
// let's deal with the reminding avatars if this pass and BREAK from the for loop
@ -498,8 +505,10 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar
// on the creation of entities for that avatar instance and the deletion of entities for this instance
avatar->removeAvatarEntitiesFromTree();
if (removalReason != KillAvatarReason::AvatarDisconnected) {
emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID());
emit DependencyManager::get<UsersScriptingInterface>()->enteredIgnoreRadius();
if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) {
emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID());
emit DependencyManager::get<UsersScriptingInterface>()->enteredIgnoreRadius();
}
workload::Transaction workloadTransaction;
workloadTransaction.remove(avatar->getSpaceIndex());
@ -725,7 +734,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic
boxHit._distance = FLT_MAX;
for (size_t i = 0; i < hit._boundJoints.size(); i++) {
assert(hit._boundJoints[i] < multiSpheres.size());
assert(hit._boundJoints[i] < (int)multiSpheres.size());
auto &mSphere = multiSpheres[hit._boundJoints[i]];
if (mSphere.isValid()) {
float boundDistance = FLT_MAX;
@ -940,7 +949,8 @@ void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptV
* It is unique among all avatars present in the domain at the time.
* @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the
* domain.
* @property {boolean} isReplicated - <strong>Deprecated.</strong>
* @property {boolean} isReplicated - <span class="important">Deprecated: This property is deprecated and will be
* removed.</span>
* @property {Vec3} position - The position of the avatar.
* @property {number} palOrbOffset - The vertical offset from the avatar's position that an overlay orb should be displayed at.
*/

View file

@ -262,6 +262,15 @@ public slots:
*/
void updateAvatarRenderStatus(bool shouldRenderAvatars);
/**jsdoc
* Displays other avatars skeletons debug graphics.
* @function AvatarManager.setEnableDebugDrawOtherSkeletons
* @param {boolean} enabled - <code>true</code> to show the debug graphics, <code>false</code> to hide.
*/
void setEnableDebugDrawOtherSkeletons(bool isEnabled) {
_drawOtherAvatarSkeletons = isEnabled;
}
protected:
AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) override;
@ -299,6 +308,7 @@ private:
workload::SpacePointer _space;
AvatarTransit::TransitConfig _transitConfig;
bool _drawOtherAvatarSkeletons { false };
};
#endif // hifi_AvatarManager_h

View file

@ -168,6 +168,7 @@ MyAvatar::MyAvatar(QThread* thread) :
_displayNameSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "displayName", ""),
_collisionSoundURLSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "collisionSoundURL", QUrl(_collisionSoundURL)),
_useSnapTurnSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "useSnapTurn", _useSnapTurn),
_hoverWhenUnsupportedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "hoverWhenUnsupported", _hoverWhenUnsupported),
_userHeightSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "userHeight", DEFAULT_AVATAR_HEIGHT),
_flyingHMDSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "flyingHMD", _flyingPrefHMD),
_movementReferenceSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "movementReference", _movementReference),
@ -948,6 +949,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
bool collisionlessAllowed = zoneInteractionProperties.second;
_characterController.setZoneFlyingAllowed(zoneAllowsFlying || !isPhysicsEnabled);
_characterController.setComfortFlyingAllowed(_enableFlying);
_characterController.setHoverWhenUnsupported(_hoverWhenUnsupported);
_characterController.setCollisionlessAllowed(collisionlessAllowed);
}
@ -1041,11 +1043,15 @@ void MyAvatar::updateJointFromController(controller::Action poseKey, ThreadSafeV
assert(QThread::currentThread() == thread());
auto userInputMapper = DependencyManager::get<UserInputMapper>();
controller::Pose controllerPose = userInputMapper->getPoseState(poseKey);
Transform transform;
transform.setTranslation(controllerPose.getTranslation());
transform.setRotation(controllerPose.getRotation());
glm::mat4 controllerMatrix = transform.getMatrix();
matrixCache.set(controllerMatrix);
if (controllerPose.isValid()) {
Transform transform;
transform.setTranslation(controllerPose.getTranslation());
transform.setRotation(controllerPose.getRotation());
glm::mat4 controllerMatrix = transform.getMatrix();
matrixCache.set(controllerMatrix);
} else {
matrixCache.invalidate();
}
}
// best called at end of main loop, after physics.
@ -1199,6 +1205,15 @@ void MyAvatar::overrideAnimation(const QString& url, float fps, bool loop, float
_skeletonModel->getRig().overrideAnimation(url, fps, loop, firstFrame, lastFrame);
}
void MyAvatar::overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "overrideHandAnimation", Q_ARG(bool, isLeft), Q_ARG(const QString&, url), Q_ARG(float, fps),
Q_ARG(bool, loop), Q_ARG(float, firstFrame), Q_ARG(float, lastFrame));
return;
}
_skeletonModel->getRig().overrideHandAnimation(isLeft, url, fps, loop, firstFrame, lastFrame);
}
void MyAvatar::restoreAnimation() {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "restoreAnimation");
@ -1207,6 +1222,14 @@ void MyAvatar::restoreAnimation() {
_skeletonModel->getRig().restoreAnimation();
}
void MyAvatar::restoreHandAnimation(bool isLeft) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "restoreHandAnimation", Q_ARG(bool, isLeft));
return;
}
_skeletonModel->getRig().restoreHandAnimation(isLeft);
}
QStringList MyAvatar::getAnimationRoles() {
if (QThread::currentThread() != thread()) {
QStringList result;
@ -1288,6 +1311,7 @@ void MyAvatar::saveData() {
_displayNameSetting.set(_displayName);
_collisionSoundURLSetting.set(_collisionSoundURL);
_useSnapTurnSetting.set(_useSnapTurn);
_hoverWhenUnsupportedSetting.set(_hoverWhenUnsupported);
_userHeightSetting.set(getUserHeight());
_flyingHMDSetting.set(getFlyingHMDPref());
_movementReferenceSetting.set(getMovementReference());
@ -1613,7 +1637,9 @@ void MyAvatar::handleChangedAvatarEntityData() {
if (!skip) {
sanitizeAvatarEntityProperties(properties);
entityTree->withWriteLock([&] {
entityTree->updateEntity(id, properties);
if (entityTree->updateEntity(id, properties)) {
packetSender->queueEditAvatarEntityMessage(entityTree, id);
}
});
}
}
@ -1890,6 +1916,7 @@ void MyAvatar::loadData() {
setDisplayName(_displayNameSetting.get());
setCollisionSoundURL(_collisionSoundURLSetting.get(QUrl(DEFAULT_AVATAR_COLLISION_SOUND_URL)).toString());
setSnapTurn(_useSnapTurnSetting.get());
setHoverWhenUnsupported(_hoverWhenUnsupportedSetting.get());
setDominantHand(_dominantHandSetting.get(DOMINANT_RIGHT_HAND).toLower());
setStrafeEnabled(_strafeEnabledSetting.get(DEFAULT_STRAFE_ENABLED));
setHmdAvatarAlignmentType(_hmdAvatarAlignmentTypeSetting.get(DEFAULT_HMD_AVATAR_ALIGNMENT_TYPE).toLower());
@ -3172,17 +3199,40 @@ int MyAvatar::sendAvatarDataPacket(bool sendAll) {
return bytesSent;
}
const float RENDER_HEAD_CUTOFF_DISTANCE = 0.47f;
bool MyAvatar::cameraInsideHead(const glm::vec3& cameraPosition) const {
if (!_skeletonModel) {
return false;
}
// transform cameraPosition into rig coordinates
AnimPose rigToWorld = AnimPose(getWorldOrientation() * Quaternions::Y_180, getWorldPosition());
AnimPose worldToRig = rigToWorld.inverse();
glm::vec3 rigCameraPosition = worldToRig * cameraPosition;
// use head k-dop shape to determine if camera is inside head.
const Rig& rig = _skeletonModel->getRig();
int headJointIndex = rig.indexOfJoint("Head");
if (headJointIndex >= 0) {
const HFMModel& hfmModel = _skeletonModel->getHFMModel();
AnimPose headPose;
if (rig.getAbsoluteJointPoseInRigFrame(headJointIndex, headPose)) {
glm::vec3 displacement;
const HFMJointShapeInfo& headShapeInfo = hfmModel.joints[headJointIndex].shapeInfo;
return findPointKDopDisplacement(rigCameraPosition, headPose, headShapeInfo, displacement);
}
}
// fall back to simple distance check.
const float RENDER_HEAD_CUTOFF_DISTANCE = 0.47f;
return glm::length(cameraPosition - getHeadPosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getModelScale());
}
bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const {
bool defaultMode = renderArgs->_renderMode == RenderArgs::DEFAULT_RENDER_MODE;
bool firstPerson = qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON;
bool overrideAnim = _skeletonModel ? _skeletonModel->getRig().isPlayingOverrideAnimation() : false;
bool insideHead = cameraInsideHead(renderArgs->getViewFrustum().getPosition());
return !defaultMode || !firstPerson || !insideHead;
return !defaultMode || (!firstPerson && !insideHead) || (overrideAnim && !insideHead);
}
void MyAvatar::setHasScriptedBlendshapes(bool hasScriptedBlendshapes) {
@ -4798,7 +4848,12 @@ bool MyAvatar::isReadyForPhysics() const {
}
void MyAvatar::setSprintMode(bool sprint) {
_walkSpeedScalar = sprint ? AVATAR_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR;
if (qApp->isHMDMode()) {
_walkSpeedScalar = sprint ? AVATAR_DESKTOP_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR;
}
else {
_walkSpeedScalar = sprint ? AVATAR_HMD_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR;
}
}
void MyAvatar::setIsInWalkingState(bool isWalking) {
@ -5766,12 +5821,19 @@ void MyAvatar::releaseGrab(const QUuid& grabID) {
}
void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr<Avatar>& otherAvatar) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "addAvatarHandsToFlow",
Q_ARG(const std::shared_ptr<Avatar>&, otherAvatar));
return;
}
auto &flow = _skeletonModel->getRig().getFlow();
for (auto &handJointName : HAND_COLLISION_JOINTS) {
int jointIndex = otherAvatar->getJointIndex(handJointName);
if (jointIndex != -1) {
glm::vec3 position = otherAvatar->getJointPosition(jointIndex);
flow.setOthersCollision(otherAvatar->getID(), jointIndex, position);
if (otherAvatar != nullptr && flow.getActive()) {
for (auto &handJointName : HAND_COLLISION_JOINTS) {
int jointIndex = otherAvatar->getJointIndex(handJointName);
if (jointIndex != -1) {
glm::vec3 position = otherAvatar->getJointPosition(jointIndex);
flow.setOthersCollision(otherAvatar->getID(), jointIndex, position);
}
}
}
}

View file

@ -116,8 +116,8 @@ class MyAvatar : public Avatar {
* @property {boolean} lookAtSnappingEnabled=true - <code>true</code> if the avatar's eyes snap to look at another avatar's
* eyes when the other avatar is in the line of sight and also has <code>lookAtSnappingEnabled == true</code>.
* @property {string} skeletonModelURL - The avatar's FST file.
* @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.<br />
* <strong>Deprecated:</strong> Use avatar entities instead.
* @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* <p class="important">Deprecated: This property is deprecated and will be removed. Use avatar entities instead.</p>
* @property {string[]} jointNames - The list of joints in the current avatar model. <em>Read-only.</em>
* @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. <em>Read-only.</em>
* @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the
@ -198,7 +198,7 @@ class MyAvatar : public Avatar {
* @property {Pose} rightHandTipPose - The right hand's pose as determined by the hand controllers, relative to the avatar,
* with the position adjusted by 0.3m along the direction of the palm. <em>Read-only.</em>
*
* @property {number} energy - <strong>Deprecated:</strong> This property will be removed from the API.
* @property {number} energy - <span class="important">Deprecated: This property will be removed.</span>
* @property {boolean} isAway - <code>true</code> if your avatar is away (i.e., inactive), <code>false</code> if it is
* active.
*
@ -213,8 +213,9 @@ class MyAvatar : public Avatar {
* was set <code>false</code> because the zone may disallow collisionless avatars.
* @property {boolean} otherAvatarsCollisionsEnabled - Set to <code>true</code> to enable the avatar to collide with other
* avatars, <code>false</code> to disable collisions with other avatars.
* @property {boolean} characterControllerEnabled - Synonym of <code>collisionsEnabled</code>.<br />
* <strong>Deprecated:</strong> Use <code>collisionsEnabled</code> instead.
* @property {boolean} characterControllerEnabled - Synonym of <code>collisionsEnabled</code>.
* <p class="important">Deprecated: This property is deprecated and will be removed. Use <code>collisionsEnabled</code>
* instead.</p>
* @property {boolean} useAdvancedMovementControls - Returns and sets the value of the Interface setting, Settings >
* Controls > Walking. Note: Setting the value has no effect unless Interface is restarted.
* @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Controls > Show room
@ -597,6 +598,26 @@ public:
*/
Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame);
/**jsdoc
* <code>overrideHandAnimation()</code> Gets the overrides the default hand poses that are triggered with controller buttons.
* use {@link MyAvatar.restoreHandAnimation}.</p> to restore the default poses.
* @function MyAvatar.overrideHandAnimation
* @param isLeft {boolean} Set true if using the left hand
* @param url {string} The URL to the animation file. Animation files need to be FBX format, but only need to contain the
* avatar skeleton and animation data.
* @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed.
* @param loop {boolean} Set to true if the animation should loop.
* @param firstFrame {number} The frame the animation should start at.
* @param lastFrame {number} The frame the animation should end at
* @example <caption> Override left hand animation for three seconds. </caption>
* // Override the left hand pose then restore the default pose.
* MyAvatar.overrideHandAnimation(isLeft, ANIM_URL, 30, true, 0, 53);
* Script.setTimeout(function () {
* MyAvatar.restoreHandAnimation();
* }, 3000);
*/
Q_INVOKABLE void overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame);
/**jsdoc
* Restores the default animations.
* <p>The avatar animation system includes a set of default animations along with rules for how those animations are blended
@ -615,6 +636,24 @@ public:
*/
Q_INVOKABLE void restoreAnimation();
/**jsdoc
* Restores the default hand animation state machine that is driven by the state machine in the avatar-animation json.
* <p>The avatar animation system includes a set of default animations along with rules for how those animations are blended
* together with procedural data (such as look at vectors, hand sensors etc.). Playing your own custom animations will
* override the default animations. <code>restoreHandAnimation()</code> is used to restore the default hand poses
* If you aren't currently playing an override hand
* animation, this function has no effect.</p>
* @function MyAvatar.restoreHandAnimation
* @param isLeft {boolean} Set to true if using the left hand
* @example <caption> Override left hand animation for three seconds. </caption>
* // Override the left hand pose then restore the default pose.
* MyAvatar.overrideHandAnimation(isLeft, ANIM_URL, 30, true, 0, 53);
* Script.setTimeout(function () {
* MyAvatar.restoreHandAnimation();
* }, 3000);
*/
Q_INVOKABLE void restoreHandAnimation(bool isLeft);
/**jsdoc
* Gets the current animation roles.
* <p>Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together
@ -759,6 +798,18 @@ public:
* @param {number} index
*/
Q_INVOKABLE void setControlScheme(int index) { _controlSchemeIndex = (index >= 0 && index <= 2) ? index : 0; }
/**jsdoc
* @function MyAvatar.hoverWhenUnsupported
* @returns {boolean}
*/
Q_INVOKABLE bool hoverWhenUnsupported() const { return _hoverWhenUnsupported; }
/**jsdoc
* @function MyAvatar.setHoverWhenUnsupported
* @param {boolean} on
*/
Q_INVOKABLE void setHoverWhenUnsupported(bool on) { _hoverWhenUnsupported = on; }
/**jsdoc
* Sets the avatar's dominant hand.
* @function MyAvatar.setDominantHand
@ -1519,14 +1570,14 @@ public:
* @function MyAvatar.setCharacterControllerEnabled
* @param {boolean} enabled - <code>true</code> to enable the avatar to collide with entities, <code>false</code> to
* disable.
* @deprecated Use {@link MyAvatar.setCollisionsEnabled} instead.
* @deprecated This function is deprecated and will be removed. Use {@link MyAvatar.setCollisionsEnabled} instead.
*/
Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated
/**jsdoc
* @function MyAvatar.getCharacterControllerEnabled
* @returns {boolean} <code>true</code> if the avatar will currently collide with entities, <code>false</code> if it won't.
* @deprecated Use {@link MyAvatar.getCollisionsEnabled} instead.
* @deprecated This function is deprecated and will be removed. Use {@link MyAvatar.getCollisionsEnabled} instead.
*/
Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated
@ -1872,7 +1923,7 @@ public slots:
/**jsdoc
* @function MyAvatar.clearScaleRestriction
* @deprecated This function is deprecated and will be removed from the API.
* @deprecated This function is deprecated and will be removed.
*/
void clearScaleRestriction();
@ -1881,7 +1932,8 @@ public slots:
* Adds a thrust to your avatar's current thrust to be applied for a short while.
* @function MyAvatar.addThrust
* @param {Vec3} thrust - The thrust direction and magnitude.
* @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead.
* @deprecated This function is deprecated and will be removed. Use {@link MyAvatar|MyAvatar.motorVelocity} and related
* properties instead.
*/
// Set/Get update the thrust that will move the avatar around
void addThrust(glm::vec3 newThrust) { _thrust += newThrust; };
@ -1890,7 +1942,8 @@ public slots:
* Gets the thrust currently being applied to your avatar.
* @function MyAvatar.getThrust
* @returns {Vec3} The thrust currently being applied to your avatar.
* @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead.
* @deprecated This function is deprecated and will be removed. Use {@link MyAvatar|MyAvatar.motorVelocity} and related
* properties instead.
*/
glm::vec3 getThrust() { return _thrust; };
@ -1898,7 +1951,8 @@ public slots:
* Sets the thrust to be applied to your avatar for a short while.
* @function MyAvatar.setThrust
* @param {Vec3} thrust - The thrust direction and magnitude.
* @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead.
* @deprecated This function is deprecated and will be removed. Use {@link MyAvatar|MyAvatar.motorVelocity} and related
* properties instead.
*/
void setThrust(glm::vec3 newThrust) { _thrust = newThrust; }
@ -2243,7 +2297,7 @@ signals:
* {@link MyAvatar.setAttachmentData|setAttachmentData}.
* @function MyAvatar.attachmentsChanged
* @returns {Signal}
* @deprecated Use avatar entities instead.
* @deprecated This signal is deprecated and will be removed. Use avatar entities instead.
*/
void attachmentsChanged();
@ -2442,6 +2496,7 @@ private:
ThreadSafeValueCache<QUrl> _prefOverrideAnimGraphUrl;
QUrl _fstAnimGraphOverrideUrl;
bool _useSnapTurn { true };
bool _hoverWhenUnsupported{ true };
ThreadSafeValueCache<QString> _dominantHand { DOMINANT_RIGHT_HAND };
ThreadSafeValueCache<QString> _hmdAvatarAlignmentType { DEFAULT_HMD_AVATAR_ALIGNMENT_TYPE };
ThreadSafeValueCache<bool> _strafeEnabled{ DEFAULT_STRAFE_ENABLED };
@ -2637,6 +2692,7 @@ private:
Setting::Handle<QString> _displayNameSetting;
Setting::Handle<QUrl> _collisionSoundURLSetting;
Setting::Handle<bool> _useSnapTurnSetting;
Setting::Handle<bool> _hoverWhenUnsupportedSetting;
Setting::Handle<float> _userHeightSetting;
Setting::Handle<bool> _flyingHMDSetting;
Setting::Handle<int> _movementReferenceSetting;

View file

@ -334,7 +334,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
eyeParams.leftEyeJointIndex = _rig.indexOfJoint("LeftEye");
eyeParams.rightEyeJointIndex = _rig.indexOfJoint("RightEye");
_rig.updateFromEyeParameters(eyeParams);
if (_owningAvatar->getHasProceduralEyeFaceMovement()) {
_rig.updateFromEyeParameters(eyeParams);
}
updateFingers();
}

View file

@ -16,6 +16,7 @@
#include "Application.h"
#include "AvatarMotionState.h"
#include "DetailedMotionState.h"
#include "DebugDraw.h"
const float DISPLAYNAME_FADE_TIME = 0.5f;
const float DISPLAYNAME_FADE_FACTOR = pow(0.01f, 1.0f / DISPLAYNAME_FADE_TIME);
@ -358,6 +359,58 @@ void OtherAvatar::simulate(float deltaTime, bool inView) {
}
}
void OtherAvatar::debugJointData() const {
// Get a copy of the joint data
auto jointData = getJointData();
auto skeletonData = getSkeletonData();
if ((int)skeletonData.size() == jointData.size() && jointData.size() != 0) {
const vec4 RED(1.0f, 0.0f, 0.0f, 1.0f);
const vec4 GREEN(0.0f, 1.0f, 0.0f, 1.0f);
const vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f);
const vec4 LIGHT_RED(1.0f, 0.5f, 0.5f, 1.0f);
const vec4 LIGHT_GREEN(0.5f, 1.0f, 0.5f, 1.0f);
const vec4 LIGHT_BLUE(0.5f, 0.5f, 1.0f, 1.0f);
const vec4 GREY(0.3f, 0.3f, 0.3f, 1.0f);
const vec4 WHITE(1.0f, 1.0f, 1.0f, 1.0f);
const float AXIS_LENGTH = 0.1f;
AnimPoseVec absoluteJointPoses;
AnimPose rigToAvatar = AnimPose(Quaternions::Y_180 * getWorldOrientation(), getWorldPosition());
bool drawBones = false;
for (int i = 0; i < jointData.size(); i++) {
float jointScale = skeletonData[i].defaultScale * getTargetScale() * METERS_PER_CENTIMETER;
auto absoluteRotation = jointData[i].rotationIsDefaultPose ? skeletonData[i].defaultRotation : jointData[i].rotation;
auto localJointTranslation = jointScale * (jointData[i].translationIsDefaultPose ? skeletonData[i].defaultTranslation : jointData[i].translation);
bool isHips = skeletonData[i].jointName == "Hips";
if (isHips) {
localJointTranslation = glm::vec3(0.0f);
drawBones = true;
}
AnimPose absoluteParentPose;
int parentIndex = skeletonData[i].parentIndex;
if (parentIndex != -1 && parentIndex < (int)absoluteJointPoses.size()) {
absoluteParentPose = absoluteJointPoses[parentIndex];
}
AnimPose absoluteJointPose = AnimPose(absoluteRotation, absoluteParentPose.trans() + absoluteParentPose.rot() * localJointTranslation);
auto jointPose = rigToAvatar * absoluteJointPose;
auto parentPose = rigToAvatar * absoluteParentPose;
if (drawBones) {
glm::vec3 xAxis = jointPose.rot() * Vectors::UNIT_X;
glm::vec3 yAxis = jointPose.rot() * Vectors::UNIT_Y;
glm::vec3 zAxis = jointPose.rot() * Vectors::UNIT_Z;
DebugDraw::getInstance().drawRay(jointPose.trans(), jointPose.trans() + AXIS_LENGTH * xAxis, jointData[i].rotationIsDefaultPose ? LIGHT_RED : RED);
DebugDraw::getInstance().drawRay(jointPose.trans(), jointPose.trans() + AXIS_LENGTH * yAxis, jointData[i].rotationIsDefaultPose ? LIGHT_GREEN : GREEN);
DebugDraw::getInstance().drawRay(jointPose.trans(), jointPose.trans() + AXIS_LENGTH * zAxis, jointData[i].rotationIsDefaultPose ? LIGHT_BLUE : BLUE);
if (!isHips) {
DebugDraw::getInstance().drawRay(jointPose.trans(), parentPose.trans(), jointData[i].translationIsDefaultPose ? WHITE : GREY);
}
}
absoluteJointPoses.push_back(absoluteJointPose);
}
}
}
void OtherAvatar::handleChangedAvatarEntityData() {
PerformanceTimer perfTimer("attachments");

View file

@ -66,7 +66,7 @@ public:
void setCollisionWithOtherAvatarsFlags() override;
void simulate(float deltaTime, bool inView) override;
void debugJointData() const;
friend AvatarManager;
protected:

View file

@ -41,307 +41,246 @@
#include "ui/SecurityImageProvider.h"
#include "scripting/HMDScriptingInterface.h"
static const char* KEY_FILE = "hifikey";
static const char* INSTRUCTIONS_FILE = "backup_instructions.html";
static const char* IMAGE_HEADER = "-----BEGIN SECURITY IMAGE-----\n";
static const char* IMAGE_FOOTER = "-----END SECURITY IMAGE-----\n";
namespace {
const char* KEY_FILE = "hifikey";
const char* INSTRUCTIONS_FILE = "backup_instructions.html";
const char* IMAGE_HEADER = "-----BEGIN SECURITY IMAGE-----\n";
const char* IMAGE_FOOTER = "-----END SECURITY IMAGE-----\n";
void initialize() {
static bool initialized = false;
if (!initialized) {
SSL_load_error_strings();
SSL_library_init();
OpenSSL_add_all_algorithms();
initialized = true;
}
}
QString keyFilePath() {
auto accountManager = DependencyManager::get<AccountManager>();
return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE));
}
bool Wallet::copyKeyFileFrom(const QString& pathname) {
QString existing = getKeyFilePath();
qCDebug(commerce) << "Old keyfile" << existing;
if (!existing.isEmpty()) {
QString backup = QString(existing).insert(existing.indexOf(KEY_FILE) - 1,
QDateTime::currentDateTime().toString(Qt::ISODate).replace(":", ""));
qCDebug(commerce) << "Renaming old keyfile to" << backup;
if (!QFile::rename(existing, backup)) {
qCCritical(commerce) << "Unable to backup" << existing << "to" << backup;
return false;
void initialize() {
static bool initialized = false;
if (!initialized) {
SSL_load_error_strings();
SSL_library_init();
OpenSSL_add_all_algorithms();
initialized = true;
}
}
QString destination = keyFilePath();
bool result = QFile::copy(pathname, destination);
qCDebug(commerce) << "copy" << pathname << "to" << destination << "=>" << result;
return result;
}
// use the cached _passphrase if it exists, otherwise we need to prompt
int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) {
// just return a hardcoded pwd for now
auto wallet = DependencyManager::get<Wallet>();
auto passphrase = wallet->getPassphrase();
if (passphrase && !passphrase->isEmpty()) {
QString saltedPassphrase(*passphrase);
saltedPassphrase.append(wallet->getSalt());
strcpy(password, saltedPassphrase.toUtf8().constData());
return static_cast<int>(passphrase->size());
} else {
// this shouldn't happen - so lets log it to tell us we have
// a problem with the flow...
qCCritical(commerce) << "no cached passphrase while decrypting!";
return 0;
QString keyFilePath() {
auto accountManager = DependencyManager::get<AccountManager>();
return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE));
}
}
EC_KEY* readKeys(const char* filename) {
FILE* fp;
EC_KEY *key = NULL;
if ((fp = fopen(filename, "rt"))) {
// file opened successfully
qCDebug(commerce) << "opened key file" << filename;
// use the cached _passphrase if it exists, otherwise we need to prompt
int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) {
// just return a hardcoded pwd for now
auto wallet = DependencyManager::get<Wallet>();
auto passphrase = wallet->getPassphrase();
if (passphrase && !passphrase->isEmpty()) {
QString saltedPassphrase(*passphrase);
saltedPassphrase.append(wallet->getSalt());
strcpy(password, saltedPassphrase.toUtf8().constData());
return static_cast<int>(passphrase->size());
} else {
// this shouldn't happen - so lets log it to tell us we have
// a problem with the flow...
qCCritical(commerce) << "no cached passphrase while decrypting!";
return 0;
}
}
if ((key = PEM_read_EC_PUBKEY(fp, NULL, NULL, NULL))) {
// now read private key
EC_KEY* readKeys(QString filename) {
QFile file(filename);
EC_KEY* key = NULL;
if (file.open(QFile::ReadOnly)) {
// file opened successfully
qCDebug(commerce) << "opened key file" << filename;
qCDebug(commerce) << "read public key";
QByteArray pemKeyBytes = file.readAll();
BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length());
if ((key = PEM_read_bio_EC_PUBKEY(bufio, NULL, NULL, NULL))) {
// now read private key
if ((key = PEM_read_ECPrivateKey(fp, &key, passwordCallback, NULL))) {
qCDebug(commerce) << "read private key";
fclose(fp);
return key;
qCDebug(commerce) << "read public key";
if ((key = PEM_read_bio_ECPrivateKey(bufio, &key, passwordCallback, NULL))) {
qCDebug(commerce) << "read private key";
} else {
qCDebug(commerce) << "failed to read private key";
}
} else {
qCDebug(commerce) << "failed to read public key";
}
qCDebug(commerce) << "failed to read private key";
BIO_free(bufio);
file.close();
} else {
qCDebug(commerce) << "failed to read public key";
qCDebug(commerce) << "failed to open key file" << filename;
}
fclose(fp);
} else {
qCDebug(commerce) << "failed to open key file" << filename;
}
return key;
}
bool Wallet::writeBackupInstructions() {
QString inputFilename(PathUtils::resourcesPath() + "html/commerce/backup_instructions.html");
QString outputFilename = PathUtils::getAppDataFilePath(INSTRUCTIONS_FILE);
QFile inputFile(inputFilename);
QFile outputFile(outputFilename);
bool retval = false;
if (getKeyFilePath().isEmpty())
{
return false;
return key;
}
if (QFile::exists(inputFilename) && inputFile.open(QIODevice::ReadOnly)) {
if (outputFile.open(QIODevice::ReadWrite)) {
// Read the data from the original file, then close it
QByteArray fileData = inputFile.readAll();
inputFile.close();
// Translate the data from the original file into a QString
QString text(fileData);
// Replace the necessary string
text.replace(QString("HIFIKEY_PATH_REPLACEME"), keyFilePath());
// Write the new text back to the file
outputFile.write(text.toUtf8());
// Close the output file
outputFile.close();
retval = true;
qCDebug(commerce) << "wrote html file successfully";
} else {
qCDebug(commerce) << "failed to open output html file" << outputFilename;
}
} else {
qCDebug(commerce) << "failed to open input html file" << inputFilename;
}
return retval;
}
bool writeKeys(const char* filename, EC_KEY* keys) {
FILE* fp;
bool retval = false;
if ((fp = fopen(filename, "wt"))) {
if (!PEM_write_EC_PUBKEY(fp, keys)) {
fclose(fp);
bool writeKeys(QString filename, EC_KEY* keys) {
BIO* bio = BIO_new(BIO_s_mem());
bool retval = false;
if (!PEM_write_bio_EC_PUBKEY(bio, keys)) {
BIO_free(bio);
qCCritical(commerce) << "failed to write public key";
return retval;
}
if (!PEM_write_ECPrivateKey(fp, keys, EVP_des_ede3_cbc(), NULL, 0, passwordCallback, NULL)) {
fclose(fp);
if (!PEM_write_bio_ECPrivateKey(bio, keys, EVP_des_ede3_cbc(), NULL, 0, passwordCallback, NULL)) {
BIO_free(bio);
qCCritical(commerce) << "failed to write private key";
return retval;
}
retval = true;
qCDebug(commerce) << "wrote keys successfully";
fclose(fp);
} else {
qCDebug(commerce) << "failed to open key file" << filename;
}
return retval;
}
QFile file(filename);
if (file.open(QIODevice::WriteOnly)) {
const char* bio_data;
long bio_size = BIO_get_mem_data(bio, &bio_data);
bool Wallet::setWallet(const QByteArray& wallet) {
QFile file(keyFilePath());
if (!file.open(QIODevice::WriteOnly)) {
qCCritical(commerce) << "Unable to open wallet for write in" << keyFilePath();
return false;
}
if (file.write(wallet) != wallet.count()) {
qCCritical(commerce) << "Unable to write wallet in" << keyFilePath();
return false;
}
file.close();
return true;
}
QByteArray Wallet::getWallet() {
QFile file(keyFilePath());
if (!file.open(QIODevice::ReadOnly)) {
qCInfo(commerce) << "No existing wallet in" << keyFilePath();
return QByteArray();
}
QByteArray wallet = file.readAll();
file.close();
return wallet;
}
QPair<QByteArray*, QByteArray*> generateECKeypair() {
EC_KEY* keyPair = EC_KEY_new_by_curve_name(NID_secp256k1);
QPair<QByteArray*, QByteArray*> retval{};
EC_KEY_set_asn1_flag(keyPair, OPENSSL_EC_NAMED_CURVE);
if (!EC_KEY_generate_key(keyPair)) {
qCDebug(commerce) << "Error generating EC Keypair -" << ERR_get_error();
QByteArray keyBytes(bio_data, bio_size);
file.write(keyBytes);
retval = true;
qCDebug(commerce) << "wrote keys successfully";
file.close();
} else {
qCDebug(commerce) << "failed to open key file" << filename;
}
BIO_free(bio);
return retval;
}
// grab the public key and private key from the file
unsigned char* publicKeyDER = NULL;
int publicKeyLength = i2d_EC_PUBKEY(keyPair, &publicKeyDER);
QPair<QByteArray*, QByteArray*> generateECKeypair() {
EC_KEY* keyPair = EC_KEY_new_by_curve_name(NID_secp256k1);
QPair<QByteArray*, QByteArray*> retval {};
unsigned char* privateKeyDER = NULL;
int privateKeyLength = i2d_ECPrivateKey(keyPair, &privateKeyDER);
EC_KEY_set_asn1_flag(keyPair, OPENSSL_EC_NAMED_CURVE);
if (!EC_KEY_generate_key(keyPair)) {
qCDebug(commerce) << "Error generating EC Keypair -" << ERR_get_error();
return retval;
}
if (publicKeyLength <= 0 || privateKeyLength <= 0) {
qCDebug(commerce) << "Error getting DER public or private key from EC struct -" << ERR_get_error();
// grab the public key and private key from the file
unsigned char* publicKeyDER = NULL;
int publicKeyLength = i2d_EC_PUBKEY(keyPair, &publicKeyDER);
unsigned char* privateKeyDER = NULL;
int privateKeyLength = i2d_ECPrivateKey(keyPair, &privateKeyDER);
if (publicKeyLength <= 0 || privateKeyLength <= 0) {
qCDebug(commerce) << "Error getting DER public or private key from EC struct -" << ERR_get_error();
// cleanup the EC struct
EC_KEY_free(keyPair);
// cleanup the public and private key DER data, if required
if (publicKeyLength > 0) {
OPENSSL_free(publicKeyDER);
}
if (privateKeyLength > 0) {
OPENSSL_free(privateKeyDER);
}
return retval;
}
if (!writeKeys(keyFilePath(), keyPair)) {
qCDebug(commerce) << "couldn't save keys!";
return retval;
}
// cleanup the EC struct
EC_KEY_free(keyPair);
// cleanup the public and private key DER data, if required
if (publicKeyLength > 0) {
OPENSSL_free(publicKeyDER);
}
if (privateKeyLength > 0) {
OPENSSL_free(privateKeyDER);
}
// prepare the return values. TODO: Fix this - we probably don't really even want the
// private key at all (better to read it when we need it?). Or maybe we do, when we have
// multiple keys?
retval.first = new QByteArray(reinterpret_cast<char*>(publicKeyDER), publicKeyLength);
retval.second = new QByteArray(reinterpret_cast<char*>(privateKeyDER), privateKeyLength);
// cleanup the publicKeyDER and publicKeyDER data
OPENSSL_free(publicKeyDER);
OPENSSL_free(privateKeyDER);
return retval;
}
// END copied code (which will soon change)
// the public key can just go into a byte array
QByteArray readPublicKey(QString filename) {
QByteArray retval;
QFile file(filename);
if (file.open(QIODevice::ReadOnly)) {
// file opened successfully
qCDebug(commerce) << "opened key file" << filename;
if (!writeKeys(keyFilePath().toStdString().c_str(), keyPair)) {
qCDebug(commerce) << "couldn't save keys!";
return retval;
}
QByteArray pemKeyBytes = file.readAll();
BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length());
EC_KEY_free(keyPair);
EC_KEY* key = PEM_read_bio_EC_PUBKEY(bufio, NULL, NULL, NULL);
if (key) {
// file read successfully
unsigned char* publicKeyDER = NULL;
int publicKeyLength = i2d_EC_PUBKEY(key, &publicKeyDER);
// TODO: check for 0 length?
// prepare the return values. TODO: Fix this - we probably don't really even want the
// private key at all (better to read it when we need it?). Or maybe we do, when we have
// multiple keys?
retval.first = new QByteArray(reinterpret_cast<char*>(publicKeyDER), publicKeyLength);
retval.second = new QByteArray(reinterpret_cast<char*>(privateKeyDER), privateKeyLength);
// cleanup
EC_KEY_free(key);
// cleanup the publicKeyDER and publicKeyDER data
OPENSSL_free(publicKeyDER);
OPENSSL_free(privateKeyDER);
return retval;
}
// END copied code (which will soon change)
qCDebug(commerce) << "parsed public key file successfully";
// the public key can just go into a byte array
QByteArray readPublicKey(const char* filename) {
FILE* fp;
EC_KEY* key = NULL;
if ((fp = fopen(filename, "r"))) {
// file opened successfully
qCDebug(commerce) << "opened key file" << filename;
if ((key = PEM_read_EC_PUBKEY(fp, NULL, NULL, NULL))) {
// file read successfully
unsigned char* publicKeyDER = NULL;
int publicKeyLength = i2d_EC_PUBKEY(key, &publicKeyDER);
// TODO: check for 0 length?
// cleanup
EC_KEY_free(key);
fclose(fp);
qCDebug(commerce) << "parsed public key file successfully";
QByteArray retval((char*)publicKeyDER, publicKeyLength);
OPENSSL_free(publicKeyDER);
return retval;
QByteArray retval((char*)publicKeyDER, publicKeyLength);
OPENSSL_free(publicKeyDER);
BIO_free(bufio);
file.close();
return retval;
} else {
qCDebug(commerce) << "couldn't parse" << filename;
}
BIO_free(bufio);
file.close();
} else {
qCDebug(commerce) << "couldn't parse" << filename;
qCDebug(commerce) << "couldn't open" << filename;
}
fclose(fp);
} else {
qCDebug(commerce) << "couldn't open" << filename;
return QByteArray();
}
return QByteArray();
}
// the private key should be read/copied into heap memory. For now, we need the EC_KEY struct
// so I'll return that.
EC_KEY* readPrivateKey(const char* filename) {
FILE* fp;
EC_KEY* key = NULL;
if ((fp = fopen(filename, "r"))) {
// file opened successfully
qCDebug(commerce) << "opened key file" << filename;
if ((key = PEM_read_ECPrivateKey(fp, &key, passwordCallback, NULL))) {
qCDebug(commerce) << "parsed private key file successfully";
// the private key should be read/copied into heap memory. For now, we need the EC_KEY struct
// so I'll return that.
EC_KEY* readPrivateKey(QString filename) {
QFile file(filename);
EC_KEY* key = NULL;
if (file.open(QIODevice::ReadOnly)) {
// file opened successfully
qCDebug(commerce) << "opened key file" << filename;
QByteArray pemKeyBytes = file.readAll();
BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length());
if ((key = PEM_read_bio_ECPrivateKey(bufio, &key, passwordCallback, NULL))) {
qCDebug(commerce) << "parsed private key file successfully";
} else {
qCDebug(commerce) << "couldn't parse" << filename;
// if the passphrase is wrong, then let's not cache it
DependencyManager::get<Wallet>()->setPassphrase("");
}
BIO_free(bufio);
file.close();
} else {
qCDebug(commerce) << "couldn't parse" << filename;
// if the passphrase is wrong, then let's not cache it
DependencyManager::get<Wallet>()->setPassphrase("");
qCDebug(commerce) << "couldn't open" << filename;
}
fclose(fp);
} else {
qCDebug(commerce) << "couldn't open" << filename;
return key;
}
return key;
}
// QT's QByteArray will convert to Base64 without any embedded newlines. This just
// writes it with embedded newlines, which is more readable.
void outputBase64WithNewlines(QFile& file, const QByteArray& b64Array) {
for (int i = 0; i < b64Array.size(); i += 64) {
file.write(b64Array.mid(i, 64));
file.write("\n");
// QT's QByteArray will convert to Base64 without any embedded newlines. This just
// writes it with embedded newlines, which is more readable.
void outputBase64WithNewlines(QFile& file, const QByteArray& b64Array) {
for (int i = 0; i < b64Array.size(); i += 64) {
file.write(b64Array.mid(i, 64));
file.write("\n");
}
}
}
void initializeAESKeys(unsigned char* ivec, unsigned char* ckey, const QByteArray& salt) {
// use the ones in the wallet
auto wallet = DependencyManager::get<Wallet>();
memcpy(ivec, wallet->getIv(), 16);
memcpy(ckey, wallet->getCKey(), 32);
}
void initializeAESKeys(unsigned char* ivec, unsigned char* ckey, const QByteArray& salt) {
// use the ones in the wallet
auto wallet = DependencyManager::get<Wallet>();
memcpy(ivec, wallet->getIv(), 16);
memcpy(ckey, wallet->getCKey(), 32);
}
} // close unnamed namespace
Wallet::Wallet() {
auto nodeList = DependencyManager::get<NodeList>();
@ -361,7 +300,7 @@ Wallet::Wallet() {
if (wallet->getKeyFilePath().isEmpty() || !wallet->getSecurityImage()) {
if (keyStatus == "preexisting") {
status = (uint) WalletStatus::WALLET_STATUS_PREEXISTING;
} else{
} else {
status = (uint) WalletStatus::WALLET_STATUS_NOT_SET_UP;
}
} else if (!wallet->walletIsAuthenticatedWithPassphrase()) {
@ -371,7 +310,6 @@ Wallet::Wallet() {
} else {
status = (uint) WalletStatus::WALLET_STATUS_READY;
}
walletScriptingInterface->setWalletStatus(status);
});
@ -405,6 +343,88 @@ Wallet::~Wallet() {
}
}
bool Wallet::setWallet(const QByteArray& wallet) {
QFile file(keyFilePath());
if (!file.open(QIODevice::WriteOnly)) {
qCCritical(commerce) << "Unable to open wallet for write in" << keyFilePath();
return false;
}
if (file.write(wallet) != wallet.count()) {
qCCritical(commerce) << "Unable to write wallet in" << keyFilePath();
return false;
}
file.close();
return true;
}
QByteArray Wallet::getWallet() {
QFile file(keyFilePath());
if (!file.open(QIODevice::ReadOnly)) {
qCInfo(commerce) << "No existing wallet in" << keyFilePath();
return QByteArray();
}
QByteArray wallet = file.readAll();
file.close();
return wallet;
}
bool Wallet::copyKeyFileFrom(const QString& pathname) {
QString existing = getKeyFilePath();
qCDebug(commerce) << "Old keyfile" << existing;
if (!existing.isEmpty()) {
QString backup = QString(existing).insert(existing.indexOf(KEY_FILE) - 1,
QDateTime::currentDateTime().toString(Qt::ISODate).replace(":", ""));
qCDebug(commerce) << "Renaming old keyfile to" << backup;
if (!QFile::rename(existing, backup)) {
qCCritical(commerce) << "Unable to backup" << existing << "to" << backup;
return false;
}
}
QString destination = keyFilePath();
bool result = QFile::copy(pathname, destination);
qCDebug(commerce) << "copy" << pathname << "to" << destination << "=>" << result;
return result;
}
bool Wallet::writeBackupInstructions() {
QString inputFilename(PathUtils::resourcesPath() + "html/commerce/backup_instructions.html");
QString outputFilename = PathUtils::getAppDataFilePath(INSTRUCTIONS_FILE);
QFile inputFile(inputFilename);
QFile outputFile(outputFilename);
bool retval = false;
if (getKeyFilePath().isEmpty()) {
return false;
}
if (QFile::exists(inputFilename) && inputFile.open(QIODevice::ReadOnly)) {
if (outputFile.open(QIODevice::ReadWrite)) {
// Read the data from the original file, then close it
QByteArray fileData = inputFile.readAll();
inputFile.close();
// Translate the data from the original file into a QString
QString text(fileData);
// Replace the necessary string
text.replace(QString("HIFIKEY_PATH_REPLACEME"), keyFilePath());
// Write the new text back to the file
outputFile.write(text.toUtf8());
// Close the output file
outputFile.close();
retval = true;
qCDebug(commerce) << "wrote html file successfully";
} else {
qCDebug(commerce) << "failed to open output html file" << outputFilename;
}
} else {
qCDebug(commerce) << "failed to open input html file" << inputFilename;
}
return retval;
}
bool Wallet::setPassphrase(const QString& passphrase) {
if (_passphrase) {
delete _passphrase;
@ -569,10 +589,10 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() {
}
// otherwise, we have a passphrase but no keys, so we have to check
auto publicKey = readPublicKey(keyFilePath().toStdString().c_str());
auto publicKey = readPublicKey(keyFilePath());
if (publicKey.size() > 0) {
if (auto key = readPrivateKey(keyFilePath().toStdString().c_str())) {
if (auto key = readPrivateKey(keyFilePath())) {
EC_KEY_free(key);
// be sure to add the public key so we don't do this over and over
@ -631,8 +651,7 @@ QStringList Wallet::listPublicKeys() {
QString Wallet::signWithKey(const QByteArray& text, const QString& key) {
EC_KEY* ecPrivateKey = NULL;
auto keyFilePathString = keyFilePath().toStdString();
if ((ecPrivateKey = readPrivateKey(keyFilePath().toStdString().c_str()))) {
if ((ecPrivateKey = readPrivateKey(keyFilePath()))) {
unsigned char* sig = new unsigned char[ECDSA_size(ecPrivateKey)];
unsigned int signatureBytes = 0;
@ -641,12 +660,8 @@ QString Wallet::signWithKey(const QByteArray& text, const QString& key) {
QByteArray hashedPlaintext = QCryptographicHash::hash(text, QCryptographicHash::Sha256);
int retrn = ECDSA_sign(0,
reinterpret_cast<const unsigned char*>(hashedPlaintext.constData()),
hashedPlaintext.size(),
sig,
&signatureBytes, ecPrivateKey);
int retrn = ECDSA_sign(0, reinterpret_cast<const unsigned char*>(hashedPlaintext.constData()), hashedPlaintext.size(),
sig, &signatureBytes, ecPrivateKey);
EC_KEY_free(ecPrivateKey);
QByteArray signature(reinterpret_cast<const char*>(sig), signatureBytes);
@ -682,7 +697,6 @@ void Wallet::updateImageProvider() {
}
void Wallet::chooseSecurityImage(const QString& filename) {
if (_securityImage) {
delete _securityImage;
}
@ -754,7 +768,7 @@ QString Wallet::getKeyFilePath() {
}
bool Wallet::writeWallet(const QString& newPassphrase) {
EC_KEY* keys = readKeys(keyFilePath().toStdString().c_str());
EC_KEY* keys = readKeys(keyFilePath());
auto ledger = DependencyManager::get<Ledger>();
// Remove any existing locker, because it will be out of date.
if (!_publicKeys.isEmpty() && !ledger->receiveAt(_publicKeys.first(), _publicKeys.first(), QByteArray())) {
@ -768,7 +782,7 @@ bool Wallet::writeWallet(const QString& newPassphrase) {
setPassphrase(newPassphrase);
}
if (writeKeys(tempFileName.toStdString().c_str(), keys)) {
if (writeKeys(tempFileName, keys)) {
if (writeSecurityImage(_securityImage, tempFileName)) {
// ok, now move the temp file to the correct spot
QFile(QString(keyFilePath())).remove();
@ -834,10 +848,10 @@ void Wallet::handleChallengeOwnershipPacket(QSharedPointer<ReceivedMessage> pack
challengingNodeUUID = packet->read(challengingNodeUUIDByteArraySize);
}
EC_KEY* ec = readKeys(keyFilePath().toStdString().c_str());
EC_KEY* ec = readKeys(keyFilePath());
QString sig;
if (ec) {
if (ec) {
ERR_clear_error();
sig = signWithKey(text, ""); // base64 signature, QByteArray cast (on return) to QString FIXME should pass ec as string so we can tell which key to sign with
status = 1;

View file

@ -244,6 +244,7 @@ void GraphicsEngine::render_performFrame() {
finalFramebuffer = framebufferCache->getFramebuffer();
}
std::queue<Application::SnapshotOperator> snapshotOperators;
if (!_programsCompiled.load()) {
gpu::doInBatch("splashFrame", _gpuContext, [&](gpu::Batch& batch) {
batch.setFramebuffer(finalFramebuffer);
@ -271,6 +272,7 @@ void GraphicsEngine::render_performFrame() {
PROFILE_RANGE(render, "/runRenderFrame");
renderArgs._hudOperator = displayPlugin->getHUDOperator();
renderArgs._hudTexture = qApp->getApplicationOverlay().getOverlayTexture();
renderArgs._takingSnapshot = qApp->takeSnapshotOperators(snapshotOperators);
renderArgs._blitFramebuffer = finalFramebuffer;
render_runRenderFrame(&renderArgs);
}
@ -285,6 +287,7 @@ void GraphicsEngine::render_performFrame() {
frameBufferCache->releaseFramebuffer(framebuffer);
}
};
frame->snapshotOperators = snapshotOperators;
// deliver final scene rendering commands to the display plugin
{
PROFILE_RANGE(render, "/pluginOutput");

View file

@ -233,16 +233,19 @@ PointerEvent LaserPointer::buildPointerEvent(const PickedObject& target, const P
// If we just started triggering and we haven't moved too much, don't update intersection and pos2D
TriggerState& state = hover ? _latestState : _states[button];
float sensorToWorldScale = DependencyManager::get<AvatarManager>()->getMyAvatar()->getSensorToWorldScale();
float deadspotSquared = TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED * sensorToWorldScale * sensorToWorldScale;
bool withinDeadspot = usecTimestampNow() - state.triggerStartTime < POINTER_MOVE_DELAY && glm::distance2(pos2D, state.triggerPos2D) < deadspotSquared;
if ((state.triggering || state.wasTriggering) && !state.deadspotExpired && withinDeadspot) {
pos2D = state.triggerPos2D;
intersection = state.intersection;
surfaceNormal = state.surfaceNormal;
}
if (!withinDeadspot) {
state.deadspotExpired = true;
auto avatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
if (avatar) {
float sensorToWorldScale = avatar->getSensorToWorldScale();
float deadspotSquared = TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED * sensorToWorldScale * sensorToWorldScale;
bool withinDeadspot = usecTimestampNow() - state.triggerStartTime < POINTER_MOVE_DELAY && glm::distance2(pos2D, state.triggerPos2D) < deadspotSquared;
if ((state.triggering || state.wasTriggering) && !state.deadspotExpired && withinDeadspot) {
pos2D = state.triggerPos2D;
intersection = state.intersection;
surfaceNormal = state.surfaceNormal;
}
if (!withinDeadspot) {
state.deadspotExpired = true;
}
}
return PointerEvent(pos2D, intersection, surfaceNormal, direction);

View file

@ -174,14 +174,10 @@ void Audio::setPTTDesktop(bool enabled) {
_pttDesktop = enabled;
}
});
if (!enabled) {
// Set to default behavior (unmuted for Desktop) on Push-To-Talk disable.
setMutedDesktop(true);
} else {
// Should be muted when not pushing to talk while PTT is enabled.
if (enabled || _settingsLoaded) {
// Set to default behavior (muted for Desktop) on Push-To-Talk disable or when enabled. Settings also need to be loaded.
setMutedDesktop(true);
}
if (changed) {
emit pushToTalkChanged(enabled);
emit pushToTalkDesktopChanged(enabled);
@ -202,12 +198,9 @@ void Audio::setPTTHMD(bool enabled) {
_pttHMD = enabled;
}
});
if (!enabled) {
// Set to default behavior (unmuted for HMD) on Push-To-Talk disable.
setMutedHMD(false);
} else {
// Should be muted when not pushing to talk while PTT is enabled.
setMutedHMD(true);
if (enabled || _settingsLoaded) {
// Set to default behavior (unmuted for HMD) on Push-To-Talk disable or muted for when PTT is enabled.
setMutedHMD(enabled);
}
if (changed) {
@ -231,6 +224,7 @@ void Audio::loadData() {
auto client = DependencyManager::get<AudioClient>().data();
QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false));
_settingsLoaded = true;
}
bool Audio::getPTTHMD() const {
@ -357,10 +351,12 @@ void Audio::onContextChanged() {
changed = true;
}
});
if (isHMD) {
setMuted(getMutedHMD());
} else {
setMuted(getMutedDesktop());
if (_settingsLoaded) {
bool isMuted = isHMD ? getMutedHMD() : getMutedDesktop();
setMuted(isMuted);
// always set audio client muted state on context changed - sometimes setMuted does not catch it.
auto client = DependencyManager::get<AudioClient>().data();
QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false));
}
if (changed) {
emit contextChanged(isHMD ? Audio::HMD : Audio::DESKTOP);

View file

@ -40,25 +40,40 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable {
* @hifi-server-entity
* @hifi-assignment-client
*
* @property {boolean} muted - <code>true</code> if the audio input is muted, otherwise <code>false</code>.
* @property {boolean} mutedDesktop - <code>true</code> if the audio input is muted, otherwise <code>false</code>.
* @property {boolean} muted - <code>true</code> if the audio input is muted for the current user context (desktop or HMD),
* otherwise <code>false</code>.
* @property {boolean} mutedDesktop - <code>true</code> if desktop audio input is muted, otherwise <code>false</code>.
* @property {boolean} mutedHMD - <code>true</code> if the HMD input is muted, otherwise <code>false</code>.
* @property {boolean} warnWhenMuted - <code>true</code> if the "muted" warning is enabled, otherwise <code>false</code>.
* When enabled, if you speak while your microphone is muted, "muted" is displayed on the screen as a warning.
* @property {boolean} noiseReduction - <code>true</code> if noise reduction is enabled, otherwise <code>false</code>. When
* enabled, the input audio signal is blocked (fully attenuated) when it falls below an adaptive threshold set just
* above the noise floor.
* @property {number} inputVolume - Adjusts the volume of the input audio, range <code>0.0</code> &ndash; <code>1.0</code>.
* If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some
* devices, and others might only support values of <code>0.0</code> and <code>1.0</code>.
* @property {number} inputLevel - The loudness of the audio input, range <code>0.0</code> (no sound) &ndash;
* <code>1.0</code> (the onset of clipping). <em>Read-only.</em>
* @property {boolean} clipping - <code>true</code> if the audio input is clipping, otherwise <code>false</code>.
* @property {number} inputVolume - Adjusts the volume of the input audio; range <code>0.0</code> &ndash; <code>1.0</code>.
* If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some
* devices, and others might only support values of <code>0.0</code> and <code>1.0</code>.
* @property {boolean} isStereoInput - <code>true</code> if the input audio is being used in stereo, otherwise
* <code>false</code>. Some devices do not support stereo, in which case the value is always <code>false</code>.
* @property {string} context - The current context of the audio: either <code>"Desktop"</code> or <code>"HMD"</code>.
* <em>Read-only.</em>
* @property {object} devices <em>Read-only.</em> <strong>Deprecated:</strong> This property is deprecated and will be
* removed.
* @property {boolean} isSoloing <em>Read-only.</em> <code>true</code> if any nodes are soloed.
* @property {Uuid[]} soloList <em>Read-only.</em> Get the list of currently soloed node UUIDs.
* @property {object} devices - <em>Read-only.</em>
* <p class="important">Deprecated: This property is deprecated and will be removed.
* @property {boolean} pushToTalk - <code>true</code> if push-to-talk is enabled for the current user context (desktop or
* HMD), otherwise <code>false</code>.
* @property {boolean} pushToTalkDesktop - <code>true</code> if desktop push-to-talk is enabled, otherwise
* <code>false</code>.
* @property {boolean} pushToTalkHMD - <code>true</code> if HMD push-to-talk is enabled, otherwise <code>false</code>.
* @property {boolean} pushingToTalk - <code>true</code> if the user is currently pushing-to-talk, otherwise
* <code>false</code>.
*
* @comment The following properties are from AudioScriptingInterface.h.
* @property {boolean} isStereoInput - <code>true</code> if the input audio is being used in stereo, otherwise
* <code>false</code>. Some devices do not support stereo, in which case the value is always <code>false</code>.
* @property {boolean} isSoloing - <code>true</code> if currently audio soloing, i.e., playing audio from only specific
* avatars. <em>Read-only.</em>
* @property {Uuid[]} soloList - The list of currently soloed avatar IDs. Empty list if not currently audio soloing.
* <em>Read-only.</em>
*/
Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged)
@ -117,23 +132,23 @@ public:
/**jsdoc
* @function Audio.setInputDevice
* @param {object} device
* @param {boolean} isHMD
* @param {object} device - Device.
* @param {boolean} isHMD - Is HMD.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device, bool isHMD);
/**jsdoc
* @function Audio.setOutputDevice
* @param {object} device
* @param {boolean} isHMD
* @param {object} device - Device.
* @param {boolean} isHMD - Is HMD.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device, bool isHMD);
/**jsdoc
* Enable or disable reverberation. Reverberation is done by the client, on the post-mix audio. The reverberation options
* come from either the domain's audio zone if used &mdash; configured on the server &mdash; or as scripted by
* Enables or disables reverberation. Reverberation is done by the client on the post-mix audio. The reverberation options
* come from either the domain's audio zone configured on the server or settings scripted by
* {@link Audio.setReverbOptions|setReverbOptions}.
* @function Audio.setReverb
* @param {boolean} enable - <code>true</code> to enable reverberation, <code>false</code> to disable.
@ -165,69 +180,71 @@ public:
Q_INVOKABLE void setReverb(bool enable);
/**jsdoc
* Configure reverberation options. Use {@link Audio.setReverb|setReverb} to enable or disable reverberation.
* Configures reverberation options. Use {@link Audio.setReverb|setReverb} to enable or disable reverberation.
* @function Audio.setReverbOptions
* @param {AudioEffectOptions} options - The reverberation options.
*/
Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options);
/**jsdoc
* Sets the avatar gain at the server.
* Units are Decibels (dB)
* Sets the gain (relative volume) that avatars' voices are played at. This gain is used at the server.
* @function Audio.setAvatarGain
* @param {number} gain (in dB)
*/
* @param {number} gain - Avatar gain (dB) at the server.
*/
Q_INVOKABLE void setAvatarGain(float gain);
/**jsdoc
* Gets the avatar gain at the server.
* Gets the gain (relative volume) that avatars' voices are played at. This gain is used at the server.
* @function Audio.getAvatarGain
* @returns {number} gain (in dB)
*/
* @returns {number} Avatar gain (dB) at the server.
* @example <caption>Report current audio gain settings.</caption>
* // 0 value = normal volume; -ve value = quieter; +ve value = louder.
* print("Avatar gain: " + Audio.getAvatarGain());
* print("Environment server gain: " + Audio.getInjectorGain());
* print("Environment local gain: " + Audio.getLocalInjectorGain());
* print("System gain: " + Audio.getSystemInjectorGain());
*/
Q_INVOKABLE float getAvatarGain();
/**jsdoc
* Sets the injector gain at the server.
* Units are Decibels (dB)
* Sets the gain (relative volume) that environment sounds from the server are played at.
* @function Audio.setInjectorGain
* @param {number} gain (in dB)
*/
* @param {number} gain - Injector gain (dB) at the server.
*/
Q_INVOKABLE void setInjectorGain(float gain);
/**jsdoc
* Gets the injector gain at the server.
* Gets the gain (relative volume) that environment sounds from the server are played at.
* @function Audio.getInjectorGain
* @returns {number} gain (in dB)
*/
* @returns {number} Injector gain (dB) at the server.
*/
Q_INVOKABLE float getInjectorGain();
/**jsdoc
* Sets the local injector gain in the client.
* Units are Decibels (dB)
* Sets the gain (relative volume) that environment sounds from the client are played at.
* @function Audio.setLocalInjectorGain
* @param {number} gain (in dB)
*/
* @param {number} gain - Injector gain (dB) in the client.
*/
Q_INVOKABLE void setLocalInjectorGain(float gain);
/**jsdoc
* Gets the local injector gain in the client.
* Gets the gain (relative volume) that environment sounds from the client are played at.
* @function Audio.getLocalInjectorGain
* @returns {number} gain (in dB)
*/
* @returns {number} Injector gain (dB) in the client.
*/
Q_INVOKABLE float getLocalInjectorGain();
/**jsdoc
* Sets the injector gain for system sounds.
* Units are Decibels (dB)
* Sets the gain (relative volume) that system sounds are played at.
* @function Audio.setSystemInjectorGain
* @param {number} gain (in dB)
*/
* @param {number} gain - Injector gain (dB) in the client.
*/
Q_INVOKABLE void setSystemInjectorGain(float gain);
/**jsdoc
* Gets the injector gain for system sounds.
* Gets the gain (relative volume) that system sounds are played at.
* @function Audio.getSystemInjectorGain
* @returns {number} gain (in dB)
* @returns {number} Injector gain (dB) in the client.
*/
Q_INVOKABLE float getSystemInjectorGain();
@ -253,13 +270,13 @@ public:
Q_INVOKABLE bool startRecording(const QString& filename);
/**jsdoc
* Finish making an audio recording started with {@link Audio.startRecording|startRecording}.
* Finishes making an audio recording started with {@link Audio.startRecording|startRecording}.
* @function Audio.stopRecording
*/
Q_INVOKABLE void stopRecording();
/**jsdoc
* Check whether an audio recording is currently being made.
* Checks whether an audio recording is currently being made.
* @function Audio.getRecording
* @returns {boolean} <code>true</code> if an audio recording is currently being made, otherwise <code>false</code>.
*/
@ -275,9 +292,10 @@ signals:
void nop();
/**jsdoc
* Triggered when the audio input is muted or unmuted.
* Triggered when the audio input is muted or unmuted for the current context (desktop or HMD).
* @function Audio.mutedChanged
* @param {boolean} isMuted - <code>true</code> if the audio input is muted, otherwise <code>false</code>.
* @param {boolean} isMuted - <code>true</code> if the audio input is muted for the current context (desktop or HMD),
* otherwise <code>false</code>.
* @returns {Signal}
* @example <caption>Report when audio input is muted or unmuted</caption>
* Audio.mutedChanged.connect(function (isMuted) {
@ -287,47 +305,55 @@ signals:
void mutedChanged(bool isMuted);
/**jsdoc
* Triggered when desktop audio input is muted or unmuted.
* @function Audio.mutedDesktopChanged
* @param {boolean} isMuted - <code>true</code> if the audio input is muted for desktop mode, otherwise <code>false</code>.
* @returns {Signal}
*/
* Triggered when desktop audio input is muted or unmuted.
* @function Audio.mutedDesektopChanged
* @param {boolean} isMuted - <code>true</code> if desktop audio input is muted, otherwise <code>false</code>.
* @returns {Signal}
* @example <caption>Report when desktop muting changes.</caption>
* Audio.mutedDesktopChanged.connect(function (isMuted) {
* print("Desktop muted: " + isMuted);
* });
*/
void mutedDesktopChanged(bool isMuted);
/**jsdoc
* Triggered when HMD audio input is muted or unmuted.
* @function Audio.mutedHMDChanged
* @param {boolean} isMuted - <code>true</code> if the audio input is muted for HMD mode, otherwise <code>false</code>.
* @returns {Signal}
*/
* Triggered when HMD audio input is muted or unmuted.
* @function Audio.mutedHMDChanged
* @param {boolean} isMuted - <code>true</code> if HMD audio input is muted, otherwise <code>false</code>.
* @returns {Signal}
*/
void mutedHMDChanged(bool isMuted);
/**
* Triggered when Push-to-Talk has been enabled or disabled.
* @function Audio.pushToTalkChanged
* @param {boolean} enabled - <code>true</code> if Push-to-Talk is enabled, otherwise <code>false</code>.
* @returns {Signal}
*/
/**jsdoc
* Triggered when push-to-talk is enabled or disabled for the current context (desktop or HMD).
* @function Audio.pushToTalkChanged
* @param {boolean} enabled - <code>true</code> if push-to-talk is enabled, otherwise <code>false</code>.
* @returns {Signal}
* @example <caption>Report when push-to-talk changes.</caption>
* Audio.pushToTalkChanged.connect(function (enabled) {
* print("Push to talk: " + (enabled ? "on" : "off"));
* });
*/
void pushToTalkChanged(bool enabled);
/**
* Triggered when Push-to-Talk has been enabled or disabled for desktop mode.
* @function Audio.pushToTalkDesktopChanged
* @param {boolean} enabled - <code>true</code> if Push-to-Talk is emabled for Desktop mode, otherwise <code>false</code>.
* @returns {Signal}
*/
/**jsdoc
* Triggered when push-to-talk is enabled or disabled for desktop mode.
* @function Audio.pushToTalkDesktopChanged
* @param {boolean} enabled - <code>true</code> if push-to-talk is enabled for desktop mode, otherwise <code>false</code>.
* @returns {Signal}
*/
void pushToTalkDesktopChanged(bool enabled);
/**
* Triggered when Push-to-Talk has been enabled or disabled for HMD mode.
* @function Audio.pushToTalkHMDChanged
* @param {boolean} enabled - <code>true</code> if Push-to-Talk is emabled for HMD mode, otherwise <code>false</code>.
* @returns {Signal}
*/
/**jsdoc
* Triggered when push-to-talk is enabled or disabled for HMD mode.
* @function Audio.pushToTalkHMDChanged
* @param {boolean} enabled - <code>true</code> if push-to-talk is enabled for HMD mode, otherwise <code>false</code>.
* @returns {Signal}
*/
void pushToTalkHMDChanged(bool enabled);
/**jsdoc
* Triggered when the audio input noise reduction is enabled or disabled.
* Triggered when audio input noise reduction is enabled or disabled.
* @function Audio.noiseReductionChanged
* @param {boolean} isEnabled - <code>true</code> if audio input noise reduction is enabled, otherwise <code>false</code>.
* @returns {Signal}
@ -346,8 +372,8 @@ signals:
* Triggered when the input audio volume changes.
* @function Audio.inputVolumeChanged
* @param {number} volume - The requested volume to be applied to the audio input, range <code>0.0</code> &ndash;
* <code>1.0</code>. The resulting value of <code>Audio.inputVolume</code> depends on the capabilities of the device:
* for example, the volume can't be changed on some devices, and others might only support values of <code>0.0</code>
* <code>1.0</code>. The resulting value of <code>Audio.inputVolume</code> depends on the capabilities of the device.
* For example, the volume can't be changed on some devices, while others might only support values of <code>0.0</code>
* and <code>1.0</code>.
* @returns {Signal}
*/
@ -379,11 +405,11 @@ signals:
void contextChanged(const QString& context);
/**jsdoc
* Triggered when pushing to talk.
* @function Audio.pushingToTalkChanged
* @param {boolean} talking - <code>true</code> if broadcasting with PTT, <code>false</code> otherwise.
* @returns {Signal}
*/
* Triggered when the user starts or stops push-to-talk.
* @function Audio.pushingToTalkChanged
* @param {boolean} talking - <code>true</code> if started push-to-talk, <code>false</code> if stopped push-to-talk.
* @returns {Signal}
*/
void pushingToTalkChanged(bool talking);
public slots:
@ -409,6 +435,7 @@ protected:
private:
bool _settingsLoaded { false };
float _inputVolume { 1.0f };
float _inputLevel { 0.0f };
float _localInjectorGain { 0.0f }; // in dB

View file

@ -284,7 +284,7 @@ public slots:
* Disable default Interface actions for a joystick.
* @function Controller.captureJoystick
* @param {number} joystickID - The integer ID of the joystick.
* @deprecated This function no longer has any effect.
* @deprecated This function is deprecated and will be removed. It no longer has any effect.
*/
virtual void captureJoystick(int joystickIndex);
@ -293,7 +293,7 @@ public slots:
* {@link Controller.captureJoystick|captureJoystick}.
* @function Controller.releaseJoystick
* @param {number} joystickID - The integer ID of the joystick.
* @deprecated This function no longer has any effect.
* @deprecated This function is deprecated and will be removed. It no longer has any effect.
*/
virtual void releaseJoystick(int joystickIndex);

View file

@ -0,0 +1,46 @@
//
// RefreshRateScriptingInterface.h
// interface/src/scrfipting
//
// Created by Dante Ruiz on 2019-04-15.
// Copyright 2019 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_RefreshRateScriptingInterface_h
#define hifi_RefreshRateScriptingInterface_h
#include <QtCore/QObject>
#include <Application.h>
class RefreshRateScriptingInterface : public QObject {
Q_OBJECT
public:
RefreshRateScriptingInterface() = default;
~RefreshRateScriptingInterface() = default;
public:
Q_INVOKABLE QString getRefreshRateProfile() {
RefreshRateManager& refreshRateManager = qApp->getRefreshRateManager();
return QString::fromStdString(RefreshRateManager::refreshRateProfileToString(refreshRateManager.getRefreshRateProfile()));
}
Q_INVOKABLE QString getRefreshRateRegime() {
RefreshRateManager& refreshRateManager = qApp->getRefreshRateManager();
return QString::fromStdString(RefreshRateManager::refreshRateRegimeToString(refreshRateManager.getRefreshRateRegime()));
}
Q_INVOKABLE QString getUXMode() {
RefreshRateManager& refreshRateManager = qApp->getRefreshRateManager();
return QString::fromStdString(RefreshRateManager::uxModeToString(refreshRateManager.getUXMode()));
}
Q_INVOKABLE int getActiveRefreshRate() {
return qApp->getRefreshRateManager().getActiveRefreshRate();
}
};
#endif

View file

@ -199,3 +199,13 @@ void TestScriptingInterface::setOtherAvatarsReplicaCount(int count) {
int TestScriptingInterface::getOtherAvatarsReplicaCount() {
return qApp->getOtherAvatarsReplicaCount();
}
void TestScriptingInterface::setMinimumGPUTextureMemStabilityCount(int count) {
QMetaObject::invokeMethod(qApp, "setMinimumGPUTextureMemStabilityCount", Qt::DirectConnection, Q_ARG(int, count));
}
bool TestScriptingInterface::isTextureLoadingComplete() {
bool result;
QMetaObject::invokeMethod(qApp, "gpuTextureMemSizeStable", Qt::DirectConnection, Q_RETURN_ARG(bool, result));
return result;
}

View file

@ -163,6 +163,20 @@ public slots:
*/
Q_INVOKABLE int getOtherAvatarsReplicaCount();
/**jsdoc
* Set number of cycles texture size is required to be stable
* @function Entities.setMinimumGPUTextureMemStabilityCount
* @param {number} count - Number of cycles to wait
*/
Q_INVOKABLE void setMinimumGPUTextureMemStabilityCount(int count);
/**jsdoc
* Check whether all textures have been loaded.
* @function Entities.isTextureLoadingComplete
* @returns {boolean} <code>true</code> texture memory usage is not increasing
*/
Q_INVOKABLE bool isTextureLoadingComplete();
private:
bool waitForCondition(qint64 maxWaitMs, std::function<bool()> condition);
QString _testResultsLocation;

View file

@ -27,6 +27,7 @@
#include "MainWindow.h"
#include "Menu.h"
#include "OffscreenUi.h"
#include "commerce/QmlCommerce.h"
static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation";
@ -134,15 +135,17 @@ void WindowScriptingInterface::disconnectedFromDomain() {
void WindowScriptingInterface::openUrl(const QUrl& url) {
if (!url.isEmpty()) {
if (url.scheme() == URL_SCHEME_HIFI) {
auto scheme = url.scheme();
if (scheme == URL_SCHEME_HIFI) {
DependencyManager::get<AddressManager>()->handleLookupString(url.toString());
} else if (scheme == URL_SCHEME_HIFIAPP) {
DependencyManager::get<QmlCommerce>()->openSystemApp(url.path());
} else {
#if defined(Q_OS_ANDROID)
QMap<QString, QString> args;
args["url"] = url.toString();
AndroidHelper::instance().requestActivity("WebView", true, args);
#else
// address manager did not handle - ask QDesktopServices to handle
QDesktopServices::openUrl(url);
#endif
}

View file

@ -535,9 +535,10 @@ public slots:
int openMessageBox(QString title, QString text, int buttons, int defaultButton);
/**jsdoc
* Open a URL in the Interface window or other application, depending on the URL's scheme. If the URL starts with
* <code>hifi://</code> then that URL is navigated to in Interface, otherwise the URL is opened in the application the OS
* associates with the URL's scheme (e.g., a Web browser for <code>http://</code>).
* Open a URL in the Interface window or other application, depending on the URL's scheme. The following schemes are supported:
* <code>hifi</code> (navigate to the URL in Interface), <code>hifiapp<code> (open a system app in Interface). Other schemes will either be handled by the OS
* (e.g. <code>http</code>, <code>https</code>, <code>mailto</code>) or will create a confirmation dialog asking the user to confirm that they want to try to open
* the URL.
* @function Window.openUrl
* @param {string} url - The URL to open.
*/

View file

@ -910,6 +910,9 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) {
});
_layerIndex = 0;
addIncludeItemsToMallets();
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
scaleKeyboard(myAvatar->getSensorToWorldScale());
});
request->send();

View file

@ -98,6 +98,7 @@ public:
bool isPassword() const;
void setPassword(bool password);
void enableRightMallet();
void scaleKeyboard(float sensorToWorldScale);
void enableLeftMallet();
void disableRightMallet();
void disableLeftMallet();
@ -122,7 +123,6 @@ public slots:
void handleTriggerContinue(const QUuid& id, const PointerEvent& event);
void handleHoverBegin(const QUuid& id, const PointerEvent& event);
void handleHoverEnd(const QUuid& id, const PointerEvent& event);
void scaleKeyboard(float sensorToWorldScale);
private:
struct Anchor {

View file

@ -138,7 +138,7 @@ void LoginDialog::login(const QString& username, const QString& password) const
void LoginDialog::loginThroughOculus() {
qDebug() << "Attempting to login through Oculus";
if (auto oculusPlatformPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) {
oculusPlatformPlugin->requestNonceAndUserID([this] (QString nonce, QString oculusID) {
oculusPlatformPlugin->requestNonceAndUserID([] (QString nonce, QString oculusID) {
DependencyManager::get<AccountManager>()->requestAccessTokenWithOculus(nonce, oculusID);
});
}
@ -279,10 +279,6 @@ void LoginDialog::createAccountFromSteam(QString username) {
}
}
void LoginDialog::openUrl(const QString& url) const {
QDesktopServices::openUrl(QUrl(url));
}
void LoginDialog::linkCompleted(QNetworkReply* reply) {
emit handleLinkCompleted();
}

View file

@ -80,8 +80,6 @@ protected slots:
Q_INVOKABLE void signup(const QString& email, const QString& username, const QString& password);
Q_INVOKABLE void openUrl(const QString& url) const;
Q_INVOKABLE bool getLoginDialogPoppedUp() const;
};

View file

@ -82,6 +82,28 @@ void setupPreferences() {
preferences->addPreference(new CheckPreference(GRAPHICS_QUALITY, "Show Shadows", getterShadow, setterShadow));
}
{
auto getter = []()->QString {
RefreshRateManager::RefreshRateProfile refreshRateProfile = qApp->getRefreshRateManager().getRefreshRateProfile();
return QString::fromStdString(RefreshRateManager::refreshRateProfileToString(refreshRateProfile));
};
auto setter = [](QString value) {
std::string profileName = value.toStdString();
RefreshRateManager::RefreshRateProfile refreshRateProfile = RefreshRateManager::refreshRateProfileFromString(profileName);
qApp->getRefreshRateManager().setRefreshRateProfile(refreshRateProfile);
};
auto preference = new ComboBoxPreference(GRAPHICS_QUALITY, "Refresh Rate", getter, setter);
QStringList refreshRateProfiles
{ QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::ECO)),
QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::INTERACTIVE)),
QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::REALTIME)) };
preference->setItems(refreshRateProfiles);
preferences->addPreference(preference);
}
// UI
static const QString UI_CATEGORY { "User Interface" };
{
@ -278,6 +300,12 @@ void setupPreferences() {
preference->setIndented(true);
preferences->addPreference(preference);
}
{
auto getter = [myAvatar]() -> bool { return myAvatar->hoverWhenUnsupported(); };
auto setter = [myAvatar](bool value) { myAvatar->setHoverWhenUnsupported(value); };
auto preference = new CheckPreference(VR_MOVEMENT, "Hover When Unsupported", getter, setter);
preferences->addPreference(preference);
}
{
auto getter = [myAvatar]()->int { return myAvatar->getMovementReference(); };
auto setter = [myAvatar](int value) { myAvatar->setMovementReference(value); };

View file

@ -159,47 +159,57 @@ void Snapshot::save360Snapshot(const glm::vec3& cameraPosition,
secondaryCameraRenderConfig->setOrientation(CAMERA_ORIENTATION_DOWN);
_snapshotIndex = 0;
_taking360Snapshot = true;
_snapshotTimer.start(SNAPSHOT_360_TIMER_INTERVAL);
}
void Snapshot::takeNextSnapshot() {
SecondaryCameraJobConfig* config =
static_cast<SecondaryCameraJobConfig*>(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera"));
if (_taking360Snapshot) {
if (!_waitingOnSnapshot) {
_waitingOnSnapshot = true;
qApp->addSnapshotOperator(std::make_tuple([this](const QImage& snapshot) {
// Order is:
// 0. Down
// 1. Front
// 2. Left
// 3. Back
// 4. Right
// 5. Up
if (_snapshotIndex < 6) {
_imageArray[_snapshotIndex] = snapshot;
}
// Order is:
// 0. Down
// 1. Front
// 2. Left
// 3. Back
// 4. Right
// 5. Up
if (_snapshotIndex < 6) {
_imageArray[_snapshotIndex] = qApp->getActiveDisplayPlugin()->getSecondaryCameraScreenshot();
}
SecondaryCameraJobConfig* config = static_cast<SecondaryCameraJobConfig*>(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera"));
if (_snapshotIndex == 0) {
// Setup for Front Image capture
config->setOrientation(CAMERA_ORIENTATION_FRONT);
} else if (_snapshotIndex == 1) {
// Setup for Left Image capture
config->setOrientation(CAMERA_ORIENTATION_LEFT);
} else if (_snapshotIndex == 2) {
// Setup for Back Image capture
config->setOrientation(CAMERA_ORIENTATION_BACK);
} else if (_snapshotIndex == 3) {
// Setup for Right Image capture
config->setOrientation(CAMERA_ORIENTATION_RIGHT);
} else if (_snapshotIndex == 4) {
// Setup for Up Image capture
config->setOrientation(CAMERA_ORIENTATION_UP);
} else if (_snapshotIndex == 5) {
_taking360Snapshot = false;
}
if (_snapshotIndex == 0) {
// Setup for Front Image capture
config->setOrientation(CAMERA_ORIENTATION_FRONT);
} else if (_snapshotIndex == 1) {
// Setup for Left Image capture
config->setOrientation(CAMERA_ORIENTATION_LEFT);
} else if (_snapshotIndex == 2) {
// Setup for Back Image capture
config->setOrientation(CAMERA_ORIENTATION_BACK);
} else if (_snapshotIndex == 3) {
// Setup for Right Image capture
config->setOrientation(CAMERA_ORIENTATION_RIGHT);
} else if (_snapshotIndex == 4) {
// Setup for Up Image capture
config->setOrientation(CAMERA_ORIENTATION_UP);
} else if (_snapshotIndex > 5) {
_waitingOnSnapshot = false;
_snapshotIndex++;
}, 0.0f, false));
}
} else {
_snapshotTimer.stop();
// Reset secondary camera render config
static_cast<ToneMappingConfig*>(
qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping"))
->setCurve(1);
SecondaryCameraJobConfig* config = static_cast<SecondaryCameraJobConfig*>(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera"));
static_cast<ToneMappingConfig*>(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping"))->setCurve(1);
config->resetSizeSpectatorCamera(qApp->getWindow()->geometry().width(), qApp->getWindow()->geometry().height());
config->setProperty("attachedEntityId", _oldAttachedEntityId);
config->setProperty("vFoV", _oldvFoV);
@ -217,8 +227,6 @@ void Snapshot::takeNextSnapshot() {
QtConcurrent::run([this]() { convertToEquirectangular(); });
}
}
_snapshotIndex++;
}
void Snapshot::convertToCubemap() {

View file

@ -97,6 +97,8 @@ private:
bool _cubemapOutputFormat;
QTimer _snapshotTimer;
qint16 _snapshotIndex;
bool _waitingOnSnapshot { false };
bool _taking360Snapshot { false };
bool _oldEnabled;
QVariant _oldAttachedEntityId;
QVariant _oldOrientation;

View file

@ -27,7 +27,6 @@ QString SnapshotAnimated::snapshotAnimatedPath;
QString SnapshotAnimated::snapshotStillPath;
QVector<QImage> SnapshotAnimated::snapshotAnimatedFrameVector;
QVector<qint64> SnapshotAnimated::snapshotAnimatedFrameDelayVector;
Application* SnapshotAnimated::app;
float SnapshotAnimated::aspectRatio;
QSharedPointer<WindowScriptingInterface> SnapshotAnimated::snapshotAnimatedDM;
GifWriter SnapshotAnimated::snapshotAnimatedGifWriter;
@ -36,12 +35,11 @@ GifWriter SnapshotAnimated::snapshotAnimatedGifWriter;
Setting::Handle<bool> SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true);
Setting::Handle<float> SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS);
void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer<WindowScriptingInterface> dm) {
void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, QSharedPointer<WindowScriptingInterface> dm) {
// If we're not in the middle of capturing an animated snapshot...
if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) {
SnapshotAnimated::snapshotAnimatedTimer = new QTimer();
SnapshotAnimated::aspectRatio = aspectRatio;
SnapshotAnimated::app = app;
SnapshotAnimated::snapshotAnimatedDM = dm;
// Define the output location of the still and animated snapshots.
SnapshotAnimated::snapshotStillPath = pathStill;
@ -62,44 +60,45 @@ void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio
void SnapshotAnimated::captureFrames() {
if (SnapshotAnimated::snapshotAnimatedTimerRunning) {
// Get a screenshot from the display, then scale the screenshot down,
// then convert it to the image format the GIF library needs,
// then save all that to the QImage named "frame"
QImage frame(SnapshotAnimated::app->getActiveDisplayPlugin()->getScreenshot(SnapshotAnimated::aspectRatio));
frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH);
SnapshotAnimated::snapshotAnimatedFrameVector.append(frame);
qApp->addSnapshotOperator(std::make_tuple([](const QImage& snapshot) {
// Get a screenshot from the display, then scale the screenshot down,
// then convert it to the image format the GIF library needs,
// then save all that to the QImage named "frame"
QImage frame = snapshot.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH);
SnapshotAnimated::snapshotAnimatedFrameVector.append(frame);
// If that was the first frame...
if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) {
// Record the current frame timestamp
SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch();
// Record the first frame timestamp
SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp;
SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10);
// If this is an intermediate or the final frame...
} else {
// Push the current frame delay onto the vector
SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10));
// Record the current frame timestamp
SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch();
// If that was the first frame...
if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) {
// Record the current frame timestamp
SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch();
// Record the first frame timestamp
SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp;
SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10);
// If this is an intermediate or the final frame...
} else {
// Push the current frame delay onto the vector
SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10));
// Record the current frame timestamp
SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch();
// If that was the last frame...
if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) {
SnapshotAnimated::snapshotAnimatedTimerRunning = false;
// Notify the user that we're processing the snapshot
// This also pops up the "Share" dialog. The unprocessed GIF will be visualized as a loading icon until processingGifCompleted() is called.
emit SnapshotAnimated::snapshotAnimatedDM->processingGifStarted(SnapshotAnimated::snapshotStillPath);
// Kick off the thread that'll pack the frames into the GIF
QtConcurrent::run(processFrames);
// Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE
// that the slot will not be called again in the future.
// See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html
SnapshotAnimated::snapshotAnimatedTimer->stop();
delete SnapshotAnimated::snapshotAnimatedTimer;
// If that was the last frame...
if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) {
SnapshotAnimated::snapshotAnimatedTimerRunning = false;
}
}
}
}, SnapshotAnimated::aspectRatio, true));
} else {
// Notify the user that we're processing the snapshot
// This also pops up the "Share" dialog. The unprocessed GIF will be visualized as a loading icon until processingGifCompleted() is called.
emit SnapshotAnimated::snapshotAnimatedDM->processingGifStarted(SnapshotAnimated::snapshotStillPath);
// Kick off the thread that'll pack the frames into the GIF
QtConcurrent::run(processFrames);
// Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE
// that the slot will not be called again in the future.
// See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html
SnapshotAnimated::snapshotAnimatedTimer->stop();
delete SnapshotAnimated::snapshotAnimatedTimer;
}
}

View file

@ -42,7 +42,6 @@ private:
static QVector<QImage> snapshotAnimatedFrameVector;
static QVector<qint64> snapshotAnimatedFrameDelayVector;
static QSharedPointer<WindowScriptingInterface> snapshotAnimatedDM;
static Application* app;
static float aspectRatio;
static GifWriter snapshotAnimatedGifWriter;
@ -51,7 +50,7 @@ private:
static void processFrames();
static void clearTempVariables();
public:
static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer<WindowScriptingInterface> dm);
static void saveSnapshotAnimated(QString pathStill, float aspectRatio, QSharedPointer<WindowScriptingInterface> dm);
static bool isAlreadyTakingSnapshotAnimated() { return snapshotAnimatedFirstFrameTimestamp != 0; };
static Setting::Handle<bool> alsoTakeAnimatedSnapshot;
static Setting::Handle<float> snapshotAnimatedDuration;

View file

@ -132,6 +132,14 @@ void Stats::updateStats(bool force) {
STAT_UPDATE(notUpdatedAvatarCount, avatarManager->getNumAvatarsNotUpdated());
STAT_UPDATE(serverCount, (int)nodeList->size());
STAT_UPDATE_FLOAT(renderrate, qApp->getRenderLoopRate(), 0.1f);
RefreshRateManager& refreshRateManager = qApp->getRefreshRateManager();
std::string refreshRateMode = RefreshRateManager::refreshRateProfileToString(refreshRateManager.getRefreshRateProfile());
std::string refreshRateRegime = RefreshRateManager::refreshRateRegimeToString(refreshRateManager.getRefreshRateRegime());
std::string uxMode = RefreshRateManager::uxModeToString(refreshRateManager.getUXMode());
STAT_UPDATE(refreshRateMode, QString::fromStdString(refreshRateMode));
STAT_UPDATE(refreshRateRegime, QString::fromStdString(refreshRateRegime));
STAT_UPDATE(uxMode, QString::fromStdString(uxMode));
STAT_UPDATE(refreshRateTarget, refreshRateManager.getActiveRefreshRate());
if (qApp->getActiveDisplayPlugin()) {
auto displayPlugin = qApp->getActiveDisplayPlugin();
auto stats = displayPlugin->getHardwareStats();

View file

@ -206,6 +206,10 @@ class Stats : public QQuickItem {
STATS_PROPERTY(float, presentdroprate, 0)
STATS_PROPERTY(int, gameLoopRate, 0)
STATS_PROPERTY(int, avatarCount, 0)
STATS_PROPERTY(int, refreshRateTarget, 0)
STATS_PROPERTY(QString, refreshRateMode, QString())
STATS_PROPERTY(QString, refreshRateRegime, QString())
STATS_PROPERTY(QString, uxMode, QString())
STATS_PROPERTY(int, heroAvatarCount, 0)
STATS_PROPERTY(int, physicsObjectCount, 0)
STATS_PROPERTY(int, updatedAvatarCount, 0)
@ -1067,6 +1071,15 @@ signals:
*/
void decimatedTextureCountChanged();
void refreshRateTargetChanged();
void refreshRateModeChanged();
void refreshRateRegimeChanged();
void uxModeChanged();
// QQuickItem signals.
/**jsdoc

View file

@ -11,11 +11,12 @@
#include "AnimContext.h"
AnimContext::AnimContext(bool enableDebugDrawIKTargets, bool enableDebugDrawIKConstraints, bool enableDebugDrawIKChains,
const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix) :
const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix, int evaluationCount) :
_enableDebugDrawIKTargets(enableDebugDrawIKTargets),
_enableDebugDrawIKConstraints(enableDebugDrawIKConstraints),
_enableDebugDrawIKChains(enableDebugDrawIKChains),
_geometryToRigMatrix(geometryToRigMatrix),
_rigToWorldMatrix(rigToWorldMatrix)
_rigToWorldMatrix(rigToWorldMatrix),
_evaluationCount(evaluationCount)
{
}

View file

@ -24,6 +24,7 @@ enum class AnimNodeType {
BlendLinearMove,
Overlay,
StateMachine,
RandomSwitchStateMachine,
Manipulator,
InverseKinematics,
DefaultPose,
@ -37,13 +38,14 @@ class AnimContext {
public:
AnimContext() {}
AnimContext(bool enableDebugDrawIKTargets, bool enableDebugDrawIKConstraints, bool enableDebugDrawIKChains,
const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix);
const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix, int evaluationCount);
bool getEnableDebugDrawIKTargets() const { return _enableDebugDrawIKTargets; }
bool getEnableDebugDrawIKConstraints() const { return _enableDebugDrawIKConstraints; }
bool getEnableDebugDrawIKChains() const { return _enableDebugDrawIKChains; }
const glm::mat4& getGeometryToRigMatrix() const { return _geometryToRigMatrix; }
const glm::mat4& getRigToWorldMatrix() const { return _rigToWorldMatrix; }
int getEvaluationCount() const { return _evaluationCount; }
float getDebugAlpha(const QString& key) const {
auto it = _debugAlphaMap.find(key);
@ -85,6 +87,7 @@ protected:
bool _enableDebugDrawIKChains { false };
glm::mat4 _geometryToRigMatrix;
glm::mat4 _rigToWorldMatrix;
int _evaluationCount{ 0 };
// used for debugging internal state of animation system.
mutable DebugAlphaMap _debugAlphaMap;

View file

@ -43,6 +43,7 @@ public:
friend class AnimDebugDraw;
friend void buildChildMap(std::map<QString, Pointer>& map, Pointer node);
friend class AnimStateMachine;
friend class AnimRandomSwitch;
AnimNode(Type type, const QString& id) : _type(type), _id(id) {}
virtual ~AnimNode() {}

View file

@ -22,6 +22,7 @@
#include "AnimationLogging.h"
#include "AnimOverlay.h"
#include "AnimStateMachine.h"
#include "AnimRandomSwitch.h"
#include "AnimManipulator.h"
#include "AnimInverseKinematics.h"
#include "AnimDefaultPose.h"
@ -38,6 +39,7 @@ static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const Q
static AnimNode::Pointer loadBlendLinearMoveNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadRandomSwitchStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadDefaultPoseNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
@ -51,6 +53,7 @@ static const float ANIM_GRAPH_LOAD_PRIORITY = 10.0f;
// returns node on success, nullptr on failure.
static bool processDoNothing(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; }
bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
bool processRandomSwitchStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static const char* animNodeTypeToString(AnimNode::Type type) {
switch (type) {
@ -59,6 +62,7 @@ static const char* animNodeTypeToString(AnimNode::Type type) {
case AnimNode::Type::BlendLinearMove: return "blendLinearMove";
case AnimNode::Type::Overlay: return "overlay";
case AnimNode::Type::StateMachine: return "stateMachine";
case AnimNode::Type::RandomSwitchStateMachine: return "randomSwitchStateMachine";
case AnimNode::Type::Manipulator: return "manipulator";
case AnimNode::Type::InverseKinematics: return "inverseKinematics";
case AnimNode::Type::DefaultPose: return "defaultPose";
@ -92,6 +96,16 @@ static AnimStateMachine::InterpType stringToInterpType(const QString& str) {
}
}
static AnimRandomSwitch::InterpType stringToRandomInterpType(const QString& str) {
if (str == "snapshotBoth") {
return AnimRandomSwitch::InterpType::SnapshotBoth;
} else if (str == "snapshotPrev") {
return AnimRandomSwitch::InterpType::SnapshotPrev;
} else {
return AnimRandomSwitch::InterpType::NumTypes;
}
}
static const char* animManipulatorJointVarTypeToString(AnimManipulator::JointVar::Type type) {
switch (type) {
case AnimManipulator::JointVar::Type::Absolute: return "absolute";
@ -122,6 +136,7 @@ static NodeLoaderFunc animNodeTypeToLoaderFunc(AnimNode::Type type) {
case AnimNode::Type::BlendLinearMove: return loadBlendLinearMoveNode;
case AnimNode::Type::Overlay: return loadOverlayNode;
case AnimNode::Type::StateMachine: return loadStateMachineNode;
case AnimNode::Type::RandomSwitchStateMachine: return loadRandomSwitchStateMachineNode;
case AnimNode::Type::Manipulator: return loadManipulatorNode;
case AnimNode::Type::InverseKinematics: return loadInverseKinematicsNode;
case AnimNode::Type::DefaultPose: return loadDefaultPoseNode;
@ -140,6 +155,7 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) {
case AnimNode::Type::BlendLinearMove: return processDoNothing;
case AnimNode::Type::Overlay: return processDoNothing;
case AnimNode::Type::StateMachine: return processStateMachineNode;
case AnimNode::Type::RandomSwitchStateMachine: return processRandomSwitchStateMachineNode;
case AnimNode::Type::Manipulator: return processDoNothing;
case AnimNode::Type::InverseKinematics: return processDoNothing;
case AnimNode::Type::DefaultPose: return processDoNothing;
@ -463,6 +479,11 @@ static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const
return node;
}
static AnimNode::Pointer loadRandomSwitchStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
auto node = std::make_shared<AnimRandomSwitch>(id);
return node;
}
static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr);
@ -780,6 +801,141 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj,
return true;
}
bool processRandomSwitchStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl) {
auto smNode = std::static_pointer_cast<AnimRandomSwitch>(node);
assert(smNode);
READ_STRING(currentState, jsonObj, nodeId, jsonUrl, false);
READ_OPTIONAL_FLOAT(randomSwitchTimeMin, jsonObj, -1.0f);
READ_OPTIONAL_FLOAT(randomSwitchTimeMax, jsonObj, -1.0f);
READ_OPTIONAL_STRING(triggerRandomSwitch, jsonObj);
READ_OPTIONAL_FLOAT(triggerTimeMin, jsonObj, -1.0f);
READ_OPTIONAL_FLOAT(triggerTimeMax, jsonObj, -1.0f);
READ_OPTIONAL_STRING(transitionVar, jsonObj);
auto statesValue = jsonObj.value("states");
if (!statesValue.isArray()) {
qCCritical(animation) << "AnimNodeLoader, bad array \"states\" in random switch state Machine node, id =" << nodeId;
return false;
}
// build a map for all children by name.
std::map<QString, int> childMap;
buildChildMap(childMap, node);
// first pass parse all the states and build up the state and transition map.
using StringPair = std::pair<QString, QString>;
using TransitionMap = std::multimap<AnimRandomSwitch::RandomSwitchState::Pointer, StringPair>;
TransitionMap transitionMap;
using RandomStateMap = std::map<QString, AnimRandomSwitch::RandomSwitchState::Pointer>;
RandomStateMap randomStateMap;
auto randomStatesArray = statesValue.toArray();
for (const auto& randomStateValue : randomStatesArray) {
if (!randomStateValue.isObject()) {
qCCritical(animation) << "AnimNodeLoader, bad state object in \"random states\", id =" << nodeId;
return false;
}
auto stateObj = randomStateValue.toObject();
READ_STRING(id, stateObj, nodeId, jsonUrl, false);
READ_FLOAT(interpTarget, stateObj, nodeId, jsonUrl, false);
READ_FLOAT(interpDuration, stateObj, nodeId, jsonUrl, false);
READ_OPTIONAL_STRING(interpType, stateObj);
READ_FLOAT(priority, stateObj, nodeId, jsonUrl, false);
READ_BOOL(resume, stateObj, nodeId, jsonUrl, false);
READ_OPTIONAL_STRING(interpTargetVar, stateObj);
READ_OPTIONAL_STRING(interpDurationVar, stateObj);
READ_OPTIONAL_STRING(interpTypeVar, stateObj);
auto iter = childMap.find(id);
if (iter == childMap.end()) {
qCCritical(animation) << "AnimNodeLoader, could not find random stateMachine child (state) with nodeId =" << nodeId << "random stateId =" << id;
return false;
}
AnimRandomSwitch::InterpType interpTypeEnum = AnimRandomSwitch::InterpType::SnapshotPrev; // default value
if (!interpType.isEmpty()) {
interpTypeEnum = stringToRandomInterpType(interpType);
if (interpTypeEnum == AnimRandomSwitch::InterpType::NumTypes) {
qCCritical(animation) << "AnimNodeLoader, bad interpType on random state Machine state, nodeId = " << nodeId << "random stateId =" << id;
return false;
}
}
auto randomStatePtr = std::make_shared<AnimRandomSwitch::RandomSwitchState>(id, iter->second, interpTarget, interpDuration, interpTypeEnum, priority, resume);
if (priority > 0.0f) {
smNode->addToPrioritySum(priority);
}
assert(randomStatePtr);
if (!interpTargetVar.isEmpty()) {
randomStatePtr->setInterpTargetVar(interpTargetVar);
}
if (!interpDurationVar.isEmpty()) {
randomStatePtr->setInterpDurationVar(interpDurationVar);
}
if (!interpTypeVar.isEmpty()) {
randomStatePtr->setInterpTypeVar(interpTypeVar);
}
smNode->addState(randomStatePtr);
randomStateMap.insert(RandomStateMap::value_type(randomStatePtr->getID(), randomStatePtr));
auto transitionsValue = stateObj.value("transitions");
if (!transitionsValue.isArray()) {
qCritical(animation) << "AnimNodeLoader, bad array \"transitions\" in random state Machine node, stateId =" << id << "nodeId =" << nodeId;
return false;
}
auto transitionsArray = transitionsValue.toArray();
for (const auto& transitionValue : transitionsArray) {
if (!transitionValue.isObject()) {
qCritical(animation) << "AnimNodeLoader, bad transition object in \"transitions\", random stateId =" << id << "nodeId =" << nodeId;
return false;
}
auto transitionObj = transitionValue.toObject();
READ_STRING(var, transitionObj, nodeId, jsonUrl, false);
READ_STRING(randomSwitchState, transitionObj, nodeId, jsonUrl, false);
transitionMap.insert(TransitionMap::value_type(randomStatePtr, StringPair(var, randomSwitchState)));
}
}
// second pass: now iterate thru all transitions and add them to the appropriate states.
for (auto& transition : transitionMap) {
AnimRandomSwitch::RandomSwitchState::Pointer srcState = transition.first;
auto iter = randomStateMap.find(transition.second.second);
if (iter != randomStateMap.end()) {
srcState->addTransition(AnimRandomSwitch::RandomSwitchState::Transition(transition.second.first, iter->second));
} else {
qCCritical(animation) << "AnimNodeLoader, bad random state machine transition from srcState =" << srcState->_id << "dstState =" << transition.second.second << "nodeId =" << nodeId;
return false;
}
}
auto iter = randomStateMap.find(currentState);
if (iter == randomStateMap.end()) {
qCCritical(animation) << "AnimNodeLoader, bad currentState =" << currentState << "could not find child node" << "id =" << nodeId;
}
smNode->setCurrentState(iter->second);
smNode->setRandomSwitchTimeMin(randomSwitchTimeMin);
smNode->setRandomSwitchTimeMax(randomSwitchTimeMax);
smNode->setTriggerRandomSwitchVar(triggerRandomSwitch);
smNode->setTriggerTimeMin(triggerTimeMin);
smNode->setTriggerTimeMax(triggerTimeMax);
smNode->setTransitionVar(transitionVar);
return true;
}
AnimNodeLoader::AnimNodeLoader(const QUrl& url) :
_url(url)
{

View file

@ -0,0 +1,212 @@
//
// AnimRandomSwitch.cpp
//
// Created by Angus Antley on 4/8/2019.
// Copyright (c) 2019 High Fidelity, Inc. All rights reserved.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AnimRandomSwitch.h"
#include "AnimUtil.h"
#include "AnimationLogging.h"
AnimRandomSwitch::AnimRandomSwitch(const QString& id) :
AnimNode(AnimNode::Type::RandomSwitchStateMachine, id) {
}
AnimRandomSwitch::~AnimRandomSwitch() {
}
const AnimPoseVec& AnimRandomSwitch::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) {
float parentDebugAlpha = context.getDebugAlpha(_id);
AnimRandomSwitch::RandomSwitchState::Pointer desiredState = _currentState;
if (abs(_randomSwitchEvaluationCount - context.getEvaluationCount()) > 1 || animVars.lookup(_triggerRandomSwitchVar, false)) {
// get a random number and decide which motion to choose.
bool currentStateHasPriority = false;
float dice = randFloatInRange(0.0f, 1.0f);
float lowerBound = 0.0f;
for (const RandomSwitchState::Pointer& randState : _randomStates) {
if (randState->getPriority() > 0.0f) {
float upperBound = lowerBound + (randState->getPriority() / _totalPriorities);
if ((dice > lowerBound) && (dice < upperBound)) {
desiredState = randState;
}
lowerBound = upperBound;
// this indicates if the curent state is one that can be selected randomly, or is one that was transitioned to by the random duration timer.
currentStateHasPriority = currentStateHasPriority || (_currentState == randState);
}
}
if (abs(_randomSwitchEvaluationCount - context.getEvaluationCount()) > 1) {
_duringInterp = false;
switchRandomState(animVars, context, desiredState, _duringInterp);
} else {
// firing a random switch, be sure that we aren't completing a previously triggered transition
if (currentStateHasPriority) {
if (desiredState->getID() != _currentState->getID()) {
_duringInterp = true;
switchRandomState(animVars, context, desiredState, _duringInterp);
} else {
_duringInterp = false;
}
}
}
_triggerTime = randFloatInRange(_triggerTimeMin, _triggerTimeMax);
_randomSwitchTime = randFloatInRange(_randomSwitchTimeMin, _randomSwitchTimeMax);
} else {
// here we are checking to see if we want a temporary movement
// evaluate currentState transitions
auto transitionState = evaluateTransitions(animVars);
if (transitionState != _currentState) {
_duringInterp = true;
switchRandomState(animVars, context, transitionState, _duringInterp);
_triggerTime = randFloatInRange(_triggerTimeMin, _triggerTimeMax);
_randomSwitchTime = randFloatInRange(_randomSwitchTimeMin, _randomSwitchTimeMax);
}
}
_triggerTime -= dt;
if ((_triggerTime < 0.0f) && (_triggerTimeMin > 0.0f) && (_triggerTimeMax > 0.0f)) {
_triggerTime = randFloatInRange(_triggerTimeMin, _triggerTimeMax);
triggersOut.setTrigger(_transitionVar);
}
_randomSwitchTime -= dt;
if ((_randomSwitchTime < 0.0f) && (_randomSwitchTimeMin > 0.0f) && (_randomSwitchTimeMax > 0.0f)) {
_randomSwitchTime = randFloatInRange(_randomSwitchTimeMin, _randomSwitchTimeMax);
// restart the trigger timer if it is also enabled
_triggerTime = randFloatInRange(_triggerTimeMin, _triggerTimeMax);
triggersOut.setTrigger(_triggerRandomSwitchVar);
}
assert(_currentState);
auto currentStateNode = _children[_currentState->getChildIndex()];
assert(currentStateNode);
if (_duringInterp) {
_alpha += _alphaVel * dt;
if (_alpha < 1.0f) {
AnimPoseVec* nextPoses = nullptr;
AnimPoseVec* prevPoses = nullptr;
AnimPoseVec localNextPoses;
if (_interpType == InterpType::SnapshotBoth) {
// interp between both snapshots
prevPoses = &_prevPoses;
nextPoses = &_nextPoses;
} else if (_interpType == InterpType::SnapshotPrev) {
// interp between the prev snapshot and evaluated next target.
// this is useful for interping into a blend
localNextPoses = currentStateNode->evaluate(animVars, context, dt, triggersOut);
prevPoses = &_prevPoses;
nextPoses = &localNextPoses;
} else {
assert(false);
}
if (_poses.size() > 0 && nextPoses && prevPoses && nextPoses->size() > 0 && prevPoses->size() > 0) {
::blend(_poses.size(), &(prevPoses->at(0)), &(nextPoses->at(0)), _alpha, &_poses[0]);
}
context.setDebugAlpha(_currentState->getID(), _alpha * parentDebugAlpha, _children[_currentState->getChildIndex()]->getType());
} else {
_duringInterp = false;
_prevPoses.clear();
_nextPoses.clear();
}
}
if (!_duringInterp){
context.setDebugAlpha(_currentState->getID(), parentDebugAlpha, _children[_currentState->getChildIndex()]->getType());
_poses = currentStateNode->evaluate(animVars, context, dt, triggersOut);
}
_randomSwitchEvaluationCount = context.getEvaluationCount();
processOutputJoints(triggersOut);
context.addStateMachineInfo(_id, _currentState->getID(), _previousState->getID(), _duringInterp, _alpha);
if (_duringInterp) {
// hack: add previoius state to debug alpha map, with parens around it's name.
context.setDebugAlpha(QString("(%1)").arg(_previousState->getID()), 1.0f - _alpha, AnimNodeType::Clip);
}
return _poses;
}
void AnimRandomSwitch::setCurrentState(RandomSwitchState::Pointer randomState) {
_previousState = _currentState ? _currentState : randomState;
_currentState = randomState;
}
void AnimRandomSwitch::addState(RandomSwitchState::Pointer randomState) {
_randomStates.push_back(randomState);
}
void AnimRandomSwitch::switchRandomState(const AnimVariantMap& animVars, const AnimContext& context, RandomSwitchState::Pointer desiredState, bool shouldInterp) {
auto nextStateNode = _children[desiredState->getChildIndex()];
if (shouldInterp) {
const float FRAMES_PER_SECOND = 30.0f;
auto prevStateNode = _children[_currentState->getChildIndex()];
_alpha = 0.0f;
float duration = std::max(0.001f, animVars.lookup(desiredState->_interpDurationVar, desiredState->_interpDuration));
_alphaVel = FRAMES_PER_SECOND / duration;
_interpType = (InterpType)animVars.lookup(desiredState->_interpTypeVar, (int)desiredState->_interpType);
// because dt is 0, we should not encounter any triggers
const float dt = 0.0f;
AnimVariantMap triggers;
if (_interpType == InterpType::SnapshotBoth) {
// snapshot previous pose.
_prevPoses = _poses;
// snapshot next pose at the target frame.
if (!desiredState->getResume()) {
nextStateNode->setCurrentFrame(desiredState->_interpTarget);
}
_nextPoses = nextStateNode->evaluate(animVars, context, dt, triggers);
} else if (_interpType == InterpType::SnapshotPrev) {
// snapshot previoius pose
_prevPoses = _poses;
// no need to evaluate _nextPoses we will do it dynamically during the interp,
// however we need to set the current frame.
if (!desiredState->getResume()) {
nextStateNode->setCurrentFrame(desiredState->_interpTarget - duration);
}
} else {
assert(false);
}
} else {
if (!desiredState->getResume()) {
nextStateNode->setCurrentFrame(desiredState->_interpTarget);
}
}
#ifdef WANT_DEBUG
qCDebug(animation) << "AnimRandomSwitch::switchState:" << _currentState->getID() << "->" << desiredState->getID() << "duration =" << duration << "targetFrame =" << desiredState->_interpTarget << "interpType = " << (int)_interpType;
#endif
setCurrentState(desiredState);
}
AnimRandomSwitch::RandomSwitchState::Pointer AnimRandomSwitch::evaluateTransitions(const AnimVariantMap& animVars) const {
assert(_currentState);
for (auto& transition : _currentState->_transitions) {
if (animVars.lookup(transition._var, false)) {
return transition._randomSwitchState;
}
}
return _currentState;
}
const AnimPoseVec& AnimRandomSwitch::getPosesInternal() const {
return _poses;
}

View file

@ -0,0 +1,184 @@
//
// AnimRandomSwitch.h
//
// Created by Angus Antley on 4/8/19.
// Copyright (c) 2019 High Fidelity, Inc. All rights reserved.
//
// 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_AnimRandomSwitch_h
#define hifi_AnimRandomSwitch_h
#include <string>
#include <vector>
#include "AnimNode.h"
// Random Switch State Machine for random transitioning between children AnimNodes
//
// This is mechanisim for choosing and playing a random animation and smoothly interpolating/fading
// between them. A RandomSwitch has a set of States, which typically reference
// child AnimNodes. Each Random Switch State has a list of Transitions, which are evaluated
// to determine when we should switch to a new State. Parameters for the smooth
// interpolation/fading are read from the Random Switch State that you are transitioning to.
//
// The currentState can be set directly via the setCurrentStateVar() and will override
// any State transitions.
//
// Each Random Switch State has two parameters that can be changed via AnimVars,
// * interpTarget - (frames) The destination frame of the interpolation. i.e. the first frame of the animation that will
// visible after interpolation is complete.
// * interpDuration - (frames) The total length of time it will take to interp between the current pose and the
// interpTarget frame.
// * interpType - How the interpolation is performed.
// * priority - this number represents how likely this Random Switch State will be chosen.
// the priority for each Random Switch State will be normalized, so their relative size is what is important
// * resume - if resume is false then if this state is chosen twice in a row it will remember what frame it was playing on.
// * SnapshotBoth: Stores two snapshots, the previous animation before interpolation begins and the target state at the
// interTarget frame. Then during the interpolation period the two snapshots are interpolated to produce smooth motion between them.
// * SnapshotPrev: Stores a snapshot of the previous animation before interpolation begins. However the target state is
// evaluated dynamically. During the interpolation period the previous snapshot is interpolated with the target pose
// to produce smooth motion between them. This mode is useful for interping into a blended animation where the actual
// blend factor is not known at the start of the interp or is might change dramatically during the interp.
//
class AnimRandomSwitch : public AnimNode {
public:
friend class AnimNodeLoader;
friend bool processRandomSwitchStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl);
enum class InterpType {
SnapshotBoth = 0,
SnapshotPrev,
NumTypes
};
protected:
class RandomSwitchState {
public:
friend AnimRandomSwitch;
friend bool processRandomSwitchStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl);
using Pointer = std::shared_ptr<RandomSwitchState>;
using ConstPointer = std::shared_ptr<const RandomSwitchState>;
class Transition {
public:
friend AnimRandomSwitch;
Transition(const QString& var, RandomSwitchState::Pointer randomState) : _var(var), _randomSwitchState(randomState) {}
protected:
QString _var;
RandomSwitchState::Pointer _randomSwitchState;
};
RandomSwitchState(const QString& id, int childIndex, float interpTarget, float interpDuration, InterpType interpType, float priority, bool resume) :
_id(id),
_childIndex(childIndex),
_interpTarget(interpTarget),
_interpDuration(interpDuration),
_interpType(interpType),
_priority(priority),
_resume(resume){
}
void setInterpTargetVar(const QString& interpTargetVar) { _interpTargetVar = interpTargetVar; }
void setInterpDurationVar(const QString& interpDurationVar) { _interpDurationVar = interpDurationVar; }
void setInterpTypeVar(const QString& interpTypeVar) { _interpTypeVar = interpTypeVar; }
int getChildIndex() const { return _childIndex; }
float getPriority() const { return _priority; }
bool getResume() const { return _resume; }
const QString& getID() const { return _id; }
protected:
void setInterpTarget(float interpTarget) { _interpTarget = interpTarget; }
void setInterpDuration(float interpDuration) { _interpDuration = interpDuration; }
void setPriority(float priority) { _priority = priority; }
void setResumeFlag(bool resume) { _resume = resume; }
void addTransition(const Transition& transition) { _transitions.push_back(transition); }
QString _id;
int _childIndex;
float _interpTarget; // frames
float _interpDuration; // frames
InterpType _interpType;
float _priority {0.0f};
bool _resume {false};
QString _interpTargetVar;
QString _interpDurationVar;
QString _interpTypeVar;
std::vector<Transition> _transitions;
private:
// no copies
RandomSwitchState(const RandomSwitchState&) = delete;
RandomSwitchState& operator=(const RandomSwitchState&) = delete;
};
public:
explicit AnimRandomSwitch(const QString& id);
virtual ~AnimRandomSwitch() override;
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) override;
void setCurrentStateVar(QString& currentStateVar) { _currentStateVar = currentStateVar; }
protected:
void setCurrentState(RandomSwitchState::Pointer randomState);
void setTriggerRandomSwitchVar(const QString& triggerRandomSwitchVar) { _triggerRandomSwitchVar = triggerRandomSwitchVar; }
void setRandomSwitchTimeMin(float randomSwitchTimeMin) { _randomSwitchTimeMin = randomSwitchTimeMin; }
void setRandomSwitchTimeMax(float randomSwitchTimeMax) { _randomSwitchTimeMax = randomSwitchTimeMax; }
void setTransitionVar(const QString& transitionVar) { _transitionVar = transitionVar; }
void setTriggerTimeMin(float triggerTimeMin) { _triggerTimeMin = triggerTimeMin; }
void setTriggerTimeMax(float triggerTimeMax) { _triggerTimeMax = triggerTimeMax; }
void addToPrioritySum(float priority) { _totalPriorities += priority; }
void addState(RandomSwitchState::Pointer randomState);
void switchRandomState(const AnimVariantMap& animVars, const AnimContext& context, RandomSwitchState::Pointer desiredState, bool shouldInterp);
RandomSwitchState::Pointer evaluateTransitions(const AnimVariantMap& animVars) const;
// for AnimDebugDraw rendering
virtual const AnimPoseVec& getPosesInternal() const override;
AnimPoseVec _poses;
int _randomSwitchEvaluationCount { 0 };
// interpolation state
bool _duringInterp = false;
InterpType _interpType{ InterpType::SnapshotPrev };
float _alphaVel = 0.0f;
float _alpha = 0.0f;
AnimPoseVec _prevPoses;
AnimPoseVec _nextPoses;
float _totalPriorities { 0.0f };
RandomSwitchState::Pointer _currentState;
RandomSwitchState::Pointer _previousState;
std::vector<RandomSwitchState::Pointer> _randomStates;
QString _currentStateVar;
QString _triggerRandomSwitchVar;
QString _transitionVar;
float _triggerTimeMin { 10.0f };
float _triggerTimeMax { 20.0f };
float _triggerTime { 0.0f };
float _randomSwitchTimeMin { 10.0f };
float _randomSwitchTimeMax { 20.0f };
float _randomSwitchTime { 0.0f };
private:
// no copies
AnimRandomSwitch(const AnimRandomSwitch&) = delete;
AnimRandomSwitch& operator=(const AnimRandomSwitch&) = delete;
};
#endif // hifi_AnimRandomSwitch_h

View file

@ -128,7 +128,7 @@ const AnimPoseVec& AnimTwoBoneIK::evaluate(const AnimVariantMap& animVars, const
if (triggersOut.hasKey(endEffectorPositionVar)) {
targetPose.trans() = triggersOut.lookupRigToGeometry(endEffectorPositionVar, tipPose.trans());
} else if (animVars.hasKey(endEffectorRotationVar)) {
} else if (animVars.hasKey(endEffectorPositionVar)) {
targetPose.trans() = animVars.lookupRigToGeometry(endEffectorPositionVar, tipPose.trans());
}
@ -147,9 +147,11 @@ const AnimPoseVec& AnimTwoBoneIK::evaluate(const AnimVariantMap& animVars, const
// http://mathworld.wolfram.com/Circle-CircleIntersection.html
float midAngle = 0.0f;
if (d < r0 + r1) {
if ((d < r0 + r1) && (d > 0.0f) && (r0 > 0.0f) && (r1 > 0.0f)) {
float y = sqrtf((-d + r1 - r0) * (-d - r1 + r0) * (-d + r1 + r0) * (d + r1 + r0)) / (2.0f * d);
midAngle = PI - (acosf(y / r0) + acosf(y / r1));
float yR0Quotient = glm::clamp(y / r0, -1.0f, 1.0f);
float yR1Quotient = glm::clamp(y / r1, -1.0f, 1.0f);
midAngle = PI - (acosf(yR0Quotient) + acosf(yR1Quotient));
}
// compute midJoint rotation

View file

@ -142,3 +142,72 @@ glm::quat computeBodyFacingFromHead(const glm::quat& headRot, const glm::vec3& u
return glmExtractRotation(bodyMat);
}
const float INV_SQRT_3 = 1.0f / sqrtf(3.0f);
const int DOP14_COUNT = 14;
const glm::vec3 DOP14_NORMALS[DOP14_COUNT] = {
Vectors::UNIT_X,
-Vectors::UNIT_X,
Vectors::UNIT_Y,
-Vectors::UNIT_Y,
Vectors::UNIT_Z,
-Vectors::UNIT_Z,
glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3),
-glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3),
-glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3),
-glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3),
-glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3)
};
// returns true if the given point lies inside of the k-dop, specified by shapeInfo & shapePose.
// if the given point does lie within the k-dop, it also returns the amount of displacement necessary to push that point outward
// such that it lies on the surface of the kdop.
bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const HFMJointShapeInfo& shapeInfo, glm::vec3& displacementOut) {
// transform point into local space of jointShape.
glm::vec3 localPoint = shapePose.inverse().xformPoint(point);
// Only works for 14-dop shape infos.
if (shapeInfo.dots.size() != DOP14_COUNT) {
return false;
}
glm::vec3 minDisplacement(FLT_MAX);
float minDisplacementLen = FLT_MAX;
glm::vec3 p = localPoint - shapeInfo.avgPoint;
float pLen = glm::length(p);
if (pLen > 0.0f) {
int slabCount = 0;
for (int i = 0; i < DOP14_COUNT; i++) {
float dot = glm::dot(p, DOP14_NORMALS[i]);
if (dot > 0.0f && dot < shapeInfo.dots[i]) {
slabCount++;
float distToPlane = pLen * (shapeInfo.dots[i] / dot);
float displacementLen = distToPlane - pLen;
// keep track of the smallest displacement
if (displacementLen < minDisplacementLen) {
minDisplacementLen = displacementLen;
minDisplacement = (p / pLen) * displacementLen;
}
}
}
if (slabCount == (DOP14_COUNT / 2) && minDisplacementLen != FLT_MAX) {
// we are within the k-dop so push the point along the minimum displacement found
displacementOut = shapePose.xformVectorFast(minDisplacement);
return true;
} else {
// point is outside of kdop
return false;
}
} else {
// point is directly on top of shapeInfo.avgPoint.
// push the point out along the x axis.
displacementOut = shapePose.xformVectorFast(shapeInfo.points[0]);
return true;
}
}

View file

@ -128,4 +128,10 @@ protected:
bool _snapshotValid { false };
};
// returns true if the given point lies inside of the k-dop, specified by shapeInfo & shapePose.
// if the given point does lie within the k-dop, it also returns the amount of displacement necessary to push that point outward
// such that it lies on the surface of the kdop.
bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const HFMJointShapeInfo& shapeInfo, glm::vec3& displacementOut);
#endif

View file

@ -261,7 +261,7 @@ public:
qCDebug(animation) << " " << pair.first << "=" << pair.second.getString();
break;
default:
assert(("invalid AnimVariant::Type", false));
assert(false);
}
}
}

View file

@ -26,8 +26,9 @@ public:
* <tr><td><code>0</code></td><td>RotationAndPosition</td><td>Attempt to reach the rotation and position end
* effector.</td></tr>
* <tr><td><code>1</code></td><td>RotationOnly</td><td>Attempt to reach the end effector rotation only.</td></tr>
* <tr><td><code>2</code></td><td>HmdHead</td><td><strong>Deprecated:</strong> A special mode of IK that would attempt
* to prevent unnecessary bending of the spine.</td></tr>
* <tr><td><code>2</code></td><td>HmdHead</td><td>A special mode of IK that would attempt to prevent unnecessary
* bending of the spine.<br />
* <p class="important">Deprecated: This target type is deprecated and will be removed.</p></td></tr>
* <tr><td><code>3</code></td><td>HipsRelativeRotationAndPosition</td><td>Attempt to reach a rotation and position end
* effector that is not in absolute rig coordinates but is offset by the avatar hips translation.</td></tr>
* <tr><td><code>4</code></td><td>Spline</td><td>Use a cubic Hermite spline to model the human spine. This prevents

View file

@ -370,6 +370,88 @@ void Rig::restoreAnimation() {
}
}
void Rig::overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame) {
HandAnimState::ClipNodeEnum clipNodeEnum;
if (isLeft) {
if (_leftHandAnimState.clipNodeEnum == HandAnimState::None || _leftHandAnimState.clipNodeEnum == HandAnimState::B) {
clipNodeEnum = HandAnimState::A;
} else {
clipNodeEnum = HandAnimState::B;
}
} else {
if (_rightHandAnimState.clipNodeEnum == HandAnimState::None || _rightHandAnimState.clipNodeEnum == HandAnimState::B) {
clipNodeEnum = HandAnimState::A;
} else {
clipNodeEnum = HandAnimState::B;
}
}
if (_animNode) {
std::shared_ptr<AnimClip> clip;
if (isLeft) {
if (clipNodeEnum == HandAnimState::A) {
clip = std::dynamic_pointer_cast<AnimClip>(_animNode->findByName("leftHandAnimA"));
} else {
clip = std::dynamic_pointer_cast<AnimClip>(_animNode->findByName("leftHandAnimB"));
}
} else {
if (clipNodeEnum == HandAnimState::A) {
clip = std::dynamic_pointer_cast<AnimClip>(_animNode->findByName("rightHandAnimA"));
} else {
clip = std::dynamic_pointer_cast<AnimClip>(_animNode->findByName("rightHandAnimB"));
}
}
if (clip) {
// set parameters
clip->setLoopFlag(loop);
clip->setStartFrame(firstFrame);
clip->setEndFrame(lastFrame);
const float REFERENCE_FRAMES_PER_SECOND = 30.0f;
float timeScale = fps / REFERENCE_FRAMES_PER_SECOND;
clip->setTimeScale(timeScale);
clip->loadURL(url);
}
}
// notify the handAnimStateMachine the desired state.
if (isLeft) {
// store current hand anim state.
_leftHandAnimState = { clipNodeEnum, url, fps, loop, firstFrame, lastFrame };
_animVars.set("leftHandAnimNone", false);
_animVars.set("leftHandAnimA", clipNodeEnum == HandAnimState::A);
_animVars.set("leftHandAnimB", clipNodeEnum == HandAnimState::B);
} else {
// store current hand anim state.
_rightHandAnimState = { clipNodeEnum, url, fps, loop, firstFrame, lastFrame };
_animVars.set("rightHandAnimNone", false);
_animVars.set("rightHandAnimA", clipNodeEnum == HandAnimState::A);
_animVars.set("rightHandAnimB", clipNodeEnum == HandAnimState::B);
}
}
void Rig::restoreHandAnimation(bool isLeft) {
if (isLeft) {
if (_leftHandAnimState.clipNodeEnum != HandAnimState::None) {
_leftHandAnimState.clipNodeEnum = HandAnimState::None;
// notify the handAnimStateMachine the desired state.
_animVars.set("leftHandAnimNone", true);
_animVars.set("leftHandAnimA", false);
_animVars.set("leftHandAnimB", false);
}
} else {
if (_rightHandAnimState.clipNodeEnum != HandAnimState::None) {
_rightHandAnimState.clipNodeEnum = HandAnimState::None;
// notify the handAnimStateMachine the desired state.
_animVars.set("rightHandAnimNone", true);
_animVars.set("rightHandAnimA", false);
_animVars.set("rightHandAnimB", false);
}
}
}
void Rig::overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) {
NetworkAnimState::ClipNodeEnum clipNodeEnum = NetworkAnimState::None;
@ -1398,13 +1480,15 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons
if (_animNode && _enabledAnimations) {
DETAILED_PERFORMANCE_TIMER("handleTriggers");
++_evaluationCount;
updateAnimationStateHandlers();
_animVars.setRigToGeometryTransform(_rigToGeometryTransform);
if (_networkNode) {
_networkVars.setRigToGeometryTransform(_rigToGeometryTransform);
}
AnimContext context(_enableDebugDrawIKTargets, _enableDebugDrawIKConstraints, _enableDebugDrawIKChains,
getGeometryToRigTransform(), rigToWorldTransform);
getGeometryToRigTransform(), rigToWorldTransform, _evaluationCount);
// evaluate the animation
AnimVariantMap triggersOut;
@ -1521,74 +1605,6 @@ void Rig::updateHead(bool headEnabled, bool hipsEnabled, const AnimPose& headPos
}
}
const float INV_SQRT_3 = 1.0f / sqrtf(3.0f);
const int DOP14_COUNT = 14;
const glm::vec3 DOP14_NORMALS[DOP14_COUNT] = {
Vectors::UNIT_X,
-Vectors::UNIT_X,
Vectors::UNIT_Y,
-Vectors::UNIT_Y,
Vectors::UNIT_Z,
-Vectors::UNIT_Z,
glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3),
-glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3),
-glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3),
-glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3),
-glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3)
};
// returns true if the given point lies inside of the k-dop, specified by shapeInfo & shapePose.
// if the given point does lie within the k-dop, it also returns the amount of displacement necessary to push that point outward
// such that it lies on the surface of the kdop.
static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const HFMJointShapeInfo& shapeInfo, glm::vec3& displacementOut) {
// transform point into local space of jointShape.
glm::vec3 localPoint = shapePose.inverse().xformPoint(point);
// Only works for 14-dop shape infos.
if (shapeInfo.dots.size() != DOP14_COUNT) {
return false;
}
glm::vec3 minDisplacement(FLT_MAX);
float minDisplacementLen = FLT_MAX;
glm::vec3 p = localPoint - shapeInfo.avgPoint;
float pLen = glm::length(p);
if (pLen > 0.0f) {
int slabCount = 0;
for (int i = 0; i < DOP14_COUNT; i++) {
float dot = glm::dot(p, DOP14_NORMALS[i]);
if (dot > 0.0f && dot < shapeInfo.dots[i]) {
slabCount++;
float distToPlane = pLen * (shapeInfo.dots[i] / dot);
float displacementLen = distToPlane - pLen;
// keep track of the smallest displacement
if (displacementLen < minDisplacementLen) {
minDisplacementLen = displacementLen;
minDisplacement = (p / pLen) * displacementLen;
}
}
}
if (slabCount == (DOP14_COUNT / 2) && minDisplacementLen != FLT_MAX) {
// we are within the k-dop so push the point along the minimum displacement found
displacementOut = shapePose.xformVectorFast(minDisplacement);
return true;
} else {
// point is outside of kdop
return false;
}
} else {
// point is directly on top of shapeInfo.avgPoint.
// push the point out along the x axis.
displacementOut = shapePose.xformVectorFast(shapeInfo.points[0]);
return true;
}
}
glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo,
const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo) const {
glm::vec3 position = handPosition;
@ -1995,8 +2011,35 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo
return;
}
_animVars.set("isTalking", params.isTalking);
_animVars.set("notIsTalking", !params.isTalking);
if (_previousIsTalking != params.isTalking) {
if (_talkIdleInterpTime < 1.0f) {
_talkIdleInterpTime = 1.0f - _talkIdleInterpTime;
} else {
_talkIdleInterpTime = 0.0f;
}
}
_previousIsTalking = params.isTalking;
const float TOTAL_EASE_IN_TIME = 0.75f;
const float TOTAL_EASE_OUT_TIME = 1.5f;
if (params.isTalking) {
if (_talkIdleInterpTime < 1.0f) {
_talkIdleInterpTime += dt / TOTAL_EASE_IN_TIME;
float easeOutInValue = _talkIdleInterpTime < 0.5f ? 4.0f * powf(_talkIdleInterpTime, 3.0f) : 4.0f * powf((_talkIdleInterpTime - 1.0f), 3.0f) + 1.0f;
_animVars.set("idleOverlayAlpha", easeOutInValue);
} else {
_animVars.set("idleOverlayAlpha", 1.0f);
}
} else {
if (_talkIdleInterpTime < 1.0f) {
_talkIdleInterpTime += dt / TOTAL_EASE_OUT_TIME;
float easeOutInValue = _talkIdleInterpTime < 0.5f ? 4.0f * powf(_talkIdleInterpTime, 3.0f) : 4.0f * powf((_talkIdleInterpTime - 1.0f), 3.0f) + 1.0f;
float talkAlpha = 1.0f - easeOutInValue;
_animVars.set("idleOverlayAlpha", talkAlpha);
} else {
_animVars.set("idleOverlayAlpha", 0.0f);
}
}
_headEnabled = params.primaryControllerFlags[PrimaryControllerType_Head] & (uint8_t)ControllerFlags::Enabled;
bool leftHandEnabled = params.primaryControllerFlags[PrimaryControllerType_LeftHand] & (uint8_t)ControllerFlags::Enabled;
@ -2136,6 +2179,20 @@ void Rig::initAnimGraph(const QUrl& url) {
overrideAnimation(origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame);
}
if (_rightHandAnimState.clipNodeEnum != HandAnimState::None) {
// restore the right hand animation we had before reset.
HandAnimState origState = _rightHandAnimState;
_rightHandAnimState = { HandAnimState::None, "", 30.0f, false, 0.0f, 0.0f };
overrideHandAnimation(false, origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame);
}
if (_leftHandAnimState.clipNodeEnum != HandAnimState::None) {
// restore the left hand animation we had before reset.
HandAnimState origState = _leftHandAnimState;
_leftHandAnimState = { HandAnimState::None, "", 30.0f, false, 0.0f, 0.0f };
overrideHandAnimation(true, origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame);
}
// restore the role animations we had before reset.
for (auto& roleAnimState : _roleAnimStates) {
auto roleState = roleAnimState.second;

View file

@ -116,8 +116,12 @@ public:
void destroyAnimGraph();
void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame);
bool isPlayingOverrideAnimation() const { return _userAnimState.clipNodeEnum != UserAnimState::None; };
void restoreAnimation();
void overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame);
void restoreHandAnimation(bool isLeft);
void overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame);
void triggerNetworkRole(const QString& role);
void restoreNetworkAnimation();
@ -333,7 +337,7 @@ protected:
RigRole _state { RigRole::Idle };
RigRole _desiredState { RigRole::Idle };
float _desiredStateAge { 0.0f };
struct NetworkAnimState {
enum ClipNodeEnum {
None = 0,
@ -356,6 +360,27 @@ protected:
float blendTime;
};
struct HandAnimState {
enum ClipNodeEnum {
None = 0,
A,
B
};
HandAnimState() : clipNodeEnum(HandAnimState::None) {}
HandAnimState(ClipNodeEnum clipNodeEnumIn, const QString& urlIn, float fpsIn, bool loopIn, float firstFrameIn, float lastFrameIn) :
clipNodeEnum(clipNodeEnumIn), url(urlIn), fps(fpsIn), loop(loopIn), firstFrame(firstFrameIn), lastFrame(lastFrameIn) {
}
ClipNodeEnum clipNodeEnum;
QString url;
float fps;
bool loop;
float firstFrame;
float lastFrame;
};
struct UserAnimState {
enum ClipNodeEnum {
None = 0,
@ -390,10 +415,15 @@ protected:
UserAnimState _userAnimState;
NetworkAnimState _networkAnimState;
HandAnimState _rightHandAnimState;
HandAnimState _leftHandAnimState;
std::map<QString, RoleAnimState> _roleAnimStates;
int _evaluationCount{ 0 };
float _leftHandOverlayAlpha { 0.0f };
float _rightHandOverlayAlpha { 0.0f };
float _talkIdleInterpTime { 0.0f };
bool _previousIsTalking { false };
SimpleMovingAverage _averageForwardSpeed { 10 };
SimpleMovingAverage _averageLateralSpeed { 10 };

View file

@ -48,21 +48,23 @@ QScriptValue injectorOptionsToScriptValue(QScriptEngine* engine, const AudioInje
}
/**jsdoc
* Configures how an audio injector plays its audio.
* Configures where and how an audio injector plays its audio.
* @typedef {object} AudioInjector.AudioInjectorOptions
* @property {Vec3} position=Vec3.ZERO - The position in the domain to play the sound.
* @property {Quat} orientation=Quat.IDENTITY - The orientation in the domain to play the sound in.
* @property {number} volume=1.0 - Playback volume, between <code>0.0</code> and <code>1.0</code>.
* @property {number} pitch=1.0 - Alter the pitch of the sound, within +/- 2 octaves. The value is the relative sample rate to
* resample the sound at, range <code>0.0625</code> &ndash; <code>16.0</code>. A value of <code>0.0625</code> lowers the
* pitch by 2 octaves; <code>1.0</code> is no change in pitch; <code>16.0</code> raises the pitch by 2 octaves.
* resample the sound at, range <code>0.0625</code> &ndash; <code>16.0</code>.<br />
* A value of <code>0.0625</code> lowers the pitch by 2 octaves.<br />
* A value of <code>1.0</code> means there is no change in pitch.<br />
* A value of <code>16.0</code> raises the pitch by 2 octaves.
* @property {boolean} loop=false - If <code>true</code>, the sound is played repeatedly until playback is stopped.
* @property {number} secondOffset=0 - Starts playback from a specified time (seconds) within the sound file, &ge;
* <code>0</code>.
* @property {boolean} localOnly=false - IF <code>true</code>, the sound is played back locally on the client rather than to
* @property {boolean} localOnly=false - If <code>true</code>, the sound is played back locally on the client rather than to
* others via the audio mixer.
* @property {boolean} ignorePenumbra=false - <strong>Deprecated:</strong> This property is deprecated and will be
* removed.
* @property {boolean} ignorePenumbra=false - <p class="important">Deprecated: This property is deprecated and will be
* removed.</p>
*/
void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOptions& injectorOptions) {
if (!object.isObject()) {

View file

@ -124,7 +124,7 @@ typedef QSharedPointer<Sound> SharedSoundPointer;
* An audio resource, created by {@link SoundCache.getSound}, to be played back using {@link Audio.playSound}.
* <p>Supported formats:</p>
* <ul>
* <li>WAV: 16-bit uncompressed WAV at any sample rate, with 1 (mono), 2 (stereo), or 4 (ambisonic) channels.</li>
* <li>WAV: 16-bit uncompressed at any sample rate, with 1 (mono), 2 (stereo), or 4 (ambisonic) channels.</li>
* <li>MP3: Mono or stereo, at any sample rate.</li>
* <li>RAW: 48khz 16-bit mono or stereo. File name must include <code>".stereo"</code> to be interpreted as stereo.</li>
* </ul>

View file

@ -1469,6 +1469,37 @@ QStringList Avatar::getJointNames() const {
return result;
}
std::vector<AvatarSkeletonTrait::UnpackedJointData> Avatar::getSkeletonDefaultData() {
std::vector<AvatarSkeletonTrait::UnpackedJointData> defaultSkeletonData;
if (_skeletonModel->isLoaded()) {
auto& model = _skeletonModel->getHFMModel();
auto& rig = _skeletonModel->getRig();
float geometryToRigScale = extractScale(rig.getGeometryToRigTransform())[0];
QStringList jointNames = getJointNames();
int sizeCount = 0;
for (int i = 0; i < jointNames.size(); i++) {
AvatarSkeletonTrait::UnpackedJointData jointData;
jointData.jointIndex = i;
jointData.parentIndex = rig.getJointParentIndex(i);
if (jointData.parentIndex == -1) {
jointData.boneType = model.joints[i].isSkeletonJoint ? AvatarSkeletonTrait::BoneType::SkeletonRoot : AvatarSkeletonTrait::BoneType::NonSkeletonRoot;
} else {
jointData.boneType = model.joints[i].isSkeletonJoint ? AvatarSkeletonTrait::BoneType::SkeletonChild : AvatarSkeletonTrait::BoneType::NonSkeletonChild;
}
jointData.defaultRotation = rig.getAbsoluteDefaultPose(i).rot();
jointData.defaultTranslation = getDefaultJointTranslation(i);
float jointLocalScale = extractScale(model.joints[i].transform)[0];
jointData.defaultScale = jointLocalScale / geometryToRigScale;
jointData.jointName = jointNames[i];
jointData.stringLength = jointNames[i].size();
jointData.stringStart = sizeCount;
sizeCount += jointNames[i].size();
defaultSkeletonData.push_back(jointData);
}
}
return defaultSkeletonData;
}
glm::vec3 Avatar::getJointPosition(int index) const {
glm::vec3 position;
_skeletonModel->getJointPositionInWorldFrame(index, position);
@ -1535,6 +1566,8 @@ void Avatar::rigReady() {
buildSpine2SplineRatioCache();
computeMultiSphereShapes();
buildSpine2SplineRatioCache();
setSkeletonData(getSkeletonDefaultData());
sendSkeletonData();
}
// rig has been reset.

View file

@ -199,6 +199,8 @@ public:
virtual int getJointIndex(const QString& name) const override;
virtual QStringList getJointNames() const override;
std::vector<AvatarSkeletonTrait::UnpackedJointData> getSkeletonDefaultData();
/**jsdoc
* Gets the default rotation of a joint (in the current avatar) relative to its parent.
* <p>For information on the joint hierarchy used, see

View file

@ -270,28 +270,19 @@ bool SkeletonModel::getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3&
getJointPosition(_rig.indexOfJoint("RightEye"), secondEyePosition)) {
return true;
}
// no eye joints; try to estimate based on head/neck joints
glm::vec3 neckPosition, headPosition;
if (getJointPosition(_rig.indexOfJoint("Neck"), neckPosition) &&
getJointPosition(_rig.indexOfJoint("Head"), headPosition)) {
const float EYE_PROPORTION = 0.6f;
glm::vec3 baseEyePosition = glm::mix(neckPosition, headPosition, EYE_PROPORTION);
int headJointIndex = _rig.indexOfJoint("Head");
glm::vec3 headPosition;
if (getJointPosition(headJointIndex, headPosition)) {
// get head joint rotation.
glm::quat headRotation;
getJointRotation(_rig.indexOfJoint("Head"), headRotation);
const float EYES_FORWARD = 0.25f;
const float EYE_SEPARATION = 0.1f;
float headHeight = glm::distance(neckPosition, headPosition);
firstEyePosition = baseEyePosition + headRotation * glm::vec3(EYE_SEPARATION, 0.0f, EYES_FORWARD) * headHeight;
secondEyePosition = baseEyePosition + headRotation * glm::vec3(-EYE_SEPARATION, 0.0f, EYES_FORWARD) * headHeight;
return true;
} else if (getJointPosition(_rig.indexOfJoint("Head"), headPosition)) {
glm::vec3 baseEyePosition = headPosition;
glm::quat headRotation;
getJointRotation(_rig.indexOfJoint("Head"), headRotation);
const float EYES_FORWARD_HEAD_ONLY = 0.30f;
const float EYE_SEPARATION = 0.1f;
firstEyePosition = baseEyePosition + headRotation * glm::vec3(EYE_SEPARATION, 0.0f, EYES_FORWARD_HEAD_ONLY);
secondEyePosition = baseEyePosition + headRotation * glm::vec3(-EYE_SEPARATION, 0.0f, EYES_FORWARD_HEAD_ONLY);
getJointRotation(headJointIndex, headRotation);
float heightRatio = _rig.getUnscaledEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT;
glm::vec3 ipdOffset = glm::vec3(DEFAULT_AVATAR_IPD / 2.0f, 0.0f, 0.0f);
firstEyePosition = headPosition + headRotation * heightRatio * (DEFAULT_AVATAR_HEAD_TO_MIDDLE_EYE_OFFSET + ipdOffset);
secondEyePosition = headPosition + headRotation * heightRatio * (DEFAULT_AVATAR_HEAD_TO_MIDDLE_EYE_OFFSET - ipdOffset);
return true;
}
return false;

View file

@ -55,7 +55,7 @@ using namespace std;
const QString AvatarData::FRAME_NAME = "com.highfidelity.recording.AvatarData";
static const int TRANSLATION_COMPRESSION_RADIX = 14;
static const int FAUX_JOINT_COMPRESSION_RADIX = 12;
static const int HAND_CONTROLLER_COMPRESSION_RADIX = 12;
static const int SENSOR_TO_WORLD_SCALE_RADIX = 10;
static const float AUDIO_LOUDNESS_SCALE = 1024.0f;
static const float DEFAULT_AVATAR_DENSITY = 1000.0f; // density of water
@ -66,7 +66,7 @@ size_t AvatarDataPacket::maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients
return FACE_TRACKER_INFO_SIZE + numBlendshapeCoefficients * sizeof(float);
}
size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints) {
size_t AvatarDataPacket::maxJointDataSize(size_t numJoints) {
const size_t validityBitsSize = calcBitVectorSize((int)numJoints);
size_t totalSize = sizeof(uint8_t); // numJoints
@ -76,14 +76,6 @@ size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints)
totalSize += validityBitsSize; // Translations mask
totalSize += sizeof(float); // maxTranslationDimension
totalSize += numJoints * sizeof(SixByteTrans); // Translations
size_t NUM_FAUX_JOINT = 2;
totalSize += NUM_FAUX_JOINT * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints
if (hasGrabJoints) {
totalSize += sizeof(AvatarDataPacket::FarGrabJoints);
}
return totalSize;
}
@ -98,9 +90,6 @@ size_t AvatarDataPacket::minJointDataSize(size_t numJoints) {
totalSize += sizeof(float); // maxTranslationDimension
// assume no valid translations
size_t NUM_FAUX_JOINT = 2;
totalSize += NUM_FAUX_JOINT * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints
return totalSize;
}
@ -329,6 +318,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
// separately
bool hasParentInfo = false;
bool hasAvatarLocalPosition = false;
bool hasHandControllers = false;
bool hasFaceTrackerInfo = false;
@ -346,7 +336,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
hasAvatarLocalPosition = hasParent() && (sendAll ||
tranlationChangedSince(lastSentTime) ||
parentInfoChangedSince(lastSentTime));
hasHandControllers = _controllerLeftHandMatrixCache.isValid() || _controllerRightHandMatrixCache.isValid();
hasFaceTrackerInfo = !dropFaceTracking && (hasFaceTracker() || getHasScriptedBlendshapes()) &&
(sendAll || faceTrackerInfoChangedSince(lastSentTime));
hasJointData = !sendMinimum;
@ -364,6 +354,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
| (hasAdditionalFlags ? AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS : 0)
| (hasParentInfo ? AvatarDataPacket::PACKET_HAS_PARENT_INFO : 0)
| (hasAvatarLocalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION : 0)
| (hasHandControllers ? AvatarDataPacket::PACKET_HAS_HAND_CONTROLLERS : 0)
| (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0)
| (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0)
| (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0)
@ -406,7 +397,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + NUM_BYTES_RFC4122_UUID +
AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) +
AvatarDataPacket::maxJointDataSize(_jointData.size(), true) +
AvatarDataPacket::maxJointDataSize(_jointData.size()) +
AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size());
if (maxDataSize == 0) {
@ -592,7 +583,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
IF_AVATAR_SPACE(PACKET_HAS_AVATAR_LOCAL_POSITION, sizeof(getLocalPosition()) ) {
IF_AVATAR_SPACE(PACKET_HAS_AVATAR_LOCAL_POSITION, AvatarDataPacket::AVATAR_LOCAL_POSITION_SIZE) {
auto startSection = destinationBuffer;
const auto localPosition = getLocalPosition();
AVATAR_MEMCPY(localPosition);
@ -603,6 +594,23 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
IF_AVATAR_SPACE(PACKET_HAS_HAND_CONTROLLERS, AvatarDataPacket::HAND_CONTROLLERS_SIZE) {
auto startSection = destinationBuffer;
Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix());
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerLeftHandTransform.getRotation());
destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerLeftHandTransform.getTranslation(), HAND_CONTROLLER_COMPRESSION_RADIX);
Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix());
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerRightHandTransform.getRotation());
destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(), HAND_CONTROLLER_COMPRESSION_RADIX);
int numBytes = destinationBuffer - startSection;
if (outboundDataRateOut) {
outboundDataRateOut->handControllersRate.increment(numBytes);
}
}
const auto& blendshapeCoefficients = _headData->getBlendshapeCoefficients();
// If it is connected, pack up the data
IF_AVATAR_SPACE(PACKET_HAS_FACE_TRACKER_INFO, sizeof(AvatarDataPacket::FaceTrackerInfo) + (size_t)blendshapeCoefficients.size() * sizeof(float)) {
@ -638,9 +646,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
// include jointData if there is room for the most minimal section. i.e. no translations or rotations.
IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, AvatarDataPacket::minJointDataSize(numJoints)) {
// Minimum space required for another rotation joint -
// size of joint + following translation bit-vector + translation scale + faux joints:
const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize +
sizeof(float) + AvatarDataPacket::FAUX_JOINTS_SIZE;
// size of joint + following translation bit-vector + translation scale:
const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize + sizeof(float);
auto startSection = destinationBuffer;
@ -759,17 +766,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
sendStatus.translationsSent = i;
// faux joints
Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix());
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerLeftHandTransform.getRotation());
destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerLeftHandTransform.getTranslation(),
FAUX_JOINT_COMPRESSION_RADIX);
Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix());
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerRightHandTransform.getRotation());
destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(),
FAUX_JOINT_COMPRESSION_RADIX);
IF_AVATAR_SPACE(PACKET_HAS_GRAB_JOINTS, sizeof (AvatarDataPacket::FarGrabJoints)) {
// the far-grab joints may range further than 3 meters, so we can't use packFloatVec3ToSignedTwoByteFixed etc
auto startSection = destinationBuffer;
@ -902,12 +898,12 @@ bool AvatarData::shouldLogError(const quint64& now) {
}
const unsigned char* unpackFauxJoint(const unsigned char* sourceBuffer, ThreadSafeValueCache<glm::mat4>& matrixCache) {
const unsigned char* unpackHandController(const unsigned char* sourceBuffer, ThreadSafeValueCache<glm::mat4>& matrixCache) {
glm::quat orientation;
glm::vec3 position;
Transform transform;
sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, orientation);
sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, position, FAUX_JOINT_COMPRESSION_RADIX);
sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, position, HAND_CONTROLLER_COMPRESSION_RADIX);
transform.setTranslation(position);
transform.setRotation(orientation);
matrixCache.set(transform.getMatrix());
@ -952,6 +948,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
bool hasAdditionalFlags = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS);
bool hasParentInfo = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_PARENT_INFO);
bool hasAvatarLocalPosition = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION);
bool hasHandControllers = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_HAND_CONTROLLERS);
bool hasFaceTrackerInfo = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO);
bool hasJointData = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_JOINT_DATA);
bool hasJointDefaultPoseFlags = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS);
@ -1240,6 +1237,20 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
_localPositionUpdateRate.increment();
}
if (hasHandControllers) {
auto startSection = sourceBuffer;
sourceBuffer = unpackHandController(sourceBuffer, _controllerLeftHandMatrixCache);
sourceBuffer = unpackHandController(sourceBuffer, _controllerRightHandMatrixCache);
int numBytesRead = sourceBuffer - startSection;
_handControllersRate.increment(numBytesRead);
_handControllersUpdateRate.increment();
} else {
_controllerLeftHandMatrixCache.invalidate();
_controllerRightHandMatrixCache.invalidate();
}
if (hasFaceTrackerInfo) {
auto startSection = sourceBuffer;
@ -1351,10 +1362,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
<< "size:" << (int)(sourceBuffer - startPosition);
}
#endif
// faux joints
sourceBuffer = unpackFauxJoint(sourceBuffer, _controllerLeftHandMatrixCache);
sourceBuffer = unpackFauxJoint(sourceBuffer, _controllerRightHandMatrixCache);
int numBytesRead = sourceBuffer - startSection;
_jointDataRate.increment(numBytesRead);
_jointDataUpdateRate.increment();
@ -1445,6 +1452,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
* <tbody>
* <tr><td><code>"globalPosition"</code></td><td>Incoming global position.</td></tr>
* <tr><td><code>"localPosition"</code></td><td>Incoming local position.</td></tr>
* <tr><td><code>"handControllers"</code></td><td>Incoming hand controllers.</td></tr>
* <tr><td><code>"avatarBoundingBox"</code></td><td>Incoming avatar bounding box.</td></tr>
* <tr><td><code>"avatarOrientation"</code></td><td>Incoming avatar orientation.</td></tr>
* <tr><td><code>"avatarScale"</code></td><td>Incoming avatar scale.</td></tr>
@ -1483,6 +1491,8 @@ float AvatarData::getDataRate(const QString& rateName) const {
return _globalPositionRate.rate() / BYTES_PER_KILOBIT;
} else if (rateName == "localPosition") {
return _localPositionRate.rate() / BYTES_PER_KILOBIT;
} else if (rateName == "handControllers") {
return _handControllersRate.rate() / BYTES_PER_KILOBIT;
} else if (rateName == "avatarBoundingBox") {
return _avatarBoundingBoxRate.rate() / BYTES_PER_KILOBIT;
} else if (rateName == "avatarOrientation") {
@ -1547,6 +1557,7 @@ float AvatarData::getDataRate(const QString& rateName) const {
* <tbody>
* <tr><td><code>"globalPosition"</code></td><td>Global position.</td></tr>
* <tr><td><code>"localPosition"</code></td><td>Local position.</td></tr>
* <tr><td><code>"handControllers"</code></td><td>Hand controller positions and orientations.</td></tr>
* <tr><td><code>"avatarBoundingBox"</code></td><td>Avatar bounding box.</td></tr>
* <tr><td><code>"avatarOrientation"</code></td><td>Avatar orientation.</td></tr>
* <tr><td><code>"avatarScale"</code></td><td>Avatar scale.</td></tr>
@ -1571,6 +1582,8 @@ float AvatarData::getUpdateRate(const QString& rateName) const {
return _globalPositionUpdateRate.rate();
} else if (rateName == "localPosition") {
return _localPositionUpdateRate.rate();
} else if (rateName == "handControllers") {
return _handControllersUpdateRate.rate();
} else if (rateName == "avatarBoundingBox") {
return _avatarBoundingBoxUpdateRate.rate();
} else if (rateName == "avatarOrientation") {
@ -1633,6 +1646,13 @@ void AvatarData::setJointData(int index, const glm::quat& rotation, const glm::v
data.translationIsDefaultPose = false;
}
QVector<JointData> AvatarData::getJointData() const {
QVector<JointData> jointData;
QReadLocker readLock(&_jointDataLock);
jointData = _jointData;
return jointData;
}
void AvatarData::clearJointData(int index) {
if (index < 0 || index >= LOWEST_PSEUDO_JOINT_INDEX) {
return;
@ -1987,11 +2007,98 @@ QUrl AvatarData::getWireSafeSkeletonModelURL() const {
return QUrl();
}
}
QByteArray AvatarData::packSkeletonData() const {
// Send an avatar trait packet with the skeleton data before the mesh is loaded
int avatarDataSize = 0;
QByteArray avatarDataByteArray;
_avatarSkeletonDataLock.withReadLock([&] {
// Add header
AvatarSkeletonTrait::Header header;
header.maxScaleDimension = 0.0f;
header.maxTranslationDimension = 0.0f;
header.numJoints = (uint8_t)_avatarSkeletonData.size();
header.stringTableLength = 0;
for (size_t i = 0; i < _avatarSkeletonData.size(); i++) {
header.stringTableLength += (uint16_t)_avatarSkeletonData[i].jointName.size();
auto& translation = _avatarSkeletonData[i].defaultTranslation;
header.maxTranslationDimension = std::max(header.maxTranslationDimension, std::max(std::max(translation.x, translation.y), translation.z));
header.maxScaleDimension = std::max(header.maxScaleDimension, _avatarSkeletonData[i].defaultScale);
}
const int byteArraySize = (int)sizeof(AvatarSkeletonTrait::Header) + (int)(header.numJoints * sizeof(AvatarSkeletonTrait::JointData)) + header.stringTableLength;
avatarDataByteArray = QByteArray(byteArraySize, 0);
unsigned char* destinationBuffer = reinterpret_cast<unsigned char*>(avatarDataByteArray.data());
const unsigned char* const startPosition = destinationBuffer;
memcpy(destinationBuffer, &header, sizeof(header));
destinationBuffer += sizeof(AvatarSkeletonTrait::Header);
QString stringTable = "";
for (size_t i = 0; i < _avatarSkeletonData.size(); i++) {
AvatarSkeletonTrait::JointData jdata;
jdata.boneType = _avatarSkeletonData[i].boneType;
jdata.parentIndex = _avatarSkeletonData[i].parentIndex;
packFloatRatioToTwoByte((uint8_t*)(&jdata.defaultScale), _avatarSkeletonData[i].defaultScale / header.maxScaleDimension);
packOrientationQuatToSixBytes(jdata.defaultRotation, _avatarSkeletonData[i].defaultRotation);
packFloatVec3ToSignedTwoByteFixed(jdata.defaultTranslation, _avatarSkeletonData[i].defaultTranslation / header.maxTranslationDimension, TRANSLATION_COMPRESSION_RADIX);
jdata.jointIndex = (uint16_t)i;
jdata.stringStart = (uint16_t)_avatarSkeletonData[i].stringStart;
jdata.stringLength = (uint8_t)_avatarSkeletonData[i].stringLength;
stringTable += _avatarSkeletonData[i].jointName;
memcpy(destinationBuffer, &jdata, sizeof(AvatarSkeletonTrait::JointData));
destinationBuffer += sizeof(AvatarSkeletonTrait::JointData);
}
memcpy(destinationBuffer, stringTable.toUtf8(), header.stringTableLength);
destinationBuffer += header.stringTableLength;
avatarDataSize = destinationBuffer - startPosition;
});
return avatarDataByteArray.left(avatarDataSize);
}
QByteArray AvatarData::packSkeletonModelURL() const {
return getWireSafeSkeletonModelURL().toEncoded();
}
void AvatarData::unpackSkeletonData(const QByteArray& data) {
const unsigned char* startPosition = reinterpret_cast<const unsigned char*>(data.data());
const unsigned char* sourceBuffer = startPosition;
auto header = reinterpret_cast<const AvatarSkeletonTrait::Header*>(sourceBuffer);
sourceBuffer += sizeof(const AvatarSkeletonTrait::Header);
std::vector<AvatarSkeletonTrait::UnpackedJointData> joints;
for (uint8_t i = 0; i < header->numJoints; i++) {
auto jointData = reinterpret_cast<const AvatarSkeletonTrait::JointData*>(sourceBuffer);
sourceBuffer += sizeof(const AvatarSkeletonTrait::JointData);
AvatarSkeletonTrait::UnpackedJointData uJointData;
uJointData.boneType = (int)jointData->boneType;
uJointData.jointIndex = (int)i;
uJointData.stringLength = (int)jointData->stringLength;
uJointData.stringStart = (int)jointData->stringStart;
uJointData.parentIndex = ((uJointData.boneType == AvatarSkeletonTrait::BoneType::SkeletonRoot) ||
(uJointData.boneType == AvatarSkeletonTrait::BoneType::NonSkeletonRoot)) ? -1 : (int)jointData->parentIndex;
unpackOrientationQuatFromSixBytes(reinterpret_cast<const unsigned char*>(&jointData->defaultRotation), uJointData.defaultRotation);
unpackFloatVec3FromSignedTwoByteFixed(reinterpret_cast<const unsigned char*>(&jointData->defaultTranslation), uJointData.defaultTranslation, TRANSLATION_COMPRESSION_RADIX);
unpackFloatRatioFromTwoByte(reinterpret_cast<const unsigned char*>(&jointData->defaultScale), uJointData.defaultScale);
uJointData.defaultTranslation *= header->maxTranslationDimension;
uJointData.defaultScale *= header->maxScaleDimension;
joints.push_back(uJointData);
}
QString table = QString::fromUtf8(reinterpret_cast<const char*>(sourceBuffer), (int)header->stringTableLength);
for (size_t i = 0; i < joints.size(); i++) {
QStringRef subString(&table, joints[i].stringStart, joints[i].stringLength);
joints[i].jointName = subString.toString();
}
if (_clientTraitsHandler) {
_clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonData);
}
setSkeletonData(joints);
}
void AvatarData::unpackSkeletonModelURL(const QByteArray& data) {
auto skeletonModelURL = QUrl::fromEncoded(data);
setSkeletonModelURL(skeletonModelURL);
@ -2027,6 +2134,8 @@ QByteArray AvatarData::packTrait(AvatarTraits::TraitType traitType) const {
// Call packer function
if (traitType == AvatarTraits::SkeletonModelURL) {
traitBinaryData = packSkeletonModelURL();
} else if (traitType == AvatarTraits::SkeletonData) {
traitBinaryData = packSkeletonData();
}
return traitBinaryData;
@ -2048,6 +2157,8 @@ QByteArray AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, Avat
void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) {
if (traitType == AvatarTraits::SkeletonModelURL) {
unpackSkeletonModelURL(traitBinaryData);
} else if (traitType == AvatarTraits::SkeletonData) {
unpackSkeletonData(traitBinaryData);
}
}
@ -2110,7 +2221,6 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) {
}
_skeletonModelURL = expanded;
if (_clientTraitsHandler) {
_clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonModelURL);
}
@ -3008,6 +3118,26 @@ AABox AvatarData::computeBubbleBox(float bubbleScale) const {
return box;
}
void AvatarData::setSkeletonData(const std::vector<AvatarSkeletonTrait::UnpackedJointData>& skeletonData) {
_avatarSkeletonDataLock.withWriteLock([&] {
_avatarSkeletonData = skeletonData;
});
}
std::vector<AvatarSkeletonTrait::UnpackedJointData> AvatarData::getSkeletonData() const {
std::vector<AvatarSkeletonTrait::UnpackedJointData> skeletonData;
_avatarSkeletonDataLock.withReadLock([&] {
skeletonData = _avatarSkeletonData;
});
return skeletonData;
}
void AvatarData::sendSkeletonData() const{
if (_clientTraitsHandler) {
_clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonData);
}
}
AABox AvatarData::getDefaultBubbleBox() const {
AABox bubbleBox(_defaultBubbleBox);
bubbleBox.translate(_globalPosition);

View file

@ -145,6 +145,45 @@ const char AVATARDATA_FLAGS_MINIMUM = 0;
using SmallFloat = uint16_t; // a compressed float with less precision, user defined radix
namespace AvatarSkeletonTrait {
enum BoneType {
SkeletonRoot = 0,
SkeletonChild,
NonSkeletonRoot,
NonSkeletonChild
};
PACKED_BEGIN struct Header {
float maxTranslationDimension;
float maxScaleDimension;
uint8_t numJoints;
uint16_t stringTableLength;
} PACKED_END;
PACKED_BEGIN struct JointData {
uint16_t stringStart;
uint8_t stringLength;
uint8_t boneType;
uint8_t defaultTranslation[6];
uint8_t defaultRotation[6];
uint16_t defaultScale;
uint16_t jointIndex;
uint16_t parentIndex;
} PACKED_END;
struct UnpackedJointData {
int stringStart;
int stringLength;
int boneType;
glm::vec3 defaultTranslation;
glm::quat defaultRotation;
float defaultScale;
int jointIndex;
int parentIndex;
QString jointName;
};
}
namespace AvatarDataPacket {
// NOTE: every time AvatarData is sent from mixer to client, it also includes the GUIID for the session
@ -164,10 +203,11 @@ namespace AvatarDataPacket {
const HasFlags PACKET_HAS_ADDITIONAL_FLAGS = 1U << 7;
const HasFlags PACKET_HAS_PARENT_INFO = 1U << 8;
const HasFlags PACKET_HAS_AVATAR_LOCAL_POSITION = 1U << 9;
const HasFlags PACKET_HAS_FACE_TRACKER_INFO = 1U << 10;
const HasFlags PACKET_HAS_JOINT_DATA = 1U << 11;
const HasFlags PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS = 1U << 12;
const HasFlags PACKET_HAS_GRAB_JOINTS = 1U << 13;
const HasFlags PACKET_HAS_HAND_CONTROLLERS = 1U << 10;
const HasFlags PACKET_HAS_FACE_TRACKER_INFO = 1U << 11;
const HasFlags PACKET_HAS_JOINT_DATA = 1U << 12;
const HasFlags PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS = 1U << 13;
const HasFlags PACKET_HAS_GRAB_JOINTS = 1U << 14;
const size_t AVATAR_HAS_FLAGS_SIZE = 2;
using SixByteQuat = uint8_t[6];
@ -230,7 +270,7 @@ namespace AvatarDataPacket {
//
// POTENTIAL SAVINGS - 20 bytes
SixByteQuat sensorToWorldQuat; // 6 byte compressed quaternion part of sensor to world matrix
SixByteQuat sensorToWorldQuat; // 6 byte compressed quaternion part of sensor to world matrix
uint16_t sensorToWorldScale; // uniform scale of sensor to world matrix
float sensorToWorldTrans[3]; // fourth column of sensor to world matrix
// FIXME - sensorToWorldTrans might be able to be better compressed if it was
@ -258,6 +298,7 @@ namespace AvatarDataPacket {
PACKED_BEGIN struct AvatarLocalPosition {
float localPosition[3]; // parent frame translation of the avatar
} PACKED_END;
const size_t AVATAR_LOCAL_POSITION_SIZE = 12;
static_assert(sizeof(AvatarLocalPosition) == AVATAR_LOCAL_POSITION_SIZE, "AvatarDataPacket::AvatarLocalPosition size doesn't match.");
@ -273,6 +314,15 @@ namespace AvatarDataPacket {
PARENT_INFO_SIZE +
AVATAR_LOCAL_POSITION_SIZE;
PACKED_BEGIN struct HandControllers {
SixByteQuat leftHandRotation;
SixByteTrans leftHandTranslation;
SixByteQuat rightHandRotation;
SixByteTrans rightHandTranslation;
} PACKED_END;
static const size_t HAND_CONTROLLERS_SIZE = 24;
static_assert(sizeof(HandControllers) == HAND_CONTROLLERS_SIZE, "AvatarDataPacket::HandControllers size doesn't match.");
// variable length structure follows
@ -303,7 +353,7 @@ namespace AvatarDataPacket {
SixByteTrans rightHandControllerTranslation;
};
*/
size_t maxJointDataSize(size_t numJoints, bool hasGrabJoints);
size_t maxJointDataSize(size_t numJoints);
size_t minJointDataSize(size_t numJoints);
/*
@ -327,7 +377,6 @@ namespace AvatarDataPacket {
static_assert(sizeof(FarGrabJoints) == FAR_GRAB_JOINTS_SIZE, "AvatarDataPacket::FarGrabJoints size doesn't match.");
static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE;
static const size_t FAUX_JOINTS_SIZE = 2 * (sizeof(SixByteQuat) + sizeof(SixByteTrans));
struct SendStatus {
HasFlags itemFlags { 0 };
@ -404,6 +453,7 @@ class AvatarDataRate {
public:
RateCounter<> globalPositionRate;
RateCounter<> localPositionRate;
RateCounter<> handControllersRate;
RateCounter<> avatarBoundingBoxRate;
RateCounter<> avatarOrientationRate;
RateCounter<> avatarScaleRate;
@ -467,8 +517,8 @@ class AvatarData : public QObject, public SpatiallyNestable {
* @property {boolean} lookAtSnappingEnabled=true - <code>true</code> if the avatar's eyes snap to look at another avatar's
* eyes when the other avatar is in the line of sight and also has <code>lookAtSnappingEnabled == true</code>.
* @property {string} skeletonModelURL - The avatar's FST file.
* @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.<br />
* <strong>Deprecated:</strong> Use avatar entities instead.
* @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* <p class="important">Deprecated: This property is deprecated and will be removed. Use avatar entities instead.</p>
* @property {string[]} jointNames - The list of joints in the current avatar model. <em>Read-only.</em>
* @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. <em>Read-only.</em>
* @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the
@ -1076,7 +1126,7 @@ public:
* Gets information about the models currently attached to your avatar.
* @function Avatar.getAttachmentsVariant
* @returns {AttachmentData[]} Information about all models attached to your avatar.
* @deprecated Use avatar entities instead.
* @deprecated This function is deprecated and will be removed. Use avatar entities instead.
*/
// FIXME: Can this name be improved? Can it be deprecated?
Q_INVOKABLE virtual QVariantList getAttachmentsVariant() const;
@ -1087,7 +1137,7 @@ public:
* update your avatar's attachments per the changed data.
* @function Avatar.setAttachmentsVariant
* @param {AttachmentData[]} variant - The attachment data defining the models to have attached to your avatar.
* @deprecated Use avatar entities instead.
* @deprecated This function is deprecated and will be removed. Use avatar entities instead.
*/
// FIXME: Can this name be improved? Can it be deprecated?
Q_INVOKABLE virtual void setAttachmentsVariant(const QVariantList& variant);
@ -1168,7 +1218,7 @@ public:
* Gets information about the models currently attached to your avatar.
* @function Avatar.getAttachmentData
* @returns {AttachmentData[]} Information about all models attached to your avatar.
* @deprecated Use avatar entities instead.
* @deprecated This function is deprecated and will be removed. Use avatar entities instead.
* @example <caption>Report the URLs of all current attachments.</caption>
* var attachments = MyAvatar.getaAttachmentData();
* for (var i = 0; i < attachments.length; i++) {
@ -1186,7 +1236,7 @@ public:
* @function Avatar.setAttachmentData
* @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use
* <code>null</code> to remove all attachments.
* @deprecated Use avatar entities instead.
* @deprecated This function is deprecated and will be removed. Use avatar entities instead.
* @example <caption>Remove a hat attachment if your avatar is wearing it.</caption>
* var hatURL = "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx";
* var attachments = MyAvatar.getAttachmentData();
@ -1223,7 +1273,7 @@ public:
* @param {boolean} [allowDuplicates=false] - If <code>true</code> then more than one copy of any particular model may be
* attached to the same joint; if <code>false</code> then the same model cannot be attached to the same joint.
* @param {boolean} [useSaved=true] - <em>Not used.</em>
* @deprecated Use avatar entities instead.
* @deprecated This function is deprecated and will be removed. Use avatar entities instead.
* @example <caption>Attach a cowboy hat to your avatar's head.</caption>
* var attachment = {
* modelURL: "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx",
@ -1254,7 +1304,7 @@ public:
* @param {string} modelURL - The URL of the model to detach.
* @param {string} [jointName=""] - The name of the joint to detach the model from. If <code>""</code>, then the most
* recently attached model is removed from which ever joint it was attached to.
* @deprecated Use avatar entities instead.
* @deprecated This function is deprecated and will be removed. Use avatar entities instead.
*/
Q_INVOKABLE virtual void detachOne(const QString& modelURL, const QString& jointName = QString());
@ -1264,7 +1314,7 @@ public:
* @param {string} modelURL - The URL of the model to detach.
* @param {string} [jointName=""] - The name of the joint to detach the model from. If <code>""</code>, then the model is
* detached from all joints.
* @deprecated Use avatar entities instead.
* @deprecated This function is deprecated and will be removed. Use avatar entities instead.
*/
Q_INVOKABLE virtual void detachAll(const QString& modelURL, const QString& jointName = QString());
@ -1420,6 +1470,10 @@ public:
void setIsNewAvatar(bool isNewAvatar) { _isNewAvatar = isNewAvatar; }
bool getIsNewAvatar() { return _isNewAvatar; }
void setIsClientAvatar(bool isClientAvatar) { _isClientAvatar = isClientAvatar; }
void setSkeletonData(const std::vector<AvatarSkeletonTrait::UnpackedJointData>& skeletonData);
std::vector<AvatarSkeletonTrait::UnpackedJointData> getSkeletonData() const;
void sendSkeletonData() const;
QVector<JointData> getJointData() const;
signals:
@ -1598,12 +1652,13 @@ protected:
bool hasParent() const { return !getParentID().isNull(); }
bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; }
QByteArray packSkeletonData() const;
QByteArray packSkeletonModelURL() const;
QByteArray packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID);
QByteArray packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID);
void unpackSkeletonModelURL(const QByteArray& data);
void unpackSkeletonData(const QByteArray& data);
// isReplicated will be true on downstream Avatar Mixers and their clients, but false on the upstream "master"
// Audio Mixer that the replicated avatar is connected to.
@ -1671,6 +1726,7 @@ protected:
RateCounter<> _parseBufferRate;
RateCounter<> _globalPositionRate;
RateCounter<> _localPositionRate;
RateCounter<> _handControllersRate;
RateCounter<> _avatarBoundingBoxRate;
RateCounter<> _avatarOrientationRate;
RateCounter<> _avatarScaleRate;
@ -1688,6 +1744,7 @@ protected:
RateCounter<> _parseBufferUpdateRate;
RateCounter<> _globalPositionUpdateRate;
RateCounter<> _localPositionUpdateRate;
RateCounter<> _handControllersUpdateRate;
RateCounter<> _avatarBoundingBoxUpdateRate;
RateCounter<> _avatarOrientationUpdateRate;
RateCounter<> _avatarScaleUpdateRate;
@ -1720,6 +1777,9 @@ protected:
AvatarGrabDataMap _avatarGrabData;
bool _avatarGrabDataChanged { false }; // by network
mutable ReadWriteLockable _avatarSkeletonDataLock;
std::vector<AvatarSkeletonTrait::UnpackedJointData> _avatarSkeletonData;
// used to transform any sensor into world space, including the _hmdSensorMat, or hand controllers.
ThreadSafeValueCache<glm::mat4> _sensorToWorldMatrixCache { glm::mat4() };
ThreadSafeValueCache<glm::mat4> _controllerLeftHandMatrixCache { glm::mat4() };

View file

@ -332,6 +332,12 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
AvatarTraits::TraitMessageSequence seq;
// Trying to read more bytes than available, bail
if (message->getBytesLeftToRead() < (qint64)sizeof(AvatarTraits::TraitMessageSequence)) {
qWarning() << "Malformed bulk trait packet, bailling";
return;
}
message->readPrimitive(&seq);
auto traitsAckPacket = NLPacket::create(PacketType::BulkAvatarTraitsAck, sizeof(AvatarTraits::TraitMessageSequence), true);
@ -344,7 +350,14 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> mess
nodeList->sendPacket(std::move(traitsAckPacket), *avatarMixer);
}
while (message->getBytesLeftToRead()) {
while (message->getBytesLeftToRead() > 0) {
// Trying to read more bytes than available, bail
if (message->getBytesLeftToRead() < qint64(NUM_BYTES_RFC4122_UUID +
sizeof(AvatarTraits::TraitType))) {
qWarning() << "Malformed bulk trait packet, bailling";
return;
}
// read the avatar ID to figure out which avatar this is for
auto avatarID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
@ -360,6 +373,12 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> mess
auto& lastProcessedVersions = _processedTraitVersions[avatarID];
while (traitType != AvatarTraits::NullTrait && message->getBytesLeftToRead() > 0) {
// Trying to read more bytes than available, bail
if (message->getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitVersion))) {
qWarning() << "Malformed bulk trait packet, bailling";
return;
}
AvatarTraits::TraitVersion packetTraitVersion;
message->readPrimitive(&packetTraitVersion);
@ -367,8 +386,20 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> mess
bool skipBinaryTrait = false;
if (AvatarTraits::isSimpleTrait(traitType)) {
// Trying to read more bytes than available, bail
if (message->getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitWireSize))) {
qWarning() << "Malformed bulk trait packet, bailling";
return;
}
message->readPrimitive(&traitBinarySize);
// Trying to read more bytes than available, bail
if (message->getBytesLeftToRead() < traitBinarySize) {
qWarning() << "Malformed bulk trait packet, bailling";
return;
}
// check if this trait version is newer than what we already have for this avatar
if (packetTraitVersion > lastProcessedVersions[traitType]) {
auto traitData = message->read(traitBinarySize);
@ -379,11 +410,24 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> mess
skipBinaryTrait = true;
}
} else {
// Trying to read more bytes than available, bail
if (message->getBytesLeftToRead() < qint64(NUM_BYTES_RFC4122_UUID +
sizeof(AvatarTraits::TraitWireSize))) {
qWarning() << "Malformed bulk trait packet, bailling";
return;
}
AvatarTraits::TraitInstanceID traitInstanceID =
QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
message->readPrimitive(&traitBinarySize);
// Trying to read more bytes than available, bail
if (traitBinarySize < -1 || message->getBytesLeftToRead() < traitBinarySize) {
qWarning() << "Malformed bulk trait packet, bailling";
return;
}
auto& processedInstanceVersion = lastProcessedVersions.getInstanceValueRef(traitType, traitInstanceID);
if (packetTraitVersion > processedInstanceVersion) {
if (traitBinarySize == AvatarTraits::DELETED_TRAIT_SIZE) {

View file

@ -29,7 +29,7 @@ namespace AvatarTraits {
// Simple traits
SkeletonModelURL = 0,
SkeletonData,
// Instanced traits
FirstInstancedTrait,
AvatarEntity = FirstInstancedTrait,

View file

@ -107,8 +107,7 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() {
if (initialSend || *simpleIt == Updated) {
bytesWritten += AvatarTraits::packTrait(traitType, *traitsPacketList, *_owningAvatar);
if (traitType == AvatarTraits::SkeletonModelURL) {
// keep track of our skeleton version in case we get an override back
_currentSkeletonVersion = _currentTraitVersion;
@ -147,7 +146,16 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() {
void ClientTraitsHandler::processTraitOverride(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
if (sendingNode->getType() == NodeType::AvatarMixer) {
Lock lock(_traitLock);
while (message->getBytesLeftToRead()) {
while (message->getBytesLeftToRead() > 0) {
// Trying to read more bytes than available, bail
if (message->getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitType) +
sizeof(AvatarTraits::TraitVersion) +
sizeof(AvatarTraits::TraitWireSize))) {
qWarning() << "Malformed trait override packet, bailling";
return;
}
AvatarTraits::TraitType traitType;
message->readPrimitive(&traitType);
@ -157,6 +165,12 @@ void ClientTraitsHandler::processTraitOverride(QSharedPointer<ReceivedMessage> m
AvatarTraits::TraitWireSize traitBinarySize;
message->readPrimitive(&traitBinarySize);
// Trying to read more bytes than available, bail
if (traitBinarySize < -1 || message->getBytesLeftToRead() < traitBinarySize) {
qWarning() << "Malformed trait override packet, bailling";
return;
}
// only accept an override if this is for a trait type we override
// and the version matches what we last sent for skeleton
if (traitType == AvatarTraits::SkeletonModelURL

View file

@ -40,13 +40,14 @@
* @property {string} displayName - The avatar's display name.
* @property {string} sessionDisplayName - The avatar's display name, sanitized and versioned, as defined by the avatar mixer.
* It is unique among all avatars present in the domain at the time.
* @property {boolean} isReplicated - <strong>Deprecated.</strong>
* @property {boolean} isReplicated - <span class="important">Deprecated: This property is deprecated and will be
* removed.</span>
* @property {boolean} lookAtSnappingEnabled - <code>true</code> if the avatar's eyes snap to look at another avatar's eyes
* when the other avatar is in the line of sight and also has <code>lookAtSnappingEnabled == true</code>.
*
* @property {string} skeletonModelURL - The avatar's FST file.
* @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.<br />
* <strong>Deprecated:</strong> Use avatar entities instead.
* @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* <p class="important">Deprecated: This property is deprecated and will be removed. Use avatar entities instead.</p>
* @property {string[]} jointNames - The list of joints in the current avatar model.
*
* @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the

View file

@ -31,7 +31,6 @@
#include <FBXWriter.h>
#include "ModelBakingLoggingCategory.h"
#include "TextureBaker.h"
FBXBaker::FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputModelURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) {
@ -104,13 +103,15 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const
int meshIndex = 0;
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
for (auto object = rootChild.children.begin(); object != rootChild.children.end(); object++) {
auto object = rootChild.children.begin();
while (object != rootChild.children.end()) {
if (object->name == "Geometry") {
if (object->properties.at(2) == "Mesh") {
int meshNum = meshIndexToRuntimeOrder[meshIndex];
replaceMeshNodeWithDraco(*object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
meshIndex++;
}
object++;
} else if (object->name == "Model") {
for (FBXNode& modelChild : object->children) {
if (modelChild.name == "Properties60" || modelChild.name == "Properties70") {
@ -136,9 +137,33 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const
meshIndex++;
}
}
object++;
} else if (object->name == "Texture" || object->name == "Video") {
// this is an embedded texture, we need to remove it from the FBX
object = rootChild.children.erase(object);
} else if (object->name == "Material") {
for (FBXNode& materialChild : object->children) {
if (materialChild.name == "Properties60" || materialChild.name == "Properties70") {
// This is a properties node
// Remove the material texture scale because that is now included in the material JSON
// Texture nodes are removed, so their texture scale is effectively gone already
static const QVariant MAYA_UV_SCALE = hifi::ByteArray("Maya|uv_scale");
static const QVariant MAYA_UV_OFFSET = hifi::ByteArray("Maya|uv_offset");
for (int i = 0; i < materialChild.children.size(); i++) {
const auto& prop = materialChild.children[i];
const auto& propertyName = prop.properties.at(0);
if (propertyName == MAYA_UV_SCALE ||
propertyName == MAYA_UV_OFFSET) {
materialChild.children.removeAt(i);
--i;
}
}
}
}
object++;
} else {
object++;
}
if (hasErrors()) {

Some files were not shown because too many files have changed in this diff Show more