Merge branch 'master' into M22075
# Conflicts: # libraries/controllers/src/controllers/Actions.cpp
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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", "");
|
||||
|
|
BIN
interface/resources/avatar/animations/idleWS.fbx
Normal file
BIN
interface/resources/avatar/animations/idleWS_all.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_LFF_all.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_RFF_all.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_lookaround01.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_once_armstretch.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_once_bigstretch.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_once_checkwatch.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_once_headtilt.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_once_lookaround.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_once_neckstretch.fbx
Normal file
BIN
interface/resources/avatar/animations/idle_once_slownod.fbx
Normal file
BIN
interface/resources/avatar/animations/talk04.fbx
Normal file
BIN
interface/resources/avatar/animations/talk_righthand.fbx
Normal 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 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 246 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 331 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 308 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 229 KiB |
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 267 KiB |
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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...";
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
149
interface/src/RefreshRateManager.cpp
Normal 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();
|
||||
});
|
||||
}
|
83
interface/src/RefreshRateManager.h
Normal 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
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ public:
|
|||
void setCollisionWithOtherAvatarsFlags() override;
|
||||
|
||||
void simulate(float deltaTime, bool inView) override;
|
||||
|
||||
void debugJointData() const;
|
||||
friend AvatarManager;
|
||||
|
||||
protected:
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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> – <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) –
|
||||
* <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> – <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 — configured on the server — 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> –
|
||||
* <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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
46
interface/src/scripting/RefreshRateScriptingInterface.h
Normal 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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -910,6 +910,9 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) {
|
|||
});
|
||||
_layerIndex = 0;
|
||||
addIncludeItemsToMallets();
|
||||
|
||||
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
|
||||
scaleKeyboard(myAvatar->getSensorToWorldScale());
|
||||
});
|
||||
|
||||
request->send();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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); };
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -97,6 +97,8 @@ private:
|
|||
bool _cubemapOutputFormat;
|
||||
QTimer _snapshotTimer;
|
||||
qint16 _snapshotIndex;
|
||||
bool _waitingOnSnapshot { false };
|
||||
bool _taking360Snapshot { false };
|
||||
bool _oldEnabled;
|
||||
QVariant _oldAttachedEntityId;
|
||||
QVariant _oldOrientation;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
212
libraries/animation/src/AnimRandomSwitch.cpp
Normal 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;
|
||||
}
|
184
libraries/animation/src/AnimRandomSwitch.h
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|