mirror of
https://github.com/JulianGro/overte.git
synced 2025-08-05 04:37:14 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into red
This commit is contained in:
commit
cc7f03e7e4
52 changed files with 868 additions and 424 deletions
|
@ -469,7 +469,7 @@ void Agent::aboutToFinish() {
|
|||
}
|
||||
|
||||
// our entity tree is going to go away so tell that to the EntityScriptingInterface
|
||||
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(NULL);
|
||||
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(nullptr);
|
||||
|
||||
// cleanup the AssetClient thread
|
||||
QThread* assetThread = DependencyManager::get<AssetClient>()->thread();
|
||||
|
|
|
@ -60,7 +60,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
|
|||
auto nodeList = DependencyManager::set<NodeList>(NodeType::Unassigned, listenPort);
|
||||
|
||||
auto animationCache = DependencyManager::set<AnimationCache>();
|
||||
auto entityScriptingInterface = DependencyManager::set<EntityScriptingInterface>();
|
||||
auto entityScriptingInterface = DependencyManager::set<EntityScriptingInterface>(false);
|
||||
|
||||
DependencyManager::registerInheritance<EntityActionFactoryInterface, AssignmentActionFactory>();
|
||||
auto actionFactory = DependencyManager::set<AssignmentActionFactory>();
|
||||
|
|
|
@ -458,7 +458,7 @@ int AudioMixer::prepareMixForListeningNode(Node* node) {
|
|||
if (otherNodeStream->getType() == PositionalAudioStream::Microphone) {
|
||||
streamUUID = otherNode->getUUID();
|
||||
}
|
||||
|
||||
|
||||
// clear out the pre-mix samples before filling it up with this source
|
||||
memset(_preMixSamples, 0, sizeof(_preMixSamples));
|
||||
|
||||
|
@ -498,7 +498,7 @@ void AudioMixer::sendAudioEnvironmentPacket(SharedNodePointer node) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
AvatarAudioStream* stream = nodeData->getAvatarAudioStream();
|
||||
bool dataChanged = (stream->hasReverb() != hasReverb) ||
|
||||
|
@ -550,18 +550,25 @@ void AudioMixer::handleNodeAudioPacket(QSharedPointer<ReceivedMessage> message,
|
|||
|
||||
void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
|
||||
if (sendingNode->isAllowedEditor()) {
|
||||
qDebug() << "Received a mute environment packet of" << message->getSize() << "bytes";
|
||||
|
||||
auto newPacket = NLPacket::create(PacketType::MuteEnvironment, message->getSize());
|
||||
// Copy payload
|
||||
newPacket->write(message->getRawMessage(), message->getSize());
|
||||
glm::vec3 position;
|
||||
float radius;
|
||||
|
||||
auto newPacket = NLPacket::create(PacketType::MuteEnvironment, sizeof(position) + sizeof(radius));
|
||||
|
||||
// read the position and radius from the sent packet
|
||||
message->readPrimitive(&position);
|
||||
message->readPrimitive(&radius);
|
||||
|
||||
// write them to our packet
|
||||
newPacket->writePrimitive(position);
|
||||
newPacket->writePrimitive(radius);
|
||||
|
||||
nodeList->eachNode([&](const SharedNodePointer& node){
|
||||
if (node->getType() == NodeType::Agent && node->getActiveSocket() &&
|
||||
node->getLinkedData() && node != sendingNode) {
|
||||
nodeList->sendPacket(std::move(newPacket), *node);
|
||||
nodeList->sendUnreliablePacket(*newPacket, *node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -649,185 +656,185 @@ void AudioMixer::sendStatsPacket() {
|
|||
}
|
||||
|
||||
void AudioMixer::run() {
|
||||
|
||||
|
||||
qDebug() << "Waiting for connection to domain to request settings from domain-server.";
|
||||
|
||||
|
||||
// wait until we have the domain-server settings, otherwise we bail
|
||||
DomainHandler& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
|
||||
connect(&domainHandler, &DomainHandler::settingsReceived, this, &AudioMixer::domainSettingsRequestComplete);
|
||||
connect(&domainHandler, &DomainHandler::settingsReceiveFail, this, &AudioMixer::domainSettingsRequestFailed);
|
||||
|
||||
|
||||
ThreadedAssignment::commonInit(AUDIO_MIXER_LOGGING_TARGET_NAME, NodeType::AudioMixer);
|
||||
}
|
||||
|
||||
void AudioMixer::domainSettingsRequestComplete() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
|
||||
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
|
||||
|
||||
nodeList->linkedDataCreateCallback = [](Node* node) {
|
||||
node->setLinkedData(std::unique_ptr<AudioMixerClientData> { new AudioMixerClientData });
|
||||
};
|
||||
|
||||
|
||||
DomainHandler& domainHandler = nodeList->getDomainHandler();
|
||||
const QJsonObject& settingsObject = domainHandler.getSettingsObject();
|
||||
|
||||
|
||||
// check the settings object to see if we have anything we can parse out
|
||||
parseSettingsObject(settingsObject);
|
||||
|
||||
|
||||
// queue up a connection to start broadcasting mixes now that we're ready to go
|
||||
QMetaObject::invokeMethod(this, "broadcastMixes", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void AudioMixer::broadcastMixes() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
int64_t nextFrame = 0;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
|
||||
int64_t usecToSleep = AudioConstants::NETWORK_FRAME_USECS;
|
||||
|
||||
|
||||
const int TRAILING_AVERAGE_FRAMES = 100;
|
||||
int framesSinceCutoffEvent = TRAILING_AVERAGE_FRAMES;
|
||||
|
||||
|
||||
while (!_isFinished) {
|
||||
const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f;
|
||||
const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f;
|
||||
|
||||
|
||||
const float RATIO_BACK_OFF = 0.02f;
|
||||
|
||||
|
||||
const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES;
|
||||
const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO;
|
||||
|
||||
|
||||
if (usecToSleep < 0) {
|
||||
usecToSleep = 0;
|
||||
}
|
||||
|
||||
|
||||
_trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio)
|
||||
+ (usecToSleep * CURRENT_FRAME_RATIO / (float) AudioConstants::NETWORK_FRAME_USECS);
|
||||
|
||||
|
||||
float lastCutoffRatio = _performanceThrottlingRatio;
|
||||
bool hasRatioChanged = false;
|
||||
|
||||
|
||||
if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) {
|
||||
if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) {
|
||||
// we're struggling - change our min required loudness to reduce some load
|
||||
_performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio));
|
||||
|
||||
|
||||
qDebug() << "Mixer is struggling, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was"
|
||||
<< lastCutoffRatio << "and is now" << _performanceThrottlingRatio;
|
||||
hasRatioChanged = true;
|
||||
} else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) {
|
||||
// we've recovered and can back off the required loudness
|
||||
_performanceThrottlingRatio = _performanceThrottlingRatio - RATIO_BACK_OFF;
|
||||
|
||||
|
||||
if (_performanceThrottlingRatio < 0) {
|
||||
_performanceThrottlingRatio = 0;
|
||||
}
|
||||
|
||||
|
||||
qDebug() << "Mixer is recovering, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was"
|
||||
<< lastCutoffRatio << "and is now" << _performanceThrottlingRatio;
|
||||
hasRatioChanged = true;
|
||||
}
|
||||
|
||||
|
||||
if (hasRatioChanged) {
|
||||
// set out min audability threshold from the new ratio
|
||||
_minAudibilityThreshold = LOUDNESS_TO_DISTANCE_RATIO / (2.0f * (1.0f - _performanceThrottlingRatio));
|
||||
qDebug() << "Minimum audability required to be mixed is now" << _minAudibilityThreshold;
|
||||
|
||||
|
||||
framesSinceCutoffEvent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!hasRatioChanged) {
|
||||
++framesSinceCutoffEvent;
|
||||
}
|
||||
|
||||
|
||||
quint64 now = usecTimestampNow();
|
||||
if (now - _lastPerSecondCallbackTime > USECS_PER_SECOND) {
|
||||
perSecondActions();
|
||||
_lastPerSecondCallbackTime = now;
|
||||
}
|
||||
|
||||
|
||||
nodeList->eachNode([&](const SharedNodePointer& node) {
|
||||
|
||||
|
||||
if (node->getLinkedData()) {
|
||||
AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData();
|
||||
|
||||
|
||||
// this function will attempt to pop a frame from each audio stream.
|
||||
// a pointer to the popped data is stored as a member in InboundAudioStream.
|
||||
// That's how the popped audio data will be read for mixing (but only if the pop was successful)
|
||||
nodeData->checkBuffersBeforeFrameSend();
|
||||
|
||||
|
||||
// if the stream should be muted, send mute packet
|
||||
if (nodeData->getAvatarAudioStream()
|
||||
&& shouldMute(nodeData->getAvatarAudioStream()->getQuietestFrameLoudness())) {
|
||||
auto mutePacket = NLPacket::create(PacketType::NoisyMute, 0);
|
||||
nodeList->sendPacket(std::move(mutePacket), *node);
|
||||
}
|
||||
|
||||
|
||||
if (node->getType() == NodeType::Agent && node->getActiveSocket()
|
||||
&& nodeData->getAvatarAudioStream()) {
|
||||
|
||||
|
||||
int streamsMixed = prepareMixForListeningNode(node.data());
|
||||
|
||||
|
||||
std::unique_ptr<NLPacket> mixPacket;
|
||||
|
||||
|
||||
if (streamsMixed > 0) {
|
||||
int mixPacketBytes = sizeof(quint16) + AudioConstants::NETWORK_FRAME_BYTES_STEREO;
|
||||
mixPacket = NLPacket::create(PacketType::MixedAudio, mixPacketBytes);
|
||||
|
||||
|
||||
// pack sequence number
|
||||
quint16 sequence = nodeData->getOutgoingSequenceNumber();
|
||||
mixPacket->writePrimitive(sequence);
|
||||
|
||||
|
||||
// pack mixed audio samples
|
||||
mixPacket->write(reinterpret_cast<char*>(_mixSamples),
|
||||
AudioConstants::NETWORK_FRAME_BYTES_STEREO);
|
||||
} else {
|
||||
int silentPacketBytes = sizeof(quint16) + sizeof(quint16);
|
||||
mixPacket = NLPacket::create(PacketType::SilentAudioFrame, silentPacketBytes);
|
||||
|
||||
|
||||
// pack sequence number
|
||||
quint16 sequence = nodeData->getOutgoingSequenceNumber();
|
||||
mixPacket->writePrimitive(sequence);
|
||||
|
||||
|
||||
// pack number of silent audio samples
|
||||
quint16 numSilentSamples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO;
|
||||
mixPacket->writePrimitive(numSilentSamples);
|
||||
}
|
||||
|
||||
|
||||
// Send audio environment
|
||||
sendAudioEnvironmentPacket(node);
|
||||
|
||||
|
||||
// send mixed audio packet
|
||||
nodeList->sendPacket(std::move(mixPacket), *node);
|
||||
nodeData->incrementOutgoingMixedAudioSequenceNumber();
|
||||
|
||||
|
||||
// send an audio stream stats packet if it's time
|
||||
if (_sendAudioStreamStats) {
|
||||
nodeData->sendAudioStreamStatsPackets(node);
|
||||
_sendAudioStreamStats = false;
|
||||
}
|
||||
|
||||
|
||||
++_sumListeners;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
++_numStatFrames;
|
||||
|
||||
|
||||
// since we're a while loop we need to help Qt's event processing
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
|
||||
if (_isFinished) {
|
||||
// at this point the audio-mixer is done
|
||||
// check if we have a deferred delete event to process (which we should once finished)
|
||||
QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
usecToSleep = (++nextFrame * AudioConstants::NETWORK_FRAME_USECS) - (timer.nsecsElapsed() / 1000);
|
||||
|
||||
if (usecToSleep > 0) {
|
||||
|
@ -1105,5 +1112,3 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -289,6 +289,8 @@ void OctreeServer::initHTTPManager(int port) {
|
|||
_httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this);
|
||||
}
|
||||
|
||||
const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz";
|
||||
|
||||
bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) {
|
||||
|
||||
#ifdef FORCE_CRASH
|
||||
|
@ -310,7 +312,6 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
#endif
|
||||
|
||||
bool showStats = false;
|
||||
QString persistFile = "/" + getPersistFilename();
|
||||
|
||||
if (connection->requestOperation() == QNetworkAccessManager::GetOperation) {
|
||||
if (url.path() == "/") {
|
||||
|
@ -320,7 +321,7 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
_tree->resetEditStats();
|
||||
resetSendingStats();
|
||||
showStats = true;
|
||||
} else if ((url.path() == persistFile) || (url.path() == persistFile + "/")) {
|
||||
} else if ((url.path() == PERSIST_FILE_DOWNLOAD_PATH) || (url.path() == PERSIST_FILE_DOWNLOAD_PATH + "/")) {
|
||||
if (_persistFileDownload) {
|
||||
QByteArray persistFileContents = getPersistFileContents();
|
||||
if (persistFileContents.length() > 0) {
|
||||
|
@ -374,9 +375,9 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
statsString += "\r\n";
|
||||
|
||||
if (_persistFileDownload) {
|
||||
statsString += QString("Persist file: <a href='%1'>%1</a>\r\n").arg(persistFile);
|
||||
statsString += QString("Persist file: <a href='%1'>Click to Download</a>\r\n").arg(PERSIST_FILE_DOWNLOAD_PATH);
|
||||
} else {
|
||||
statsString += QString("Persist file: %1\r\n").arg(persistFile);
|
||||
statsString += QString("Persist file: %1\r\n").arg(_persistFilePath);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -1030,13 +1031,12 @@ void OctreeServer::readConfiguration() {
|
|||
qDebug() << "wantPersist=" << _wantPersist;
|
||||
|
||||
if (_wantPersist) {
|
||||
QString persistFilename;
|
||||
if (!readOptionString(QString("persistFilename"), settingsSectionObject, persistFilename)) {
|
||||
persistFilename = getMyDefaultPersistFilename();
|
||||
if (!readOptionString("persistFilePath", settingsSectionObject, _persistFilePath)
|
||||
&& !readOptionString("persistFilename", settingsSectionObject, _persistFilePath)) {
|
||||
_persistFilePath = getMyDefaultPersistFilename();
|
||||
}
|
||||
|
||||
strcpy(_persistFilename, qPrintable(persistFilename));
|
||||
qDebug("persistFilename=%s", _persistFilename);
|
||||
qDebug() << "persistFilePath=" << _persistFilePath;
|
||||
|
||||
_persistAsFileType = "json.gz";
|
||||
|
||||
|
@ -1145,8 +1145,26 @@ void OctreeServer::domainSettingsRequestComplete() {
|
|||
if (_wantPersist) {
|
||||
// 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
|
||||
auto persistPath = ServerPathUtils::getDataFilePath("entities/" + QString(_persistFilename));
|
||||
if (!QFile::exists(persistPath)) {
|
||||
QDir persistPath { _persistFilePath };
|
||||
QString absoluteFilePath = 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
|
||||
absoluteFilePath = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_persistFilePath);
|
||||
}
|
||||
|
||||
static const QString ENTITY_PERSIST_EXTENSION = ".json.gz";
|
||||
|
||||
// force the persist file to end with .json.gz
|
||||
if (!absoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) {
|
||||
absoluteFilePath += ENTITY_PERSIST_EXTENSION;
|
||||
} else {
|
||||
// make sure the casing of .json.gz is correct
|
||||
absoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
if (!QFile::exists(absoluteFilePath)) {
|
||||
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";
|
||||
|
@ -1154,7 +1172,7 @@ void OctreeServer::domainSettingsRequestComplete() {
|
|||
|
||||
// This is the old persist path, based on the current persist filename, which could
|
||||
// be a custom filename set by the user.
|
||||
auto oldPersistPath = QDir(oldResourcesDirectory).absoluteFilePath(_persistFilename);
|
||||
auto oldPersistPath = QDir(oldResourcesDirectory).absoluteFilePath(_persistFilePath);
|
||||
|
||||
// This is the old default persist path.
|
||||
auto oldDefaultPersistPath = QDir(oldResourcesDirectory).absoluteFilePath(OLD_DEFAULT_PERSIST_FILENAME);
|
||||
|
@ -1172,23 +1190,24 @@ void OctreeServer::domainSettingsRequestComplete() {
|
|||
pathToCopyFrom = oldDefaultPersistPath;
|
||||
}
|
||||
|
||||
QDir persistFileDirectory = QDir(persistPath).filePath("..");
|
||||
QDir persistFileDirectory { QDir::cleanPath(absoluteFilePath + "/..") };
|
||||
|
||||
if (!persistFileDirectory.exists()) {
|
||||
qDebug() << "Creating data directory " << persistFileDirectory.absolutePath();
|
||||
persistFileDirectory.mkpath(".");
|
||||
}
|
||||
|
||||
if (shouldCopy) {
|
||||
qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << persistPath;
|
||||
qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << absoluteFilePath;
|
||||
|
||||
QFile::copy(pathToCopyFrom, persistPath);
|
||||
QFile::copy(pathToCopyFrom, absoluteFilePath);
|
||||
} else {
|
||||
qDebug() << "No existing persist file found";
|
||||
}
|
||||
}
|
||||
|
||||
// now set up PersistThread
|
||||
_persistThread = new OctreePersistThread(_tree, persistPath, _persistInterval,
|
||||
_persistThread = new OctreePersistThread(_tree, absoluteFilePath, _persistInterval,
|
||||
_wantBackup, _settings, _debugTimestampNow, _persistAsFileType);
|
||||
_persistThread->initialize(true);
|
||||
}
|
||||
|
|
|
@ -169,7 +169,7 @@ protected:
|
|||
int _statusPort;
|
||||
QString _statusHost;
|
||||
|
||||
char _persistFilename[MAX_FILENAME_LENGTH];
|
||||
QString _persistFilePath;
|
||||
QString _persistAsFileType;
|
||||
int _packetsPerClientPerInterval;
|
||||
int _packetsTotalPerInterval;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": 1.0,
|
||||
"version": 1.1,
|
||||
"settings": [
|
||||
{
|
||||
"name": "metaverse",
|
||||
|
@ -383,9 +383,9 @@
|
|||
"assignment-types": [6],
|
||||
"settings": [
|
||||
{
|
||||
"name": "persistFilename",
|
||||
"label": "Entities Filename",
|
||||
"help": "the path to the file entities are stored in. Make sure the path exists.",
|
||||
"name": "persistFilePath",
|
||||
"label": "Entities File Path",
|
||||
"help": "The path to the file entities are stored in. If this path is relative it will be relative to the application data directory. The filename must end in .json.gz.",
|
||||
"placeholder": "models.json.gz",
|
||||
"default": "models.json.gz",
|
||||
"advanced": true
|
||||
|
|
|
@ -124,6 +124,41 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
|
|||
// reload the master and user config so that the merged config is right
|
||||
_configMap.loadMasterAndUserConfig(argumentList);
|
||||
}
|
||||
} else if (oldVersion < 1.1) {
|
||||
static const QString ENTITY_SERVER_SETTINGS_KEY = "entity_server_settings";
|
||||
static const QString ENTITY_FILE_NAME_KEY = "persistFilename";
|
||||
static const QString ENTITY_FILE_PATH_KEYPATH = ENTITY_SERVER_SETTINGS_KEY + ".persistFilePath";
|
||||
|
||||
// this was prior to change of poorly named entitiesFileName to entitiesFilePath
|
||||
QVariant* persistFileNameVariant = valueForKeyPath(_configMap.getMergedConfig(),
|
||||
ENTITY_SERVER_SETTINGS_KEY + "." + ENTITY_FILE_NAME_KEY);
|
||||
if (persistFileNameVariant && persistFileNameVariant->canConvert(QMetaType::QString)) {
|
||||
QString persistFileName = persistFileNameVariant->toString();
|
||||
|
||||
qDebug() << "Migrating persistFilename to persistFilePath for entity-server settings";
|
||||
|
||||
// grab the persistFilePath option, create it if it doesn't exist
|
||||
QVariant* persistFilePath = valueForKeyPath(_configMap.getUserConfig(), ENTITY_FILE_PATH_KEYPATH, true);
|
||||
|
||||
// write the migrated value
|
||||
*persistFilePath = persistFileName;
|
||||
|
||||
// remove the old setting
|
||||
QVariant* entityServerVariant = valueForKeyPath(_configMap.getUserConfig(), ENTITY_SERVER_SETTINGS_KEY);
|
||||
if (entityServerVariant && entityServerVariant->canConvert(QMetaType::QVariantMap)) {
|
||||
QVariantMap entityServerMap = entityServerVariant->toMap();
|
||||
entityServerMap.remove(ENTITY_FILE_NAME_KEY);
|
||||
|
||||
*entityServerVariant = entityServerMap;
|
||||
}
|
||||
|
||||
// write the new settings to the json file
|
||||
persistToFile();
|
||||
|
||||
// reload the master and user config so that the merged config is right
|
||||
_configMap.loadMasterAndUserConfig(argumentList);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,41 +22,44 @@ var MINIMUM_DROP_DISTANCE_FROM_JOINT = 0.4;
|
|||
var ATTACHED_ENTITY_SEARCH_DISTANCE = 10.0;
|
||||
var ATTACHED_ENTITIES_SETTINGS_KEY = "ATTACHED_ENTITIES";
|
||||
var DRESSING_ROOM_DISTANCE = 2.0;
|
||||
var SHOW_TOOL_BAR = false;
|
||||
|
||||
// tool bar
|
||||
|
||||
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
|
||||
var BUTTON_SIZE = 32;
|
||||
var PADDING = 3;
|
||||
Script.include(["libraries/toolBars.js"]);
|
||||
var toolBar = new ToolBar(0, 0, ToolBar.VERTICAL, "highfidelity.attachedEntities.toolbar", function(screenSize) {
|
||||
return {
|
||||
x: (BUTTON_SIZE + PADDING),
|
||||
y: (screenSize.y / 2 - BUTTON_SIZE * 2 + PADDING)
|
||||
};
|
||||
});
|
||||
var saveButton = toolBar.addOverlay("image", {
|
||||
width: BUTTON_SIZE,
|
||||
height: BUTTON_SIZE,
|
||||
imageURL: "http://headache.hungry.com/~seth/hifi/save.png",
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
alpha: 1
|
||||
});
|
||||
var loadButton = toolBar.addOverlay("image", {
|
||||
width: BUTTON_SIZE,
|
||||
height: BUTTON_SIZE,
|
||||
imageURL: "http://headache.hungry.com/~seth/hifi/load.png",
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
alpha: 1
|
||||
});
|
||||
if (SHOW_TOOL_BAR) {
|
||||
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
|
||||
var BUTTON_SIZE = 32;
|
||||
var PADDING = 3;
|
||||
Script.include(["libraries/toolBars.js"]);
|
||||
var toolBar = new ToolBar(0, 0, ToolBar.VERTICAL, "highfidelity.attachedEntities.toolbar", function(screenSize) {
|
||||
return {
|
||||
x: (BUTTON_SIZE + PADDING),
|
||||
y: (screenSize.y / 2 - BUTTON_SIZE * 2 + PADDING)
|
||||
};
|
||||
});
|
||||
var saveButton = toolBar.addOverlay("image", {
|
||||
width: BUTTON_SIZE,
|
||||
height: BUTTON_SIZE,
|
||||
imageURL: ".../save.png",
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
alpha: 1
|
||||
});
|
||||
var loadButton = toolBar.addOverlay("image", {
|
||||
width: BUTTON_SIZE,
|
||||
height: BUTTON_SIZE,
|
||||
imageURL: ".../load.png",
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
alpha: 1
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function mousePressEvent(event) {
|
||||
|
@ -73,10 +76,14 @@ function mousePressEvent(event) {
|
|||
}
|
||||
|
||||
function scriptEnding() {
|
||||
toolBar.cleanup();
|
||||
if (SHOW_TOOL_BAR) {
|
||||
toolBar.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
Controller.mousePressEvent.connect(mousePressEvent);
|
||||
if (SHOW_TOOL_BAR) {
|
||||
Controller.mousePressEvent.connect(mousePressEvent);
|
||||
}
|
||||
Script.scriptEnding.connect(scriptEnding);
|
||||
|
||||
|
||||
|
@ -116,6 +123,7 @@ function AttachedEntitiesManager() {
|
|||
manager.checkIfWearable(parsedMessage.grabbedEntity, parsedMessage.joint)
|
||||
// manager.saveAttachedEntities();
|
||||
} else if (parsedMessage.action === 'shared-release') {
|
||||
manager.updateRelativeOffsets(parsedMessage.grabbedEntity);
|
||||
// manager.saveAttachedEntities();
|
||||
} else if (parsedMessage.action === 'equip') {
|
||||
// manager.saveAttachedEntities();
|
||||
|
@ -173,11 +181,12 @@ function AttachedEntitiesManager() {
|
|||
parentJointIndex: bestJointIndex
|
||||
};
|
||||
|
||||
if (!this.avatarIsInDressingRoom() &&
|
||||
bestJointOffset && bestJointOffset.constructor === Array && bestJointOffset.length > 1) {
|
||||
// don't snap the entity to the preferred position if the avatar is in the dressing room.
|
||||
wearProps.localPosition = bestJointOffset[0];
|
||||
wearProps.localRotation = bestJointOffset[1];
|
||||
if (bestJointOffset && bestJointOffset.constructor === Array && bestJointOffset.length > 1) {
|
||||
if (!this.avatarIsInDressingRoom()) {
|
||||
// don't snap the entity to the preferred position if the avatar is in the dressing room.
|
||||
wearProps.localPosition = bestJointOffset[0];
|
||||
wearProps.localRotation = bestJointOffset[1];
|
||||
}
|
||||
}
|
||||
Entities.editEntity(grabbedEntity, wearProps);
|
||||
} else if (props.parentID != NULL_UUID) {
|
||||
|
@ -189,12 +198,19 @@ function AttachedEntitiesManager() {
|
|||
}
|
||||
}
|
||||
|
||||
this.updateRelativeOffsets = function(entityID, props) {
|
||||
this.updateRelativeOffsets = function(entityID) {
|
||||
// save the preferred (current) relative position and rotation into the user-data of the entity
|
||||
var wearableData = getEntityCustomData('wearable', entityID, DEFAULT_WEARABLE_DATA);
|
||||
var currentJointName = MyAvatar.getJointNames()[props.parentJointIndex];
|
||||
wearableData.joints[currentJointName] = [props.localPosition, props.localRotation];
|
||||
setEntityCustomData('wearable', entityID, wearableData);
|
||||
var props = Entities.getEntityProperties(entityID);
|
||||
if (props.parentID == MyAvatar.sessionUUID) {
|
||||
grabData = getEntityCustomData('grabKey', entityID, {});
|
||||
grabbableData = getEntityCustomData('grabbableKey', entityID, {});
|
||||
var wearableData = getEntityCustomData('wearable', entityID, DEFAULT_WEARABLE_DATA);
|
||||
var currentJointName = MyAvatar.getJointNames()[props.parentJointIndex];
|
||||
wearableData.joints[currentJointName] = [props.localPosition, props.localRotation];
|
||||
setEntityCustomData('wearable', entityID, wearableData);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
this.saveAttachedEntities = function() {
|
||||
|
@ -203,12 +219,8 @@ function AttachedEntitiesManager() {
|
|||
var nearbyEntities = Entities.findEntities(MyAvatar.position, ATTACHED_ENTITY_SEARCH_DISTANCE);
|
||||
for (i = 0; i < nearbyEntities.length; i++) {
|
||||
var entityID = nearbyEntities[i];
|
||||
var props = Entities.getEntityProperties(entityID);
|
||||
if (props.parentID == MyAvatar.sessionUUID) {
|
||||
grabData = getEntityCustomData('grabKey', entityID, {});
|
||||
grabbableData = getEntityCustomData('grabbableKey', entityID, {});
|
||||
this.updateRelativeOffsets(entityID, props);
|
||||
props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them
|
||||
if (this.updateRelativeOffsets(entityID)) {
|
||||
var props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them
|
||||
this.scrubProperties(props);
|
||||
saveData.push(props);
|
||||
}
|
||||
|
|
|
@ -69,13 +69,14 @@ var PICK_MAX_DISTANCE = 500; // max length of pick-ray
|
|||
// near grabbing
|
||||
//
|
||||
|
||||
var GRAB_RADIUS = 0.03; // if the ray misses but an object is this close, it will still be selected
|
||||
var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected
|
||||
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
|
||||
var NEAR_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable.
|
||||
var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected
|
||||
var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things
|
||||
var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object
|
||||
var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed
|
||||
var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size
|
||||
|
||||
//
|
||||
// equip
|
||||
|
@ -256,6 +257,7 @@ function MyController(hand) {
|
|||
//for visualizations
|
||||
this.overlayLine = null;
|
||||
this.particleBeamObject = null;
|
||||
this.grabSphere = null;
|
||||
|
||||
//for lights
|
||||
this.spotlight = null;
|
||||
|
@ -336,6 +338,7 @@ function MyController(hand) {
|
|||
}
|
||||
|
||||
this.setState = function(newState) {
|
||||
this.grabSphereOff();
|
||||
if (WANT_DEBUG || WANT_DEBUG_STATE) {
|
||||
print("STATE (" + this.hand + "): " + stateToName(this.state) + " --> " +
|
||||
stateToName(newState) + ", hand: " + this.hand);
|
||||
|
@ -416,6 +419,37 @@ function MyController(hand) {
|
|||
}
|
||||
}
|
||||
|
||||
this.grabSphereOn = function() {
|
||||
var color = {red: 0, green: 255, blue: 0};
|
||||
if (this.grabSphere === null) {
|
||||
var sphereProperties = {
|
||||
position: this.getHandPosition(),
|
||||
size: GRAB_RADIUS*2,
|
||||
color: color,
|
||||
alpha: 0.1,
|
||||
solid: true,
|
||||
visible: true
|
||||
}
|
||||
this.grabSphere = Overlays.addOverlay("sphere", sphereProperties);
|
||||
} else {
|
||||
Overlays.editOverlay(this.grabSphere, {
|
||||
position: this.getHandPosition(),
|
||||
size: GRAB_RADIUS*2,
|
||||
color: color,
|
||||
alpha: 0.1,
|
||||
solid: true,
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.grabSphereOff = function() {
|
||||
if (this.grabSphere !== null) {
|
||||
Overlays.deleteOverlay(this.grabSphere);
|
||||
this.grabSphere = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.overlayLineOn = function(closePoint, farPoint, color) {
|
||||
if (this.overlayLine === null) {
|
||||
var lineProperties = {
|
||||
|
@ -644,7 +678,6 @@ function MyController(hand) {
|
|||
this.searchSphereDistance = DEFAULT_SEARCH_SPHERE_DISTANCE;
|
||||
this.intersectionDistance = 0.0;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.particleBeamOff = function() {
|
||||
|
@ -749,6 +782,11 @@ function MyController(hand) {
|
|||
|
||||
// the trigger is being pressed, so do a ray test to see what we are hitting
|
||||
var handPosition = this.getHandPosition();
|
||||
|
||||
if (SHOW_GRAB_SPHERE) {
|
||||
this.grabSphereOn();
|
||||
}
|
||||
|
||||
var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
|
||||
var currentHandRotation = Controller.getPoseValue(controllerHandInput).rotation;
|
||||
var handDeltaRotation = Quat.multiply(currentHandRotation, Quat.inverse(this.startingHandRotation));
|
||||
|
@ -1221,11 +1259,14 @@ function MyController(hand) {
|
|||
|
||||
this.hasPresetOffsets = function() {
|
||||
var wearableData = getEntityCustomData('wearable', this.grabbedEntity, {joints: {}});
|
||||
var allowedJoints = wearableData.joints;
|
||||
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
|
||||
if (handJointName in allowedJoints) {
|
||||
return true;
|
||||
if ("joints" in wearableData) {
|
||||
var allowedJoints = wearableData.joints;
|
||||
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
|
||||
if (handJointName in allowedJoints) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
this.getPresetPosition = function() {
|
||||
|
|
|
@ -21,3 +21,4 @@ Script.load("controllers/squeezeHands.js");
|
|||
Script.load("grab.js");
|
||||
Script.load("directory.js");
|
||||
Script.load("dialTone.js");
|
||||
Script.load("attachedEntitiesManager.js");
|
||||
|
|
|
@ -235,7 +235,12 @@ var usersWindow = (function () {
|
|||
FRIENDS_BUTTON_HEIGHT = FRIENDS_BUTTON_SVG_HEIGHT,
|
||||
FRIENDS_BUTTON_COLOR = { red: 225, green: 225, blue: 225 },
|
||||
FRIENDS_BUTTON_ALPHA = 0.95,
|
||||
FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends",
|
||||
FRIENDS_WINDOW_WIDTH = 290,
|
||||
FRIENDS_WINDOW_HEIGHT = 500,
|
||||
FRIENDS_WINDOW_TITLE = "Add/Remove Friends",
|
||||
friendsButton,
|
||||
friendsWindow,
|
||||
|
||||
OPTION_BACKGROUND_COLOR = { red: 60, green: 60, blue: 60 },
|
||||
OPTION_BACKGROUND_ALPHA = 0.1,
|
||||
|
@ -643,7 +648,17 @@ var usersWindow = (function () {
|
|||
}
|
||||
|
||||
if (clickedOverlay === friendsButton) {
|
||||
GlobalServices.editFriends();
|
||||
if (!friendsWindow) {
|
||||
friendsWindow = new OverlayWebWindow({
|
||||
title: FRIENDS_WINDOW_TITLE,
|
||||
width: FRIENDS_WINDOW_WIDTH,
|
||||
height: FRIENDS_WINDOW_HEIGHT,
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
friendsWindow.setURL(FRIENDS_WINDOW_URL);
|
||||
friendsWindow.setVisible(true);
|
||||
friendsWindow.raise();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -247,7 +247,9 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoff", "state": "takeoff" },
|
||||
{ "var": "isInAir", "state": "inAir" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -263,7 +265,9 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoff", "state": "takeoff" },
|
||||
{ "var": "isInAir", "state": "inAir" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -278,7 +282,9 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoff", "state": "takeoff" },
|
||||
{ "var": "isInAir", "state": "inAir" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -293,7 +299,9 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoff", "state": "takeoff" },
|
||||
{ "var": "isInAir", "state": "inAir" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -308,7 +316,9 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoff", "state": "takeoff" },
|
||||
{ "var": "isInAir", "state": "inAir" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -323,7 +333,9 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoff", "state": "takeoff" },
|
||||
{ "var": "isInAir", "state": "inAir" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -338,7 +350,9 @@
|
|||
{ "var": "isMovingLeft", "state": "strafeLeft" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoff", "state": "takeoff" },
|
||||
{ "var": "isInAir", "state": "inAir" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -353,7 +367,9 @@
|
|||
{ "var": "isMovingLeft", "state": "strafeLeft" },
|
||||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoff", "state": "takeoff" },
|
||||
{ "var": "isInAir", "state": "inAir" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -361,6 +377,7 @@
|
|||
"interpTarget": 30,
|
||||
"interpDuration": 30,
|
||||
"transitions": [
|
||||
{ "var": "isNotAway", "state": "awayOutro" },
|
||||
{ "var": "awayIntroOnDone", "state": "away"}
|
||||
]
|
||||
},
|
||||
|
@ -385,8 +402,27 @@
|
|||
"interpTarget": 6,
|
||||
"interpDuration": 6,
|
||||
"transitions": [
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isNotFlying", "state": "idle" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "takeoff",
|
||||
"interpTarget": 0,
|
||||
"interpDuration": 6,
|
||||
"transitions": [
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isNotTakeoff", "state": "inAir" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inAir",
|
||||
"interpTarget": 0,
|
||||
"interpDuration": 6,
|
||||
"transitions": [
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isNotInAir", "state": "idle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -685,6 +721,64 @@
|
|||
"loopFlag": true
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "takeoff",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "https://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_takeoff.fbx",
|
||||
"startFrame": 1.0,
|
||||
"endFrame": 2.5,
|
||||
"timeScale": 1.0,
|
||||
"loopFlag": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "inAir",
|
||||
"type": "blendLinear",
|
||||
"data": {
|
||||
"alpha": 0.0,
|
||||
"alphaVar": "inAirAlpha"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "inAirPreApex",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "https://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_in_air.fbx",
|
||||
"startFrame": 0.0,
|
||||
"endFrame": 0.0,
|
||||
"timeScale": 0.0,
|
||||
"loopFlag": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "inAirApex",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "https://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_in_air.fbx",
|
||||
"startFrame": 6.0,
|
||||
"endFrame": 6.0,
|
||||
"timeScale": 1.0,
|
||||
"loopFlag": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "inAirPostApex",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "https://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_in_air.fbx",
|
||||
"startFrame": 11.0,
|
||||
"endFrame": 11.0,
|
||||
"timeScale": 1.0,
|
||||
"loopFlag": false
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtWebEngine 1.1;
|
||||
|
||||
import "../desktop"
|
||||
import ".."
|
||||
|
@ -7,6 +8,13 @@ import ".."
|
|||
Desktop {
|
||||
id: desktop
|
||||
|
||||
Component.onCompleted: {
|
||||
WebEngine.settings.javascriptCanOpenWindows = false;
|
||||
WebEngine.settings.javascriptCanAccessClipboard = false;
|
||||
WebEngine.settings.spatialNavigationEnabled = true;
|
||||
WebEngine.settings.localContentCanAccessRemoteUrls = true;
|
||||
}
|
||||
|
||||
// The tool window, one instance
|
||||
property alias toolWindow: toolWindow
|
||||
ToolWindow { id: toolWindow }
|
||||
|
|
|
@ -6,8 +6,10 @@ import "../controls"
|
|||
Frame {
|
||||
id: frame
|
||||
|
||||
property bool wideTopMargin: (window && (window.closable || window.title));
|
||||
|
||||
Rectangle {
|
||||
anchors { margins: -iconSize; topMargin: -iconSize * ((window && window.closable) ? 2 : 1); }
|
||||
anchors { margins: -iconSize; topMargin: -iconSize * (wideTopMargin ? 2 : 1); }
|
||||
anchors.fill: parent;
|
||||
color: "#7f7f7f7f";
|
||||
radius: 3;
|
||||
|
|
|
@ -348,7 +348,7 @@ bool setupEssentials(int& argc, char** argv) {
|
|||
DependencyManager::set<BandwidthRecorder>();
|
||||
DependencyManager::set<ResourceCacheSharedItems>();
|
||||
DependencyManager::set<DesktopScriptingInterface>();
|
||||
DependencyManager::set<EntityScriptingInterface>();
|
||||
DependencyManager::set<EntityScriptingInterface>(true);
|
||||
DependencyManager::set<RecordingScriptingInterface>();
|
||||
DependencyManager::set<WindowScriptingInterface>();
|
||||
DependencyManager::set<HMDScriptingInterface>();
|
||||
|
@ -4615,22 +4615,6 @@ void Application::activeChanged(Qt::ApplicationState state) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
void Application::showFriendsWindow() {
|
||||
const QString FRIENDS_WINDOW_OBJECT_NAME = "FriendsWindow";
|
||||
const QString FRIENDS_WINDOW_TITLE = "Add/Remove Friends";
|
||||
const QString FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends";
|
||||
const int FRIENDS_WINDOW_WIDTH = 290;
|
||||
const int FRIENDS_WINDOW_HEIGHT = 500;
|
||||
auto webWindowClass = _window->findChildren<WebWindowClass>(FRIENDS_WINDOW_OBJECT_NAME);
|
||||
if (webWindowClass.empty()) {
|
||||
auto friendsWindow = new WebWindowClass(FRIENDS_WINDOW_TITLE, FRIENDS_WINDOW_URL, FRIENDS_WINDOW_WIDTH,
|
||||
FRIENDS_WINDOW_HEIGHT);
|
||||
friendsWindow->setParent(_window);
|
||||
friendsWindow->setObjectName(FRIENDS_WINDOW_OBJECT_NAME);
|
||||
connect(friendsWindow, &WebWindowClass::closed, &WebWindowClass::deleteLater);
|
||||
friendsWindow->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::postLambdaEvent(std::function<void()> f) {
|
||||
if (this->thread() == QThread::currentThread()) {
|
||||
|
|
|
@ -167,11 +167,11 @@ public:
|
|||
virtual controller::ScriptingInterface* getControllerScriptingInterface() { return _controllerScriptingInterface; }
|
||||
virtual void registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine) override;
|
||||
|
||||
virtual ViewFrustum* getCurrentViewFrustum() { return getDisplayViewFrustum(); }
|
||||
virtual QThread* getMainThread() { return thread(); }
|
||||
virtual PickRay computePickRay(float x, float y) const;
|
||||
virtual glm::vec3 getAvatarPosition() const;
|
||||
virtual qreal getDevicePixelRatio();
|
||||
virtual ViewFrustum* getCurrentViewFrustum() override { return getDisplayViewFrustum(); }
|
||||
virtual QThread* getMainThread() override { return thread(); }
|
||||
virtual PickRay computePickRay(float x, float y) const override;
|
||||
virtual glm::vec3 getAvatarPosition() const override;
|
||||
virtual qreal getDevicePixelRatio() override;
|
||||
|
||||
void setActiveDisplayPlugin(const QString& pluginName);
|
||||
|
||||
|
@ -245,8 +245,6 @@ public slots:
|
|||
void handleLocalServerConnection();
|
||||
void readArgumentsFromLocalSocket();
|
||||
|
||||
void showFriendsWindow();
|
||||
|
||||
void packageModel();
|
||||
|
||||
void openUrl(const QUrl& url);
|
||||
|
|
|
@ -1132,8 +1132,12 @@ glm::quat Avatar::getRightPalmRotation() const {
|
|||
glm::vec3 Avatar::getUncachedLeftPalmPosition() const {
|
||||
assert(QThread::currentThread() == thread()); // main thread access only
|
||||
glm::quat leftPalmRotation;
|
||||
getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftPalmRotation);
|
||||
glm::vec3 leftPalmPosition;
|
||||
if (_skeletonModel.getLeftGrabPosition(leftPalmPosition)) {
|
||||
return leftPalmPosition;
|
||||
}
|
||||
// avatar didn't have a LeftHandMiddle1 joint, fall back on this:
|
||||
getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftPalmRotation);
|
||||
getSkeletonModel().getLeftHandPosition(leftPalmPosition);
|
||||
leftPalmPosition += HAND_TO_PALM_OFFSET * glm::inverse(leftPalmRotation);
|
||||
return leftPalmPosition;
|
||||
|
@ -1149,8 +1153,12 @@ glm::quat Avatar::getUncachedLeftPalmRotation() const {
|
|||
glm::vec3 Avatar::getUncachedRightPalmPosition() const {
|
||||
assert(QThread::currentThread() == thread()); // main thread access only
|
||||
glm::quat rightPalmRotation;
|
||||
getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightPalmRotation);
|
||||
glm::vec3 rightPalmPosition;
|
||||
if (_skeletonModel.getRightGrabPosition(rightPalmPosition)) {
|
||||
return rightPalmPosition;
|
||||
}
|
||||
// avatar didn't have a RightHandMiddle1 joint, fall back on this:
|
||||
getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightPalmRotation);
|
||||
getSkeletonModel().getRightHandPosition(rightPalmPosition);
|
||||
rightPalmPosition += HAND_TO_PALM_OFFSET * glm::inverse(rightPalmRotation);
|
||||
return rightPalmPosition;
|
||||
|
|
|
@ -164,10 +164,10 @@ public:
|
|||
virtual void setOrientation(const glm::quat& orientation) override;
|
||||
|
||||
// these call through to the SpatiallyNestable versions, but they are here to expose these to javascript.
|
||||
Q_INVOKABLE virtual const QUuid getParentID() const { return SpatiallyNestable::getParentID(); }
|
||||
Q_INVOKABLE virtual void setParentID(const QUuid& parentID);
|
||||
Q_INVOKABLE virtual quint16 getParentJointIndex() const { return SpatiallyNestable::getParentJointIndex(); }
|
||||
Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex);
|
||||
Q_INVOKABLE virtual const QUuid getParentID() const override { return SpatiallyNestable::getParentID(); }
|
||||
Q_INVOKABLE virtual void setParentID(const QUuid& parentID) override;
|
||||
Q_INVOKABLE virtual quint16 getParentJointIndex() const override { return SpatiallyNestable::getParentJointIndex(); }
|
||||
Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex) override;
|
||||
|
||||
// NOT thread safe, must be called on main thread.
|
||||
glm::vec3 getUncachedLeftPalmPosition() const;
|
||||
|
|
|
@ -61,7 +61,7 @@ public:
|
|||
|
||||
void addDirtyFlags(uint32_t flags) { _dirtyFlags |= flags; }
|
||||
|
||||
virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const;
|
||||
virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override;
|
||||
|
||||
friend class AvatarManager;
|
||||
friend class Avatar;
|
||||
|
@ -72,7 +72,7 @@ protected:
|
|||
~AvatarMotionState();
|
||||
|
||||
virtual bool isReadyToComputeShape() const override { return true; }
|
||||
virtual btCollisionShape* computeNewShape();
|
||||
virtual btCollisionShape* computeNewShape() override;
|
||||
|
||||
// The AvatarMotionState keeps a RAW backpointer to its Avatar because all AvatarMotionState
|
||||
// instances are "owned" by their corresponding Avatar instance and are deleted in the Avatar dtor.
|
||||
|
|
|
@ -1311,21 +1311,22 @@ void MyAvatar::preRender(RenderArgs* renderArgs) {
|
|||
_prevShouldDrawHead = shouldDrawHead;
|
||||
}
|
||||
|
||||
const float RENDER_HEAD_CUTOFF_DISTANCE = 0.50f;
|
||||
const float RENDER_HEAD_CUTOFF_DISTANCE = 0.5f;
|
||||
|
||||
bool MyAvatar::cameraInsideHead() const {
|
||||
const Head* head = getHead();
|
||||
const glm::vec3 cameraPosition = qApp->getCamera()->getPosition();
|
||||
return glm::length(cameraPosition - head->getEyePosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getUniformScale());
|
||||
return glm::length(cameraPosition - getDefaultEyePosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getUniformScale());
|
||||
}
|
||||
|
||||
bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const {
|
||||
return ((renderArgs->_renderMode != RenderArgs::DEFAULT_RENDER_MODE) ||
|
||||
(qApp->getCamera()->getMode() != CAMERA_MODE_FIRST_PERSON) ||
|
||||
!cameraInsideHead());
|
||||
bool defaultMode = renderArgs->_renderMode == RenderArgs::DEFAULT_RENDER_MODE;
|
||||
bool firstPerson = qApp->getCamera()->getMode() == CAMERA_MODE_FIRST_PERSON;
|
||||
bool insideHead = cameraInsideHead();
|
||||
return !defaultMode || !firstPerson || !insideHead;
|
||||
}
|
||||
|
||||
void MyAvatar::updateOrientation(float deltaTime) {
|
||||
|
||||
// Smoothly rotate body with arrow keys
|
||||
float targetSpeed = _driveKeys[YAW] * _yawSpeed;
|
||||
if (targetSpeed != 0.0f) {
|
||||
|
@ -1510,7 +1511,8 @@ void MyAvatar::updatePosition(float deltaTime) {
|
|||
// rotate velocity into camera frame
|
||||
glm::quat rotation = getHead()->getCameraOrientation();
|
||||
glm::vec3 localVelocity = glm::inverse(rotation) * _targetVelocity;
|
||||
glm::vec3 newLocalVelocity = applyKeyboardMotor(deltaTime, localVelocity, isHovering());
|
||||
bool isHovering = _characterController.getState() == CharacterController::State::Hover;
|
||||
glm::vec3 newLocalVelocity = applyKeyboardMotor(deltaTime, localVelocity, isHovering);
|
||||
newLocalVelocity = applyScriptedMotor(deltaTime, newLocalVelocity);
|
||||
|
||||
// rotate back into world-frame
|
||||
|
@ -1579,10 +1581,6 @@ bool findAvatarAvatarPenetration(const glm::vec3 positionA, float radiusA, float
|
|||
return false;
|
||||
}
|
||||
|
||||
bool MyAvatar::isHovering() const {
|
||||
return _characterController.isHovering();
|
||||
}
|
||||
|
||||
void MyAvatar::increaseSize() {
|
||||
if ((1.0f + SCALING_RATIO) * _targetScale < MAX_AVATAR_SCALE) {
|
||||
_targetScale *= (1.0f + SCALING_RATIO);
|
||||
|
|
|
@ -239,8 +239,6 @@ public:
|
|||
glm::quat getCustomListenOrientation() { return _customListenOrientation; }
|
||||
void setCustomListenOrientation(glm::quat customListenOrientation) { _customListenOrientation = customListenOrientation; }
|
||||
|
||||
bool isHovering() const;
|
||||
|
||||
public slots:
|
||||
void increaseSize();
|
||||
void decreaseSize();
|
||||
|
|
|
@ -67,7 +67,7 @@ void MyCharacterController::updateShapeIfNecessary() {
|
|||
_rigidBody->setAngularFactor(0.0f);
|
||||
_rigidBody->setWorldTransform(btTransform(glmToBullet(_avatar->getOrientation()),
|
||||
glmToBullet(_avatar->getPosition())));
|
||||
if (_isHovering) {
|
||||
if (_state == State::Hover) {
|
||||
_rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f));
|
||||
} else {
|
||||
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp);
|
||||
|
|
|
@ -72,6 +72,20 @@ void SkeletonModel::initJointStates() {
|
|||
emit skeletonLoaded();
|
||||
}
|
||||
|
||||
Rig::CharacterControllerState convertCharacterControllerState(CharacterController::State state) {
|
||||
switch (state) {
|
||||
default:
|
||||
case CharacterController::State::Ground:
|
||||
return Rig::CharacterControllerState::Ground;
|
||||
case CharacterController::State::Takeoff:
|
||||
return Rig::CharacterControllerState::Takeoff;
|
||||
case CharacterController::State::InAir:
|
||||
return Rig::CharacterControllerState::InAir;
|
||||
case CharacterController::State::Hover:
|
||||
return Rig::CharacterControllerState::Hover;
|
||||
};
|
||||
}
|
||||
|
||||
// Called within Model::simulate call, below.
|
||||
void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||
Head* head = _owningAvatar->getHead();
|
||||
|
@ -133,7 +147,8 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
|||
|
||||
_rig->updateFromHandParameters(handParams, deltaTime);
|
||||
|
||||
_rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation(), myAvatar->isHovering());
|
||||
Rig::CharacterControllerState ccState = convertCharacterControllerState(myAvatar->getCharacterController()->getState());
|
||||
_rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation(), ccState);
|
||||
|
||||
// evaluate AnimGraph animation and update jointStates.
|
||||
Model::updateRig(deltaTime, parentTransform);
|
||||
|
@ -235,6 +250,56 @@ void SkeletonModel::applyPalmData(int jointIndex, const PalmData& palm) {
|
|||
}
|
||||
}
|
||||
|
||||
bool SkeletonModel::getLeftGrabPosition(glm::vec3& position) const {
|
||||
int knuckleIndex = _rig->indexOfJoint("LeftHandMiddle1");
|
||||
int handIndex = _rig->indexOfJoint("LeftHand");
|
||||
if (knuckleIndex >= 0 && handIndex >= 0) {
|
||||
glm::quat handRotation;
|
||||
glm::vec3 knucklePosition;
|
||||
glm::vec3 handPosition;
|
||||
if (!getJointPositionInWorldFrame(knuckleIndex, knucklePosition)) {
|
||||
return false;
|
||||
}
|
||||
if (!getJointPositionInWorldFrame(handIndex, handPosition)) {
|
||||
return false;
|
||||
}
|
||||
if (!getJointRotationInWorldFrame(handIndex, handRotation)) {
|
||||
return false;
|
||||
}
|
||||
float halfPalmLength = glm::distance(knucklePosition, handPosition) * 0.5f;
|
||||
// z azis is standardized to be out of the palm. move from the knuckle-joint away from the palm
|
||||
// by 1/2 the palm length.
|
||||
position = knucklePosition + handRotation * (glm::vec3(0.0f, 0.0f, 1.0f) * halfPalmLength);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SkeletonModel::getRightGrabPosition(glm::vec3& position) const {
|
||||
int knuckleIndex = _rig->indexOfJoint("RightHandMiddle1");
|
||||
int handIndex = _rig->indexOfJoint("RightHand");
|
||||
if (knuckleIndex >= 0 && handIndex >= 0) {
|
||||
glm::quat handRotation;
|
||||
glm::vec3 knucklePosition;
|
||||
glm::vec3 handPosition;
|
||||
if (!getJointPositionInWorldFrame(knuckleIndex, knucklePosition)) {
|
||||
return false;
|
||||
}
|
||||
if (!getJointPositionInWorldFrame(handIndex, handPosition)) {
|
||||
return false;
|
||||
}
|
||||
if (!getJointRotationInWorldFrame(handIndex, handRotation)) {
|
||||
return false;
|
||||
}
|
||||
float halfPalmLength = glm::distance(knucklePosition, handPosition) * 0.5f;
|
||||
// z azis is standardized to be out of the palm. move from the knuckle-joint away from the palm
|
||||
// by 1/2 the palm length.
|
||||
position = knucklePosition + handRotation * (glm::vec3(0.0f, 0.0f, 1.0f) * halfPalmLength);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SkeletonModel::getLeftHandPosition(glm::vec3& position) const {
|
||||
return getJointPositionInWorldFrame(getLeftHandJointIndex(), position);
|
||||
}
|
||||
|
|
|
@ -39,6 +39,9 @@ public:
|
|||
/// Returns the index of the right hand joint, or -1 if not found.
|
||||
int getRightHandJointIndex() const { return isActive() ? _geometry->getFBXGeometry().rightHandJointIndex : -1; }
|
||||
|
||||
bool getLeftGrabPosition(glm::vec3& position) const;
|
||||
bool getRightGrabPosition(glm::vec3& position) const;
|
||||
|
||||
/// Retrieve the position of the left hand
|
||||
/// \return true whether or not the position was found
|
||||
bool getLeftHandPosition(glm::vec3& position) const;
|
||||
|
|
|
@ -144,6 +144,3 @@ void GlobalServicesScriptingInterface::updateDownloadInfo() {
|
|||
emit downloadInfoChanged(getDownloadInfo());
|
||||
}
|
||||
|
||||
void GlobalServicesScriptingInterface::editFriends() {
|
||||
QMetaObject::invokeMethod(qApp, "showFriendsWindow");
|
||||
}
|
||||
|
|
|
@ -45,8 +45,7 @@ public:
|
|||
public slots:
|
||||
DownloadInfoResult getDownloadInfo();
|
||||
void updateDownloadInfo();
|
||||
void editFriends();
|
||||
|
||||
|
||||
private slots:
|
||||
void loggedOut();
|
||||
void checkDownloadInfo();
|
||||
|
|
|
@ -20,7 +20,7 @@ class ImageOverlay : public QmlOverlay {
|
|||
|
||||
public:
|
||||
static QString const TYPE;
|
||||
virtual QString getType() const { return TYPE; }
|
||||
virtual QString getType() const override { return TYPE; }
|
||||
static QUrl const URL;
|
||||
|
||||
ImageOverlay();
|
||||
|
|
|
@ -30,7 +30,7 @@ public:
|
|||
void setText(const QString& text);
|
||||
|
||||
|
||||
TextOverlay* createClone() const;
|
||||
TextOverlay* createClone() const override;
|
||||
QSizeF textSize(const QString& text) const; // Pixels
|
||||
};
|
||||
|
||||
|
|
|
@ -504,7 +504,7 @@ static const std::vector<float> FORWARD_SPEEDS = { 0.4f, 1.4f, 4.5f }; // m/s
|
|||
static const std::vector<float> BACKWARD_SPEEDS = { 0.6f, 1.45f }; // m/s
|
||||
static const std::vector<float> LATERAL_SPEEDS = { 0.2f, 0.65f }; // m/s
|
||||
|
||||
void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, bool isHovering) {
|
||||
void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState) {
|
||||
|
||||
glm::vec3 front = worldRotation * IDENTITY_FRONT;
|
||||
|
||||
|
@ -572,11 +572,21 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
const float TURN_ENTER_SPEED_THRESHOLD = 0.5f; // rad/sec
|
||||
const float TURN_EXIT_SPEED_THRESHOLD = 0.2f; // rad/sec
|
||||
|
||||
if (isHovering) {
|
||||
if (ccState == CharacterControllerState::Hover) {
|
||||
if (_desiredState != RigRole::Hover) {
|
||||
_desiredStateAge = 0.0f;
|
||||
}
|
||||
_desiredState = RigRole::Hover;
|
||||
} else if (ccState == CharacterControllerState::InAir) {
|
||||
if (_desiredState != RigRole::InAir) {
|
||||
_desiredStateAge = 0.0f;
|
||||
}
|
||||
_desiredState = RigRole::InAir;
|
||||
} else if (ccState == CharacterControllerState::Takeoff) {
|
||||
if (_desiredState != RigRole::Takeoff) {
|
||||
_desiredStateAge = 0.0f;
|
||||
}
|
||||
_desiredState = RigRole::Takeoff;
|
||||
} else {
|
||||
float moveThresh;
|
||||
if (_state != RigRole::Move) {
|
||||
|
@ -614,6 +624,13 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
|
||||
const float STATE_CHANGE_HYSTERESIS_TIMER = 0.1f;
|
||||
|
||||
// Skip hystersis timer for jump transitions.
|
||||
if (_desiredState == RigRole::Takeoff) {
|
||||
_desiredStateAge = STATE_CHANGE_HYSTERESIS_TIMER;
|
||||
} else if (_state == RigRole::InAir && _desiredState != RigRole::InAir) {
|
||||
_desiredStateAge = STATE_CHANGE_HYSTERESIS_TIMER;
|
||||
}
|
||||
|
||||
if ((_desiredStateAge >= STATE_CHANGE_HYSTERESIS_TIMER) && _desiredState != _state) {
|
||||
_state = _desiredState;
|
||||
_desiredStateAge = 0.0f;
|
||||
|
@ -662,6 +679,10 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
_animVars.set("isNotTurning", true);
|
||||
_animVars.set("isFlying", false);
|
||||
_animVars.set("isNotFlying", true);
|
||||
_animVars.set("isTakeoff", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
_animVars.set("isInAir", false);
|
||||
_animVars.set("isNotInAir", true);
|
||||
}
|
||||
} else if (_state == RigRole::Turn) {
|
||||
if (turningSpeed > 0.0f) {
|
||||
|
@ -682,6 +703,11 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
_animVars.set("isNotMoving", true);
|
||||
_animVars.set("isFlying", false);
|
||||
_animVars.set("isNotFlying", true);
|
||||
_animVars.set("isTakeoff", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
_animVars.set("isInAir", false);
|
||||
_animVars.set("isNotInAir", true);
|
||||
|
||||
} else if (_state == RigRole::Idle ) {
|
||||
// default anim vars to notMoving and notTurning
|
||||
_animVars.set("isMovingForward", false);
|
||||
|
@ -694,7 +720,12 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
_animVars.set("isNotTurning", true);
|
||||
_animVars.set("isFlying", false);
|
||||
_animVars.set("isNotFlying", true);
|
||||
} else {
|
||||
_animVars.set("isTakeoff", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
_animVars.set("isInAir", false);
|
||||
_animVars.set("isNotInAir", true);
|
||||
|
||||
} else if (_state == RigRole::Hover) {
|
||||
// flying.
|
||||
_animVars.set("isMovingForward", false);
|
||||
_animVars.set("isMovingBackward", false);
|
||||
|
@ -706,15 +737,61 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
_animVars.set("isNotTurning", true);
|
||||
_animVars.set("isFlying", true);
|
||||
_animVars.set("isNotFlying", false);
|
||||
_animVars.set("isTakeoff", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
_animVars.set("isInAir", false);
|
||||
_animVars.set("isNotInAir", true);
|
||||
|
||||
} else if (_state == RigRole::Takeoff) {
|
||||
// jumping in-air
|
||||
_animVars.set("isMovingForward", false);
|
||||
_animVars.set("isMovingBackward", false);
|
||||
_animVars.set("isMovingLeft", false);
|
||||
_animVars.set("isMovingRight", false);
|
||||
_animVars.set("isNotMoving", true);
|
||||
_animVars.set("isTurningLeft", false);
|
||||
_animVars.set("isTurningRight", false);
|
||||
_animVars.set("isNotTurning", true);
|
||||
_animVars.set("isFlying", false);
|
||||
_animVars.set("isNotFlying", true);
|
||||
_animVars.set("isTakeoff", true);
|
||||
_animVars.set("isNotTakeoff", false);
|
||||
_animVars.set("isInAir", true);
|
||||
_animVars.set("isNotInAir", false);
|
||||
|
||||
} else if (_state == RigRole::InAir) {
|
||||
// jumping in-air
|
||||
_animVars.set("isMovingForward", false);
|
||||
_animVars.set("isMovingBackward", false);
|
||||
_animVars.set("isMovingLeft", false);
|
||||
_animVars.set("isMovingRight", false);
|
||||
_animVars.set("isNotMoving", true);
|
||||
_animVars.set("isTurningLeft", false);
|
||||
_animVars.set("isTurningRight", false);
|
||||
_animVars.set("isNotTurning", true);
|
||||
_animVars.set("isFlying", false);
|
||||
_animVars.set("isNotFlying", true);
|
||||
_animVars.set("isTakeoff", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
_animVars.set("isInAir", true);
|
||||
_animVars.set("isNotInAir", false);
|
||||
|
||||
// compute blend based on velocity
|
||||
const float JUMP_SPEED = 3.5f;
|
||||
float alpha = glm::clamp(-worldVelocity.y / JUMP_SPEED, -1.0f, 1.0f) + 1.0f;
|
||||
_animVars.set("inAirAlpha", alpha);
|
||||
}
|
||||
|
||||
t += deltaTime;
|
||||
|
||||
if (_enableInverseKinematics) {
|
||||
_animVars.set("ikOverlayAlpha", 1.0f);
|
||||
} else {
|
||||
_animVars.set("ikOverlayAlpha", 0.0f);
|
||||
if (_enableInverseKinematics != _lastEnableInverseKinematics) {
|
||||
if (_enableInverseKinematics) {
|
||||
_animVars.set("ikOverlayAlpha", 1.0f);
|
||||
} else {
|
||||
_animVars.set("ikOverlayAlpha", 0.0f);
|
||||
}
|
||||
}
|
||||
_lastEnableInverseKinematics = _enableInverseKinematics;
|
||||
}
|
||||
|
||||
_lastFront = front;
|
||||
|
|
|
@ -73,6 +73,13 @@ public:
|
|||
glm::quat rightOrientation = glm::quat(); // rig space (z forward)
|
||||
};
|
||||
|
||||
enum class CharacterControllerState {
|
||||
Ground = 0,
|
||||
Takeoff,
|
||||
InAir,
|
||||
Hover
|
||||
};
|
||||
|
||||
virtual ~Rig() {}
|
||||
|
||||
void destroyAnimGraph();
|
||||
|
@ -141,7 +148,7 @@ public:
|
|||
glm::mat4 getJointTransform(int jointIndex) const;
|
||||
|
||||
// Start or stop animations as needed.
|
||||
void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, bool isHovering);
|
||||
void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState);
|
||||
|
||||
// Regardless of who started the animations or how many, update the joints.
|
||||
void updateAnimations(float deltaTime, glm::mat4 rootTransform);
|
||||
|
@ -271,7 +278,9 @@ public:
|
|||
Idle = 0,
|
||||
Turn,
|
||||
Move,
|
||||
Hover
|
||||
Hover,
|
||||
Takeoff,
|
||||
InAir
|
||||
};
|
||||
RigRole _state { RigRole::Idle };
|
||||
RigRole _desiredState { RigRole::Idle };
|
||||
|
@ -292,6 +301,7 @@ public:
|
|||
std::map<QString, AnimNode::Pointer> _origRoleAnimations;
|
||||
std::vector<AnimNode::Pointer> _prefetchedAnimations;
|
||||
|
||||
bool _lastEnableInverseKinematics { false };
|
||||
bool _enableInverseKinematics { true };
|
||||
|
||||
private:
|
||||
|
|
|
@ -51,7 +51,7 @@ public:
|
|||
virtual bool apply(glm::quat& rotation) const override;
|
||||
|
||||
void setLowerSpine(bool lowerSpine) { _lowerSpine = lowerSpine; }
|
||||
virtual bool isLowerSpine() const { return _lowerSpine; }
|
||||
virtual bool isLowerSpine() const override { return _lowerSpine; }
|
||||
|
||||
// SwingLimitFunction is an implementation of the constraint check described in the paper:
|
||||
// "The Parameterization of Joint Rotation with the Unit Quaternion" by Quang Liu and Edmond C. Prakash
|
||||
|
|
|
@ -22,8 +22,8 @@ public:
|
|||
static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties);
|
||||
RenderableBoxEntityItem(const EntityItemID& entityItemID) : BoxEntityItem(entityItemID) { }
|
||||
|
||||
virtual void render(RenderArgs* args);
|
||||
virtual void setUserData(const QString& value);
|
||||
virtual void render(RenderArgs* args) override;
|
||||
virtual void setUserData(const QString& value) override;
|
||||
|
||||
SIMPLE_RENDERABLE()
|
||||
private:
|
||||
|
|
|
@ -20,12 +20,12 @@ public:
|
|||
static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties);
|
||||
RenderableLightEntityItem(const EntityItemID& entityItemID) : LightEntityItem(entityItemID) { }
|
||||
|
||||
virtual void render(RenderArgs* args);
|
||||
virtual bool supportsDetailedRayIntersection() const { return true; }
|
||||
virtual void render(RenderArgs* args) override;
|
||||
virtual bool supportsDetailedRayIntersection() const override { return true; }
|
||||
virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
|
||||
bool& keepSearching, OctreeElementPointer& element, float& distance,
|
||||
BoxFace& face, glm::vec3& surfaceNormal,
|
||||
void** intersectedObject, bool precisionPicking) const;
|
||||
void** intersectedObject, bool precisionPicking) const override;
|
||||
|
||||
SIMPLE_RENDERABLE();
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ public:
|
|||
_lineVerticesID(GeometryCache::UNKNOWN_ID)
|
||||
{ }
|
||||
|
||||
virtual void render(RenderArgs* args);
|
||||
virtual void render(RenderArgs* args) override;
|
||||
|
||||
SIMPLE_RENDERABLE();
|
||||
|
||||
|
|
|
@ -22,8 +22,8 @@ public:
|
|||
static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties);
|
||||
RenderableSphereEntityItem(const EntityItemID& entityItemID) : SphereEntityItem(entityItemID) { }
|
||||
|
||||
virtual void render(RenderArgs* args);
|
||||
virtual void setUserData(const QString& value);
|
||||
virtual void render(RenderArgs* args) override;
|
||||
virtual void setUserData(const QString& value) override;
|
||||
|
||||
SIMPLE_RENDERABLE();
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ public:
|
|||
RenderableTextEntityItem(const EntityItemID& entityItemID) : TextEntityItem(entityItemID) { }
|
||||
~RenderableTextEntityItem() { delete _textRenderer; }
|
||||
|
||||
virtual void render(RenderArgs* args);
|
||||
virtual void render(RenderArgs* args) override;
|
||||
|
||||
SIMPLE_RENDERABLE();
|
||||
|
||||
|
|
|
@ -25,8 +25,8 @@ public:
|
|||
RenderableWebEntityItem(const EntityItemID& entityItemID);
|
||||
~RenderableWebEntityItem();
|
||||
|
||||
virtual void render(RenderArgs* args);
|
||||
virtual void setSourceUrl(const QString& value);
|
||||
virtual void render(RenderArgs* args) override;
|
||||
virtual void setSourceUrl(const QString& value) override;
|
||||
|
||||
void setProxyWindow(QWindow* proxyWindow);
|
||||
QObject* getEventHandler();
|
||||
|
|
|
@ -25,8 +25,9 @@
|
|||
#include "ZoneEntityItem.h"
|
||||
|
||||
|
||||
EntityScriptingInterface::EntityScriptingInterface() :
|
||||
_entityTree(NULL)
|
||||
EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership) :
|
||||
_entityTree(NULL),
|
||||
_bidOnSimulationOwnership(bidOnSimulationOwnership)
|
||||
{
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged);
|
||||
|
@ -130,17 +131,20 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
|
|||
_entityTree->withWriteLock([&] {
|
||||
EntityItemPointer entity = _entityTree->addEntity(id, propertiesWithSimID);
|
||||
if (entity) {
|
||||
// This Node is creating a new object. If it's in motion, set this Node as the simulator.
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
const QUuid myNodeID = nodeList->getSessionUUID();
|
||||
propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY);
|
||||
if (propertiesWithSimID.parentRelatedPropertyChanged()) {
|
||||
// due to parenting, the server may not know where something is in world-space, so include the bounding cube.
|
||||
propertiesWithSimID.setQueryAACube(entity->getQueryAACube());
|
||||
}
|
||||
|
||||
// and make note of it now, so we can act on it right away.
|
||||
entity->setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY);
|
||||
if (_bidOnSimulationOwnership) {
|
||||
// This Node is creating a new object. If it's in motion, set this Node as the simulator.
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
const QUuid myNodeID = nodeList->getSessionUUID();
|
||||
|
||||
// and make note of it now, so we can act on it right away.
|
||||
propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY);
|
||||
entity->setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY);
|
||||
}
|
||||
|
||||
entity->setLastBroadcast(usecTimestampNow());
|
||||
} else {
|
||||
|
@ -255,7 +259,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
|
|||
properties.setType(entity->getType());
|
||||
bool hasTerseUpdateChanges = properties.hasTerseUpdateChanges();
|
||||
bool hasPhysicsChanges = properties.hasMiscPhysicsChanges() || hasTerseUpdateChanges;
|
||||
if (hasPhysicsChanges) {
|
||||
if (_bidOnSimulationOwnership && hasPhysicsChanges) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
const QUuid myNodeID = nodeList->getSessionUUID();
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra
|
|||
class EntityScriptingInterface : public OctreeScriptingInterface, public Dependency {
|
||||
Q_OBJECT
|
||||
public:
|
||||
EntityScriptingInterface();
|
||||
EntityScriptingInterface(bool bidOnSimulationOwnership);
|
||||
|
||||
EntityEditPacketSender* getEntityPacketSender() const { return (EntityEditPacketSender*)getPacketSender(); }
|
||||
virtual NodeType_t getServerNodeType() const { return NodeType::EntityServer; }
|
||||
|
@ -204,7 +204,8 @@ private:
|
|||
bool precisionPicking, const QVector<EntityItemID>& entityIdsToInclude, const QVector<EntityItemID>& entityIdsToDiscard);
|
||||
|
||||
EntityTreePointer _entityTree;
|
||||
EntitiesScriptEngineProvider* _entitiesScriptEngine = nullptr;
|
||||
EntitiesScriptEngineProvider* _entitiesScriptEngine { nullptr };
|
||||
bool _bidOnSimulationOwnership { false };
|
||||
};
|
||||
|
||||
#endif // hifi_EntityScriptingInterface_h
|
||||
|
|
|
@ -194,7 +194,7 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
|
|||
// the entire update is suspect --> ignore it
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
} else if (simulationBlocked) {
|
||||
simulationBlocked = senderID != entity->getSimulatorID();
|
||||
}
|
||||
if (simulationBlocked) {
|
||||
|
|
|
@ -15,11 +15,18 @@
|
|||
|
||||
#include "PhysicsCollisionGroups.h"
|
||||
#include "ObjectMotionState.h"
|
||||
#include "PhysicsLogging.h"
|
||||
|
||||
const btVector3 LOCAL_UP_AXIS(0.0f, 1.0f, 0.0f);
|
||||
const float JUMP_SPEED = 3.5f;
|
||||
const float MAX_FALL_HEIGHT = 20.0f;
|
||||
|
||||
#ifdef DEBUG_STATE_CHANGE
|
||||
#define SET_STATE(desiredState, reason) setState(desiredState, reason)
|
||||
#else
|
||||
#define SET_STATE(desiredState, reason) setState(desiredState)
|
||||
#endif
|
||||
|
||||
// helper class for simple ray-traces from character
|
||||
class ClosestNotMe : public btCollisionWorld::ClosestRayResultCallback {
|
||||
public:
|
||||
|
@ -51,19 +58,17 @@ CharacterController::CharacterController() {
|
|||
_followDesiredBodyTransform.setIdentity();
|
||||
_followTimeRemaining = 0.0f;
|
||||
_jumpSpeed = JUMP_SPEED;
|
||||
_isOnGround = false;
|
||||
_isJumping = false;
|
||||
_isFalling = false;
|
||||
_isHovering = true;
|
||||
_state = State::Hover;
|
||||
_isPushingUp = false;
|
||||
_jumpToHoverStart = 0;
|
||||
_jumpButtonDownStart = 0;
|
||||
_jumpButtonDownCount = 0;
|
||||
_takeoffToInAirStart = 0;
|
||||
_followTime = 0.0f;
|
||||
_followLinearDisplacement = btVector3(0, 0, 0);
|
||||
_followAngularDisplacement = btQuaternion::getIdentity();
|
||||
_hasSupport = false;
|
||||
|
||||
_pendingFlags = PENDING_FLAG_UPDATE_SHAPE;
|
||||
|
||||
}
|
||||
|
||||
bool CharacterController::needsRemoval() const {
|
||||
|
@ -107,6 +112,8 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
|
|||
}
|
||||
}
|
||||
|
||||
static const float COS_PI_OVER_THREE = cosf(PI / 3.0f);
|
||||
|
||||
bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) const {
|
||||
int numManifolds = collisionWorld->getDispatcher()->getNumManifolds();
|
||||
for (int i = 0; i < numManifolds; i++) {
|
||||
|
@ -119,8 +126,10 @@ bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) cons
|
|||
btManifoldPoint& pt = contactManifold->getContactPoint(j);
|
||||
|
||||
// check to see if contact point is touching the bottom sphere of the capsule.
|
||||
// and the contact normal is not slanted too much.
|
||||
float contactPointY = (obA == _rigidBody) ? pt.m_localPointA.getY() : pt.m_localPointB.getY();
|
||||
if (contactPointY < -_halfHeight) {
|
||||
btVector3 normal = (obA == _rigidBody) ? pt.m_normalWorldOnB : -pt.m_normalWorldOnB;
|
||||
if (contactPointY < -_halfHeight && normal.dot(_currentUp) > COS_PI_OVER_THREE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -153,72 +162,61 @@ void CharacterController::preStep(btCollisionWorld* collisionWorld) {
|
|||
}
|
||||
|
||||
void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) {
|
||||
btVector3 actualVelocity = _rigidBody->getLinearVelocity();
|
||||
btScalar actualSpeed = actualVelocity.length();
|
||||
|
||||
btVector3 desiredVelocity = _walkVelocity;
|
||||
btScalar desiredSpeed = desiredVelocity.length();
|
||||
|
||||
const btScalar MIN_UP_PUSH = 0.1f;
|
||||
if (desiredVelocity.dot(_currentUp) < MIN_UP_PUSH) {
|
||||
_isPushingUp = false;
|
||||
}
|
||||
|
||||
const btScalar MIN_SPEED = 0.001f;
|
||||
if (_isHovering) {
|
||||
if (desiredSpeed < MIN_SPEED) {
|
||||
if (actualSpeed < MIN_SPEED) {
|
||||
_rigidBody->setLinearVelocity(btVector3(0.0f, 0.0f, 0.0f));
|
||||
} else {
|
||||
const btScalar HOVER_BRAKING_TIMESCALE = 0.1f;
|
||||
btScalar tau = glm::max(dt / HOVER_BRAKING_TIMESCALE, 1.0f);
|
||||
_rigidBody->setLinearVelocity((1.0f - tau) * actualVelocity);
|
||||
}
|
||||
} else {
|
||||
const btScalar HOVER_ACCELERATION_TIMESCALE = 0.1f;
|
||||
btScalar tau = dt / HOVER_ACCELERATION_TIMESCALE;
|
||||
_rigidBody->setLinearVelocity(actualVelocity - tau * (actualVelocity - desiredVelocity));
|
||||
|
||||
btVector3 actualVelocity = _rigidBody->getLinearVelocity();
|
||||
if (actualVelocity.length() < MIN_SPEED) {
|
||||
actualVelocity = btVector3(0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
btVector3 desiredVelocity = _walkVelocity;
|
||||
if (desiredVelocity.length() < MIN_SPEED) {
|
||||
desiredVelocity = btVector3(0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// decompose into horizontal and vertical components.
|
||||
btVector3 actualVertVelocity = actualVelocity.dot(_currentUp) * _currentUp;
|
||||
btVector3 actualHorizVelocity = actualVelocity - actualVertVelocity;
|
||||
btVector3 desiredVertVelocity = desiredVelocity.dot(_currentUp) * _currentUp;
|
||||
btVector3 desiredHorizVelocity = desiredVelocity - desiredVertVelocity;
|
||||
|
||||
btVector3 finalVelocity;
|
||||
|
||||
switch (_state) {
|
||||
case State::Ground:
|
||||
case State::Takeoff:
|
||||
{
|
||||
// horizontal ground control
|
||||
const btScalar WALK_ACCELERATION_TIMESCALE = 0.1f;
|
||||
btScalar tau = dt / WALK_ACCELERATION_TIMESCALE;
|
||||
finalVelocity = tau * desiredHorizVelocity + (1.0f - tau) * actualHorizVelocity + actualVertVelocity;
|
||||
}
|
||||
} else {
|
||||
if (onGround()) {
|
||||
// walking on ground
|
||||
if (desiredSpeed < MIN_SPEED) {
|
||||
if (actualSpeed < MIN_SPEED) {
|
||||
_rigidBody->setLinearVelocity(btVector3(0.0f, 0.0f, 0.0f));
|
||||
} else {
|
||||
const btScalar HOVER_BRAKING_TIMESCALE = 0.1f;
|
||||
btScalar tau = dt / HOVER_BRAKING_TIMESCALE;
|
||||
_rigidBody->setLinearVelocity((1.0f - tau) * actualVelocity);
|
||||
}
|
||||
} else {
|
||||
// TODO: modify desiredVelocity using floor normal
|
||||
const btScalar WALK_ACCELERATION_TIMESCALE = 0.1f;
|
||||
btScalar tau = dt / WALK_ACCELERATION_TIMESCALE;
|
||||
btVector3 velocityCorrection = tau * (desiredVelocity - actualVelocity);
|
||||
// subtract vertical component
|
||||
velocityCorrection -= velocityCorrection.dot(_currentUp) * _currentUp;
|
||||
_rigidBody->setLinearVelocity(actualVelocity + velocityCorrection);
|
||||
}
|
||||
} else {
|
||||
// transitioning to flying
|
||||
btVector3 velocityCorrection = desiredVelocity - actualVelocity;
|
||||
break;
|
||||
case State::InAir:
|
||||
{
|
||||
// horizontal air control
|
||||
const btScalar IN_AIR_ACCELERATION_TIMESCALE = 2.0f;
|
||||
btScalar tau = dt / IN_AIR_ACCELERATION_TIMESCALE;
|
||||
finalVelocity = tau * desiredHorizVelocity + (1.0f - tau) * actualHorizVelocity + actualVertVelocity;
|
||||
}
|
||||
break;
|
||||
case State::Hover:
|
||||
{
|
||||
// vertical and horizontal air control
|
||||
const btScalar FLY_ACCELERATION_TIMESCALE = 0.2f;
|
||||
btScalar tau = dt / FLY_ACCELERATION_TIMESCALE;
|
||||
if (!_isPushingUp) {
|
||||
// actually falling --> compute a different velocity attenuation factor
|
||||
const btScalar FALL_ACCELERATION_TIMESCALE = 2.0f;
|
||||
tau = dt / FALL_ACCELERATION_TIMESCALE;
|
||||
// zero vertical component
|
||||
velocityCorrection -= velocityCorrection.dot(_currentUp) * _currentUp;
|
||||
}
|
||||
_rigidBody->setLinearVelocity(actualVelocity + tau * velocityCorrection);
|
||||
finalVelocity = tau * desiredVelocity + (1.0f - tau) * actualVelocity;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
_rigidBody->setLinearVelocity(finalVelocity);
|
||||
|
||||
// Dynamicaly compute a follow velocity to move this body toward the _followDesiredBodyTransform.
|
||||
// Rather then add this velocity to velocity the RigidBody, we explicitly teleport the RigidBody towards its goal.
|
||||
// This mirrors the computation done in MyAvatar::FollowHelper::postPhysicsUpdate().
|
||||
// These two computations must be kept in sync.
|
||||
|
||||
const float MINIMUM_TIME_REMAINING = 0.005f;
|
||||
const float MAX_DISPLACEMENT = 0.5f * _radius;
|
||||
_followTimeRemaining -= dt;
|
||||
|
@ -254,21 +252,7 @@ void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) {
|
|||
}
|
||||
|
||||
void CharacterController::jump() {
|
||||
// check for case where user is holding down "jump" key...
|
||||
// we'll eventually tansition to "hover"
|
||||
if (!_isJumping) {
|
||||
if (!_isHovering) {
|
||||
_jumpToHoverStart = usecTimestampNow();
|
||||
_pendingFlags |= PENDING_FLAG_JUMP;
|
||||
}
|
||||
} else {
|
||||
quint64 now = usecTimestampNow();
|
||||
const quint64 JUMP_TO_HOVER_PERIOD = 75 * (USECS_PER_SECOND / 100);
|
||||
if (now - _jumpToHoverStart > JUMP_TO_HOVER_PERIOD) {
|
||||
_isPushingUp = true;
|
||||
setHovering(true);
|
||||
}
|
||||
}
|
||||
_pendingFlags |= PENDING_FLAG_JUMP;
|
||||
}
|
||||
|
||||
bool CharacterController::onGround() const {
|
||||
|
@ -276,18 +260,44 @@ bool CharacterController::onGround() const {
|
|||
return _floorDistance < FLOOR_PROXIMITY_THRESHOLD || _hasSupport;
|
||||
}
|
||||
|
||||
void CharacterController::setHovering(bool hover) {
|
||||
if (hover != _isHovering) {
|
||||
_isHovering = hover;
|
||||
_isJumping = false;
|
||||
#ifdef DEBUG_STATE_CHANGE
|
||||
static const char* stateToStr(CharacterController::State state) {
|
||||
switch (state) {
|
||||
case CharacterController::State::Ground:
|
||||
return "Ground";
|
||||
case CharacterController::State::Takeoff:
|
||||
return "Takeoff";
|
||||
case CharacterController::State::InAir:
|
||||
return "InAir";
|
||||
case CharacterController::State::Hover:
|
||||
return "Hover";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
#endif // #ifdef DEBUG_STATE_CHANGE
|
||||
|
||||
if (_rigidBody) {
|
||||
if (hover) {
|
||||
#ifdef DEBUG_STATE_CHANGE
|
||||
void CharacterController::setState(State desiredState, const char* reason) {
|
||||
#else
|
||||
void CharacterController::setState(State desiredState) {
|
||||
#endif
|
||||
if (desiredState != _state) {
|
||||
#ifdef DEBUG_STATE_CHANGE
|
||||
qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason;
|
||||
#endif
|
||||
if (desiredState == State::Hover && _state != State::Hover) {
|
||||
// hover enter
|
||||
if (_rigidBody) {
|
||||
_rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f));
|
||||
} else {
|
||||
}
|
||||
} else if (_state == State::Hover && desiredState != State::Hover) {
|
||||
// hover exit
|
||||
if (_rigidBody) {
|
||||
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp);
|
||||
}
|
||||
}
|
||||
_state = desiredState;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -335,9 +345,8 @@ void CharacterController::setEnabled(bool enabled) {
|
|||
_pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION;
|
||||
}
|
||||
_pendingFlags &= ~ PENDING_FLAG_ADD_TO_SIMULATION;
|
||||
_isOnGround = false;
|
||||
}
|
||||
setHovering(true);
|
||||
SET_STATE(State::Hover, "setEnabled");
|
||||
_enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
@ -345,7 +354,7 @@ void CharacterController::setEnabled(bool enabled) {
|
|||
void CharacterController::updateUpAxis(const glm::quat& rotation) {
|
||||
btVector3 oldUp = _currentUp;
|
||||
_currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS);
|
||||
if (!_isHovering) {
|
||||
if (_state != State::Hover) {
|
||||
const btScalar MIN_UP_ERROR = 0.01f;
|
||||
if (oldUp.distance(_currentUp) > MIN_UP_ERROR) {
|
||||
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp);
|
||||
|
@ -410,6 +419,10 @@ void CharacterController::preSimulation() {
|
|||
if (_enabled && _dynamicsWorld) {
|
||||
// slam body to where it is supposed to be
|
||||
_rigidBody->setWorldTransform(_characterBodyTransform);
|
||||
btVector3 velocity = _rigidBody->getLinearVelocity();
|
||||
|
||||
btVector3 actualVertVelocity = velocity.dot(_currentUp) * _currentUp;
|
||||
btVector3 actualHorizVelocity = velocity - actualVertVelocity;
|
||||
|
||||
// scan for distant floor
|
||||
// rayStart is at center of bottom sphere
|
||||
|
@ -424,32 +437,72 @@ void CharacterController::preSimulation() {
|
|||
_dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback);
|
||||
if (rayCallback.hasHit()) {
|
||||
_floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius;
|
||||
const btScalar MIN_HOVER_HEIGHT = 3.0f;
|
||||
if (_isHovering && _floorDistance < MIN_HOVER_HEIGHT && !_isPushingUp) {
|
||||
setHovering(false);
|
||||
}
|
||||
// TODO: use collision events rather than ray-trace test to disable jumping
|
||||
const btScalar JUMP_PROXIMITY_THRESHOLD = 0.1f * _radius;
|
||||
if (_floorDistance < JUMP_PROXIMITY_THRESHOLD) {
|
||||
_isJumping = false;
|
||||
}
|
||||
} else if (!_hasSupport) {
|
||||
} else {
|
||||
_floorDistance = FLT_MAX;
|
||||
setHovering(true);
|
||||
}
|
||||
|
||||
if (_pendingFlags & PENDING_FLAG_JUMP) {
|
||||
_pendingFlags &= ~ PENDING_FLAG_JUMP;
|
||||
if (onGround()) {
|
||||
_isJumping = true;
|
||||
btVector3 velocity = _rigidBody->getLinearVelocity();
|
||||
const btScalar JUMP_PROXIMITY_THRESHOLD = 0.1f * _radius;
|
||||
const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 200 * MSECS_PER_SECOND;
|
||||
const btScalar MIN_HOVER_HEIGHT = 2.5f;
|
||||
const quint64 JUMP_TO_HOVER_PERIOD = 750 * MSECS_PER_SECOND;
|
||||
const btScalar MAX_WALKING_SPEED = 2.5f;
|
||||
|
||||
quint64 now = usecTimestampNow();
|
||||
|
||||
// record a time stamp when the jump button was first pressed.
|
||||
if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) {
|
||||
if (_pendingFlags & PENDING_FLAG_JUMP) {
|
||||
_jumpButtonDownStart = now;
|
||||
_jumpButtonDownCount++;
|
||||
}
|
||||
}
|
||||
|
||||
bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP;
|
||||
bool flyingFast = _state == State::Hover && actualHorizVelocity.length() > (MAX_WALKING_SPEED * 0.75f);
|
||||
|
||||
switch (_state) {
|
||||
case State::Ground:
|
||||
if (!rayCallback.hasHit() && !_hasSupport) {
|
||||
SET_STATE(State::Hover, "no ground");
|
||||
} else if (_pendingFlags & PENDING_FLAG_JUMP) {
|
||||
_takeOffJumpButtonID = _jumpButtonDownCount;
|
||||
SET_STATE(State::Takeoff, "jump pressed");
|
||||
}
|
||||
break;
|
||||
case State::Takeoff:
|
||||
if (!rayCallback.hasHit() && !_hasSupport) {
|
||||
SET_STATE(State::Hover, "no ground");
|
||||
} else if ((now - _takeoffToInAirStart) > TAKE_OFF_TO_IN_AIR_PERIOD) {
|
||||
SET_STATE(State::InAir, "takeoff done");
|
||||
_takeoffToInAirStart = now + USECS_PER_SECOND * 86500.0f;
|
||||
velocity += _jumpSpeed * _currentUp;
|
||||
_rigidBody->setLinearVelocity(velocity);
|
||||
}
|
||||
break;
|
||||
case State::InAir: {
|
||||
if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < JUMP_PROXIMITY_THRESHOLD) || _hasSupport)) {
|
||||
SET_STATE(State::Ground, "hit ground");
|
||||
} else if (jumpButtonHeld && (_takeOffJumpButtonID != _jumpButtonDownCount)) {
|
||||
SET_STATE(State::Hover, "double jump button");
|
||||
} else if (jumpButtonHeld && (now - _jumpButtonDownStart) > JUMP_TO_HOVER_PERIOD) {
|
||||
SET_STATE(State::Hover, "jump button held");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case State::Hover:
|
||||
if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) {
|
||||
SET_STATE(State::InAir, "near ground");
|
||||
} else if (((_floorDistance < JUMP_PROXIMITY_THRESHOLD) || _hasSupport) && !flyingFast) {
|
||||
SET_STATE(State::Ground, "touching ground");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
_followTime = 0.0f;
|
||||
|
||||
_previousFlags = _pendingFlags;
|
||||
_pendingFlags &= ~PENDING_FLAG_JUMP;
|
||||
|
||||
_followTime = 0.0f;
|
||||
_followLinearDisplacement = btVector3(0, 0, 0);
|
||||
_followAngularDisplacement = btQuaternion::getIdentity();
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ class btRigidBody;
|
|||
class btCollisionWorld;
|
||||
class btDynamicsWorld;
|
||||
|
||||
//#define DEBUG_STATE_CHANGE
|
||||
|
||||
class CharacterController : public btCharacterControllerInterface {
|
||||
public:
|
||||
CharacterController();
|
||||
|
@ -75,8 +77,14 @@ public:
|
|||
|
||||
glm::vec3 getLinearVelocity() const;
|
||||
|
||||
bool isHovering() const { return _isHovering; }
|
||||
void setHovering(bool enabled);
|
||||
enum class State {
|
||||
Ground = 0,
|
||||
Takeoff,
|
||||
InAir,
|
||||
Hover
|
||||
};
|
||||
|
||||
State getState() const { return _state; }
|
||||
|
||||
void setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale);
|
||||
|
||||
|
@ -86,6 +94,12 @@ public:
|
|||
bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation);
|
||||
|
||||
protected:
|
||||
#ifdef DEBUG_STATE_CHANGE
|
||||
void setState(State state, const char* reason);
|
||||
#else
|
||||
void setState(State state);
|
||||
#endif
|
||||
|
||||
void updateUpAxis(const glm::quat& rotation);
|
||||
bool checkForSupport(btCollisionWorld* collisionWorld) const;
|
||||
|
||||
|
@ -100,7 +114,10 @@ protected:
|
|||
|
||||
glm::vec3 _boxScale; // used to compute capsule shape
|
||||
|
||||
quint64 _jumpToHoverStart;
|
||||
quint64 _takeoffToInAirStart;
|
||||
quint64 _jumpButtonDownStart;
|
||||
quint32 _jumpButtonDownCount;
|
||||
quint32 _takeOffJumpButtonID;
|
||||
|
||||
btScalar _halfHeight;
|
||||
btScalar _radius;
|
||||
|
@ -116,16 +133,13 @@ protected:
|
|||
btQuaternion _followAngularDisplacement;
|
||||
|
||||
bool _enabled;
|
||||
bool _isOnGround;
|
||||
bool _isJumping;
|
||||
bool _isFalling;
|
||||
bool _isHovering;
|
||||
State _state;
|
||||
bool _isPushingUp;
|
||||
|
||||
btDynamicsWorld* _dynamicsWorld { nullptr };
|
||||
btRigidBody* _rigidBody { nullptr };
|
||||
uint32_t _pendingFlags { 0 };
|
||||
|
||||
uint32_t _previousFlags { 0 };
|
||||
};
|
||||
|
||||
#endif // hifi_CharacterControllerInterface_h
|
||||
|
|
|
@ -29,13 +29,13 @@ public:
|
|||
virtual ~EntityMotionState();
|
||||
|
||||
void updateServerPhysicsVariables();
|
||||
virtual bool handleEasyChanges(uint32_t& flags);
|
||||
virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine);
|
||||
virtual bool handleEasyChanges(uint32_t& flags) override;
|
||||
virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override;
|
||||
|
||||
/// \return PhysicsMotionType based on params set in EntityItem
|
||||
virtual PhysicsMotionType computePhysicsMotionType() const;
|
||||
virtual PhysicsMotionType computePhysicsMotionType() const override;
|
||||
|
||||
virtual bool isMoving() const;
|
||||
virtual bool isMoving() const override;
|
||||
|
||||
// this relays incoming position/rotation to the RigidBody
|
||||
virtual void getWorldTransform(btTransform& worldTrans) const override;
|
||||
|
@ -48,8 +48,8 @@ public:
|
|||
bool shouldSendUpdate(uint32_t simulationStep, const QUuid& sessionID);
|
||||
void sendUpdate(OctreeEditPacketSender* packetSender, const QUuid& sessionID, uint32_t step);
|
||||
|
||||
virtual uint32_t getIncomingDirtyFlags();
|
||||
virtual void clearIncomingDirtyFlags();
|
||||
virtual uint32_t getIncomingDirtyFlags() override;
|
||||
virtual void clearIncomingDirtyFlags() override;
|
||||
|
||||
void incrementAccelerationNearlyGravityCount() { _accelerationNearlyGravityCount++; }
|
||||
void resetAccelerationNearlyGravityCount() { _accelerationNearlyGravityCount = 0; }
|
||||
|
@ -80,7 +80,7 @@ public:
|
|||
|
||||
virtual QString getName() const override;
|
||||
|
||||
virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const;
|
||||
virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override;
|
||||
|
||||
// eternal logic can suggest a simuator priority bid for the next outgoing update
|
||||
void setOutgoingPriority(quint8 priority);
|
||||
|
@ -93,7 +93,7 @@ protected:
|
|||
#endif
|
||||
|
||||
virtual bool isReadyToComputeShape() const override;
|
||||
virtual btCollisionShape* computeNewShape();
|
||||
virtual btCollisionShape* computeNewShape() override;
|
||||
virtual void setMotionType(PhysicsMotionType motionType);
|
||||
|
||||
// In the glorious future (when entities lib depends on physics lib) the EntityMotionState will be
|
||||
|
|
|
@ -45,6 +45,20 @@ private:
|
|||
bool _navigationFocused { false };
|
||||
};
|
||||
|
||||
QString fixupHifiUrl(const QString& urlString) {
|
||||
static const QString ACCESS_TOKEN_PARAMETER = "access_token";
|
||||
static const QString ALLOWED_HOST = "metaverse.highfidelity.com";
|
||||
QUrl url(urlString);
|
||||
QUrlQuery query(url);
|
||||
if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) {
|
||||
AccountManager& accountManager = AccountManager::getInstance();
|
||||
query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token);
|
||||
url.setQuery(query.query());
|
||||
return url.toString();
|
||||
}
|
||||
return urlString;
|
||||
}
|
||||
|
||||
class UrlHandler : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
@ -60,20 +74,7 @@ public:
|
|||
|
||||
// FIXME hack for authentication, remove when we migrate to Qt 5.6
|
||||
Q_INVOKABLE QString fixupUrl(const QString& originalUrl) {
|
||||
static const QString ACCESS_TOKEN_PARAMETER = "access_token";
|
||||
static const QString ALLOWED_HOST = "metaverse.highfidelity.com";
|
||||
QString result = originalUrl;
|
||||
QUrl url(originalUrl);
|
||||
QUrlQuery query(url);
|
||||
if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) {
|
||||
qDebug() << "Updating URL with auth token";
|
||||
AccountManager& accountManager = AccountManager::getInstance();
|
||||
query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token);
|
||||
url.setQuery(query.query());
|
||||
result = url.toString();
|
||||
}
|
||||
|
||||
return result;
|
||||
return fixupHifiUrl(originalUrl);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -49,8 +49,11 @@ QString QmlWebWindowClass::getURL() const {
|
|||
return result.toString();
|
||||
}
|
||||
|
||||
// HACK find a good place to declare and store this
|
||||
extern QString fixupHifiUrl(const QString& urlString);
|
||||
|
||||
void QmlWebWindowClass::setURL(const QString& urlString) {
|
||||
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
|
||||
_qmlWindow->setProperty(URL_PROPERTY, urlString);
|
||||
_qmlWindow->setProperty(URL_PROPERTY, fixupHifiUrl(urlString));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(function() {
|
||||
var self = this;
|
||||
var baseURL = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/";
|
||||
var version = 8;
|
||||
var version = 9;
|
||||
this.preload = function(entityId) {
|
||||
self.soundPlaying = false;
|
||||
self.entityId = entityId;
|
||||
|
@ -20,7 +20,7 @@
|
|||
stereo: true,
|
||||
loop: true,
|
||||
localOnly: true,
|
||||
volume: 0.5
|
||||
volume: 0.035
|
||||
};
|
||||
|
||||
this.sound = SoundCache.getSound(self.soundURL);
|
||||
|
@ -36,7 +36,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
this.enterEntity = function(entityID) {
|
||||
print("entering audio zone");
|
||||
if (self.sound.downloaded) {
|
||||
|
@ -49,7 +48,6 @@
|
|||
}
|
||||
|
||||
|
||||
|
||||
this.leaveEntity = function(entityID) {
|
||||
print("leaving audio area " + self.userData.name);
|
||||
if (self.soundPlaying !== false) {
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
stereo: true,
|
||||
loop: false,
|
||||
localOnly: true,
|
||||
volume: 0.5
|
||||
volume: 0.035
|
||||
};
|
||||
this.sound = SoundCache.getSound(this.soundURL);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
stereo: true,
|
||||
loop: false,
|
||||
localOnly: true,
|
||||
volume: 0.5,
|
||||
volume: 0.035,
|
||||
position: this.position
|
||||
};
|
||||
this.sound = SoundCache.getSound(this.soundURL);
|
||||
|
|
|
@ -25,8 +25,9 @@
|
|||
loop: false,
|
||||
localOnly: false,
|
||||
position: this.position,
|
||||
volume: 0.5
|
||||
volume: 0.035
|
||||
};
|
||||
|
||||
this.teleportSound = SoundCache.getSound("https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/Audio/whoosh.wav");
|
||||
//print('Script.clearTimeout PRELOADING A ZOOM ENTITY')
|
||||
print(" portal destination is " + portalDestination);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var version = 1003;
|
||||
var version = 1004;
|
||||
var cellLayout;
|
||||
var baseLocation = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/";
|
||||
|
||||
|
|
|
@ -299,31 +299,31 @@
|
|||
},
|
||||
dynamic: true,
|
||||
userData: JSON.stringify({
|
||||
wearable: {
|
||||
joints: {
|
||||
RightHand: [{
|
||||
x: 0.07079616189002991,
|
||||
y: 0.20177987217903137,
|
||||
z: 0.06374628841876984
|
||||
}, {
|
||||
x: -0.5863648653030396,
|
||||
y: -0.46007341146469116,
|
||||
z: 0.46949487924575806,
|
||||
w: -0.4733745753765106
|
||||
}],
|
||||
LeftHand: [{
|
||||
x: 0.1802254319190979,
|
||||
y: 0.13442856073379517,
|
||||
z: 0.08504903316497803
|
||||
}, {
|
||||
x: 0.2198076844215393,
|
||||
y: -0.7377811074256897,
|
||||
z: 0.2780133783817291,
|
||||
w: 0.574519157409668
|
||||
}]
|
||||
}
|
||||
},
|
||||
grabbableKey: {
|
||||
wearable: {
|
||||
joints: {
|
||||
RightHand: [{
|
||||
x: 0.07079616189002991,
|
||||
y: 0.20177987217903137,
|
||||
z: 0.06374628841876984
|
||||
}, {
|
||||
x: -0.5863648653030396,
|
||||
y: -0.46007341146469116,
|
||||
z: 0.46949487924575806,
|
||||
w: -0.4733745753765106
|
||||
}],
|
||||
LeftHand: [{
|
||||
x: 0.1802254319190979,
|
||||
y: 0.13442856073379517,
|
||||
z: 0.08504903316497803
|
||||
}, {
|
||||
x: 0.2198076844215393,
|
||||
y: -0.7377811074256897,
|
||||
z: 0.2780133783817291,
|
||||
w: 0.574519157409668
|
||||
}]
|
||||
}
|
||||
},
|
||||
invertSolidWhileHeld: true
|
||||
},
|
||||
resetMe: {
|
||||
|
|
|
@ -283,31 +283,31 @@ MasterReset = function() {
|
|||
damping: 0.5,
|
||||
collisionSoundURL: "http://hifi-content.s3.amazonaws.com/james/pistol/sounds/drop.wav",
|
||||
userData: JSON.stringify({
|
||||
wearable: {
|
||||
joints: {
|
||||
RightHand: [{
|
||||
x: 0.07079616189002991,
|
||||
y: 0.20177987217903137,
|
||||
z: 0.06374628841876984
|
||||
}, {
|
||||
x: -0.5863648653030396,
|
||||
y: -0.46007341146469116,
|
||||
z: 0.46949487924575806,
|
||||
w: -0.4733745753765106
|
||||
}],
|
||||
LeftHand: [{
|
||||
x: 0.1802254319190979,
|
||||
y: 0.13442856073379517,
|
||||
z: 0.08504903316497803
|
||||
}, {
|
||||
x: 0.2198076844215393,
|
||||
y: -0.7377811074256897,
|
||||
z: 0.2780133783817291,
|
||||
w: 0.574519157409668
|
||||
}]
|
||||
}
|
||||
},
|
||||
grabbableKey: {
|
||||
wearable: {
|
||||
joints: {
|
||||
RightHand: [{
|
||||
x: 0.07079616189002991,
|
||||
y: 0.20177987217903137,
|
||||
z: 0.06374628841876984
|
||||
}, {
|
||||
x: -0.5863648653030396,
|
||||
y: -0.46007341146469116,
|
||||
z: 0.46949487924575806,
|
||||
w: -0.4733745753765106
|
||||
}],
|
||||
LeftHand: [{
|
||||
x: 0.1802254319190979,
|
||||
y: 0.13442856073379517,
|
||||
z: 0.08504903316497803
|
||||
}, {
|
||||
x: 0.2198076844215393,
|
||||
y: -0.7377811074256897,
|
||||
z: 0.2780133783817291,
|
||||
w: 0.574519157409668
|
||||
}]
|
||||
}
|
||||
},
|
||||
invertSolidWhileHeld: true
|
||||
},
|
||||
resetMe: {
|
||||
|
|
Loading…
Reference in a new issue