Merge branch 'master' into M22075

# Conflicts:
#	libraries/controllers/src/controllers/Actions.cpp
This commit is contained in:
David Rowe 2019-04-27 10:51:13 +12:00
commit 407ce745d5
334 changed files with 11949 additions and 4369 deletions

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

@ -64,6 +64,10 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
"UDP port for this assignment client (or monitor)", "port");
parser.addOption(portOption);
const QCommandLineOption minChildListenPort(ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION,
"Minimum UDP listen port", "port");
parser.addOption(minChildListenPort);
const QCommandLineOption walletDestinationOption(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION,
"set wallet destination", "wallet-uuid");
parser.addOption(walletDestinationOption);
@ -195,6 +199,11 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
assignmentServerPort = parser.value(assignmentServerPortOption).toInt();
}
quint16 childMinListenPort = 0;
if (argumentVariantMap.contains(ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION)) {
childMinListenPort = argumentVariantMap.value(ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION).toUInt();
}
// check for an overidden listen port
quint16 listenPort = 0;
if (argumentVariantMap.contains(ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION)) {
@ -234,8 +243,8 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
if (numForks || minForks || maxForks) {
AssignmentClientMonitor* monitor = new AssignmentClientMonitor(numForks, minForks, maxForks,
requestAssignmentType, assignmentPool,
listenPort, walletUUID, assignmentServerHostname,
requestAssignmentType, assignmentPool, listenPort,
childMinListenPort, walletUUID, assignmentServerHostname,
assignmentServerPort, httpStatusPort, logDirectory);
monitor->setParent(this);
connect(this, &QCoreApplication::aboutToQuit, monitor, &AssignmentClientMonitor::aboutToQuit);

View file

@ -20,6 +20,7 @@ const QString ASSIGNMENT_POOL_OPTION = "pool";
const QString ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION = "p";
const QString ASSIGNMENT_WALLET_DESTINATION_ID_OPTION = "wallet";
const QString CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION = "a";
const QString ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION = "min-listen-port";
const QString CUSTOM_ASSIGNMENT_SERVER_PORT_OPTION = "server-port";
const QString ASSIGNMENT_NUM_FORKS_OPTION = "n";
const QString ASSIGNMENT_MIN_FORKS_OPTION = "min";

View file

@ -40,7 +40,7 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen
const unsigned int minAssignmentClientForks,
const unsigned int maxAssignmentClientForks,
Assignment::Type requestAssignmentType, QString assignmentPool,
quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname,
quint16 listenPort, quint16 childMinListenPort, QUuid walletUUID, QString assignmentServerHostname,
quint16 assignmentServerPort, quint16 httpStatusServerPort, QString logDirectory) :
_httpManager(QHostAddress::LocalHost, httpStatusServerPort, "", this),
_numAssignmentClientForks(numAssignmentClientForks),
@ -50,8 +50,8 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen
_assignmentPool(assignmentPool),
_walletUUID(walletUUID),
_assignmentServerHostname(assignmentServerHostname),
_assignmentServerPort(assignmentServerPort)
_assignmentServerPort(assignmentServerPort),
_childMinListenPort(childMinListenPort)
{
qDebug() << "_requestAssignmentType =" << _requestAssignmentType;
@ -100,8 +100,13 @@ void AssignmentClientMonitor::simultaneousWaitOnChildren(int waitMsecs) {
}
}
void AssignmentClientMonitor::childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus) {
auto message = "Child process " + QString::number(pid) + " has %1 with exit code " + QString::number(exitCode) + ".";
void AssignmentClientMonitor::childProcessFinished(qint64 pid, quint16 listenPort, int exitCode, QProcess::ExitStatus exitStatus) {
auto message = "Child process " + QString::number(pid) + " on port " + QString::number(listenPort) +
"has %1 with exit code " + QString::number(exitCode) + ".";
if (listenPort) {
_childListenPorts.remove(listenPort);
}
if (_childProcesses.remove(pid)) {
message.append(" Removed from internal map.");
@ -153,6 +158,23 @@ void AssignmentClientMonitor::aboutToQuit() {
void AssignmentClientMonitor::spawnChildClient() {
QProcess* assignmentClient = new QProcess(this);
quint16 listenPort = 0;
// allocate a port
if (_childMinListenPort) {
for (listenPort = _childMinListenPort; _childListenPorts.contains(listenPort); listenPort++) {
if (_maxAssignmentClientForks &&
(listenPort >= _maxAssignmentClientForks + _childMinListenPort)) {
listenPort = 0;
qDebug() << "Insufficient listen ports";
break;
}
}
}
if (listenPort) {
_childListenPorts.insert(listenPort);
}
// unparse the parts of the command-line that the child cares about
QStringList _childArguments;
if (_assignmentPool != "") {
@ -176,6 +198,11 @@ void AssignmentClientMonitor::spawnChildClient() {
_childArguments.append(QString::number(_requestAssignmentType));
}
if (listenPort) {
_childArguments.append("-" + ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION);
_childArguments.append(QString::number(listenPort));
}
// tell children which assignment monitor port to use
// for now they simply talk to us on localhost
_childArguments.append("--" + ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION);
@ -247,8 +274,8 @@ void AssignmentClientMonitor::spawnChildClient() {
auto pid = assignmentClient->processId();
// make sure we hear that this process has finished when it does
connect(assignmentClient, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
this, [this, pid](int exitCode, QProcess::ExitStatus exitStatus) {
childProcessFinished(pid, exitCode, exitStatus);
this, [this, listenPort, pid](int exitCode, QProcess::ExitStatus exitStatus) {
childProcessFinished(pid, listenPort, exitCode, exitStatus);
});
qDebug() << "Spawned a child client with PID" << assignmentClient->processId();

View file

@ -37,14 +37,15 @@ class AssignmentClientMonitor : public QObject, public HTTPRequestHandler {
public:
AssignmentClientMonitor(const unsigned int numAssignmentClientForks, const unsigned int minAssignmentClientForks,
const unsigned int maxAssignmentClientForks, Assignment::Type requestAssignmentType,
QString assignmentPool, quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname,
quint16 assignmentServerPort, quint16 httpStatusServerPort, QString logDirectory);
QString assignmentPool, quint16 listenPort, quint16 childMinListenPort, QUuid walletUUID,
QString assignmentServerHostname, quint16 assignmentServerPort, quint16 httpStatusServerPort,
QString logDirectory);
~AssignmentClientMonitor();
void stopChildProcesses();
private slots:
void checkSpares();
void childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus);
void childProcessFinished(qint64 pid, quint16 port, int exitCode, QProcess::ExitStatus exitStatus);
void handleChildStatusPacket(QSharedPointer<ReceivedMessage> message);
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override;
@ -75,6 +76,9 @@ private:
QMap<qint64, ACProcess> _childProcesses;
quint16 _childMinListenPort;
QSet<quint16> _childListenPorts;
bool _wantsChildFileLogging { false };
};

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

@ -588,8 +588,8 @@ void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) {
// check the payload to see if we have asked for dynamicJitterBuffer support
const QString DYNAMIC_JITTER_BUFFER_JSON_KEY = "dynamic_jitter_buffer";
bool enableDynamicJitterBuffer = audioBufferGroupObject[DYNAMIC_JITTER_BUFFER_JSON_KEY].toBool();
if (enableDynamicJitterBuffer) {
qCDebug(audio) << "Enabling dynamic jitter buffers.";
if (!enableDynamicJitterBuffer) {
qCDebug(audio) << "Disabling dynamic jitter buffers.";
bool ok;
const QString DESIRED_JITTER_BUFFER_FRAMES_KEY = "static_desired_jitter_buffer_frames";
@ -599,7 +599,7 @@ void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) {
}
qCDebug(audio) << "Static desired jitter buffer frames:" << _numStaticJitterFrames;
} else {
qCDebug(audio) << "Disabling dynamic jitter buffers.";
qCDebug(audio) << "Enabling dynamic jitter buffers.";
_numStaticJitterFrames = DISABLE_STATIC_JITTER_FRAMES;
}

View file

@ -549,38 +549,28 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre
// grab the stream from the ring buffer
AudioRingBuffer::ConstIterator streamPopOutput = streamToAdd->getLastPopOutput();
// stereo sources are not passed through HRTF
if (streamToAdd->isStereo()) {
// apply the avatar gain adjustment
gain *= mixableStream.hrtf->getGainAdjustment();
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
const float scale = 1 / 32768.0f; // int16_t to float
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) {
_mixSamples[2*i+0] += (float)streamPopOutput[2*i+0] * gain * scale;
_mixSamples[2*i+1] += (float)streamPopOutput[2*i+1] * gain * scale;
}
// stereo sources are not passed through HRTF
mixableStream.hrtf->mixStereo(_bufferSamples, _mixSamples, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.manualStereoMixes;
} else if (isEcho) {
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
// echo sources are not passed through HRTF
const float scale = 1/32768.0f; // int16_t to float
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) {
float sample = (float)streamPopOutput[i] * gain * scale;
_mixSamples[2*i+0] += sample;
_mixSamples[2*i+1] += sample;
}
mixableStream.hrtf->mixMono(_bufferSamples, _mixSamples, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.manualEchoMixes;
} else {
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
mixableStream.hrtf->render(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.hrtfRenders;
}
}

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

@ -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

@ -481,3 +481,15 @@ function prepareAccessTokenPrompt(callback) {
swal.close();
});
}
function getMetaverseUrl(callback) {
$.ajax('/api/metaverse_info', {
success: function(data) {
callback(data.metaverse_url);
},
error: function() {
callback(URLs.METAVERSE_URL);
}
});
}

View file

@ -16,47 +16,55 @@ $(document).ready(function(){
Settings.extraGroupsAtEnd = Settings.extraDomainGroupsAtEnd;
Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex;
var METAVERSE_URL = URLs.METAVERSE_URL;
Settings.afterReloadActions = function() {
// call our method to setup the HF account button
setupHFAccountButton();
// call our method to setup the place names table
setupPlacesTable();
getMetaverseUrl(function(metaverse_url) {
METAVERSE_URL = metaverse_url;
setupDomainNetworkingSettings();
// setupDomainLabelSetting();
// call our method to setup the HF account button
setupHFAccountButton();
setupSettingsBackup();
// call our method to setup the place names table
setupPlacesTable();
if (domainIDIsSet()) {
// now, ask the API for what places, if any, point to this domain
reloadDomainInfo();
setupDomainNetworkingSettings();
// setupDomainLabelSetting();
// we need to ask the API what a shareable name for this domain is
getShareName(function(success, shareName) {
if (success) {
var shareLink = "https://hifi.place/" + shareName;
$('#visit-domain-link').attr("href", shareLink).show();
}
});
}
setupSettingsBackup();
if (Settings.data.values.wizard.cloud_domain) {
$('#manage-cloud-domains-link').show();
if (domainIDIsSet()) {
// now, ask the API for what places, if any, point to this domain
reloadDomainInfo();
var cloudWizardExit = qs["cloud-wizard-exit"];
if (cloudWizardExit != undefined) {
$('#cloud-domains-alert').show();
// we need to ask the API what a shareable name for this domain is
getShareName(function(success, shareName) {
if (success) {
var shareLink = "https://hifi.place/" + shareName;
$('#visit-domain-link').attr("href", shareLink).show();
}
});
} else if (accessTokenIsSet()) {
$('#' + Settings.GET_TEMPORARY_NAME_BTN_ID).show();
}
$(Settings.DOMAIN_ID_SELECTOR).siblings('span').append("</br><strong>Changing the domain ID for a Cloud Domain may result in an incorrect status for the domain on your Cloud Domains page.</strong>");
} else {
// append the domain selection modal
appendDomainIDButtons();
}
if (Settings.data.values.wizard.cloud_domain) {
$('#manage-cloud-domains-link').show();
handleAction();
var cloudWizardExit = qs["cloud-wizard-exit"];
if (cloudWizardExit != undefined) {
$('#cloud-domains-alert').show();
}
$(Settings.DOMAIN_ID_SELECTOR).siblings('span').append("</br><strong>Changing the domain ID for a Cloud Domain may result in an incorrect status for the domain on your Cloud Domains page.</strong>");
} else {
// append the domain selection modal
appendDomainIDButtons();
}
handleAction();
});
}
Settings.handlePostSettings = function(formJSON) {
@ -258,7 +266,7 @@ $(document).ready(function(){
buttonSetting.button_label = "Connect High Fidelity Account";
buttonSetting.html_id = Settings.CONNECT_ACCOUNT_BTN_ID;
buttonSetting.href = URLs.METAVERSE_URL + "/user/tokens/new?for_domain_server=true";
buttonSetting.href = METAVERSE_URL + "/user/tokens/new?for_domain_server=true";
// since we do not have an access token we change hide domain ID and auto networking settings
// without an access token niether of them can do anything
@ -645,7 +653,7 @@ $(document).ready(function(){
label: 'Places',
html_id: Settings.PLACES_TABLE_ID,
help: "The following places currently point to this domain.</br>To point places to this domain, "
+ " go to the <a href='" + URLs.METAVERSE_URL + "/user/places'>My Places</a> "
+ " go to the <a href='" + METAVERSE_URL + "/user/places'>My Places</a> "
+ "page in your High Fidelity Metaverse account.",
read_only: true,
can_add_new_rows: false,
@ -952,7 +960,7 @@ $(document).ready(function(){
modal_buttons["success"] = {
label: 'Create new domain',
callback: function() {
window.open(URLs.METAVERSE_URL + "/user/domains", '_blank');
window.open(METAVERSE_URL + "/user/domains", '_blank');
}
}
modal_body = "<p>You do not have any domains in your High Fidelity account." +
@ -1000,7 +1008,7 @@ $(document).ready(function(){
showSpinnerAlert('Creating temporary place name');
// make a get request to get a temporary domain
$.post(URLs.METAVERSE_URL + '/api/v1/domains/temporary', function(data){
$.post(METAVERSE_URL + '/api/v1/domains/temporary', function(data){
if (data.status == "success") {
var domain = data.data.domain;

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 {

View file

@ -1916,6 +1916,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString URI_SETTINGS = "/settings";
const QString URI_CONTENT_UPLOAD = "/content/upload";
const QString URI_RESTART = "/restart";
const QString URI_API_METAVERSE_INFO = "/api/metaverse_info";
const QString URI_API_PLACES = "/api/places";
const QString URI_API_DOMAINS = "/api/domains";
const QString URI_API_DOMAINS_ID = "/api/domains/";
@ -2164,6 +2165,15 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
} else if (url.path() == URI_RESTART) {
connection->respond(HTTPConnection::StatusCode200);
restart();
return true;
} else if (url.path() == URI_API_METAVERSE_INFO) {
QJsonObject rootJSON {
{ "metaverse_url", NetworkingConstants::METAVERSE_SERVER_URL().toString() }
};
QJsonDocument docJSON{ rootJSON };
connectionPtr->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8());
return true;
} else if (url.path() == URI_API_DOMAINS) {
return forwardMetaverseAPIRequest(connection, "/api/v1/domains", "");

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 @@
}
]
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 267 KiB

View file

@ -53,6 +53,16 @@
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
}
#image_button {
position: absolute;
width: 463;
height: 410;
top: 155;
left: 8;
right: 8;
bottom: 146;
}
#report_problem {
position: fixed;
@ -67,17 +77,23 @@
var handControllerImageURL = null;
var index = 0;
var count = 3;
var handControllerRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#vr-controls";
var keyboardRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/desktop.html#movement-controls";
var gamepadRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#gamepad";
function showKbm() {
document.getElementById("main_image").setAttribute("src", "img/tablet-help-keyboard.jpg");
document.getElementById("image_button").setAttribute("href", keyboardRefURL);
}
function showHandControllers() {
document.getElementById("main_image").setAttribute("src", handControllerImageURL);
document.getElementById("image_button").setAttribute("href", handControllerRefURL);
}
function showGamepad() {
document.getElementById("main_image").setAttribute("src", "img/tablet-help-gamepad.jpg");
document.getElementById("image_button").setAttribute("href", gamepadRefURL);
}
function cycleRight() {
@ -171,6 +187,7 @@
<img id="main_image" src="img/tablet-help-keyboard.jpg" width="480px" height="720px"></img>
<a href="#" id="left_button" onmousedown="cycleLeft()"></a>
<a href="#" id="right_button" onmousedown="cycleRight()"></a>
<a href="#" id="image_button"></a>
</div>
<a href="mailto:support@highfidelity.com" id="report_problem">Report Problem</a>
</body>

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

@ -336,6 +336,8 @@ Rectangle {
case Qt.Key_Return:
case Qt.Key_Enter:
event.accepted = true;
keypressTimer.stop();
root.searchString = searchField.text;
searchField.text = "";
getMarketplaceItems();
@ -950,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"
@ -825,7 +827,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
audioDLLPath += "/audioWin7";
}
QCoreApplication::addLibraryPath(audioDLLPath);
#endif
#endif
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
DependencyManager::registerInheritance<AvatarHashMap, AvatarManager>();
@ -1456,6 +1458,34 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_overlays.init(); // do this before scripts load
DependencyManager::set<ContextOverlayInterface>();
auto offscreenUi = getOffscreenUI();
connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() {
// Now that we've loaded the menu and thus switched to the previous display plugin
// we can unlock the desktop repositioning code, since all the positions will be
// relative to the desktop size for this plugin
auto offscreenUi = getOffscreenUI();
auto desktop = offscreenUi->getDesktop();
if (desktop) {
desktop->setProperty("repositionLocked", false);
}
});
connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() {
#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML)
// Do not show login dialog if requested not to on the command line
QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY);
int index = arguments().indexOf(hifiNoLoginCommandLineKey);
if (index != -1) {
resumeAfterLoginDialogActionTaken();
return;
}
showLoginScreen();
#else
resumeAfterLoginDialogActionTaken();
#endif
});
// Initialize the user interface and menu system
// Needs to happen AFTER the render engine initialization to access its configuration
initializeUi();
@ -1790,6 +1820,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
@ -1810,34 +1842,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
updateVerboseLogging();
// Now that we've loaded the menu and thus switched to the previous display plugin
// we can unlock the desktop repositioning code, since all the positions will be
// relative to the desktop size for this plugin
auto offscreenUi = getOffscreenUI();
connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() {
auto offscreenUi = getOffscreenUI();
auto desktop = offscreenUi->getDesktop();
if (desktop) {
desktop->setProperty("repositionLocked", false);
}
});
connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() {
#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML)
// Do not show login dialog if requested not to on the command line
QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY);
int index = arguments().indexOf(hifiNoLoginCommandLineKey);
if (index != -1) {
resumeAfterLoginDialogActionTaken();
return;
}
showLoginScreen();
#else
resumeAfterLoginDialogActionTaken();
#endif
});
// Make sure we don't time out during slow operations at startup
updateHeartbeat();
QTimer* settingsTimer = new QTimer();
@ -1866,12 +1870,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();
}
{
@ -2342,7 +2340,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;
@ -2370,7 +2368,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);
@ -2410,7 +2408,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
@ -2448,6 +2446,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();
@ -2624,6 +2630,8 @@ void Application::onAboutToQuit() {
_aboutToQuit = true;
cleanupBeforeQuit();
getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::SHUTDOWN);
}
void Application::cleanupBeforeQuit() {
@ -3233,6 +3241,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);
@ -3381,6 +3390,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
@ -3986,6 +3996,15 @@ static void dumpEventQueue(QThread* thread) {
}
#endif // DEBUG_EVENT_QUEUE
bool Application::notify(QObject * object, QEvent * event) {
if (thread() == QThread::currentThread()) {
PROFILE_RANGE_IF_LONGER(app, "notify", 2)
return QApplication::notify(object, event);
}
return QApplication::notify(object, event);
}
bool Application::event(QEvent* event) {
if (_aboutToQuit) {
@ -4043,6 +4062,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;
@ -4104,6 +4126,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;
}
@ -4390,6 +4418,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) {
@ -4398,6 +4433,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();
@ -5477,6 +5515,13 @@ void Application::pauseUntilLoginDetermined() {
// disconnect domain handler.
nodeList->getDomainHandler().disconnect();
// From now on, it's permissible to call resumeAfterLoginDialogActionTaken()
_resumeAfterLoginDialogActionTaken_SafeToRun = true;
if (_resumeAfterLoginDialogActionTaken_WasPostponed) {
// resumeAfterLoginDialogActionTaken() was already called, but it aborted. Now it's safe to call it again.
resumeAfterLoginDialogActionTaken();
}
}
void Application::resumeAfterLoginDialogActionTaken() {
@ -5485,6 +5530,11 @@ void Application::resumeAfterLoginDialogActionTaken() {
return;
}
if (!_resumeAfterLoginDialogActionTaken_SafeToRun) {
_resumeAfterLoginDialogActionTaken_WasPostponed = true;
return;
}
if (!isHMDMode() && getDesktopTabletBecomesToolbarSetting()) {
auto toolbar = DependencyManager::get<ToolbarScriptingInterface>()->getToolbar("com.highfidelity.interface.toolbar.system");
toolbar->writeProperty("visible", true);
@ -5553,6 +5603,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) {
@ -5741,9 +5793,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
}
@ -5759,12 +5808,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;
}
@ -5808,14 +5851,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);
}
}
}
@ -6728,11 +6763,11 @@ void Application::updateRenderArgs(float deltaTime) {
// Configure the type of display / stereo
appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR);
}
}
appRenderArgs._renderArgs._stencilMode = getActiveDisplayPlugin()->getStencilMaskMode();
if (appRenderArgs._renderArgs._stencilMode == StencilMode::MESH) {
appRenderArgs._renderArgs._stencilMaskOperator = getActiveDisplayPlugin()->getStencilMaskMeshOperator();
}
appRenderArgs._renderArgs._stencilMaskMode = getActiveDisplayPlugin()->getStencilMaskMode();
if (appRenderArgs._renderArgs._stencilMaskMode == StencilMaskMode::MESH) {
appRenderArgs._renderArgs._stencilMaskOperator = getActiveDisplayPlugin()->getStencilMaskMeshOperator();
}
{
@ -7352,6 +7387,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());
@ -8269,19 +8305,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(),
@ -8422,11 +8445,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) {
@ -8435,19 +8470,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) {
@ -8752,6 +8788,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) {
@ -8841,6 +8878,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();
@ -9150,7 +9195,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));
}
}
@ -9267,6 +9312,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>();
@ -9309,6 +9392,3 @@ void Application::toggleAwayMode(){
#endif
#include "Application.moc"

View file

@ -1,4 +1,4 @@
//
//
// Application.h
// interface/src
//
@ -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"
@ -156,6 +157,7 @@ public:
void updateCamera(RenderArgs& renderArgs, float deltaTime);
void resizeGL();
bool notify(QObject *, QEvent *) override;
bool event(QEvent* event) override;
bool eventFilter(QObject* object, QEvent* event) override;
@ -202,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(); }
@ -344,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);
@ -403,8 +412,6 @@ public slots:
static void packageModel();
void openUrl(const QUrl& url) const;
void resetSensors(bool andReload = false);
void setActiveFaceTracker() const;
@ -471,6 +478,9 @@ public slots:
QString getGraphicsCardType();
bool gpuTextureMemSizeStable();
void showUrlHandler(const QUrl& url);
private slots:
void onDesktopRootItemCreated(QQuickItem* qmlContext);
void onDesktopRootContextCreated(QQmlContext* qmlContext);
@ -565,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);
@ -716,6 +725,7 @@ private:
QUuid _loginDialogID;
QUuid _avatarInputsBarID;
LoginStateManager _loginStateManager;
RefreshRateManager _refreshRateManager;
quint64 _lastFaceTrackerUpdate;
@ -788,6 +798,9 @@ private:
AudioInjectorPointer _snapshotSoundInjector;
SharedSoundPointer _snapshotSound;
SharedSoundPointer _sampleSound;
std::mutex _snapshotMutex;
std::queue<SnapshotOperator> _snapshotOperators;
bool _hasPrimarySnapshot { false };
DisplayPluginPointer _autoSwitchDisplayModeSupportedHMDPlugin;
QString _autoSwitchDisplayModeSupportedHMDPluginName;
@ -807,5 +820,9 @@ private:
bool _showTrackedObjects { false };
bool _prevShowTrackedObjects { false };
bool _resumeAfterLoginDialogActionTaken_WasPostponed { false };
bool _resumeAfterLoginDialogActionTaken_SafeToRun { false };
bool _startUpFinished { false };
};
#endif // hifi_Application_h

View file

@ -19,14 +19,22 @@ class FancyCamera : public Camera {
Q_OBJECT
/**jsdoc
* @namespace
* @augments Camera
*/
// FIXME: JSDoc 3.5.5 doesn't augment @property definitions. The following definition is repeated in Camera.h.
/**jsdoc
* @property {Uuid} cameraEntity The ID of the entity that the camera position and orientation follow when the camera is in
* entity mode.
* The <code>Camera</code> API provides access to the "camera" that defines your view in desktop and HMD display modes.
*
* @namespace Camera
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
*
* @property {Vec3} position - The position of the camera. You can set this value only when the camera is in independent
* mode.
* @property {Quat} orientation - The orientation of the camera. You can set this value only when the camera is in
* independent mode.
* @property {Camera.Mode} mode - The camera mode.
* @property {ViewFrustum} frustum - The camera frustum.
* @property {Uuid} cameraEntity - The ID of the entity that is used for the camera position and orientation when the
* camera is in entity mode.
*/
Q_PROPERTY(QUuid cameraEntity READ getCameraEntity WRITE setCameraEntity)
@ -38,25 +46,25 @@ public:
public slots:
/**jsdoc
* Get the ID of the entity that the camera is set to use the position and orientation from when it's in entity mode. You can
* also get the entity ID using the <code>Camera.cameraEntity</code> property.
* @function Camera.getCameraEntity
* @returns {Uuid} The ID of the entity that the camera is set to follow when in entity mode; <code>null</code> if no camera
* entity has been set.
*/
/**jsdoc
* Gets the ID of the entity that the camera is set to follow (i.e., use the position and orientation from) when it's in
* entity mode. You can also get the entity ID using the {@link Camera|Camera.cameraEntity} property.
* @function Camera.getCameraEntity
* @returns {Uuid} The ID of the entity that the camera is set to follow when in entity mode; <code>null</code> if no
* camera entity has been set.
*/
QUuid getCameraEntity() const;
/**jsdoc
* Set the entity that the camera should use the position and orientation from when it's in entity mode. You can also set the
* entity using the <code>Camera.cameraEntity</code> property.
* @function Camera.setCameraEntity
* @param {Uuid} entityID The entity that the camera should follow when it's in entity mode.
* @example <caption>Move your camera to the position and orientation of the closest entity.</caption>
* Camera.setModeString("entity");
* var entity = Entities.findClosestEntity(MyAvatar.position, 100.0);
* Camera.setCameraEntity(entity);
*/
* Sets the entity that the camera should follow (i.e., use the position and orientation from) when it's in entity mode.
* You can also set the entity using the {@link Camera|Camera.cameraEntity} property.
* @function Camera.setCameraEntity
* @param {Uuid} entityID - The entity that the camera should follow when it's in entity mode.
* @example <caption>Move your camera to the position and orientation of the closest entity.</caption>
* Camera.setModeString("entity");
* var entity = Entities.findClosestEntity(MyAvatar.position, 100.0);
* Camera.setCameraEntity(entity);
*/
void setCameraEntity(QUuid entityID);
private:

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
@ -497,9 +504,11 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar
// it might not fire until after we create a new instance for the same remote avatar, which creates a race
// on the creation of entities for that avatar instance and the deletion of entities for this instance
avatar->removeAvatarEntitiesFromTree();
if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == KillAvatarReason::NoReason) {
emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID());
emit DependencyManager::get<UsersScriptingInterface>()->enteredIgnoreRadius();
if (removalReason != KillAvatarReason::AvatarDisconnected) {
if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) {
emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID());
emit DependencyManager::get<UsersScriptingInterface>()->enteredIgnoreRadius();
}
workload::Transaction workloadTransaction;
workloadTransaction.remove(avatar->getSpaceIndex());
@ -509,7 +518,7 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar
render::Transaction transaction;
avatar->removeFromScene(avatar, scene, transaction);
scene->enqueueTransaction(transaction);
} else if (removalReason == KillAvatarReason::AvatarDisconnected) {
} else {
// remove from node sets, if present
DependencyManager::get<NodeList>()->removeFromIgnoreMuteSets(avatar->getSessionUUID());
DependencyManager::get<UsersScriptingInterface>()->avatarDisconnected(avatar->getSessionUUID());
@ -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;
@ -932,6 +941,19 @@ void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptV
}
}
/**jsdoc
* PAL (People Access List) data for an avatar.
* @typedef {object} AvatarManager.PalData
* @property {Uuid} sessionUUID - The avatar's session ID. <code>""</code> if the avatar is your own.
* @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 {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the
* domain.
* @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.
*/
QVariantMap AvatarManager::getPalData(const QStringList& specificAvatarIdentifiers) {
QJsonArray palData;

View file

@ -37,10 +37,11 @@
using SortedAvatar = std::pair<float, std::shared_ptr<Avatar>>;
/**jsdoc
* The AvatarManager API has properties and methods which manage Avatars within the same domain.
* The <code>AvatarManager</code> API provides information about avatars within the current domain. The avatars available are
* those that Interface has displayed and therefore knows about.
*
* <p><strong>Note:</strong> This API is also provided to Interface and client entity scripts as the synonym,
* <code>AvatarList</code>. For assignment client scripts, see the separate {@link AvatarList} API.
* <p><strong>Warning:</strong> This API is also provided to Interface, client entity, and avatar scripts as the synonym,
* "<code>AvatarList</code>". For assignment client scripts, see the separate {@link AvatarList} API.</p>
*
* @namespace AvatarManager
*
@ -48,8 +49,9 @@ using SortedAvatar = std::pair<float, std::shared_ptr<Avatar>>;
* @hifi-client-entity
* @hifi-avatar
*
* @borrows AvatarList.getAvatarIdentifiers as getAvatarIdentifiers
* @borrows AvatarList.getAvatarsInRange as getAvatarsInRange
* @borrows AvatarList.getAvatar as getAvatar
* @comment AvatarList.getAvatarIdentifiers as getAvatarIdentifiers - Don't borrow because behavior is slightly different.
* @comment AvatarList.getAvatarsInRange as getAvatarsInRange - Don't borrow because behavior is slightly different.
* @borrows AvatarList.avatarAddedEvent as avatarAddedEvent
* @borrows AvatarList.avatarRemovedEvent as avatarRemovedEvent
* @borrows AvatarList.avatarSessionChangedEvent as avatarSessionChangedEvent
@ -67,6 +69,31 @@ class AvatarManager : public AvatarHashMap {
public:
/**jsdoc
* Gets the IDs of all avatars known about in the domain.
* Your own avatar is included in the list as a <code>null</code> value.
* @function AvatarManager.getAvatarIdentifiers
* @returns {Uuid[]} The IDs of all known avatars in the domain.
* @example <caption>Report the IDS of all avatars within the domain.</caption>
* var avatars = AvatarManager.getAvatarIdentifiers();
* print("Avatars in the domain: " + JSON.stringify(avatars));
* // A null item is included for your avatar.
*/
/**jsdoc
* Gets the IDs of all avatars known about within a specified distance from a point.
* Your own avatar's ID is included in the list if it is in range.
* @function AvatarManager.getAvatarsInRange
* @param {Vec3} position - The point about which the search is performed.
* @param {number} range - The search radius.
* @returns {Uuid[]} The IDs of all known avatars within the search distance from the position.
* @example <caption>Report the IDs of all avatars within 10m of your avatar.</caption>
* var RANGE = 10;
* var avatars = AvatarManager.getAvatarsInRange(MyAvatar.position, RANGE);
* print("Nearby avatars: " + JSON.stringify(avatars));
* print("Own avatar: " + MyAvatar.sessionUUID);
*/
/// Registers the script types associated with the avatar manager.
static void registerMetaTypes(QScriptEngine* engine);
@ -79,9 +106,7 @@ public:
glm::vec3 getMyAvatarPosition() const { return _myAvatar->getWorldPosition(); }
/**jsdoc
* @function AvatarManager.getAvatar
* @param {Uuid} avatarID
* @returns {AvatarData}
* @comment Uses the base class's JSDoc.
*/
// Null/Default-constructed QUuids will return MyAvatar
Q_INVOKABLE virtual ScriptAvatarData* getAvatar(QUuid avatarID) override { return new ScriptAvatar(getAvatarBySessionID(avatarID)); }
@ -112,36 +137,53 @@ public:
void handleCollisionEvents(const CollisionEvents& collisionEvents);
/**jsdoc
* Gets the amount of avatar mixer data being generated by an avatar other than your own.
* @function AvatarManager.getAvatarDataRate
* @param {Uuid} sessionID
* @param {string} [rateName=""]
* @returns {number}
* @param {Uuid} sessionID - The ID of the avatar whose data rate you're retrieving.
* @param {AvatarDataRate} [rateName=""] - The type of avatar mixer data to get the data rate of.
* @returns {number} The data rate in kbps; <code>0</code> if the avatar is your own.
*/
Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
/**jsdoc
* Gets the update rate of avatar mixer data being generated by an avatar other than your own.
* @function AvatarManager.getAvatarUpdateRate
* @param {Uuid} sessionID
* @param {string} [rateName=""]
* @returns {number}
* @param {Uuid} sessionID - The ID of the avatar whose update rate you're retrieving.
* @param {AvatarUpdateRate} [rateName=""] - The type of avatar mixer data to get the update rate of.
* @returns {number} The update rate in Hz; <code>0</code> if the avatar is your own.
*/
Q_INVOKABLE float getAvatarUpdateRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
/**jsdoc
* Gets the simulation rate of an avatar other than your own.
* @function AvatarManager.getAvatarSimulationRate
* @param {Uuid} sessionID
* @param {string} [rateName=""]
* @returns {number}
* @param {Uuid} sessionID - The ID of the avatar whose simulation you're retrieving.
* @param {AvatarSimulationRate} [rateName=""] - The type of avatar data to get the simulation rate of.
* @returns {number} The simulation rate in Hz; <code>0</code> if the avatar is your own.
*/
Q_INVOKABLE float getAvatarSimulationRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
/**jsdoc
* Find the first avatar intersected by a {@link PickRay}.
* @function AvatarManager.findRayIntersection
* @param {PickRay} ray
* @param {Uuid[]} [avatarsToInclude=[]]
* @param {Uuid[]} [avatarsToDiscard=[]]
* @param {boolean} pickAgainstMesh
* @returns {RayToAvatarIntersectionResult}
* @param {PickRay} ray - The ray to use for finding avatars.
* @param {Uuid[]} [avatarsToInclude=[]] - If not empty then search is restricted to these avatars.
* @param {Uuid[]} [avatarsToDiscard=[]] - Avatars to ignore in the search.
* @param {boolean} [pickAgainstMesh=true] - If <code>true</code> then the exact intersection with the avatar mesh is
* calculated, if <code>false</code> then the intersection is approximate.
* @returns {RayToAvatarIntersectionResult} The result of the search for the first intersected avatar.
* @example <caption>Find the first avatar directly in front of you.</caption>
* var pickRay = {
* origin: MyAvatar.position,
* direction: Quat.getFront(MyAvatar.orientation)
* };
*
* var intersection = AvatarManager.findRayIntersection(pickRay);
* if (intersection.intersects) {
* print("Avatar found: " + JSON.stringify(intersection));
* } else {
* print("No avatar found.");
* }
*/
Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersection(const PickRay& ray,
const QScriptValue& avatarIdsToInclude = QScriptValue(),
@ -149,11 +191,12 @@ public:
bool pickAgainstMesh = true);
/**jsdoc
* @function AvatarManager.findRayIntersectionVector
* @param {PickRay} ray
* @param {Uuid[]} avatarsToInclude
* @param {Uuid[]} avatarsToDiscard
* @param {boolean} pickAgainstMesh
* @returns {RayToAvatarIntersectionResult}
* @param {PickRay} ray - Ray.
* @param {Uuid[]} avatarsToInclude - Avatars to include.
* @param {Uuid[]} avatarsToDiscard - Avatars to discard.
* @param {boolean} pickAgainstMesh - Pick against mesh.
* @returns {RayToAvatarIntersectionResult} Intersection result.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersectionVector(const PickRay& ray,
const QVector<EntityItemID>& avatarsToInclude,
@ -162,10 +205,11 @@ public:
/**jsdoc
* @function AvatarManager.findParabolaIntersectionVector
* @param {PickParabola} pick
* @param {Uuid[]} avatarsToInclude
* @param {Uuid[]} avatarsToDiscard
* @returns {ParabolaToAvatarIntersectionResult}
* @param {PickParabola} pick - Pick.
* @param {Uuid[]} avatarsToInclude - Avatars to include.
* @param {Uuid[]} avatarsToDiscard - Avatars to discard.
* @returns {ParabolaToAvatarIntersectionResult} Intersection result.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE ParabolaToAvatarIntersectionResult findParabolaIntersectionVector(const PickParabola& pick,
const QVector<EntityItemID>& avatarsToInclude,
@ -173,27 +217,31 @@ public:
/**jsdoc
* @function AvatarManager.getAvatarSortCoefficient
* @param {string} name
* @returns {number}
* @param {string} name - Name.
* @returns {number} Value.
* @deprecated This function is deprecated and will be removed.
*/
// TODO: remove this HACK once we settle on optimal default sort coefficients
Q_INVOKABLE float getAvatarSortCoefficient(const QString& name);
/**jsdoc
* @function AvatarManager.setAvatarSortCoefficient
* @param {string} name
* @param {number} value
* @param {string} name - Name
* @param {number} value - Value.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value);
/**jsdoc
* Used in the PAL for getting PAL-related data about avatars nearby. Using this method is faster
* than iterating over each avatar and obtaining data about them in JavaScript, as that method
* locks and unlocks each avatar's data structure potentially hundreds of times per update tick.
* Gets PAL (People Access List) data for one or more avatars. Using this method is faster than iterating over each avatar
* and obtaining data about each individually.
* @function AvatarManager.getPalData
* @param {string[]} [specificAvatarIdentifiers=[]] - The list of IDs of the avatars you want the PAL data for.
* If an empty list, the PAL data for all nearby avatars is returned.
* @returns {object[]} An array of objects, each object being the PAL data for an avatar.
* @param {string[]} [avatarIDs=[]] - The IDs of the avatars to get the PAL data for. If empty, then PAL data is obtained
* for all avatars.
* @returns {object<"data", AvatarManager.PalData[]>} An array of objects, each object being the PAL data for an avatar.
* @example <caption>Report the PAL data for an avatar nearby.</caption>
* var palData = AvatarManager.getPalData();
* print("PAL data for one avatar: " + JSON.stringify(palData.data[0]));
*/
Q_INVOKABLE QVariantMap getPalData(const QStringList& specificAvatarIdentifiers = QStringList());
@ -209,10 +257,20 @@ public:
public slots:
/**jsdoc
* @function AvatarManager.updateAvatarRenderStatus
* @param {boolean} shouldRenderAvatars
* @param {boolean} shouldRenderAvatars - Should render avatars.
* @deprecated This function is deprecated and will be removed.
*/
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;
@ -250,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),
@ -818,20 +819,8 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
if (_cauterizationNeedsUpdate) {
_cauterizationNeedsUpdate = false;
// Redisplay cauterized entities that are no longer children of the avatar.
auto cauterizedChild = _cauterizedChildrenOfHead.begin();
if (cauterizedChild != _cauterizedChildrenOfHead.end()) {
auto children = getChildren();
while (cauterizedChild != _cauterizedChildrenOfHead.end()) {
if (!children.contains(*cauterizedChild)) {
updateChildCauterization(*cauterizedChild, false);
cauterizedChild = _cauterizedChildrenOfHead.erase(cauterizedChild);
} else {
++cauterizedChild;
}
}
}
auto objectsToUncauterize = _cauterizedChildrenOfHead;
_cauterizedChildrenOfHead.clear();
// Update cauterization of entities that are children of the avatar.
auto headBoneSet = _skeletonModel->getCauterizeBoneSet();
forEachChild([&](SpatiallyNestablePointer object) {
@ -843,15 +832,19 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
updateChildCauterization(descendant, !_prevShouldDrawHead);
});
_cauterizedChildrenOfHead.insert(object);
} else if (_cauterizedChildrenOfHead.find(object) != _cauterizedChildrenOfHead.end()) {
// Redisplay cauterized children that are not longer children of the head.
updateChildCauterization(object, false);
objectsToUncauterize.erase(object);
} else if (objectsToUncauterize.find(object) == objectsToUncauterize.end()) {
objectsToUncauterize.insert(object);
object->forEachDescendant([&](SpatiallyNestablePointer descendant) {
updateChildCauterization(descendant, false);
objectsToUncauterize.insert(descendant);
});
_cauterizedChildrenOfHead.erase(object);
}
});
// Redisplay cauterized entities that are no longer children of the avatar.
for (auto cauterizedChild = objectsToUncauterize.begin(); cauterizedChild != objectsToUncauterize.end(); cauterizedChild++) {
updateChildCauterization(*cauterizedChild, false);
}
}
{
@ -956,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);
}
@ -1049,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.
@ -1207,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");
@ -1215,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;
@ -1296,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());
@ -1621,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);
}
});
}
}
@ -1898,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());
@ -3180,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) {
@ -3889,7 +3931,8 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette
// See https://highfidelity.fogbugz.com/f/cases/5003/findRayIntersection-has-option-to-use-collidableOnly-but-doesn-t-actually-use-colliders
QVariantMap extraInfo;
EntityItemID entityID = entityTree->evalRayIntersection(startPointIn, directionIn, include, ignore,
PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::COLLIDABLE) | PickFilter::getBitMask(PickFilter::FlagBit::PRECISE)),
PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::COLLIDABLE) | PickFilter::getBitMask(PickFilter::FlagBit::PRECISE)
| PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES) | PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES)), // exclude Local entities
element, distance, face, normalOut, extraInfo, lockType, accurateResult);
if (entityID.isNull()) {
return false;
@ -4805,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) {
@ -5773,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

@ -302,8 +302,11 @@ int main(int argc, const char* argv[]) {
PROFILE_SYNC_BEGIN(startup, "app full ctor", "");
Application app(argcExtended, const_cast<char**>(argvExtended.data()), startupTime, runningMarkerExisted);
PROFILE_SYNC_END(startup, "app full ctor", "");
#if defined(Q_OS_LINUX)
app.setWindowIcon(QIcon(PathUtils::resourcesPath() + "images/hifi-logo.svg"));
#endif
QTimer exitTimer;
if (traceDuration > 0.0f) {
exitTimer.setSingleShot(true);

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

@ -286,7 +286,7 @@ public slots:
* Disables 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);
@ -295,7 +295,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

@ -156,10 +156,10 @@ void DialogsManager::hmdTools(bool showTools) {
}
_hmdToolsDialog->show();
_hmdToolsDialog->raise();
qApp->getWindow()->activateWindow();
} else {
hmdToolsClosed();
}
qApp->getWindow()->activateWindow();
}
void DialogsManager::hmdToolsClosed() {
@ -207,4 +207,4 @@ void DialogsManager::showDomainConnectionDialog() {
_domainConnectionDialog->show();
_domainConnectionDialog->raise();
}
}

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;
}
}

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