mirror of
https://github.com/lubosz/overte.git
synced 2025-04-27 11:35:26 +02:00
Merge branch 'master' of github.com:highfidelity/hifi into webview-js
This commit is contained in:
commit
b1abbbe472
37 changed files with 1067 additions and 263 deletions
assignment-client/src/octree
domain-server
resources/web
src
interface
resources/avatar
src
libraries
animation/src
AnimInverseKinematics.cppAnimInverseKinematics.hAnimManipulator.cppAnimManipulator.hAnimNodeLoader.cppAnimOverlay.cppAnimOverlay.hAnimVariant.hIKTarget.hRig.cppRig.h
entities/src
networking/src
octree/src
script-engine/src
scripts/developer/tests
|
@ -11,12 +11,14 @@
|
|||
|
||||
#include "OctreeServer.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include <time.h>
|
||||
|
||||
#include <AccountManager.h>
|
||||
#include <Gzip.h>
|
||||
#include <HTTPConnection.h>
|
||||
#include <LogHandler.h>
|
||||
#include <shared/NetworkUtils.h>
|
||||
|
@ -924,6 +926,57 @@ void OctreeServer::handleJurisdictionRequestPacket(QSharedPointer<ReceivedMessag
|
|||
_jurisdictionSender->queueReceivedPacket(message, senderNode);
|
||||
}
|
||||
|
||||
void OctreeServer::handleOctreeFileReplacement(QSharedPointer<ReceivedMessage> message) {
|
||||
if (!_isFinished && !_isShuttingDown) {
|
||||
// these messages are only allowed to come from the domain server, so make sure that is the case
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
if (message->getSenderSockAddr() == nodeList->getDomainHandler().getSockAddr()) {
|
||||
// it's far cleaner to load up the new content upon server startup
|
||||
// so here we just store a special file at our persist path
|
||||
// and then force a stop of the server so that it can pick it up when it relaunches
|
||||
if (!_persistAbsoluteFilePath.isEmpty()) {
|
||||
|
||||
// before we restart the server and make it try and load this data, let's make sure it is valid
|
||||
auto compressedOctree = message->getMessage();
|
||||
QByteArray jsonOctree;
|
||||
|
||||
// assume we have GZipped content
|
||||
bool wasCompressed = gunzip(compressedOctree, jsonOctree);
|
||||
if (!wasCompressed) {
|
||||
// the source was not compressed, assume we were sent regular JSON data
|
||||
jsonOctree = compressedOctree;
|
||||
}
|
||||
|
||||
// check the JSON data to verify it is an object
|
||||
if (QJsonDocument::fromJson(jsonOctree).isObject()) {
|
||||
if (!wasCompressed) {
|
||||
// source was not compressed, we compress it before we write it locally
|
||||
gzip(jsonOctree, compressedOctree);
|
||||
}
|
||||
|
||||
// write the compressed octree data to a special file
|
||||
auto replacementFilePath = _persistAbsoluteFilePath.append(OctreePersistThread::REPLACEMENT_FILE_EXTENSION);
|
||||
QFile replacementFile(replacementFilePath);
|
||||
if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) {
|
||||
// we've now written our replacement file, time to take the server down so it can
|
||||
// process it when it comes back up
|
||||
qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server";
|
||||
setFinished(true);
|
||||
} else {
|
||||
qWarning() << "Could not write replacement octree data to file - refusing to process";
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Received replacement octree file that is invalid - refusing to process";
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known";
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Received an octree file replacement that was not from our domain server - refusing to process";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool OctreeServer::readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result) {
|
||||
result = false; // assume it doesn't exist
|
||||
bool optionAvailable = false;
|
||||
|
@ -1148,6 +1201,7 @@ void OctreeServer::domainSettingsRequestComplete() {
|
|||
packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket");
|
||||
packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket");
|
||||
packetReceiver.registerListener(PacketType::JurisdictionRequest, this, "handleJurisdictionRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement");
|
||||
|
||||
readConfiguration();
|
||||
|
||||
|
@ -1173,25 +1227,25 @@ void OctreeServer::domainSettingsRequestComplete() {
|
|||
// If persist filename does not exist, let's see if there is one beside the application binary
|
||||
// If there is, let's copy it over to our target persist directory
|
||||
QDir persistPath { _persistFilePath };
|
||||
QString persistAbsoluteFilePath = persistPath.absolutePath();
|
||||
_persistAbsoluteFilePath = persistPath.absolutePath();
|
||||
|
||||
if (persistPath.isRelative()) {
|
||||
// if the domain settings passed us a relative path, make an absolute path that is relative to the
|
||||
// default data directory
|
||||
persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath);
|
||||
_persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath);
|
||||
}
|
||||
|
||||
static const QString ENTITY_PERSIST_EXTENSION = ".json.gz";
|
||||
|
||||
// force the persist file to end with .json.gz
|
||||
if (!persistAbsoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) {
|
||||
persistAbsoluteFilePath += ENTITY_PERSIST_EXTENSION;
|
||||
if (!_persistAbsoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) {
|
||||
_persistAbsoluteFilePath += ENTITY_PERSIST_EXTENSION;
|
||||
} else {
|
||||
// make sure the casing of .json.gz is correct
|
||||
persistAbsoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive);
|
||||
_persistAbsoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
if (!QFile::exists(persistAbsoluteFilePath)) {
|
||||
if (!QFile::exists(_persistAbsoluteFilePath)) {
|
||||
qDebug() << "Persist file does not exist, checking for existence of persist file next to application";
|
||||
|
||||
static const QString OLD_DEFAULT_PERSIST_FILENAME = "resources/models.json.gz";
|
||||
|
@ -1217,7 +1271,7 @@ void OctreeServer::domainSettingsRequestComplete() {
|
|||
pathToCopyFrom = oldDefaultPersistPath;
|
||||
}
|
||||
|
||||
QDir persistFileDirectory { QDir::cleanPath(persistAbsoluteFilePath + "/..") };
|
||||
QDir persistFileDirectory { QDir::cleanPath(_persistAbsoluteFilePath + "/..") };
|
||||
|
||||
if (!persistFileDirectory.exists()) {
|
||||
qDebug() << "Creating data directory " << persistFileDirectory.absolutePath();
|
||||
|
@ -1225,15 +1279,15 @@ void OctreeServer::domainSettingsRequestComplete() {
|
|||
}
|
||||
|
||||
if (shouldCopy) {
|
||||
qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << persistAbsoluteFilePath;
|
||||
qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << _persistAbsoluteFilePath;
|
||||
|
||||
QFile::copy(pathToCopyFrom, persistAbsoluteFilePath);
|
||||
QFile::copy(pathToCopyFrom, _persistAbsoluteFilePath);
|
||||
} else {
|
||||
qDebug() << "No existing persist file found";
|
||||
}
|
||||
}
|
||||
|
||||
auto persistFileDirectory = QFileInfo(persistAbsoluteFilePath).absolutePath();
|
||||
auto persistFileDirectory = QFileInfo(_persistAbsoluteFilePath).absolutePath();
|
||||
if (_backupDirectoryPath.isEmpty()) {
|
||||
// Use the persist file's directory to store backups
|
||||
_backupDirectoryPath = persistFileDirectory;
|
||||
|
@ -1264,7 +1318,7 @@ void OctreeServer::domainSettingsRequestComplete() {
|
|||
qDebug() << "Backups will be stored in: " << _backupDirectoryPath;
|
||||
|
||||
// now set up PersistThread
|
||||
_persistThread = new OctreePersistThread(_tree, persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval,
|
||||
_persistThread = new OctreePersistThread(_tree, _persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval,
|
||||
_wantBackup, _settings, _debugTimestampNow, _persistAsFileType);
|
||||
_persistThread->initialize(true);
|
||||
}
|
||||
|
|
|
@ -136,6 +136,7 @@ private slots:
|
|||
void handleOctreeQueryPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleOctreeDataNackPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleJurisdictionRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleOctreeFileReplacement(QSharedPointer<ReceivedMessage> message);
|
||||
void removeSendThread();
|
||||
|
||||
protected:
|
||||
|
@ -172,6 +173,7 @@ protected:
|
|||
QString _statusHost;
|
||||
|
||||
QString _persistFilePath;
|
||||
QString _persistAbsoluteFilePath;
|
||||
QString _persistAsFileType;
|
||||
QString _backupDirectoryPath;
|
||||
int _packetsPerClientPerInterval;
|
||||
|
|
44
domain-server/resources/web/content/index.shtml
Normal file
44
domain-server/resources/web/content/index.shtml
Normal file
|
@ -0,0 +1,44 @@
|
|||
<!--#include virtual="header.html"-->
|
||||
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="alert" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Upload Entities File</h3>
|
||||
</div>
|
||||
<form id="upload-form" action="upload" enctype="multipart/form-data" method="post">
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
Upload an entities file (e.g.: models.json.gz) to replace the content of this domain.<br>
|
||||
Note: <strong>Your domain's content will be replaced by the content you upload</strong>, but the backup files of your domain's content will not immediately be changed.
|
||||
</p>
|
||||
<p>
|
||||
If your domain has any content that you would like to re-use at a later date, save a manual backup of your models.json.gz file, which is usually stored at the following paths:<br>
|
||||
<pre>C:\Users\[username]\AppData\Roaming\High Fidelity\assignment-client/entities/models.json.gz</pre>
|
||||
<pre>/Users/[username]/Library/Application Support/High Fidelity/assignment-client/entities/models.json.gz</pre>
|
||||
<pre>/home/[username]/.local/share/High Fidelity/assignment-client/entities/models.json.gz</pre>
|
||||
</p>
|
||||
<br>
|
||||
<input type="file" name="entities-file" class="form-control-file" accept=".json, .gz">
|
||||
<br>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<input type="submit" class="btn btn-info" value="Upload">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--#include virtual="footer.html"-->
|
||||
<script src='js/content.js'></script>
|
||||
<script src='/js/sweetalert.min.js'></script>
|
||||
<!--#include virtual="page-end.html"-->
|
45
domain-server/resources/web/content/js/content.js
Normal file
45
domain-server/resources/web/content/js/content.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
$(document).ready(function(){
|
||||
|
||||
function showSpinnerAlert(title) {
|
||||
swal({
|
||||
title: title,
|
||||
text: '<div class="spinner" style="color:black;"><div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div></div>',
|
||||
html: true,
|
||||
showConfirmButton: false,
|
||||
allowEscapeKey: false
|
||||
});
|
||||
}
|
||||
|
||||
var frm = $('#upload-form');
|
||||
frm.submit(function (ev) {
|
||||
$.ajax({
|
||||
type: frm.attr('method'),
|
||||
url: frm.attr('action'),
|
||||
data: new FormData($(this)[0]),
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (data) {
|
||||
swal({
|
||||
title: 'Uploaded',
|
||||
type: 'success',
|
||||
text: 'Your Entity Server is restarting to replace its local content with the uploaded file.',
|
||||
confirmButtonText: 'OK'
|
||||
})
|
||||
},
|
||||
error: function (data) {
|
||||
swal({
|
||||
title: '',
|
||||
type: 'error',
|
||||
text: 'Your entities file could not be transferred to the Entity Server.</br>Verify that the file is a <i>.json</i> or <i>.json.gz</i> entities file and try again.',
|
||||
html: true,
|
||||
confirmButtonText: 'OK',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
showSpinnerAlert("Uploading Entities File");
|
||||
});
|
||||
});
|
|
@ -36,6 +36,7 @@
|
|||
<li><a href="/assignment">New Assignment</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/content/">Content</a></li>
|
||||
<li><a href="/settings/">Settings</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
<script src='/js/underscore-keypath.min.js'></script>
|
||||
<script src='/js/bootbox.min.js'></script>
|
||||
<script src='js/bootstrap-switch.min.js'></script>
|
||||
<script src='js/sweetalert.min.js'></script>
|
||||
<script src='/js/sweetalert.min.js'></script>
|
||||
<script src='js/settings.js'></script>
|
||||
<script src='js/form2js.min.js'></script>
|
||||
<script src='js/sha256.js'></script>
|
||||
|
|
|
@ -1633,6 +1633,15 @@ QString pathForAssignmentScript(const QUuid& assignmentUUID) {
|
|||
return directory.absoluteFilePath(uuidStringWithoutCurlyBraces(assignmentUUID));
|
||||
}
|
||||
|
||||
QString DomainServer::pathForRedirect(QString path) const {
|
||||
// make sure the passed path has a leading slash
|
||||
if (!path.startsWith('/')) {
|
||||
path.insert(0, '/');
|
||||
}
|
||||
|
||||
return "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()) + path;
|
||||
}
|
||||
|
||||
const QString URI_OAUTH = "/oauth";
|
||||
bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) {
|
||||
const QString JSON_MIME_TYPE = "application/json";
|
||||
|
@ -1640,6 +1649,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
const QString URI_ASSIGNMENT = "/assignment";
|
||||
const QString URI_NODES = "/nodes";
|
||||
const QString URI_SETTINGS = "/settings";
|
||||
const QString URI_ENTITY_FILE_UPLOAD = "/content/upload";
|
||||
|
||||
const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
||||
|
||||
|
@ -1869,6 +1879,25 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
// respond with a 200 code for successful upload
|
||||
connection->respond(HTTPConnection::StatusCode200);
|
||||
|
||||
return true;
|
||||
} else if (url.path() == URI_ENTITY_FILE_UPLOAD) {
|
||||
// this is an entity file upload, ask the HTTPConnection to parse the data
|
||||
QList<FormData> formData = connection->parseFormData();
|
||||
|
||||
Headers redirectHeaders;
|
||||
|
||||
if (formData.size() > 0 && formData[0].second.size() > 0) {
|
||||
// invoke our method to hand the new octree file off to the octree server
|
||||
QMetaObject::invokeMethod(this, "handleOctreeFileReplacement",
|
||||
Qt::QueuedConnection, Q_ARG(QByteArray, formData[0].second));
|
||||
|
||||
// respond with a 200 for success
|
||||
connection->respond(HTTPConnection::StatusCode200);
|
||||
} else {
|
||||
// respond with a 400 for failure
|
||||
connection->respond(HTTPConnection::StatusCode400);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} else if (connection->requestOperation() == QNetworkAccessManager::DeleteOperation) {
|
||||
|
@ -2159,8 +2188,7 @@ Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileR
|
|||
cookieHeaders.insert("Set-Cookie", cookieString.toUtf8());
|
||||
|
||||
// redirect the user back to the homepage so they can present their cookie and be authenticated
|
||||
QString redirectString = "http://" + _hostname + ":" + QString::number(_httpManager.serverPort());
|
||||
cookieHeaders.insert("Location", redirectString.toUtf8());
|
||||
cookieHeaders.insert("Location", pathForRedirect().toUtf8());
|
||||
|
||||
return cookieHeaders;
|
||||
}
|
||||
|
@ -2560,3 +2588,20 @@ void DomainServer::setupGroupCacheRefresh() {
|
|||
_metaverseGroupCacheTimer->start(REFRESH_GROUPS_INTERVAL_MSECS);
|
||||
}
|
||||
}
|
||||
|
||||
void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) {
|
||||
// enumerate the nodes and find any octree type servers with active sockets
|
||||
|
||||
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
||||
limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) {
|
||||
return node->getType() == NodeType::EntityServer && node->getActiveSocket();
|
||||
}, [&octreeFile, limitedNodeList](const SharedNodePointer& octreeNode) {
|
||||
// setup a packet to send to this octree server with the new octree file data
|
||||
auto octreeFilePacketList = NLPacketList::create(PacketType::OctreeFileReplacement, QByteArray(), true, true);
|
||||
octreeFilePacketList->write(octreeFile);
|
||||
|
||||
qDebug() << "Sending an octree file replacement of" << octreeFile.size() << "bytes to" << octreeNode;
|
||||
|
||||
limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -100,6 +100,8 @@ private slots:
|
|||
void handleSuccessfulICEServerAddressUpdate(QNetworkReply& requestReply);
|
||||
void handleFailedICEServerAddressUpdate(QNetworkReply& requestReply);
|
||||
|
||||
void handleOctreeFileReplacement(QByteArray octreeFile);
|
||||
|
||||
signals:
|
||||
void iceServerChanged();
|
||||
void userConnected();
|
||||
|
@ -161,6 +163,8 @@ private:
|
|||
|
||||
void setupGroupCacheRefresh();
|
||||
|
||||
QString pathForRedirect(QString path = QString()) const;
|
||||
|
||||
SubnetList _acSubnetWhitelist;
|
||||
|
||||
DomainGatekeeper _gatekeeper;
|
||||
|
|
|
@ -50,6 +50,12 @@
|
|||
"type": "inverseKinematics",
|
||||
"data": {
|
||||
"targets": [
|
||||
{
|
||||
"jointName": "Hips",
|
||||
"positionVar": "hipsPosition",
|
||||
"rotationVar": "hipsRotation",
|
||||
"typeVar": "hipsType"
|
||||
},
|
||||
{
|
||||
"jointName": "RightHand",
|
||||
"positionVar": "rightHandPosition",
|
||||
|
@ -75,10 +81,10 @@
|
|||
"typeVar": "leftFootType"
|
||||
},
|
||||
{
|
||||
"jointName": "Neck",
|
||||
"positionVar": "neckPosition",
|
||||
"rotationVar": "neckRotation",
|
||||
"typeVar": "neckType"
|
||||
"jointName": "Spine2",
|
||||
"positionVar": "spine2Position",
|
||||
"rotationVar": "spine2Rotation",
|
||||
"typeVar": "spine2Type"
|
||||
},
|
||||
{
|
||||
"jointName": "Head",
|
||||
|
@ -91,20 +97,27 @@
|
|||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "manipulatorOverlay",
|
||||
"id": "hipsManipulatorOverlay",
|
||||
"type": "overlay",
|
||||
"data": {
|
||||
"alpha": 1.0,
|
||||
"boneSet": "spineOnly"
|
||||
"alpha": 0.0,
|
||||
"boneSet": "hipsOnly"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "spineLean",
|
||||
"id": "hipsManipulator",
|
||||
"type": "manipulator",
|
||||
"data": {
|
||||
"alpha": 0.0,
|
||||
"alphaVar": "hipsManipulatorAlpha",
|
||||
"joints": [
|
||||
{ "type": "absoluteRotation", "jointName": "Spine", "var": "lean" }
|
||||
{
|
||||
"jointName": "Hips",
|
||||
"rotationType": "absolute",
|
||||
"translationType": "absolute",
|
||||
"rotationVar": "hipsManipulatorRotation",
|
||||
"translationVar": "hipsManipulatorPosition"
|
||||
}
|
||||
]
|
||||
},
|
||||
"children": []
|
||||
|
|
|
@ -798,7 +798,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain()));
|
||||
connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle()));
|
||||
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
|
||||
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails()));
|
||||
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &Application::clearDomainAvatars);
|
||||
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() {
|
||||
getOverlays().deleteOverlay(getTabletScreenID());
|
||||
getOverlays().deleteOverlay(getTabletHomeButtonID());
|
||||
|
@ -5179,7 +5179,6 @@ void Application::clearDomainOctreeDetails() {
|
|||
qCDebug(interfaceapp) << "Clearing domain octree details...";
|
||||
|
||||
resetPhysicsReadyInformation();
|
||||
getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities
|
||||
|
||||
// reset our node to stats and node to jurisdiction maps... since these must be changing...
|
||||
_entityServerJurisdictions.withWriteLock([&] {
|
||||
|
@ -5198,14 +5197,18 @@ void Application::clearDomainOctreeDetails() {
|
|||
skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT);
|
||||
|
||||
_recentlyClearedDomain = true;
|
||||
|
||||
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
|
||||
|
||||
DependencyManager::get<AnimationCache>()->clearUnusedResources();
|
||||
DependencyManager::get<ModelCache>()->clearUnusedResources();
|
||||
DependencyManager::get<SoundCache>()->clearUnusedResources();
|
||||
DependencyManager::get<TextureCache>()->clearUnusedResources();
|
||||
}
|
||||
|
||||
void Application::clearDomainAvatars() {
|
||||
getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities
|
||||
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
|
||||
}
|
||||
|
||||
void Application::domainChanged(const QString& domainHostname) {
|
||||
updateWindowTitle();
|
||||
// disable physics until we have enough information about our new location to not cause craziness.
|
||||
|
@ -5275,33 +5278,8 @@ void Application::nodeKilled(SharedNodePointer node) {
|
|||
if (node->getType() == NodeType::AudioMixer) {
|
||||
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "audioMixerKilled");
|
||||
} else if (node->getType() == NodeType::EntityServer) {
|
||||
QUuid nodeUUID = node->getUUID();
|
||||
// see if this is the first we've heard of this node...
|
||||
_entityServerJurisdictions.withReadLock([&] {
|
||||
if (_entityServerJurisdictions.find(nodeUUID) == _entityServerJurisdictions.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto rootCode = _entityServerJurisdictions[nodeUUID].getRootOctalCode();
|
||||
VoxelPositionSize rootDetails;
|
||||
voxelDetailsForCode(rootCode.get(), rootDetails);
|
||||
|
||||
qCDebug(interfaceapp, "model server going away...... v[%f, %f, %f, %f]",
|
||||
(double)rootDetails.x, (double)rootDetails.y, (double)rootDetails.z, (double)rootDetails.s);
|
||||
|
||||
});
|
||||
|
||||
// If the model server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server
|
||||
_entityServerJurisdictions.withWriteLock([&] {
|
||||
_entityServerJurisdictions.erase(_entityServerJurisdictions.find(nodeUUID));
|
||||
});
|
||||
|
||||
// also clean up scene stats for that server
|
||||
_octreeServerSceneStats.withWriteLock([&] {
|
||||
if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) {
|
||||
_octreeServerSceneStats.erase(nodeUUID);
|
||||
}
|
||||
});
|
||||
// we lost an entity server, clear all of the domain octree details
|
||||
clearDomainOctreeDetails();
|
||||
} else if (node->getType() == NodeType::AvatarMixer) {
|
||||
// our avatar mixer has gone away - clear the hash of avatars
|
||||
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
|
||||
|
|
|
@ -409,6 +409,7 @@ public slots:
|
|||
private slots:
|
||||
void showDesktop();
|
||||
void clearDomainOctreeDetails();
|
||||
void clearDomainAvatars();
|
||||
void aboutToQuit();
|
||||
|
||||
void resettingDomain();
|
||||
|
|
|
@ -465,6 +465,10 @@ public:
|
|||
void removeHoldAction(AvatarActionHold* holdAction); // thread-safe
|
||||
void updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose);
|
||||
|
||||
// derive avatar body position and orientation from the current HMD Sensor location.
|
||||
// results are in HMD frame
|
||||
glm::mat4 deriveBodyFromHMDSensor() const;
|
||||
|
||||
public slots:
|
||||
void increaseSize();
|
||||
void decreaseSize();
|
||||
|
@ -553,9 +557,7 @@ private:
|
|||
|
||||
void setVisibleInSceneIfReady(Model* model, const render::ScenePointer& scene, bool visiblity);
|
||||
|
||||
// derive avatar body position and orientation from the current HMD Sensor location.
|
||||
// results are in HMD frame
|
||||
glm::mat4 deriveBodyFromHMDSensor() const;
|
||||
private:
|
||||
|
||||
virtual void updatePalms() override {}
|
||||
void lateUpdatePalms();
|
||||
|
|
|
@ -119,7 +119,13 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
|||
headParams.rigHeadPosition = extractTranslation(rigHMDMat);
|
||||
headParams.rigHeadOrientation = extractRotation(rigHMDMat);
|
||||
headParams.worldHeadOrientation = extractRotation(worldHMDMat);
|
||||
|
||||
// TODO: if hips target sensor is valid.
|
||||
// Copy it into headParams.hipsMatrix, and set headParams.hipsEnabled to true.
|
||||
|
||||
headParams.hipsEnabled = false;
|
||||
} else {
|
||||
headParams.hipsEnabled = false;
|
||||
headParams.isInHMD = false;
|
||||
|
||||
// We don't have a valid localHeadPosition.
|
||||
|
|
|
@ -86,7 +86,9 @@ void AnimInverseKinematics::setTargetVars(
|
|||
void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std::vector<IKTarget>& targets, const AnimPoseVec& underPoses) {
|
||||
// build a list of valid targets from _targetVarVec and animVars
|
||||
_maxTargetIndex = -1;
|
||||
_hipsTargetIndex = -1;
|
||||
bool removeUnfoundJoints = false;
|
||||
|
||||
for (auto& targetVar : _targetVarVec) {
|
||||
if (targetVar.jointIndex == -1) {
|
||||
// this targetVar hasn't been validated yet...
|
||||
|
@ -105,15 +107,18 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std::
|
|||
AnimPose defaultPose = _skeleton->getAbsolutePose(targetVar.jointIndex, underPoses);
|
||||
glm::quat rotation = animVars.lookupRigToGeometry(targetVar.rotationVar, defaultPose.rot());
|
||||
glm::vec3 translation = animVars.lookupRigToGeometry(targetVar.positionVar, defaultPose.trans());
|
||||
if (target.getType() == IKTarget::Type::HipsRelativeRotationAndPosition) {
|
||||
translation += _hipsOffset;
|
||||
}
|
||||
|
||||
target.setPose(rotation, translation);
|
||||
target.setIndex(targetVar.jointIndex);
|
||||
targets.push_back(target);
|
||||
if (targetVar.jointIndex > _maxTargetIndex) {
|
||||
_maxTargetIndex = targetVar.jointIndex;
|
||||
}
|
||||
|
||||
// record the index of the hips ik target.
|
||||
if (target.getIndex() == _hipsIndex) {
|
||||
_hipsTargetIndex = (int)targets.size() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,18 +247,21 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
|
|||
// the tip's parent-relative as we proceed up the chain
|
||||
glm::quat tipParentOrientation = absolutePoses[pivotIndex].rot();
|
||||
|
||||
// NOTE: if this code is removed, the head will remain rigid, causing the spine/hips to thrust forward backward
|
||||
// as the head is nodded.
|
||||
if (targetType == IKTarget::Type::HmdHead) {
|
||||
|
||||
// rotate tip directly to target orientation
|
||||
tipOrientation = target.getRotation();
|
||||
glm::quat tipRelativeRotation = glm::normalize(tipOrientation * glm::inverse(tipParentOrientation));
|
||||
glm::quat tipRelativeRotation = glm::inverse(tipParentOrientation) * tipOrientation;
|
||||
|
||||
// enforce tip's constraint
|
||||
// then enforce tip's constraint
|
||||
RotationConstraint* constraint = getConstraint(tipIndex);
|
||||
if (constraint) {
|
||||
bool constrained = constraint->apply(tipRelativeRotation);
|
||||
if (constrained) {
|
||||
tipOrientation = glm::normalize(tipRelativeRotation * tipParentOrientation);
|
||||
tipRelativeRotation = glm::normalize(tipOrientation * glm::inverse(tipParentOrientation));
|
||||
tipOrientation = tipParentOrientation * tipRelativeRotation;
|
||||
tipRelativeRotation = tipRelativeRotation;
|
||||
}
|
||||
}
|
||||
// store the relative rotation change in the accumulator
|
||||
|
@ -277,7 +285,9 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
|
|||
|
||||
const float MIN_AXIS_LENGTH = 1.0e-4f;
|
||||
RotationConstraint* constraint = getConstraint(pivotIndex);
|
||||
if (constraint && constraint->isLowerSpine() && tipIndex != _headIndex) {
|
||||
|
||||
// only allow swing on lowerSpine if there is a hips IK target.
|
||||
if (_hipsTargetIndex < 0 && constraint && constraint->isLowerSpine() && tipIndex != _headIndex) {
|
||||
// for these types of targets we only allow twist at the lower-spine
|
||||
// (this prevents the hand targets from bending the spine too much and thereby driving the hips too far)
|
||||
glm::vec3 twistAxis = absolutePoses[pivotIndex].trans() - absolutePoses[pivotsParentIndex].trans();
|
||||
|
@ -420,13 +430,13 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
|
|||
_relativePoses[i].trans() = underPoses[i].trans();
|
||||
}
|
||||
|
||||
if (!_relativePoses.empty()) {
|
||||
if (!underPoses.empty()) {
|
||||
// Sometimes the underpose itself can violate the constraints. Rather than
|
||||
// clamp the animation we dynamically expand each constraint to accomodate it.
|
||||
std::map<int, RotationConstraint*>::iterator constraintItr = _constraints.begin();
|
||||
while (constraintItr != _constraints.end()) {
|
||||
int index = constraintItr->first;
|
||||
constraintItr->second->dynamicallyAdjustLimits(_relativePoses[index].rot());
|
||||
constraintItr->second->dynamicallyAdjustLimits(underPoses[index].rot());
|
||||
++constraintItr;
|
||||
}
|
||||
}
|
||||
|
@ -441,64 +451,76 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
|
|||
computeTargets(animVars, targets, underPoses);
|
||||
}
|
||||
|
||||
// debug render ik targets
|
||||
if (context.getEnableDebugDrawIKTargets()) {
|
||||
const vec4 WHITE(1.0f);
|
||||
glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3());
|
||||
|
||||
for (auto& target : targets) {
|
||||
glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation());
|
||||
glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat;
|
||||
|
||||
QString name = QString("ikTarget%1").arg(target.getIndex());
|
||||
DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE);
|
||||
}
|
||||
} else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) {
|
||||
// remove markers if they were added last frame.
|
||||
for (auto& target : targets) {
|
||||
QString name = QString("ikTarget%1").arg(target.getIndex());
|
||||
DebugDraw::getInstance().removeMyAvatarMarker(name);
|
||||
}
|
||||
}
|
||||
|
||||
_previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets();
|
||||
|
||||
if (targets.empty()) {
|
||||
// no IK targets but still need to enforce constraints
|
||||
std::map<int, RotationConstraint*>::iterator constraintItr = _constraints.begin();
|
||||
while (constraintItr != _constraints.end()) {
|
||||
int index = constraintItr->first;
|
||||
glm::quat rotation = _relativePoses[index].rot();
|
||||
constraintItr->second->apply(rotation);
|
||||
_relativePoses[index].rot() = rotation;
|
||||
++constraintItr;
|
||||
}
|
||||
_relativePoses = underPoses;
|
||||
} else {
|
||||
|
||||
{
|
||||
PROFILE_RANGE_EX(simulation_animation, "ik/shiftHips", 0xffff00ff, 0);
|
||||
|
||||
// shift hips according to the _hipsOffset from the previous frame
|
||||
float offsetLength = glm::length(_hipsOffset);
|
||||
const float MIN_HIPS_OFFSET_LENGTH = 0.03f;
|
||||
if (offsetLength > MIN_HIPS_OFFSET_LENGTH && _hipsIndex >= 0) {
|
||||
// but only if offset is long enough
|
||||
float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength);
|
||||
if (_hipsParentIndex == -1) {
|
||||
// the hips are the root so _hipsOffset is in the correct frame
|
||||
_relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans() + scaleFactor * _hipsOffset;
|
||||
if (_hipsTargetIndex >= 0 && _hipsTargetIndex < (int)targets.size()) {
|
||||
// slam the hips to match the _hipsTarget
|
||||
AnimPose absPose = targets[_hipsTargetIndex].getPose();
|
||||
int parentIndex = _skeleton->getParentIndex(targets[_hipsTargetIndex].getIndex());
|
||||
if (parentIndex != -1) {
|
||||
_relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(parentIndex, _relativePoses).inverse() * absPose;
|
||||
} else {
|
||||
// the hips are NOT the root so we need to transform _hipsOffset into hips local-frame
|
||||
glm::quat hipsFrameRotation = _relativePoses[_hipsParentIndex].rot();
|
||||
int index = _skeleton->getParentIndex(_hipsParentIndex);
|
||||
while (index != -1) {
|
||||
hipsFrameRotation *= _relativePoses[index].rot();
|
||||
index = _skeleton->getParentIndex(index);
|
||||
_relativePoses[_hipsIndex] = absPose;
|
||||
}
|
||||
} else {
|
||||
// if there is no hips target, shift hips according to the _hipsOffset from the previous frame
|
||||
float offsetLength = glm::length(_hipsOffset);
|
||||
const float MIN_HIPS_OFFSET_LENGTH = 0.03f;
|
||||
if (offsetLength > MIN_HIPS_OFFSET_LENGTH && _hipsIndex >= 0) {
|
||||
float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength);
|
||||
glm::vec3 hipsOffset = scaleFactor * _hipsOffset;
|
||||
if (_hipsParentIndex == -1) {
|
||||
_relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans() + hipsOffset;
|
||||
} else {
|
||||
auto absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses);
|
||||
absHipsPose.trans() += hipsOffset;
|
||||
_relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose;
|
||||
}
|
||||
_relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans()
|
||||
+ glm::inverse(glm::normalize(hipsFrameRotation)) * (scaleFactor * _hipsOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// update all HipsRelative targets to account for the hips shift/ik target.
|
||||
auto shiftedHipsAbsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses);
|
||||
auto underHipsAbsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses);
|
||||
auto absHipsOffset = shiftedHipsAbsPose.trans() - underHipsAbsPose.trans();
|
||||
for (auto& target: targets) {
|
||||
if (target.getType() == IKTarget::Type::HipsRelativeRotationAndPosition) {
|
||||
auto pose = target.getPose();
|
||||
pose.trans() = pose.trans() + absHipsOffset;
|
||||
target.setPose(pose.rot(), pose.trans());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
PROFILE_RANGE_EX(simulation_animation, "ik/debugDraw", 0xffff00ff, 0);
|
||||
|
||||
// debug render ik targets
|
||||
if (context.getEnableDebugDrawIKTargets()) {
|
||||
const vec4 WHITE(1.0f);
|
||||
glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3());
|
||||
|
||||
for (auto& target : targets) {
|
||||
glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation());
|
||||
glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat;
|
||||
|
||||
QString name = QString("ikTarget%1").arg(target.getIndex());
|
||||
DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE);
|
||||
}
|
||||
} else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) {
|
||||
// remove markers if they were added last frame.
|
||||
for (auto& target : targets) {
|
||||
QString name = QString("ikTarget%1").arg(target.getIndex());
|
||||
DebugDraw::getInstance().removeMyAvatarMarker(name);
|
||||
}
|
||||
}
|
||||
|
||||
_previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets();
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -506,64 +528,70 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
|
|||
solveWithCyclicCoordinateDescent(targets);
|
||||
}
|
||||
|
||||
{
|
||||
if (_hipsTargetIndex < 0) {
|
||||
PROFILE_RANGE_EX(simulation_animation, "ik/measureHipsOffset", 0xffff00ff, 0);
|
||||
|
||||
// measure new _hipsOffset for next frame
|
||||
// by looking for discrepancies between where a targeted endEffector is
|
||||
// and where it wants to be (after IK solutions are done)
|
||||
glm::vec3 newHipsOffset = Vectors::ZERO;
|
||||
for (auto& target: targets) {
|
||||
int targetIndex = target.getIndex();
|
||||
if (targetIndex == _headIndex && _headIndex != -1) {
|
||||
// special handling for headTarget
|
||||
if (target.getType() == IKTarget::Type::RotationOnly) {
|
||||
// we want to shift the hips to bring the underPose closer
|
||||
// to where the head happens to be (overpose)
|
||||
glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans();
|
||||
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
|
||||
const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f;
|
||||
newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under);
|
||||
} else if (target.getType() == IKTarget::Type::HmdHead) {
|
||||
// we want to shift the hips to bring the head to its designated position
|
||||
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
|
||||
_hipsOffset += target.getTranslation() - actual;
|
||||
// and ignore all other targets
|
||||
newHipsOffset = _hipsOffset;
|
||||
break;
|
||||
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
|
||||
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
|
||||
glm::vec3 targetPosition = target.getTranslation();
|
||||
newHipsOffset += targetPosition - actualPosition;
|
||||
|
||||
// Add downward pressure on the hips
|
||||
newHipsOffset *= 0.95f;
|
||||
newHipsOffset -= 1.0f;
|
||||
}
|
||||
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
|
||||
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
|
||||
glm::vec3 targetPosition = target.getTranslation();
|
||||
newHipsOffset += targetPosition - actualPosition;
|
||||
}
|
||||
}
|
||||
|
||||
// smooth transitions by relaxing _hipsOffset toward the new value
|
||||
const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f;
|
||||
float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f;
|
||||
_hipsOffset += (newHipsOffset - _hipsOffset) * tau;
|
||||
|
||||
// clamp the hips offset
|
||||
float hipsOffsetLength = glm::length(_hipsOffset);
|
||||
if (hipsOffsetLength > _maxHipsOffsetLength) {
|
||||
_hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength;
|
||||
}
|
||||
|
||||
computeHipsOffset(targets, underPoses, dt);
|
||||
} else {
|
||||
_hipsOffset = Vectors::ZERO;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _relativePoses;
|
||||
}
|
||||
|
||||
void AnimInverseKinematics::computeHipsOffset(const std::vector<IKTarget>& targets, const AnimPoseVec& underPoses, float dt) {
|
||||
// measure new _hipsOffset for next frame
|
||||
// by looking for discrepancies between where a targeted endEffector is
|
||||
// and where it wants to be (after IK solutions are done)
|
||||
glm::vec3 newHipsOffset = Vectors::ZERO;
|
||||
for (auto& target: targets) {
|
||||
int targetIndex = target.getIndex();
|
||||
if (targetIndex == _headIndex && _headIndex != -1) {
|
||||
// special handling for headTarget
|
||||
if (target.getType() == IKTarget::Type::RotationOnly) {
|
||||
// we want to shift the hips to bring the underPose closer
|
||||
// to where the head happens to be (overpose)
|
||||
glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans();
|
||||
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
|
||||
const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f;
|
||||
newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under);
|
||||
} else if (target.getType() == IKTarget::Type::HmdHead) {
|
||||
// we want to shift the hips to bring the head to its designated position
|
||||
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
|
||||
_hipsOffset += target.getTranslation() - actual;
|
||||
// and ignore all other targets
|
||||
newHipsOffset = _hipsOffset;
|
||||
break;
|
||||
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
|
||||
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
|
||||
glm::vec3 targetPosition = target.getTranslation();
|
||||
newHipsOffset += targetPosition - actualPosition;
|
||||
|
||||
// Add downward pressure on the hips
|
||||
const float PRESSURE_SCALE_FACTOR = 0.95f;
|
||||
const float PRESSURE_TRANSLATION_OFFSET = 1.0f;
|
||||
newHipsOffset *= PRESSURE_SCALE_FACTOR;
|
||||
newHipsOffset -= PRESSURE_TRANSLATION_OFFSET;
|
||||
}
|
||||
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
|
||||
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
|
||||
glm::vec3 targetPosition = target.getTranslation();
|
||||
newHipsOffset += targetPosition - actualPosition;
|
||||
}
|
||||
}
|
||||
|
||||
// smooth transitions by relaxing _hipsOffset toward the new value
|
||||
const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f;
|
||||
float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f;
|
||||
_hipsOffset += (newHipsOffset - _hipsOffset) * tau;
|
||||
|
||||
// clamp the hips offset
|
||||
float hipsOffsetLength = glm::length(_hipsOffset);
|
||||
if (hipsOffsetLength > _maxHipsOffsetLength) {
|
||||
_hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength;
|
||||
}
|
||||
}
|
||||
|
||||
void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) {
|
||||
// manually adjust scale here
|
||||
const float METERS_TO_CENTIMETERS = 100.0f;
|
||||
|
@ -594,6 +622,22 @@ void AnimInverseKinematics::clearConstraints() {
|
|||
_constraints.clear();
|
||||
}
|
||||
|
||||
// set up swing limits around a swingTwistConstraint in an ellipse, where lateralSwingTheta is the swing limit for lateral swings (side to side)
|
||||
// anteriorSwingTheta is swing limit for forward and backward swings. (where x-axis of reference rotation is sideways and -z-axis is forward)
|
||||
static void setEllipticalSwingLimits(SwingTwistConstraint* stConstraint, float lateralSwingTheta, float anteriorSwingTheta) {
|
||||
assert(stConstraint);
|
||||
const int NUM_SUBDIVISIONS = 8;
|
||||
std::vector<float> minDots;
|
||||
minDots.reserve(NUM_SUBDIVISIONS);
|
||||
float dTheta = TWO_PI / NUM_SUBDIVISIONS;
|
||||
float theta = 0.0f;
|
||||
for (int i = 0; i < NUM_SUBDIVISIONS; i++) {
|
||||
minDots.push_back(cosf(glm::length(glm::vec2(anteriorSwingTheta * cosf(theta), lateralSwingTheta * sinf(theta)))));
|
||||
theta += dTheta;
|
||||
}
|
||||
stConstraint->setSwingLimits(minDots);
|
||||
}
|
||||
|
||||
void AnimInverseKinematics::initConstraints() {
|
||||
if (!_skeleton) {
|
||||
return;
|
||||
|
@ -783,41 +827,31 @@ void AnimInverseKinematics::initConstraints() {
|
|||
} else if (baseName.startsWith("Spine", Qt::CaseSensitive)) {
|
||||
SwingTwistConstraint* stConstraint = new SwingTwistConstraint();
|
||||
stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot());
|
||||
const float MAX_SPINE_TWIST = PI / 12.0f;
|
||||
const float MAX_SPINE_TWIST = PI / 20.0f;
|
||||
stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST);
|
||||
|
||||
std::vector<float> minDots;
|
||||
const float MAX_SPINE_SWING = PI / 10.0f;
|
||||
minDots.push_back(cosf(MAX_SPINE_SWING));
|
||||
stConstraint->setSwingLimits(minDots);
|
||||
// limit lateral swings more then forward-backward swings
|
||||
const float MAX_SPINE_LATERAL_SWING = PI / 30.0f;
|
||||
const float MAX_SPINE_ANTERIOR_SWING = PI / 20.0f;
|
||||
setEllipticalSwingLimits(stConstraint, MAX_SPINE_LATERAL_SWING, MAX_SPINE_ANTERIOR_SWING);
|
||||
|
||||
if (0 == baseName.compare("Spine1", Qt::CaseSensitive)
|
||||
|| 0 == baseName.compare("Spine", Qt::CaseSensitive)) {
|
||||
stConstraint->setLowerSpine(true);
|
||||
}
|
||||
|
||||
constraint = static_cast<RotationConstraint*>(stConstraint);
|
||||
} else if (baseName.startsWith("Hips2", Qt::CaseSensitive)) {
|
||||
SwingTwistConstraint* stConstraint = new SwingTwistConstraint();
|
||||
stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot());
|
||||
const float MAX_SPINE_TWIST = PI / 8.0f;
|
||||
stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST);
|
||||
|
||||
std::vector<float> minDots;
|
||||
const float MAX_SPINE_SWING = PI / 14.0f;
|
||||
minDots.push_back(cosf(MAX_SPINE_SWING));
|
||||
stConstraint->setSwingLimits(minDots);
|
||||
|
||||
constraint = static_cast<RotationConstraint*>(stConstraint);
|
||||
} else if (0 == baseName.compare("Neck", Qt::CaseSensitive)) {
|
||||
SwingTwistConstraint* stConstraint = new SwingTwistConstraint();
|
||||
stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot());
|
||||
const float MAX_NECK_TWIST = PI / 9.0f;
|
||||
const float MAX_NECK_TWIST = PI / 10.0f;
|
||||
stConstraint->setTwistLimits(-MAX_NECK_TWIST, MAX_NECK_TWIST);
|
||||
|
||||
std::vector<float> minDots;
|
||||
const float MAX_NECK_SWING = PI / 8.0f;
|
||||
minDots.push_back(cosf(MAX_NECK_SWING));
|
||||
stConstraint->setSwingLimits(minDots);
|
||||
// limit lateral swings more then forward-backward swings
|
||||
const float MAX_NECK_LATERAL_SWING = PI / 10.0f;
|
||||
const float MAX_NECK_ANTERIOR_SWING = PI / 8.0f;
|
||||
setEllipticalSwingLimits(stConstraint, MAX_NECK_LATERAL_SWING, MAX_NECK_ANTERIOR_SWING);
|
||||
|
||||
constraint = static_cast<RotationConstraint*>(stConstraint);
|
||||
} else if (0 == baseName.compare("Head", Qt::CaseSensitive)) {
|
||||
|
@ -872,7 +906,7 @@ void AnimInverseKinematics::initConstraints() {
|
|||
|
||||
// we determine the max/min angles by rotating the swing limit lines from parent- to child-frame
|
||||
// then measure the angles to swing the yAxis into alignment
|
||||
const float MIN_KNEE_ANGLE = 0.0f;
|
||||
const float MIN_KNEE_ANGLE = 0.097f; // ~5 deg
|
||||
const float MAX_KNEE_ANGLE = 7.0f * PI / 8.0f;
|
||||
glm::quat invReferenceRotation = glm::inverse(referenceRotation);
|
||||
glm::vec3 minSwingAxis = invReferenceRotation * glm::angleAxis(MIN_KNEE_ANGLE, hingeAxis) * Vectors::UNIT_Y;
|
||||
|
|
|
@ -55,6 +55,7 @@ protected:
|
|||
RotationConstraint* getConstraint(int index);
|
||||
void clearConstraints();
|
||||
void initConstraints();
|
||||
void computeHipsOffset(const std::vector<IKTarget>& targets, const AnimPoseVec& underPoses, float dt);
|
||||
|
||||
// no copies
|
||||
AnimInverseKinematics(const AnimInverseKinematics&) = delete;
|
||||
|
@ -91,6 +92,7 @@ protected:
|
|||
int _headIndex { -1 };
|
||||
int _hipsIndex { -1 };
|
||||
int _hipsParentIndex { -1 };
|
||||
int _hipsTargetIndex { -1 };
|
||||
|
||||
// _maxTargetIndex is tracked to help optimize the recalculation of absolute poses
|
||||
// during the the cyclic coordinate descent algorithm
|
||||
|
|
|
@ -12,6 +12,16 @@
|
|||
#include "AnimUtil.h"
|
||||
#include "AnimationLogging.h"
|
||||
|
||||
AnimManipulator::JointVar::JointVar(const QString& jointNameIn, Type rotationTypeIn, Type translationTypeIn,
|
||||
const QString& rotationVarIn, const QString& translationVarIn) :
|
||||
jointName(jointNameIn),
|
||||
rotationType(rotationTypeIn),
|
||||
translationType(translationTypeIn),
|
||||
rotationVar(rotationVarIn),
|
||||
translationVar(translationVarIn),
|
||||
jointIndex(-1),
|
||||
hasPerformedJointLookup(false) {}
|
||||
|
||||
AnimManipulator::AnimManipulator(const QString& id, float alpha) :
|
||||
AnimNode(AnimNode::Type::Manipulator, id),
|
||||
_alpha(alpha) {
|
||||
|
@ -36,7 +46,10 @@ const AnimPoseVec& AnimManipulator::overlay(const AnimVariantMap& animVars, cons
|
|||
}
|
||||
|
||||
for (auto& jointVar : _jointVars) {
|
||||
|
||||
if (!jointVar.hasPerformedJointLookup) {
|
||||
|
||||
// map from joint name to joint index and cache the result.
|
||||
jointVar.jointIndex = _skeleton->nameToJointIndex(jointVar.jointName);
|
||||
if (jointVar.jointIndex < 0) {
|
||||
qCWarning(animation) << "AnimManipulator could not find jointName" << jointVar.jointName << "in skeleton";
|
||||
|
@ -100,34 +113,62 @@ AnimPose AnimManipulator::computeRelativePoseFromJointVar(const AnimVariantMap&
|
|||
|
||||
AnimPose defaultAbsPose = _skeleton->getAbsolutePose(jointVar.jointIndex, underPoses);
|
||||
|
||||
if (jointVar.type == JointVar::Type::AbsoluteRotation || jointVar.type == JointVar::Type::AbsolutePosition) {
|
||||
// compute relative translation
|
||||
glm::vec3 relTrans;
|
||||
switch (jointVar.translationType) {
|
||||
case JointVar::Type::Absolute: {
|
||||
glm::vec3 absTrans = animVars.lookupRigToGeometry(jointVar.translationVar, defaultAbsPose.trans());
|
||||
|
||||
if (jointVar.type == JointVar::Type::AbsoluteRotation) {
|
||||
defaultAbsPose.rot() = animVars.lookupRigToGeometry(jointVar.var, defaultAbsPose.rot());
|
||||
} else if (jointVar.type == JointVar::Type::AbsolutePosition) {
|
||||
defaultAbsPose.trans() = animVars.lookupRigToGeometry(jointVar.var, defaultAbsPose.trans());
|
||||
// convert to from absolute to relative.
|
||||
AnimPose parentAbsPose;
|
||||
int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex);
|
||||
if (parentIndex >= 0) {
|
||||
parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses);
|
||||
}
|
||||
|
||||
// convert from absolute to relative
|
||||
relTrans = transformPoint(parentAbsPose.inverse(), absTrans);
|
||||
break;
|
||||
}
|
||||
|
||||
// because jointVar is absolute, we must use an absolute parent frame to convert into a relative pose.
|
||||
AnimPose parentAbsPose = AnimPose::identity;
|
||||
int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex);
|
||||
if (parentIndex >= 0) {
|
||||
parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses);
|
||||
}
|
||||
|
||||
// convert from absolute to relative
|
||||
return parentAbsPose.inverse() * defaultAbsPose;
|
||||
|
||||
} else {
|
||||
|
||||
// override the default rel pose
|
||||
AnimPose relPose = defaultRelPose;
|
||||
if (jointVar.type == JointVar::Type::RelativeRotation) {
|
||||
relPose.rot() = animVars.lookupRigToGeometry(jointVar.var, defaultRelPose.rot());
|
||||
} else if (jointVar.type == JointVar::Type::RelativePosition) {
|
||||
relPose.trans() = animVars.lookupRigToGeometry(jointVar.var, defaultRelPose.trans());
|
||||
}
|
||||
|
||||
return relPose;
|
||||
case JointVar::Type::Relative:
|
||||
relTrans = animVars.lookupRigToGeometryVector(jointVar.translationVar, defaultRelPose.trans());
|
||||
break;
|
||||
case JointVar::Type::UnderPose:
|
||||
relTrans = underPoses[jointVar.jointIndex].trans();
|
||||
break;
|
||||
case JointVar::Type::Default:
|
||||
default:
|
||||
relTrans = defaultRelPose.trans();
|
||||
break;
|
||||
}
|
||||
|
||||
glm::quat relRot;
|
||||
switch (jointVar.rotationType) {
|
||||
case JointVar::Type::Absolute: {
|
||||
glm::quat absRot = animVars.lookupRigToGeometry(jointVar.translationVar, defaultAbsPose.rot());
|
||||
|
||||
// convert to from absolute to relative.
|
||||
AnimPose parentAbsPose;
|
||||
int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex);
|
||||
if (parentIndex >= 0) {
|
||||
parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses);
|
||||
}
|
||||
|
||||
// convert from absolute to relative
|
||||
relRot = glm::inverse(parentAbsPose.rot()) * absRot;
|
||||
break;
|
||||
}
|
||||
case JointVar::Type::Relative:
|
||||
relRot = animVars.lookupRigToGeometry(jointVar.translationVar, defaultRelPose.rot());
|
||||
break;
|
||||
case JointVar::Type::UnderPose:
|
||||
relRot = underPoses[jointVar.jointIndex].rot();
|
||||
break;
|
||||
case JointVar::Type::Default:
|
||||
default:
|
||||
relRot = defaultRelPose.rot();
|
||||
break;
|
||||
}
|
||||
|
||||
return AnimPose(glm::vec3(1), relRot, relTrans);
|
||||
}
|
||||
|
|
|
@ -31,17 +31,20 @@ public:
|
|||
|
||||
struct JointVar {
|
||||
enum class Type {
|
||||
AbsoluteRotation = 0,
|
||||
AbsolutePosition,
|
||||
RelativeRotation,
|
||||
RelativePosition,
|
||||
Absolute,
|
||||
Relative,
|
||||
UnderPose,
|
||||
Default,
|
||||
NumTypes
|
||||
};
|
||||
|
||||
JointVar(const QString& varIn, const QString& jointNameIn, Type typeIn) : var(varIn), jointName(jointNameIn), type(typeIn), jointIndex(-1), hasPerformedJointLookup(false) {}
|
||||
QString var = "";
|
||||
JointVar(const QString& jointNameIn, Type rotationType, Type translationType, const QString& rotationVarIn, const QString& translationVarIn);
|
||||
QString jointName = "";
|
||||
Type type = Type::AbsoluteRotation;
|
||||
Type rotationType = Type::Absolute;
|
||||
Type translationType = Type::Absolute;
|
||||
QString rotationVar = "";
|
||||
QString translationVar = "";
|
||||
|
||||
int jointIndex = -1;
|
||||
bool hasPerformedJointLookup = false;
|
||||
bool isRelative = false;
|
||||
|
|
|
@ -79,10 +79,10 @@ static AnimStateMachine::InterpType stringToInterpType(const QString& str) {
|
|||
|
||||
static const char* animManipulatorJointVarTypeToString(AnimManipulator::JointVar::Type type) {
|
||||
switch (type) {
|
||||
case AnimManipulator::JointVar::Type::AbsoluteRotation: return "absoluteRotation";
|
||||
case AnimManipulator::JointVar::Type::AbsolutePosition: return "absolutePosition";
|
||||
case AnimManipulator::JointVar::Type::RelativeRotation: return "relativeRotation";
|
||||
case AnimManipulator::JointVar::Type::RelativePosition: return "relativePosition";
|
||||
case AnimManipulator::JointVar::Type::Absolute: return "absolute";
|
||||
case AnimManipulator::JointVar::Type::Relative: return "relative";
|
||||
case AnimManipulator::JointVar::Type::UnderPose: return "underPose";
|
||||
case AnimManipulator::JointVar::Type::Default: return "default";
|
||||
case AnimManipulator::JointVar::Type::NumTypes: return nullptr;
|
||||
};
|
||||
return nullptr;
|
||||
|
@ -339,7 +339,8 @@ static const char* boneSetStrings[AnimOverlay::NumBoneSets] = {
|
|||
"spineOnly",
|
||||
"empty",
|
||||
"leftHand",
|
||||
"rightHand"
|
||||
"rightHand",
|
||||
"hipsOnly"
|
||||
};
|
||||
|
||||
static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) {
|
||||
|
@ -406,17 +407,25 @@ static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const Q
|
|||
}
|
||||
auto jointObj = jointValue.toObject();
|
||||
|
||||
READ_STRING(type, jointObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(jointName, jointObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(var, jointObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(rotationType, jointObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(translationType, jointObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(rotationVar, jointObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(translationVar, jointObj, id, jsonUrl, nullptr);
|
||||
|
||||
AnimManipulator::JointVar::Type jointVarType = stringToAnimManipulatorJointVarType(type);
|
||||
if (jointVarType == AnimManipulator::JointVar::Type::NumTypes) {
|
||||
qCCritical(animation) << "AnimNodeLoader, bad type in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString();
|
||||
return nullptr;
|
||||
AnimManipulator::JointVar::Type jointVarRotationType = stringToAnimManipulatorJointVarType(rotationType);
|
||||
if (jointVarRotationType == AnimManipulator::JointVar::Type::NumTypes) {
|
||||
qCWarning(animation) << "AnimNodeLoader, bad rotationType in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString();
|
||||
jointVarRotationType = AnimManipulator::JointVar::Type::Default;
|
||||
}
|
||||
|
||||
AnimManipulator::JointVar jointVar(var, jointName, jointVarType);
|
||||
AnimManipulator::JointVar::Type jointVarTranslationType = stringToAnimManipulatorJointVarType(translationType);
|
||||
if (jointVarTranslationType == AnimManipulator::JointVar::Type::NumTypes) {
|
||||
qCWarning(animation) << "AnimNodeLoader, bad translationType in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString();
|
||||
jointVarTranslationType = AnimManipulator::JointVar::Type::Default;
|
||||
}
|
||||
|
||||
AnimManipulator::JointVar jointVar(jointName, jointVarRotationType, jointVarTranslationType, rotationVar, translationVar);
|
||||
node->addJointVar(jointVar);
|
||||
};
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ void AnimOverlay::buildBoneSet(BoneSet boneSet) {
|
|||
case SpineOnlyBoneSet: buildSpineOnlyBoneSet(); break;
|
||||
case LeftHandBoneSet: buildLeftHandBoneSet(); break;
|
||||
case RightHandBoneSet: buildRightHandBoneSet(); break;
|
||||
case HipsOnlyBoneSet: buildHipsOnlyBoneSet(); break;
|
||||
default:
|
||||
case EmptyBoneSet: buildEmptyBoneSet(); break;
|
||||
}
|
||||
|
@ -188,6 +189,13 @@ void AnimOverlay::buildRightHandBoneSet() {
|
|||
});
|
||||
}
|
||||
|
||||
void AnimOverlay::buildHipsOnlyBoneSet() {
|
||||
assert(_skeleton);
|
||||
buildEmptyBoneSet();
|
||||
int hipsJoint = _skeleton->nameToJointIndex("Hips");
|
||||
_boneSetVec[hipsJoint] = 1.0f;
|
||||
}
|
||||
|
||||
// for AnimDebugDraw rendering
|
||||
const AnimPoseVec& AnimOverlay::getPosesInternal() const {
|
||||
return _poses;
|
||||
|
|
|
@ -37,6 +37,7 @@ public:
|
|||
EmptyBoneSet,
|
||||
LeftHandBoneSet,
|
||||
RightHandBoneSet,
|
||||
HipsOnlyBoneSet,
|
||||
NumBoneSets
|
||||
};
|
||||
|
||||
|
@ -75,6 +76,7 @@ public:
|
|||
void buildEmptyBoneSet();
|
||||
void buildLeftHandBoneSet();
|
||||
void buildRightHandBoneSet();
|
||||
void buildHipsOnlyBoneSet();
|
||||
|
||||
// no copies
|
||||
AnimOverlay(const AnimOverlay&) = delete;
|
||||
|
|
|
@ -165,6 +165,15 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
glm::vec3 lookupRigToGeometryVector(const QString& key, const glm::vec3& defaultValue) const {
|
||||
if (key.isEmpty()) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
auto iter = _map.find(key);
|
||||
return iter != _map.end() ? transformVectorFast(_rigToGeometryMat, iter->second.getVec3()) : defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
const glm::quat& lookupRaw(const QString& key, const glm::quat& defaultValue) const {
|
||||
if (key.isEmpty()) {
|
||||
return defaultValue;
|
||||
|
|
|
@ -21,13 +21,14 @@ public:
|
|||
RotationOnly,
|
||||
HmdHead,
|
||||
HipsRelativeRotationAndPosition,
|
||||
Unknown,
|
||||
Unknown
|
||||
};
|
||||
|
||||
IKTarget() {}
|
||||
|
||||
const glm::vec3& getTranslation() const { return _pose.trans(); }
|
||||
const glm::quat& getRotation() const { return _pose.rot(); }
|
||||
const AnimPose& getPose() const { return _pose; }
|
||||
int getIndex() const { return _index; }
|
||||
Type getType() const { return _type; }
|
||||
|
||||
|
|
|
@ -1024,6 +1024,17 @@ void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) {
|
|||
|
||||
_animVars.set("isTalking", params.isTalking);
|
||||
_animVars.set("notIsTalking", !params.isTalking);
|
||||
|
||||
if (params.hipsEnabled) {
|
||||
_animVars.set("hipsType", (int)IKTarget::Type::RotationAndPosition);
|
||||
_animVars.set("hipsPosition", extractTranslation(params.hipsMatrix));
|
||||
_animVars.set("hipsRotation", glmExtractRotation(params.hipsMatrix) * Quaternions::Y_180);
|
||||
} else {
|
||||
_animVars.set("hipsType", (int)IKTarget::Type::Unknown);
|
||||
}
|
||||
|
||||
// by default this IK target is disabled.
|
||||
_animVars.set("spine2Type", (int)IKTarget::Type::Unknown);
|
||||
}
|
||||
|
||||
void Rig::updateFromEyeParameters(const EyeParameters& params) {
|
||||
|
@ -1161,13 +1172,19 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f
|
|||
const glm::vec3 bodyCapsuleStart = bodyCapsuleCenter - glm::vec3(0, params.bodyCapsuleHalfHeight, 0);
|
||||
const glm::vec3 bodyCapsuleEnd = bodyCapsuleCenter + glm::vec3(0, params.bodyCapsuleHalfHeight, 0);
|
||||
|
||||
// TODO: add isHipsEnabled
|
||||
bool bodySensorTrackingEnabled = params.isLeftFootEnabled || params.isRightFootEnabled;
|
||||
|
||||
if (params.isLeftEnabled) {
|
||||
|
||||
// prevent the hand IK targets from intersecting the body capsule
|
||||
glm::vec3 handPosition = params.leftPosition;
|
||||
glm::vec3 displacement;
|
||||
if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) {
|
||||
handPosition -= displacement;
|
||||
|
||||
if (!bodySensorTrackingEnabled) {
|
||||
// prevent the hand IK targets from intersecting the body capsule
|
||||
glm::vec3 displacement;
|
||||
if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) {
|
||||
handPosition -= displacement;
|
||||
}
|
||||
}
|
||||
|
||||
_animVars.set("leftHandPosition", handPosition);
|
||||
|
@ -1181,11 +1198,14 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f
|
|||
|
||||
if (params.isRightEnabled) {
|
||||
|
||||
// prevent the hand IK targets from intersecting the body capsule
|
||||
glm::vec3 handPosition = params.rightPosition;
|
||||
glm::vec3 displacement;
|
||||
if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) {
|
||||
handPosition -= displacement;
|
||||
|
||||
if (!bodySensorTrackingEnabled) {
|
||||
// prevent the hand IK targets from intersecting the body capsule
|
||||
glm::vec3 displacement;
|
||||
if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) {
|
||||
handPosition -= displacement;
|
||||
}
|
||||
}
|
||||
|
||||
_animVars.set("rightHandPosition", handPosition);
|
||||
|
|
|
@ -45,6 +45,8 @@ public:
|
|||
glm::quat worldHeadOrientation = glm::quat(); // world space (-z forward)
|
||||
glm::quat rigHeadOrientation = glm::quat(); // rig space (-z forward)
|
||||
glm::vec3 rigHeadPosition = glm::vec3(); // rig space
|
||||
glm::mat4 hipsMatrix = glm::mat4(); // rig space
|
||||
bool hipsEnabled = false;
|
||||
bool isInHMD = false;
|
||||
int neckJointIndex = -1;
|
||||
bool isTalking = false;
|
||||
|
|
|
@ -49,20 +49,20 @@
|
|||
|
||||
|
||||
|
||||
An dynamic is a callback which is registered with bullet. An dynamic is called-back every physics
|
||||
A dynamic is a callback which is registered with bullet. A dynamic is called-back every physics
|
||||
simulation step and can do whatever it wants with the various datastructures it has available. An
|
||||
dynamic, for example, can pull an EntityItem toward a point as if that EntityItem were connected to that
|
||||
point by a spring.
|
||||
|
||||
In this system, an dynamic is a property of an EntityItem (rather, an EntityItem has a property which
|
||||
In this system, a dynamic is a property of an EntityItem (rather, an EntityItem has a property which
|
||||
encodes a list of dynamics). Each dynamic has a type and some arguments. Dynamics can be created by a
|
||||
script or when receiving information via an EntityTree data-stream (either over the network or from an
|
||||
svo file).
|
||||
|
||||
In the interface, if an EntityItem has dynamics, this EntityItem will have pointers to ObjectDynamic
|
||||
subclass (like ObjectDynamicSpring) instantiations. Code in the entities library affects an dynamic-object
|
||||
subclass (like ObjectDynamicSpring) instantiations. Code in the entities library affects a dynamic-object
|
||||
via the EntityDynamicInterface (which knows nothing about bullet). When the ObjectDynamic subclass
|
||||
instance is created, it is registered as an dynamic with bullet. Bullet will call into code in this
|
||||
instance is created, it is registered as a dynamic with bullet. Bullet will call into code in this
|
||||
instance with the btDynamicInterface every physics-simulation step.
|
||||
|
||||
Because the dynamic can exist next to the interface's EntityTree or the entity-server's EntityTree,
|
||||
|
|
|
@ -348,18 +348,19 @@ void AssetClient::handleAssetGetReply(QSharedPointer<ReceivedMessage> message, S
|
|||
// Store message in case we need to disconnect from it later.
|
||||
callbacks.message = message;
|
||||
|
||||
|
||||
auto weakNode = senderNode.toWeakRef();
|
||||
connect(message.data(), &ReceivedMessage::progress, this, [this, weakNode, messageID, length](qint64 size) {
|
||||
handleProgressCallback(weakNode, messageID, size, length);
|
||||
});
|
||||
connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID]() {
|
||||
handleCompleteCallback(weakNode, messageID);
|
||||
});
|
||||
|
||||
if (message->isComplete()) {
|
||||
disconnect(message.data(), nullptr, this, nullptr);
|
||||
callbacks.completeCallback(true, error, message->readAll());
|
||||
messageCallbackMap.erase(requestIt);
|
||||
} else {
|
||||
auto weakNode = senderNode.toWeakRef();
|
||||
|
||||
connect(message.data(), &ReceivedMessage::progress, this, [this, weakNode, messageID, length](qint64 size) {
|
||||
handleProgressCallback(weakNode, messageID, size, length);
|
||||
});
|
||||
connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID]() {
|
||||
handleCompleteCallback(weakNode, messageID);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ void AssetRequest::start() {
|
|||
_assetRequestID = assetClient->getAsset(_hash, _byteRange.fromInclusive, _byteRange.toExclusive,
|
||||
[this, that, hash](bool responseReceived, AssetServerError serverError, const QByteArray& data) {
|
||||
|
||||
if (!that) {
|
||||
if (!that) {
|
||||
qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error;
|
||||
// If the request is dead, return
|
||||
return;
|
||||
|
@ -113,8 +113,10 @@ void AssetRequest::start() {
|
|||
_data = data;
|
||||
_totalReceived += data.size();
|
||||
emit progress(_totalReceived, data.size());
|
||||
|
||||
saveToCache(getUrl(), data);
|
||||
|
||||
if (!_byteRange.isSet()) {
|
||||
saveToCache(getUrl(), data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ const QSet<PacketType> NON_SOURCED_PACKETS = QSet<PacketType>()
|
|||
<< PacketType::ICEServerPeerInformation << PacketType::ICEServerQuery << PacketType::ICEServerHeartbeat
|
||||
<< PacketType::ICEServerHeartbeatACK << PacketType::ICEPing << PacketType::ICEPingReply
|
||||
<< PacketType::ICEServerHeartbeatDenied << PacketType::AssignmentClientStatus << PacketType::StopNode
|
||||
<< PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply;
|
||||
<< PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply << PacketType::OctreeFileReplacement;
|
||||
|
||||
PacketVersion versionForPacketType(PacketType packetType) {
|
||||
switch (packetType) {
|
||||
|
|
|
@ -113,7 +113,8 @@ public:
|
|||
EntityPhysics,
|
||||
EntityServerScriptLog,
|
||||
AdjustAvatarSorting,
|
||||
LAST_PACKET_TYPE = AdjustAvatarSorting
|
||||
OctreeFileReplacement,
|
||||
LAST_PACKET_TYPE = OctreeFileReplacement
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
#include "OctreePersistThread.h"
|
||||
|
||||
const int OctreePersistThread::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds
|
||||
const QString OctreePersistThread::REPLACEMENT_FILE_EXTENSION = ".replace";
|
||||
|
||||
OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval,
|
||||
bool wantBackup, const QJsonObject& settings, bool debugTimestampNow,
|
||||
|
@ -131,10 +132,47 @@ quint64 OctreePersistThread::getMostRecentBackupTimeInUsecs(const QString& forma
|
|||
return mostRecentBackupInUsecs;
|
||||
}
|
||||
|
||||
void OctreePersistThread::possiblyReplaceContent() {
|
||||
// before we load the normal file, check if there's a pending replacement file
|
||||
auto replacementFileName = _filename + REPLACEMENT_FILE_EXTENSION;
|
||||
|
||||
QFile replacementFile { replacementFileName };
|
||||
if (replacementFile.exists()) {
|
||||
// we have a replacement file to process
|
||||
qDebug() << "Replacing models file with" << replacementFileName;
|
||||
|
||||
// first take the current models file and move it to a different filename, appended with the timestamp
|
||||
QFile currentFile { _filename };
|
||||
if (currentFile.exists()) {
|
||||
static const QString FILENAME_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss";
|
||||
auto backupFileName = _filename + ".backup." + QDateTime::currentDateTime().toString(FILENAME_TIMESTAMP_FORMAT);
|
||||
|
||||
if (currentFile.rename(backupFileName)) {
|
||||
qDebug() << "Moved previous models file to" << backupFileName;
|
||||
} else {
|
||||
qWarning() << "Could not backup previous models file to" << backupFileName << "- removing replacement models file";
|
||||
|
||||
if (!replacementFile.remove()) {
|
||||
qWarning() << "Could not remove replacement models file from" << replacementFileName
|
||||
<< "- replacement will be re-attempted on next server restart";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rename the replacement file to match what the persist thread is just about to read
|
||||
if (!replacementFile.rename(_filename)) {
|
||||
qWarning() << "Could not replace models file with" << replacementFileName << "- starting with empty models file";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool OctreePersistThread::process() {
|
||||
|
||||
if (!_initialLoadComplete) {
|
||||
possiblyReplaceContent();
|
||||
|
||||
quint64 loadStarted = usecTimestampNow();
|
||||
qCDebug(octree) << "loading Octrees from file: " << _filename << "...";
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ public:
|
|||
};
|
||||
|
||||
static const int DEFAULT_PERSIST_INTERVAL;
|
||||
static const QString REPLACEMENT_FILE_EXTENSION;
|
||||
|
||||
OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory,
|
||||
int persistInterval = DEFAULT_PERSIST_INTERVAL, bool wantBackup = false,
|
||||
|
@ -60,6 +61,7 @@ protected:
|
|||
bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime);
|
||||
quint64 getMostRecentBackupTimeInUsecs(const QString& format);
|
||||
void parseSettings(const QJsonObject& settings);
|
||||
void possiblyReplaceContent();
|
||||
|
||||
private:
|
||||
OctreePointer _tree;
|
||||
|
|
|
@ -122,3 +122,10 @@ bool Quat::equal(const glm::quat& q1, const glm::quat& q2) {
|
|||
return q1 == q2;
|
||||
}
|
||||
|
||||
glm::quat Quat::cancelOutRollAndPitch(const glm::quat& q) {
|
||||
return ::cancelOutRollAndPitch(q);
|
||||
}
|
||||
|
||||
glm::quat Quat::cancelOutRoll(const glm::quat& q) {
|
||||
return ::cancelOutRoll(q);
|
||||
}
|
||||
|
|
|
@ -60,6 +60,8 @@ public slots:
|
|||
float dot(const glm::quat& q1, const glm::quat& q2);
|
||||
void print(const QString& label, const glm::quat& q);
|
||||
bool equal(const glm::quat& q1, const glm::quat& q2);
|
||||
glm::quat cancelOutRollAndPitch(const glm::quat& q);
|
||||
glm::quat cancelOutRoll(const glm::quat& q);
|
||||
};
|
||||
|
||||
#endif // hifi_Quat_h
|
||||
|
|
118
scripts/developer/tests/hipsIkTest.js
Normal file
118
scripts/developer/tests/hipsIkTest.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
//
|
||||
// hipsIKTest.js
|
||||
//
|
||||
// Created by Anthony Thibault on 4/24/17
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Test procedural manipulation of the Avatar hips IK target.
|
||||
// Pull the left and right triggers on your hand controllers, you hips should begin to gyrate in an amusing mannor.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
/* global Xform */
|
||||
Script.include("/~/system/libraries/Xform.js");
|
||||
|
||||
var calibrated = false;
|
||||
var rightTriggerPressed = false;
|
||||
var leftTriggerPressed = false;
|
||||
|
||||
var MAPPING_NAME = "com.highfidelity.hipsIkTest";
|
||||
|
||||
var mapping = Controller.newMapping(MAPPING_NAME);
|
||||
mapping.from([Controller.Standard.RTClick]).peek().to(function (value) {
|
||||
rightTriggerPressed = (value !== 0) ? true : false;
|
||||
});
|
||||
mapping.from([Controller.Standard.LTClick]).peek().to(function (value) {
|
||||
leftTriggerPressed = (value !== 0) ? true : false;
|
||||
});
|
||||
|
||||
Controller.enableMapping(MAPPING_NAME);
|
||||
|
||||
var ANIM_VARS = [
|
||||
"headType",
|
||||
"hipsType",
|
||||
"hipsPosition",
|
||||
"hipsRotation"
|
||||
];
|
||||
|
||||
var ZERO = {x: 0, y: 0, z: 0};
|
||||
var X_AXIS = {x: 1, y: 0, z: 0};
|
||||
var Y_AXIS = {x: 0, y: 1, z: 0};
|
||||
var Y_180 = {x: 0, y: 1, z: 0, w: 0};
|
||||
var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0});
|
||||
|
||||
var hips = undefined;
|
||||
|
||||
function computeCurrentXform(jointIndex) {
|
||||
var currentXform = new Xform(MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex),
|
||||
MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex));
|
||||
return Xform.mul(Y_180_XFORM, currentXform);
|
||||
}
|
||||
|
||||
function calibrate() {
|
||||
hips = computeCurrentXform(MyAvatar.getJointIndex("Hips"));
|
||||
}
|
||||
|
||||
var ikTypes = {
|
||||
RotationAndPosition: 0,
|
||||
RotationOnly: 1,
|
||||
HmdHead: 2,
|
||||
HipsRelativeRotationAndPosition: 3,
|
||||
Off: 4
|
||||
};
|
||||
|
||||
function circleOffset(radius, theta, normal) {
|
||||
var pos = {x: radius * Math.cos(theta), y: radius * Math.sin(theta), z: 0};
|
||||
var lookAtRot = Quat.lookAt(normal, ZERO, X_AXIS);
|
||||
return Vec3.multiplyQbyV(lookAtRot, pos);
|
||||
}
|
||||
|
||||
var handlerId;
|
||||
|
||||
function update(dt) {
|
||||
if (rightTriggerPressed && leftTriggerPressed) {
|
||||
if (!calibrated) {
|
||||
calibrate();
|
||||
calibrated = true;
|
||||
|
||||
if (handlerId) {
|
||||
MyAvatar.removeAnimationStateHandler(handlerId);
|
||||
handlerId = undefined;
|
||||
} else {
|
||||
|
||||
var n = Y_AXIS;
|
||||
var t = 0;
|
||||
handlerId = MyAvatar.addAnimationStateHandler(function (props) {
|
||||
|
||||
t += (1 / 60) * 4;
|
||||
var result = {}, xform;
|
||||
if (hips) {
|
||||
xform = hips;
|
||||
result.hipsType = ikTypes.RotationAndPosition;
|
||||
result.hipsPosition = Vec3.sum(xform.pos, circleOffset(0.1, t, n));
|
||||
result.hipsRotation = xform.rot;
|
||||
result.headType = ikTypes.RotationAndPosition;
|
||||
} else {
|
||||
result.headType = ikTypes.HmdHead;
|
||||
result.hipsType = props.hipsType;
|
||||
result.hipsPosition = props.hipsPosition;
|
||||
result.hipsRotation = props.hipsRotation;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, ANIM_VARS);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
calibrated = false;
|
||||
}
|
||||
}
|
||||
|
||||
Script.update.connect(update);
|
||||
|
||||
Script.scriptEnding.connect(function () {
|
||||
Controller.disableMapping(MAPPING_NAME);
|
||||
Script.update.disconnect(update);
|
||||
});
|
||||
|
307
scripts/developer/tests/viveMotionCapture.js
Normal file
307
scripts/developer/tests/viveMotionCapture.js
Normal file
|
@ -0,0 +1,307 @@
|
|||
/* global Xform */
|
||||
Script.include("/~/system/libraries/Xform.js");
|
||||
|
||||
var TRACKED_OBJECT_POSES = [
|
||||
"TrackedObject00", "TrackedObject01", "TrackedObject02", "TrackedObject03",
|
||||
"TrackedObject04", "TrackedObject05", "TrackedObject06", "TrackedObject07",
|
||||
"TrackedObject08", "TrackedObject09", "TrackedObject10", "TrackedObject11",
|
||||
"TrackedObject12", "TrackedObject13", "TrackedObject14", "TrackedObject15"
|
||||
];
|
||||
|
||||
var triggerPressHandled = false;
|
||||
var rightTriggerPressed = false;
|
||||
var leftTriggerPressed = false;
|
||||
|
||||
var MAPPING_NAME = "com.highfidelity.viveMotionCapture";
|
||||
|
||||
var mapping = Controller.newMapping(MAPPING_NAME);
|
||||
mapping.from([Controller.Standard.RTClick]).peek().to(function (value) {
|
||||
rightTriggerPressed = (value !== 0) ? true : false;
|
||||
});
|
||||
mapping.from([Controller.Standard.LTClick]).peek().to(function (value) {
|
||||
leftTriggerPressed = (value !== 0) ? true : false;
|
||||
});
|
||||
|
||||
Controller.enableMapping(MAPPING_NAME);
|
||||
|
||||
var leftFoot;
|
||||
var rightFoot;
|
||||
var hips;
|
||||
var spine2;
|
||||
|
||||
var FEET_ONLY = 0;
|
||||
var FEET_AND_HIPS = 1;
|
||||
var FEET_AND_CHEST = 2;
|
||||
var FEET_HIPS_AND_CHEST = 3;
|
||||
var AUTO = 4;
|
||||
|
||||
var SENSOR_CONFIG_NAMES = [
|
||||
"FeetOnly",
|
||||
"FeetAndHips",
|
||||
"FeetAndChest",
|
||||
"FeetHipsAndChest",
|
||||
"Auto"
|
||||
];
|
||||
|
||||
var sensorConfig = AUTO;
|
||||
|
||||
var Y_180 = {x: 0, y: 1, z: 0, w: 0};
|
||||
var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0});
|
||||
|
||||
function computeOffsetXform(defaultToReferenceXform, pose, jointIndex) {
|
||||
var poseXform = new Xform(pose.rotation, pose.translation);
|
||||
|
||||
var defaultJointXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(jointIndex),
|
||||
MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(jointIndex));
|
||||
|
||||
var referenceJointXform = Xform.mul(defaultToReferenceXform, defaultJointXform);
|
||||
|
||||
return Xform.mul(poseXform.inv(), referenceJointXform);
|
||||
}
|
||||
|
||||
function computeDefaultToReferenceXform() {
|
||||
var headIndex = MyAvatar.getJointIndex("Head");
|
||||
if (headIndex >= 0) {
|
||||
var defaultHeadXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex),
|
||||
MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex));
|
||||
var currentHeadXform = new Xform(Quat.cancelOutRollAndPitch(MyAvatar.getAbsoluteJointRotationInObjectFrame(headIndex)),
|
||||
MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex));
|
||||
|
||||
var defaultToReferenceXform = Xform.mul(currentHeadXform, defaultHeadXform.inv());
|
||||
|
||||
return defaultToReferenceXform;
|
||||
} else {
|
||||
return Xform.ident();
|
||||
}
|
||||
}
|
||||
|
||||
function calibrate() {
|
||||
|
||||
leftFoot = undefined;
|
||||
rightFoot = undefined;
|
||||
hips = undefined;
|
||||
spine2 = undefined;
|
||||
|
||||
var defaultToReferenceXform = computeDefaultToReferenceXform();
|
||||
|
||||
var poses = [];
|
||||
if (Controller.Hardware.Vive) {
|
||||
TRACKED_OBJECT_POSES.forEach(function (key) {
|
||||
var channel = Controller.Hardware.Vive[key];
|
||||
var pose = Controller.getPoseValue(channel);
|
||||
if (pose.valid) {
|
||||
poses.push({
|
||||
channel: channel,
|
||||
pose: pose
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
print("AJT: calibrating, num tracked poses = " + poses.length + ", sensorConfig = " + SENSOR_CONFIG_NAMES[sensorConfig]);
|
||||
|
||||
var config = sensorConfig;
|
||||
|
||||
if (config === AUTO) {
|
||||
if (poses.length === 2) {
|
||||
config = FEET_ONLY;
|
||||
} else if (poses.length === 3) {
|
||||
config = FEET_AND_HIPS;
|
||||
} else if (poses.length >= 4) {
|
||||
config = FEET_HIPS_AND_CHEST;
|
||||
} else {
|
||||
print("AJT: auto config failed: poses.length = " + poses.length);
|
||||
config = FEET_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
if (poses.length >= 2) {
|
||||
// sort by y
|
||||
poses.sort(function(a, b) {
|
||||
var ay = a.pose.translation.y;
|
||||
var by = b.pose.translation.y;
|
||||
return ay - by;
|
||||
});
|
||||
|
||||
if (poses[0].pose.translation.x > poses[1].pose.translation.x) {
|
||||
rightFoot = poses[0];
|
||||
leftFoot = poses[1];
|
||||
} else {
|
||||
rightFoot = poses[1];
|
||||
leftFoot = poses[0];
|
||||
}
|
||||
|
||||
// compute offsets
|
||||
rightFoot.offsetXform = computeOffsetXform(defaultToReferenceXform, rightFoot.pose, MyAvatar.getJointIndex("RightFoot"));
|
||||
leftFoot.offsetXform = computeOffsetXform(defaultToReferenceXform, leftFoot.pose, MyAvatar.getJointIndex("LeftFoot"));
|
||||
|
||||
print("AJT: rightFoot = " + JSON.stringify(rightFoot));
|
||||
print("AJT: leftFoot = " + JSON.stringify(leftFoot));
|
||||
|
||||
if (config === FEET_ONLY) {
|
||||
// we're done!
|
||||
} else if (config === FEET_AND_HIPS && poses.length >= 3) {
|
||||
hips = poses[2];
|
||||
} else if (config === FEET_AND_CHEST && poses.length >= 3) {
|
||||
spine2 = poses[2];
|
||||
} else if (config === FEET_HIPS_AND_CHEST && poses.length >= 4) {
|
||||
hips = poses[2];
|
||||
spine2 = poses[3];
|
||||
} else {
|
||||
// TODO: better error messages
|
||||
print("AJT: could not calibrate for sensor config " + SENSOR_CONFIG_NAMES[config] + ", please try again!");
|
||||
}
|
||||
|
||||
if (hips) {
|
||||
hips.offsetXform = computeOffsetXform(defaultToReferenceXform, hips.pose, MyAvatar.getJointIndex("Hips"));
|
||||
print("AJT: hips = " + JSON.stringify(hips));
|
||||
}
|
||||
|
||||
if (spine2) {
|
||||
spine2.offsetXform = computeOffsetXform(defaultToReferenceXform, spine2.pose, MyAvatar.getJointIndex("Spine2"));
|
||||
print("AJT: spine2 = " + JSON.stringify(spine2));
|
||||
}
|
||||
|
||||
} else {
|
||||
print("AJT: could not find two trackers, try again!");
|
||||
}
|
||||
}
|
||||
|
||||
var ikTypes = {
|
||||
RotationAndPosition: 0,
|
||||
RotationOnly: 1,
|
||||
HmdHead: 2,
|
||||
HipsRelativeRotationAndPosition: 3,
|
||||
Off: 4
|
||||
};
|
||||
|
||||
var handlerId;
|
||||
|
||||
function computeIKTargetXform(jointInfo) {
|
||||
var pose = Controller.getPoseValue(jointInfo.channel);
|
||||
var offsetXform = jointInfo.offsetXform;
|
||||
return Xform.mul(Y_180_XFORM, Xform.mul(new Xform(pose.rotation, pose.translation), offsetXform));
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
if (rightTriggerPressed && leftTriggerPressed) {
|
||||
if (!triggerPressHandled) {
|
||||
triggerPressHandled = true;
|
||||
if (handlerId) {
|
||||
print("AJT: UN-CALIBRATE!");
|
||||
|
||||
// go back to normal, vive pucks will be ignored.
|
||||
leftFoot = undefined;
|
||||
rightFoot = undefined;
|
||||
hips = undefined;
|
||||
spine2 = undefined;
|
||||
if (handlerId) {
|
||||
print("AJT: un-hooking animation state handler");
|
||||
MyAvatar.removeAnimationStateHandler(handlerId);
|
||||
handlerId = undefined;
|
||||
}
|
||||
} else {
|
||||
print("AJT: CALIBRATE!");
|
||||
calibrate();
|
||||
|
||||
var animVars = [];
|
||||
|
||||
if (leftFoot) {
|
||||
animVars.push("leftFootType");
|
||||
animVars.push("leftFootPosition");
|
||||
animVars.push("leftFootRotation");
|
||||
}
|
||||
if (rightFoot) {
|
||||
animVars.push("rightFootType");
|
||||
animVars.push("rightFootPosition");
|
||||
animVars.push("rightFootRotation");
|
||||
}
|
||||
if (hips) {
|
||||
animVars.push("hipsType");
|
||||
animVars.push("hipsPosition");
|
||||
animVars.push("hipsRotation");
|
||||
}
|
||||
if (spine2) {
|
||||
animVars.push("spine2Type");
|
||||
animVars.push("spine2Position");
|
||||
animVars.push("spine2Rotation");
|
||||
}
|
||||
|
||||
// hook up new anim state handler that maps vive pucks to ik system.
|
||||
handlerId = MyAvatar.addAnimationStateHandler(function (props) {
|
||||
var result = {}, xform;
|
||||
if (rightFoot) {
|
||||
xform = computeIKTargetXform(rightFoot);
|
||||
result.rightFootType = ikTypes.RotationAndPosition;
|
||||
result.rightFootPosition = xform.pos;
|
||||
result.rightFootRotation = xform.rot;
|
||||
}
|
||||
if (leftFoot) {
|
||||
xform = computeIKTargetXform(leftFoot);
|
||||
result.leftFootType = ikTypes.RotationAndPosition;
|
||||
result.leftFootPosition = xform.pos;
|
||||
result.leftFootRotation = xform.rot;
|
||||
}
|
||||
if (hips) {
|
||||
xform = computeIKTargetXform(hips);
|
||||
result.hipsType = ikTypes.RotationAndPosition;
|
||||
result.hipsPosition = xform.pos;
|
||||
result.hipsRotation = xform.rot;
|
||||
}
|
||||
if (spine2) {
|
||||
xform = computeIKTargetXform(spine2);
|
||||
result.spine2Type = ikTypes.RotationAndPosition;
|
||||
result.spine2Position = xform.pos;
|
||||
result.spine2Rotation = xform.rot;
|
||||
}
|
||||
return result;
|
||||
}, animVars);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
triggerPressHandled = false;
|
||||
}
|
||||
|
||||
var drawMarkers = false;
|
||||
if (drawMarkers) {
|
||||
var RED = {x: 1, y: 0, z: 0, w: 1};
|
||||
var BLUE = {x: 0, y: 0, z: 1, w: 1};
|
||||
|
||||
if (leftFoot) {
|
||||
var leftFootPose = Controller.getPoseValue(leftFoot.channel);
|
||||
DebugDraw.addMyAvatarMarker("leftFootTracker", leftFootPose.rotation, leftFootPose.translation, BLUE);
|
||||
}
|
||||
|
||||
if (rightFoot) {
|
||||
var rightFootPose = Controller.getPoseValue(rightFoot.channel);
|
||||
DebugDraw.addMyAvatarMarker("rightFootTracker", rightFootPose.rotation, rightFootPose.translation, RED);
|
||||
}
|
||||
|
||||
if (hips) {
|
||||
var hipsPose = Controller.getPoseValue(hips.channel);
|
||||
DebugDraw.addMyAvatarMarker("hipsTracker", hipsPose.rotation, hipsPose.translation, GREEN);
|
||||
}
|
||||
}
|
||||
|
||||
var drawReferencePose = false;
|
||||
if (drawReferencePose) {
|
||||
var GREEN = {x: 0, y: 1, z: 0, w: 1};
|
||||
var defaultToReferenceXform = computeDefaultToReferenceXform();
|
||||
var leftFootIndex = MyAvatar.getJointIndex("LeftFoot");
|
||||
if (leftFootIndex > 0) {
|
||||
var defaultLeftFootXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(leftFootIndex),
|
||||
MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(leftFootIndex));
|
||||
var referenceLeftFootXform = Xform.mul(defaultToReferenceXform, defaultLeftFootXform);
|
||||
DebugDraw.addMyAvatarMarker("leftFootTracker", referenceLeftFootXform.rot, referenceLeftFootXform.pos, GREEN);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Script.update.connect(update);
|
||||
|
||||
Script.scriptEnding.connect(function () {
|
||||
Controller.disableMapping(MAPPING_NAME);
|
||||
Script.update.disconnect(update);
|
||||
});
|
||||
|
|
@ -18,14 +18,14 @@ function shutdown() {
|
|||
});
|
||||
}
|
||||
|
||||
var WHITE = {x: 1, y: 1, z: 1, w: 1};
|
||||
var BLUE = {x: 0, y: 0, z: 1, w: 1};
|
||||
|
||||
function update(dt) {
|
||||
if (Controller.Hardware.Vive) {
|
||||
TRACKED_OBJECT_POSES.forEach(function (key) {
|
||||
var pose = Controller.getPoseValue(Controller.Hardware.Vive[key]);
|
||||
if (pose.valid) {
|
||||
DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, WHITE);
|
||||
DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE);
|
||||
} else {
|
||||
DebugDraw.removeMyAvatarMarker(key);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue