mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-09 14:33:48 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into virtualBaton
This commit is contained in:
commit
4a50d7c456
112 changed files with 2285 additions and 1990 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");
|
||||
|
|
|
@ -23,7 +23,6 @@ Script.include([
|
|||
|
||||
"libraries/ToolTip.js",
|
||||
|
||||
"libraries/entityPropertyDialogBox.js",
|
||||
"libraries/entityCameraTool.js",
|
||||
"libraries/gridTool.js",
|
||||
"libraries/entityList.js",
|
||||
|
@ -32,7 +31,6 @@ Script.include([
|
|||
|
||||
var selectionDisplay = SelectionDisplay;
|
||||
var selectionManager = SelectionManager;
|
||||
var entityPropertyDialogBox = EntityPropertyDialogBox;
|
||||
|
||||
var lightOverlayManager = new LightOverlayManager();
|
||||
|
||||
|
|
63
examples/example/ui/MyEnergyBar.js
Normal file
63
examples/example/ui/MyEnergyBar.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
Script.include("../../libraries/utils.js");
|
||||
var energyColor = {red: 0, green: 200, blue: 0};
|
||||
var lowEnergyColor = {red: 255, green: 0, blue: 0};
|
||||
var totalWidth = 200;
|
||||
var paddingRight = 50;
|
||||
var xPosition = Window.innerWidth - totalWidth - paddingRight;
|
||||
var lowEnergyThreshold = 0.3;
|
||||
var currentEnergy = 1.0;
|
||||
var energyLossRate = 0.003;
|
||||
var energyChargeRate = 0.003;
|
||||
var isGrabbing = false;
|
||||
var refractoryPeriod = 2000;
|
||||
|
||||
var lastAvatarVelocity = MyAvatar.getVelocity();
|
||||
var lastAvatarPosition = MyAvatar.position;
|
||||
|
||||
var background = Overlays.addOverlay("text", {
|
||||
x: xPosition,
|
||||
y: 20,
|
||||
width: totalWidth,
|
||||
height: 10,
|
||||
backgroundColor: {red: 255, green: 0, blue: 0}
|
||||
})
|
||||
|
||||
var bar = Overlays.addOverlay("text", {
|
||||
x: xPosition,
|
||||
y: 20,
|
||||
width: totalWidth,
|
||||
height: 10,
|
||||
backgroundColor: energyColor
|
||||
});
|
||||
|
||||
|
||||
// Takes an energy value between 0 and 1 and sets energy bar width appropriately
|
||||
function setEnergy(energy) {
|
||||
energy = clamp(energy, 0, 1);
|
||||
var barWidth = totalWidth * energy;
|
||||
var color = energy <= lowEnergyThreshold ? lowEnergyColor: energyColor;
|
||||
Overlays.editOverlay(bar, { width: barWidth, backgroundColor: color});
|
||||
}
|
||||
|
||||
function update() {
|
||||
currentEnergy = clamp(MyAvatar.energy, 0, 1);
|
||||
setEnergy(currentEnergy);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
Overlays.deleteOverlay(background);
|
||||
Overlays.deleteOverlay(bar);
|
||||
}
|
||||
|
||||
function energyChanged(newValue) {
|
||||
Entities.currentAvatarEnergy = newValue;
|
||||
}
|
||||
|
||||
function debitAvatarEnergy(value) {
|
||||
MyAvatar.energy = MyAvatar.energy - value;
|
||||
}
|
||||
Entities.costMultiplier = 0.02;
|
||||
Entities.debitEnergySource.connect(debitAvatarEnergy);
|
||||
MyAvatar.energyChanged.connect(energyChanged);
|
||||
Script.update.connect(update);
|
||||
Script.scriptEnding.connect(cleanup);
|
|
@ -51,45 +51,8 @@ function setEnergy(energy) {
|
|||
Overlays.editOverlay(bar, { width: barWidth, backgroundColor: color});
|
||||
}
|
||||
|
||||
function avatarAccelerationEnergy() {
|
||||
var AVATAR_MOVEMENT_ENERGY_CONSTANT = 0.001;
|
||||
var velocity = MyAvatar.getVelocity();
|
||||
var dV = Math.abs(Vec3.length(velocity) - Vec3.length(lastAvatarVelocity));
|
||||
var dE = Vec3.length(lastAvatarVelocity) * dV * AVATAR_MOVEMENT_ENERGY_CONSTANT;
|
||||
lastAvatarVelocity = velocity;
|
||||
return dE;
|
||||
}
|
||||
|
||||
function teleported() {
|
||||
var MAX_AVATAR_MOVEMENT_PER_FRAME = 30.0;
|
||||
var position = MyAvatar.position;
|
||||
var dP = Vec3.length(Vec3.subtract(position, lastAvatarPosition));
|
||||
lastAvatarPosition = position;
|
||||
return (dP > MAX_AVATAR_MOVEMENT_PER_FRAME);
|
||||
}
|
||||
|
||||
function audioEnergy() {
|
||||
var AUDIO_ENERGY_CONSTANT = 0.000001;
|
||||
return MyAvatar.audioLoudness * AUDIO_ENERGY_CONSTANT;
|
||||
}
|
||||
|
||||
function update() {
|
||||
// refill energy
|
||||
currentEnergy += energyChargeRate;
|
||||
|
||||
// Avatar acceleration
|
||||
currentEnergy -= avatarAccelerationEnergy();
|
||||
|
||||
// Teleport cost
|
||||
if (teleported()) {
|
||||
currentEnergy = 0;
|
||||
}
|
||||
|
||||
// Making sounds
|
||||
currentEnergy -= audioEnergy();
|
||||
|
||||
|
||||
currentEnergy = clamp(currentEnergy, 0, 1);
|
||||
currentEnergy = clamp(MyAvatar.energy, 0, 1);
|
||||
setEnergy(currentEnergy);
|
||||
}
|
||||
|
||||
|
@ -98,5 +61,10 @@ function cleanup() {
|
|||
Overlays.deleteOverlay(bar);
|
||||
}
|
||||
|
||||
function energyChanged(newValue) {
|
||||
Entities.currentAvatarEnergy = newValue;
|
||||
}
|
||||
|
||||
MyAvatar.energyChanged.connect(energyChanged);
|
||||
Script.update.connect(update);
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
|
|
@ -1,457 +0,0 @@
|
|||
//
|
||||
// entityPropertyDialogBox.js
|
||||
// examples
|
||||
//
|
||||
// Created by Brad hefta-Gaub on 10/1/14.
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
//
|
||||
// This script implements a class useful for building tools for editing entities.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var DEGREES_TO_RADIANS = Math.PI / 180.0;
|
||||
var RADIANS_TO_DEGREES = 180.0 / Math.PI;
|
||||
|
||||
EntityPropertyDialogBox = (function () {
|
||||
var that = {};
|
||||
|
||||
var propertiesForEditedEntity;
|
||||
var editEntityFormArray;
|
||||
var decimals = 3;
|
||||
var dimensionX;
|
||||
var dimensionY;
|
||||
var dimensionZ;
|
||||
var rescalePercentage;
|
||||
var editModelID = -1;
|
||||
var previousAnimationIsPlaying;
|
||||
var previousAnimationCurrentFrame;
|
||||
|
||||
that.cleanup = function () {
|
||||
};
|
||||
|
||||
that.openDialog = function (entityID) {
|
||||
print(" Edit Properties.... about to edit properties...");
|
||||
|
||||
editModelID = entityID;
|
||||
propertiesForEditedEntity = Entities.getEntityProperties(editModelID);
|
||||
var properties = propertiesForEditedEntity;
|
||||
|
||||
var array = new Array();
|
||||
var index = 0;
|
||||
|
||||
array.push({ label: "Entity Type:" + properties.type, type: "header" });
|
||||
index++;
|
||||
array.push({ label: "ID:", value: properties.id });
|
||||
index++;
|
||||
array.push({ label: "Locked:", type: "checkbox", value: properties.locked });
|
||||
index++;
|
||||
|
||||
if (properties.type == "Model") {
|
||||
array.push({ label: "Model URL:", value: properties.modelURL });
|
||||
index++;
|
||||
array.push({ label: "Shape Type:", value: properties.shapeType });
|
||||
index++;
|
||||
array.push({ label: "Compound Shape URL:", value: properties.compoundShapeURL });
|
||||
index++;
|
||||
array.push({ label: "Animation URL:", value: properties.animation.url });
|
||||
index++;
|
||||
array.push({ label: "Animation is playing:", type: "checkbox", value: properties.animation.running });
|
||||
previousAnimationIsPlaying = properties.animation.running;
|
||||
index++;
|
||||
array.push({ label: "Animation FPS:", value: properties.animation.fps });
|
||||
index++;
|
||||
array.push({ label: "Animation Frame:", value: properties.animation.currentFrame });
|
||||
previousAnimationCurrentFrame = properties.animation.currentFrame;
|
||||
index++;
|
||||
array.push({ label: "Textures:", value: properties.textures });
|
||||
index++;
|
||||
array.push({ label: "Original Textures:\n" + properties.originalTextures, type: "header" });
|
||||
index++;
|
||||
}
|
||||
|
||||
if (properties.type == "Text") {
|
||||
array.push({ label: "Text:", value: properties.text });
|
||||
index++;
|
||||
array.push({ label: "Line Height:", value: properties.lineHeight });
|
||||
index++;
|
||||
array.push({ label: "Text Color:", type: "header" });
|
||||
index++;
|
||||
array.push({ label: "Red:", value: properties.textColor.red });
|
||||
index++;
|
||||
array.push({ label: "Green:", value: properties.textColor.green });
|
||||
index++;
|
||||
array.push({ label: "Blue:", value: properties.textColor.blue });
|
||||
index++;
|
||||
array.push({ label: "Background Color:", type: "header" });
|
||||
index++;
|
||||
array.push({ label: "Red:", value: properties.backgroundColor.red });
|
||||
index++;
|
||||
array.push({ label: "Green:", value: properties.backgroundColor.green });
|
||||
index++;
|
||||
array.push({ label: "Blue:", value: properties.backgroundColor.blue });
|
||||
index++;
|
||||
}
|
||||
|
||||
if (properties.type == "PolyVox") {
|
||||
array.push({ label: "Voxel Space Size:", type: "header" });
|
||||
index++;
|
||||
|
||||
array.push({ label: "X:", value: properties.voxelVolumeSize.x.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Y:", value: properties.voxelVolumeSize.y.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Z:", value: properties.voxelVolumeSize.z.toFixed(decimals) });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Surface Extractor", value: properties.voxelSurfaceStyle });
|
||||
index++;
|
||||
|
||||
array.push({ label: "X-axis Texture URL:", value: properties.xTextureURL });
|
||||
index++;
|
||||
array.push({ label: "Y-axis Texture URL:", value: properties.yTextureURL });
|
||||
index++;
|
||||
array.push({ label: "Z-axis Texture URL:", value: properties.zTextureURL });
|
||||
index++;
|
||||
}
|
||||
|
||||
array.push({ label: "Position:", type: "header" });
|
||||
index++;
|
||||
array.push({ label: "X:", value: properties.position.x.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Y:", value: properties.position.y.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Z:", value: properties.position.z.toFixed(decimals) });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Registration X:", value: properties.registrationPoint.x.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Registration Y:", value: properties.registrationPoint.y.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Registration Z:", value: properties.registrationPoint.z.toFixed(decimals) });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Rotation:", type: "header" });
|
||||
index++;
|
||||
var angles = Quat.safeEulerAngles(properties.rotation);
|
||||
array.push({ label: "Pitch:", value: angles.x.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Yaw:", value: angles.y.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Roll:", value: angles.z.toFixed(decimals) });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Dimensions:", type: "header" });
|
||||
index++;
|
||||
array.push({ label: "Width:", value: properties.dimensions.x.toFixed(decimals) });
|
||||
dimensionX = index;
|
||||
index++;
|
||||
array.push({ label: "Height:", value: properties.dimensions.y.toFixed(decimals) });
|
||||
dimensionY = index;
|
||||
index++;
|
||||
array.push({ label: "Depth:", value: properties.dimensions.z.toFixed(decimals) });
|
||||
dimensionZ = index;
|
||||
index++;
|
||||
array.push({ label: "", type: "inlineButton", buttonLabel: "Reset to Natural Dimensions", name: "resetDimensions" });
|
||||
index++;
|
||||
array.push({ label: "Rescale Percentage:", value: 100 });
|
||||
rescalePercentage = index;
|
||||
index++;
|
||||
array.push({ label: "", type: "inlineButton", buttonLabel: "Rescale", name: "rescaleDimensions" });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Velocity:", type: "header" });
|
||||
index++;
|
||||
array.push({ label: "Linear X:", value: properties.velocity.x.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Linear Y:", value: properties.velocity.y.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Linear Z:", value: properties.velocity.z.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Linear Damping:", value: properties.damping.toFixed(decimals) });
|
||||
index++;
|
||||
// NOTE: angular velocity is in radians/sec but we display degrees/sec for users
|
||||
array.push({ label: "Angular Pitch:", value: (properties.angularVelocity.x * RADIANS_TO_DEGREES).toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Angular Yaw:", value: (properties.angularVelocity.y * RADIANS_TO_DEGREES).toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Angular Roll:", value: (properties.angularVelocity.z * RADIANS_TO_DEGREES).toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Angular Damping:", value: properties.angularDamping.toFixed(decimals) });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Gravity X:", value: properties.gravity.x.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Gravity Y:", value: properties.gravity.y.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Gravity Z:", value: properties.gravity.z.toFixed(decimals) });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Acceleration X:", value: properties.acceleration.x.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Acceleration Y:", value: properties.acceleration.y.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Acceleration Z:", value: properties.acceleration.z.toFixed(decimals) });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Collisions:", type: "header" });
|
||||
index++;
|
||||
array.push({ label: "Density:", value: properties.density.toFixed(decimals) });
|
||||
index++;
|
||||
array.push({ label: "Collisionless:", type: "checkbox", value: properties.collisionless });
|
||||
index++;
|
||||
array.push({ label: "Dynamic:", type: "checkbox", value: properties.dynamic });
|
||||
index++;
|
||||
array.push({ label: "Collision Sound URL:", value: properties.collisionSoundURL });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Lifetime:", value: properties.lifetime.toFixed(decimals) });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Visible:", type: "checkbox", value: properties.visible });
|
||||
index++;
|
||||
|
||||
array.push({ label: "Script:", value: properties.script });
|
||||
index++;
|
||||
|
||||
if (properties.type == "Box" || properties.type == "Sphere") {
|
||||
array.push({ label: "Color:", type: "header" });
|
||||
index++;
|
||||
array.push({ label: "Red:", value: properties.color.red });
|
||||
index++;
|
||||
array.push({ label: "Green:", value: properties.color.green });
|
||||
index++;
|
||||
array.push({ label: "Blue:", value: properties.color.blue });
|
||||
index++;
|
||||
}
|
||||
|
||||
if (properties.type == "Light") {
|
||||
array.push({ label: "Light Properties:", type: "header" });
|
||||
index++;
|
||||
array.push({ label: "Is Spot Light:", value: properties.isSpotlight });
|
||||
index++;
|
||||
array.push({ label: "Diffuse Red:", value: properties.diffuseColor.red });
|
||||
index++;
|
||||
array.push({ label: "Diffuse Green:", value: properties.diffuseColor.green });
|
||||
index++;
|
||||
array.push({ label: "Diffuse Blue:", value: properties.diffuseColor.blue });
|
||||
index++;
|
||||
array.push({ label: "Ambient Red:", value: properties.ambientColor.red });
|
||||
index++;
|
||||
array.push({ label: "Ambient Green:", value: properties.ambientColor.green });
|
||||
index++;
|
||||
array.push({ label: "Ambient Blue:", value: properties.ambientColor.blue });
|
||||
index++;
|
||||
array.push({ label: "Specular Red:", value: properties.specularColor.red });
|
||||
index++;
|
||||
array.push({ label: "Specular Green:", value: properties.specularColor.green });
|
||||
index++;
|
||||
array.push({ label: "Specular Blue:", value: properties.specularColor.blue });
|
||||
index++;
|
||||
array.push({ label: "Constant Attenuation:", value: properties.constantAttenuation });
|
||||
index++;
|
||||
array.push({ label: "Linear Attenuation:", value: properties.linearAttenuation });
|
||||
index++;
|
||||
array.push({ label: "Quadratic Attenuation:", value: properties.quadraticAttenuation });
|
||||
index++;
|
||||
array.push({ label: "Exponent:", value: properties.exponent });
|
||||
index++;
|
||||
array.push({ label: "Cutoff (in degrees):", value: properties.cutoff });
|
||||
index++;
|
||||
}
|
||||
|
||||
array.push({ label: "", type: "inlineButton", buttonLabel: "Preview Camera", name: "previewCamera" });
|
||||
index++;
|
||||
|
||||
array.push({ button: "Cancel" });
|
||||
index++;
|
||||
|
||||
editEntityFormArray = array;
|
||||
Window.nonBlockingForm("Edit Properties", array);
|
||||
};
|
||||
|
||||
Window.inlineButtonClicked.connect(function (name) {
|
||||
if (name == "previewCamera") {
|
||||
Camera.mode = "entity";
|
||||
Camera.cameraEntity = propertiesForEditedEntity.id;
|
||||
}
|
||||
|
||||
if (name == "resetDimensions") {
|
||||
Window.reloadNonBlockingForm([
|
||||
{ value: propertiesForEditedEntity.naturalDimensions.x.toFixed(decimals), oldIndex: dimensionX },
|
||||
{ value: propertiesForEditedEntity.naturalDimensions.y.toFixed(decimals), oldIndex: dimensionY },
|
||||
{ value: propertiesForEditedEntity.naturalDimensions.z.toFixed(decimals), oldIndex: dimensionZ }
|
||||
]);
|
||||
}
|
||||
|
||||
if (name == "rescaleDimensions") {
|
||||
var peekValues = editEntityFormArray;
|
||||
Window.peekNonBlockingFormResult(peekValues);
|
||||
var peekX = peekValues[dimensionX].value;
|
||||
var peekY = peekValues[dimensionY].value;
|
||||
var peekZ = peekValues[dimensionZ].value;
|
||||
var peekRescale = peekValues[rescalePercentage].value;
|
||||
var rescaledX = peekX * peekRescale / 100.0;
|
||||
var rescaledY = peekY * peekRescale / 100.0;
|
||||
var rescaledZ = peekZ * peekRescale / 100.0;
|
||||
|
||||
Window.reloadNonBlockingForm([
|
||||
{ value: rescaledX.toFixed(decimals), oldIndex: dimensionX },
|
||||
{ value: rescaledY.toFixed(decimals), oldIndex: dimensionY },
|
||||
{ value: rescaledZ.toFixed(decimals), oldIndex: dimensionZ },
|
||||
{ value: 100, oldIndex: rescalePercentage }
|
||||
]);
|
||||
}
|
||||
|
||||
});
|
||||
Window.nonBlockingFormClosed.connect(function() {
|
||||
array = editEntityFormArray;
|
||||
if (Window.getNonBlockingFormResult(array)) {
|
||||
var properties = propertiesForEditedEntity;
|
||||
var index = 0;
|
||||
index++; // skip type header
|
||||
index++; // skip id item
|
||||
properties.locked = array[index++].value;
|
||||
if (properties.type == "Model") {
|
||||
properties.modelURL = array[index++].value;
|
||||
properties.shapeType = array[index++].value;
|
||||
properties.compoundShapeURL = array[index++].value;
|
||||
properties.animation.url = array[index++].value;
|
||||
|
||||
var newAnimationIsPlaying = array[index++].value;
|
||||
if (previousAnimationIsPlaying != newAnimationIsPlaying) {
|
||||
properties.animation.running = newAnimationIsPlaying;
|
||||
} else {
|
||||
delete properties.animation.running;
|
||||
}
|
||||
|
||||
properties.animation.fps = array[index++].value;
|
||||
|
||||
var newAnimationCurrentFrame = array[index++].value;
|
||||
if (previousAnimationCurrentFrame != newAnimationCurrentFrame) {
|
||||
properties.animation.currentFrame = newAnimationCurrentFrame;
|
||||
} else {
|
||||
delete properties.animation.currentFrame;
|
||||
}
|
||||
|
||||
properties.textures = array[index++].value;
|
||||
index++; // skip textureNames label
|
||||
}
|
||||
|
||||
if (properties.type == "Text") {
|
||||
properties.text = array[index++].value;
|
||||
properties.lineHeight = array[index++].value;
|
||||
|
||||
index++; // skip header
|
||||
properties.textColor.red = array[index++].value;
|
||||
properties.textColor.green = array[index++].value;
|
||||
properties.textColor.blue = array[index++].value;
|
||||
|
||||
index++; // skip header
|
||||
properties.backgroundColor.red = array[index++].value;
|
||||
properties.backgroundColor.green = array[index++].value;
|
||||
properties.backgroundColor.blue = array[index++].value;
|
||||
}
|
||||
|
||||
if (properties.type == "PolyVox") {
|
||||
properties.shapeType = array[index++].value;
|
||||
|
||||
index++; // skip header
|
||||
properties.voxelVolumeSize.x = array[index++].value;
|
||||
properties.voxelVolumeSize.y = array[index++].value;
|
||||
properties.voxelVolumeSize.z = array[index++].value;
|
||||
properties.voxelSurfaceStyle = array[index++].value;
|
||||
properties.xTextureURL = array[index++].value;
|
||||
properties.yTextureURL = array[index++].value;
|
||||
properties.zTextureURL = array[index++].value;
|
||||
}
|
||||
|
||||
index++; // skip header
|
||||
properties.position.x = array[index++].value;
|
||||
properties.position.y = array[index++].value;
|
||||
properties.position.z = array[index++].value;
|
||||
properties.registrationPoint.x = array[index++].value;
|
||||
properties.registrationPoint.y = array[index++].value;
|
||||
properties.registrationPoint.z = array[index++].value;
|
||||
|
||||
index++; // skip header
|
||||
var angles = Quat.safeEulerAngles(properties.rotation);
|
||||
angles.x = array[index++].value;
|
||||
angles.y = array[index++].value;
|
||||
angles.z = array[index++].value;
|
||||
properties.rotation = Quat.fromVec3Degrees(angles);
|
||||
|
||||
index++; // skip header
|
||||
properties.dimensions.x = array[index++].value;
|
||||
properties.dimensions.y = array[index++].value;
|
||||
properties.dimensions.z = array[index++].value;
|
||||
index++; // skip reset button
|
||||
index++; // skip rescale percentage
|
||||
index++; // skip rescale button
|
||||
|
||||
index++; // skip header
|
||||
properties.velocity.x = array[index++].value;
|
||||
properties.velocity.y = array[index++].value;
|
||||
properties.velocity.z = array[index++].value;
|
||||
properties.damping = array[index++].value;
|
||||
|
||||
// NOTE: angular velocity is in radians/sec but we display degrees/sec for users
|
||||
properties.angularVelocity.x = array[index++].value * DEGREES_TO_RADIANS;
|
||||
properties.angularVelocity.y = array[index++].value * DEGREES_TO_RADIANS;
|
||||
properties.angularVelocity.z = array[index++].value * DEGREES_TO_RADIANS;
|
||||
properties.angularDamping = array[index++].value;
|
||||
|
||||
properties.gravity.x = array[index++].value;
|
||||
properties.gravity.y = array[index++].value;
|
||||
properties.gravity.z = array[index++].value;
|
||||
|
||||
properties.acceleration.x = array[index++].value;
|
||||
properties.acceleration.y = array[index++].value;
|
||||
properties.acceleration.z = array[index++].value;
|
||||
|
||||
index++; // skip header
|
||||
properties.density = array[index++].value;
|
||||
properties.collisionless = array[index++].value;
|
||||
properties.dynamic = array[index++].value;
|
||||
|
||||
properties.lifetime = array[index++].value;
|
||||
properties.visible = array[index++].value;
|
||||
properties.script = array[index++].value;
|
||||
|
||||
if (properties.type == "Box" || properties.type == "Sphere") {
|
||||
index++; // skip header
|
||||
properties.color.red = array[index++].value;
|
||||
properties.color.green = array[index++].value;
|
||||
properties.color.blue = array[index++].value;
|
||||
}
|
||||
if (properties.type == "Light") {
|
||||
index++; // skip header
|
||||
properties.isSpotlight = array[index++].value;
|
||||
properties.diffuseColor.red = array[index++].value;
|
||||
properties.diffuseColor.green = array[index++].value;
|
||||
properties.diffuseColor.blue = array[index++].value;
|
||||
properties.ambientColor.red = array[index++].value;
|
||||
properties.ambientColor.green = array[index++].value;
|
||||
properties.ambientColor.blue = array[index++].value;
|
||||
properties.specularColor.red = array[index++].value;
|
||||
properties.specularColor.green = array[index++].value;
|
||||
properties.specularColor.blue = array[index++].value;
|
||||
properties.constantAttenuation = array[index++].value;
|
||||
properties.linearAttenuation = array[index++].value;
|
||||
properties.quadraticAttenuation = array[index++].value;
|
||||
properties.exponent = array[index++].value;
|
||||
properties.cutoff = array[index++].value;
|
||||
}
|
||||
|
||||
Entities.editEntity(editModelID, properties);
|
||||
if (typeof(selectionDisplay) != "undefined") {
|
||||
selectionDisplay.select(editModelID, false);
|
||||
}
|
||||
}
|
||||
modelSelected = false;
|
||||
});
|
||||
|
||||
return that;
|
||||
|
||||
}());
|
|
@ -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,11 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoffStand", "state": "takeoffStand" },
|
||||
{ "var": "isTakeoffRun", "state": "takeoffRun" },
|
||||
{ "var": "isInAirStand", "state": "inAirStand" },
|
||||
{ "var": "isInAirRun", "state": "inAirRun" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -263,7 +267,11 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoffStand", "state": "takeoffStand" },
|
||||
{ "var": "isTakeoffRun", "state": "takeoffRun" },
|
||||
{ "var": "isInAirStand", "state": "inAirStand" },
|
||||
{ "var": "isInAirRun", "state": "inAirRun" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -278,7 +286,11 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoffStand", "state": "takeoffStand" },
|
||||
{ "var": "isTakeoffRun", "state": "takeoffRun" },
|
||||
{ "var": "isInAirStand", "state": "inAirStand" },
|
||||
{ "var": "isInAirRun", "state": "inAirRun" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -293,7 +305,11 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoffStand", "state": "takeoffStand" },
|
||||
{ "var": "isTakeoffRun", "state": "takeoffRun" },
|
||||
{ "var": "isInAirStand", "state": "inAirStand" },
|
||||
{ "var": "isInAirRun", "state": "inAirRun" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -308,7 +324,11 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoffStand", "state": "takeoffStand" },
|
||||
{ "var": "isTakeoffRun", "state": "takeoffRun" },
|
||||
{ "var": "isInAirStand", "state": "inAirStand" },
|
||||
{ "var": "isInAirRun", "state": "inAirRun" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -323,7 +343,11 @@
|
|||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoffStand", "state": "takeoffStand" },
|
||||
{ "var": "isTakeoffRun", "state": "takeoffRun" },
|
||||
{ "var": "isInAirStand", "state": "inAirStand" },
|
||||
{ "var": "isInAirRun", "state": "inAirRun" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -338,7 +362,11 @@
|
|||
{ "var": "isMovingLeft", "state": "strafeLeft" },
|
||||
{ "var": "isTurningLeft", "state": "turnLeft" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoffStand", "state": "takeoffStand" },
|
||||
{ "var": "isTakeoffRun", "state": "takeoffRun" },
|
||||
{ "var": "isInAirStand", "state": "inAirStand" },
|
||||
{ "var": "isInAirRun", "state": "inAirRun" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -353,7 +381,11 @@
|
|||
{ "var": "isMovingLeft", "state": "strafeLeft" },
|
||||
{ "var": "isTurningRight", "state": "turnRight" },
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isFlying", "state": "fly" }
|
||||
{ "var": "isFlying", "state": "fly" },
|
||||
{ "var": "isTakeoffStand", "state": "takeoffStand" },
|
||||
{ "var": "isTakeoffRun", "state": "takeoffRun" },
|
||||
{ "var": "isInAirStand", "state": "inAirStand" },
|
||||
{ "var": "isInAirRun", "state": "inAirRun" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -361,6 +393,7 @@
|
|||
"interpTarget": 30,
|
||||
"interpDuration": 30,
|
||||
"transitions": [
|
||||
{ "var": "isNotAway", "state": "awayOutro" },
|
||||
{ "var": "awayIntroOnDone", "state": "away"}
|
||||
]
|
||||
},
|
||||
|
@ -385,8 +418,47 @@
|
|||
"interpTarget": 6,
|
||||
"interpDuration": 6,
|
||||
"transitions": [
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isNotFlying", "state": "idle" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "takeoffStand",
|
||||
"interpTarget": 0,
|
||||
"interpDuration": 6,
|
||||
"transitions": [
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isNotTakeoff", "state": "inAirStand" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "takeoffRun",
|
||||
"interpTarget": 0,
|
||||
"interpDuration": 6,
|
||||
"transitions": [
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isNotTakeoff", "state": "inAirRun" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inAirStand",
|
||||
"interpTarget": 0,
|
||||
"interpDuration": 6,
|
||||
"interpType": "snapshotPrev",
|
||||
"transitions": [
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isNotInAir", "state": "idle" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inAirRun",
|
||||
"interpTarget": 0,
|
||||
"interpDuration": 6,
|
||||
"interpType": "snapshotPrev",
|
||||
"transitions": [
|
||||
{ "var": "isAway", "state": "awayIntro" },
|
||||
{ "var": "isNotInAir", "state": "idle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -685,6 +757,122 @@
|
|||
"loopFlag": true
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "takeoffStand",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_standing_takeoff.fbx",
|
||||
"startFrame": 17.0,
|
||||
"endFrame": 25.0,
|
||||
"timeScale": 1.0,
|
||||
"loopFlag": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "takeoffRun",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_takeoff.fbx",
|
||||
"startFrame": 1.0,
|
||||
"endFrame": 2.5,
|
||||
"timeScale": 0.01,
|
||||
"loopFlag": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "inAirStand",
|
||||
"type": "blendLinear",
|
||||
"data": {
|
||||
"alpha": 0.0,
|
||||
"alphaVar": "inAirAlpha"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "inAirStandPreApex",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_standing_apex.fbx",
|
||||
"startFrame": 0.0,
|
||||
"endFrame": 0.0,
|
||||
"timeScale": 0.0,
|
||||
"loopFlag": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "inAirStandApex",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_standing_apex.fbx",
|
||||
"startFrame": 1.0,
|
||||
"endFrame": 1.0,
|
||||
"timeScale": 1.0,
|
||||
"loopFlag": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "inAirStandPostApex",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_standing_apex.fbx",
|
||||
"startFrame": 2.0,
|
||||
"endFrame": 2.0,
|
||||
"timeScale": 1.0,
|
||||
"loopFlag": false
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inAirRun",
|
||||
"type": "blendLinear",
|
||||
"data": {
|
||||
"alpha": 0.0,
|
||||
"alphaVar": "inAirAlpha"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "inAirRunPreApex",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "http://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": "inAirRunApex",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "http://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": "inAirRunPostApex",
|
||||
"type": "clip",
|
||||
"data": {
|
||||
"url": "http://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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ Original.CheckBox {
|
|||
}
|
||||
label: Text {
|
||||
text: control.text
|
||||
renderType: Text.QtRendering
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
7
interface/resources/qml/controls/TextField.qml
Normal file
7
interface/resources/qml/controls/TextField.qml
Normal file
|
@ -0,0 +1,7 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
TextField {
|
||||
style: TextFieldStyle { renderType: Text.QtRendering }
|
||||
}
|
|
@ -262,11 +262,10 @@ FocusScope {
|
|||
}
|
||||
|
||||
Component { id: fileDialogBuilder; FileDialog { } }
|
||||
function fileOpenDialog(properties) {
|
||||
function fileDialog(properties) {
|
||||
return fileDialogBuilder.createObject(desktop, properties);
|
||||
}
|
||||
|
||||
|
||||
MenuMouseHandler { id: menuPopperUpper }
|
||||
function popupMenu(point) {
|
||||
menuPopperUpper.popup(desktop, rootMenu.items, point);
|
||||
|
|
|
@ -2,6 +2,8 @@ import QtQuick 2.0
|
|||
import QtQuick.Controls 1.4
|
||||
import Qt.labs.folderlistmodel 2.1
|
||||
import Qt.labs.settings 1.0
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
import QtQuick.Dialogs 1.2 as OriginalDialogs
|
||||
|
||||
import ".."
|
||||
import "../windows"
|
||||
|
@ -34,18 +36,26 @@ ModalWindow {
|
|||
// Set from OffscreenUi::getOpenFile()
|
||||
property int options; // <-- FIXME unused
|
||||
|
||||
|
||||
property bool selectDirectory: false;
|
||||
property bool showHidden: false;
|
||||
// FIXME implement
|
||||
property bool multiSelect: false;
|
||||
// FIXME implement
|
||||
property bool saveDialog: false;
|
||||
property var helper: fileDialogHelper
|
||||
property alias model: fileTableView.model
|
||||
property var drives: helper.drives()
|
||||
|
||||
signal selectedFile(var file);
|
||||
signal canceled();
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("Helper " + helper + " drives " + drives)
|
||||
drivesSelector.onCurrentTextChanged.connect(function(){
|
||||
root.dir = helper.pathToUrl(drivesSelector.currentText);
|
||||
})
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "white"
|
||||
|
@ -77,11 +87,22 @@ ModalWindow {
|
|||
size: 32
|
||||
onClicked: d.navigateHome();
|
||||
}
|
||||
|
||||
VrControls.ComboBox {
|
||||
id: drivesSelector
|
||||
width: 48
|
||||
height: homeButton.height
|
||||
model: drives
|
||||
visible: drives.length > 1
|
||||
currentIndex: 0
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: currentDirectory
|
||||
height: homeButton.height
|
||||
style: TextFieldStyle { renderType: Text.QtRendering }
|
||||
anchors { left: navControls.right; right: parent.right; top: parent.top; margins: 8 }
|
||||
property var lastValidFolder: helper.urlToPath(model.folder)
|
||||
onLastValidFolderChanged: text = lastValidFolder;
|
||||
|
@ -122,6 +143,8 @@ ModalWindow {
|
|||
currentSelectionIsFolder = fileTableView.model.isFolder(row);
|
||||
if (root.selectDirectory || !currentSelectionIsFolder) {
|
||||
currentSelection.text = helper.urlToPath(currentSelectionUrl);
|
||||
} else {
|
||||
currentSelection.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,17 +196,19 @@ ModalWindow {
|
|||
if (isFolder) {
|
||||
fileTableView.model.folder = file
|
||||
} else {
|
||||
root.selectedFile(file);
|
||||
root.destroy();
|
||||
okAction.trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: currentSelection
|
||||
style: TextFieldStyle { renderType: Text.QtRendering }
|
||||
anchors { right: root.selectDirectory ? parent.right : selectionType.left; rightMargin: 8; left: parent.left; leftMargin: 8; top: selectionType.top }
|
||||
readOnly: true
|
||||
activeFocusOnTab: false
|
||||
readOnly: !root.saveDialog
|
||||
activeFocusOnTab: !readOnly
|
||||
onActiveFocusChanged: if (activeFocus) { selectAll(); }
|
||||
onAccepted: okAction.trigger();
|
||||
}
|
||||
|
||||
FileTypeSelection {
|
||||
|
@ -203,25 +228,91 @@ ModalWindow {
|
|||
spacing: 8
|
||||
Button {
|
||||
id: openButton
|
||||
text: root.selectDirectory ? "Choose" : "Open"
|
||||
enabled: currentSelection.text ? true : false
|
||||
onClicked: { selectedFile(d.currentSelectionUrl); root.visible = false; }
|
||||
Keys.onReturnPressed: { selectedFile(d.currentSelectionUrl); root.visible = false; }
|
||||
|
||||
action: okAction
|
||||
Keys.onReturnPressed: okAction.trigger()
|
||||
KeyNavigation.up: selectionType
|
||||
KeyNavigation.left: selectionType
|
||||
KeyNavigation.right: cancelButton
|
||||
}
|
||||
Button {
|
||||
id: cancelButton
|
||||
text: "Cancel"
|
||||
action: cancelAction
|
||||
KeyNavigation.up: selectionType
|
||||
KeyNavigation.left: openButton
|
||||
KeyNavigation.right: fileTableView.contentItem
|
||||
Keys.onReturnPressed: { canceled(); root.enabled = false }
|
||||
onClicked: { canceled(); root.visible = false; }
|
||||
}
|
||||
}
|
||||
|
||||
Action {
|
||||
id: okAction
|
||||
text: root.saveDialog ? "Save" : (root.selectDirectory ? "Choose" : "Open")
|
||||
enabled: currentSelection.text ? true : false
|
||||
onTriggered: okActionTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: okActionTimer
|
||||
interval: 50
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!root.saveDialog) {
|
||||
selectedFile(d.currentSelectionUrl);
|
||||
root.destroy()
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Handle the ambiguity between different cases
|
||||
// * typed name (with or without extension)
|
||||
// * full path vs relative vs filename only
|
||||
var selection = helper.saveHelper(currentSelection.text, root.dir, selectionType.currentFilter);
|
||||
|
||||
if (!selection) {
|
||||
desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, text: "Unable to parse selection" })
|
||||
return;
|
||||
}
|
||||
|
||||
if (helper.urlIsDir(selection)) {
|
||||
root.dir = selection;
|
||||
currentSelection.text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the file is a valid target
|
||||
if (!helper.urlIsWritable(selection)) {
|
||||
desktop.messageBox({
|
||||
icon: OriginalDialogs.StandardIcon.Warning,
|
||||
buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No,
|
||||
text: "Unable to write to location " + selection
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
if (helper.urlExists(selection)) {
|
||||
var messageBox = desktop.messageBox({
|
||||
icon: OriginalDialogs.StandardIcon.Question,
|
||||
buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No,
|
||||
text: "Do you wish to overwrite " + selection + "?",
|
||||
});
|
||||
var result = messageBox.exec();
|
||||
if (OriginalDialogs.StandardButton.Yes !== result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Selecting " + selection)
|
||||
selectedFile(selection);
|
||||
root.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Action {
|
||||
id: cancelAction
|
||||
text: "Cancel"
|
||||
onTriggered: { canceled(); root.visible = false; }
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
|
|
|
@ -25,6 +25,10 @@ ModalWindow {
|
|||
destroy();
|
||||
}
|
||||
|
||||
function exec() {
|
||||
return OffscreenUi.waitForMessageBoxResult(root);
|
||||
}
|
||||
|
||||
property alias detailedText: detailedText.text
|
||||
property alias text: mainTextContainer.text
|
||||
property alias informativeText: informativeTextContainer.text
|
||||
|
|
|
@ -62,7 +62,7 @@ ModalWindow {
|
|||
Item {
|
||||
anchors { top: mainTextContainer.bottom; bottom: buttons.top; left: parent.left; right: parent.right; margins: d.spacing }
|
||||
// FIXME make a text field type that can be bound to a history for autocompletion
|
||||
TextField {
|
||||
VrControls.TextField {
|
||||
id: textResult
|
||||
focus: items ? false : true
|
||||
visible: items ? false : true
|
||||
|
|
|
@ -219,7 +219,7 @@ Window {
|
|||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
HifiControls.TextField {
|
||||
id: filterEdit
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
@ -252,7 +252,7 @@ Window {
|
|||
TableViewColumn { title: "Name"; role: "display"; }
|
||||
}
|
||||
|
||||
TextField {
|
||||
HifiControls.TextField {
|
||||
id: selectedScript
|
||||
readOnly: true
|
||||
anchors.left: parent.left
|
||||
|
|
|
@ -17,9 +17,13 @@ VrControls.ComboBox {
|
|||
onCurrentTextChanged: {
|
||||
var globRegex = /\((.*)\)$/
|
||||
var globs = globRegex.exec(currentText);
|
||||
if (!globs[1]) {
|
||||
console.warn("Unable to parse filter " + currentText);
|
||||
return;
|
||||
if (!globs || !globs[1]) {
|
||||
globRegex = /^(\*.*)$/
|
||||
globs = globRegex.exec(currentText);
|
||||
if (!globs || !globs[1]) {
|
||||
console.warn("Unable to parse filter " + currentText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
currentFilter = globs[1].split(" ");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
Preference {
|
||||
id: root
|
||||
|
@ -50,6 +51,7 @@ Preference {
|
|||
id: dataTextField
|
||||
placeholderText: root.placeholderText
|
||||
text: preference.value
|
||||
style: TextFieldStyle { renderType: Text.QtRendering }
|
||||
anchors {
|
||||
top: labelText.bottom
|
||||
left: parent.left
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import "../../dialogs"
|
||||
|
||||
|
@ -30,6 +31,7 @@ Preference {
|
|||
id: dataTextField
|
||||
placeholderText: root.placeholderText
|
||||
text: preference.value
|
||||
style: TextFieldStyle { renderType: Text.QtRendering }
|
||||
anchors {
|
||||
top: labelText.bottom
|
||||
left: parent.left
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import "../../controls"
|
||||
|
||||
Preference {
|
||||
id: root
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
Preference {
|
||||
id: root
|
||||
|
@ -24,6 +25,7 @@ Preference {
|
|||
TextField {
|
||||
id: dataTextField
|
||||
placeholderText: preference.placeholderText
|
||||
style: TextFieldStyle { renderType: Text.QtRendering }
|
||||
anchors {
|
||||
top: labelText.bottom
|
||||
left: parent.left
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.XmlListModel 2.0
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import "../../windows"
|
||||
import "../../js/Utils.js" as Utils
|
||||
|
@ -27,6 +28,7 @@ ModalWindow {
|
|||
TextField {
|
||||
id: filterEdit
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
style: TextFieldStyle { renderType: Text.QtRendering }
|
||||
placeholderText: "filter"
|
||||
onTextChanged: tableView.model.filter = text
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.XmlListModel 2.0
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import "../../windows"
|
||||
import "../../js/Utils.js" as Utils
|
||||
|
@ -26,6 +27,7 @@ ModalWindow {
|
|||
|
||||
TextField {
|
||||
id: filterEdit
|
||||
style: TextFieldStyle { renderType: Text.QtRendering }
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
placeholderText: "filter"
|
||||
onTextChanged: tableView.model.filter = text
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import "../../../windows"
|
||||
import "../../../controls" as VrControls
|
||||
|
@ -31,7 +32,7 @@ Item {
|
|||
height: modelChooserButton.height
|
||||
anchors { left: parent.left; right: parent.right; }
|
||||
Text { id: urlLabel; text: "Model URL:"; width: 80; anchors.verticalCenter: modelUrl.verticalCenter }
|
||||
TextField {
|
||||
VrControls.TextField {
|
||||
id: modelUrl;
|
||||
height: jointChooser.height;
|
||||
anchors { left: urlLabel.right; leftMargin: 8; rightMargin: 8; right: modelChooserButton.left }
|
||||
|
|
|
@ -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>();
|
||||
|
@ -1362,6 +1362,9 @@ void Application::paintGL() {
|
|||
renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE;
|
||||
renderArgs._blitFramebuffer = DependencyManager::get<FramebufferCache>()->getSelfieFramebuffer();
|
||||
|
||||
auto inputs = AvatarInputs::getInstance();
|
||||
_mirrorViewRect.moveTo(inputs->x(), inputs->y());
|
||||
|
||||
renderRearViewMirror(&renderArgs, _mirrorViewRect);
|
||||
|
||||
renderArgs._blitFramebuffer.reset();
|
||||
|
@ -4612,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);
|
||||
|
|
|
@ -214,7 +214,7 @@ void Avatar::simulate(float deltaTime) {
|
|||
Head* head = getHead();
|
||||
head->setPosition(headPosition);
|
||||
head->setScale(getUniformScale());
|
||||
head->simulate(deltaTime, false, _shouldAnimate);
|
||||
head->simulate(deltaTime, false, !_shouldAnimate);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -268,17 +268,6 @@ QVector<AvatarManager::LocalLight> AvatarManager::getLocalLights() const {
|
|||
return _localLights;
|
||||
}
|
||||
|
||||
QVector<QUuid> AvatarManager::getAvatarIdentifiers() {
|
||||
QReadLocker locker(&_hashLock);
|
||||
return _avatarHash.keys().toVector();
|
||||
}
|
||||
|
||||
AvatarData* AvatarManager::getAvatar(QUuid avatarID) {
|
||||
// Null/Default-constructed QUuids will return MyAvatar
|
||||
return getAvatarBySessionID(avatarID).get();
|
||||
}
|
||||
|
||||
|
||||
void AvatarManager::getObjectsToRemoveFromPhysics(VectorOfMotionStates& result) {
|
||||
result.clear();
|
||||
result.swap(_motionStatesToRemoveFromPhysics);
|
||||
|
|
|
@ -39,7 +39,7 @@ public:
|
|||
void init();
|
||||
|
||||
MyAvatar* getMyAvatar() { return _myAvatar.get(); }
|
||||
AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID);
|
||||
AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) override;
|
||||
|
||||
void updateMyAvatar(float deltaTime);
|
||||
void updateOtherAvatars(float deltaTime);
|
||||
|
@ -56,9 +56,6 @@ public:
|
|||
|
||||
Q_INVOKABLE void setLocalLights(const QVector<AvatarManager::LocalLight>& localLights);
|
||||
Q_INVOKABLE QVector<AvatarManager::LocalLight> getLocalLights() const;
|
||||
// Currently, your own avatar will be included as the null avatar id.
|
||||
Q_INVOKABLE QVector<QUuid> getAvatarIdentifiers();
|
||||
Q_INVOKABLE AvatarData* getAvatar(QUuid avatarID);
|
||||
|
||||
|
||||
void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -298,8 +298,20 @@ void MyAvatar::update(float deltaTime) {
|
|||
auto audio = DependencyManager::get<AudioClient>();
|
||||
head->setAudioLoudness(audio->getLastInputLoudness());
|
||||
head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness());
|
||||
|
||||
simulate(deltaTime);
|
||||
|
||||
simulate(deltaTime);
|
||||
|
||||
currentEnergy += energyChargeRate;
|
||||
currentEnergy -= getAccelerationEnergy();
|
||||
currentEnergy -= getAudioEnergy();
|
||||
|
||||
if(didTeleport()) {
|
||||
currentEnergy = 0.0f;
|
||||
}
|
||||
currentEnergy = max(0.0f, min(currentEnergy,1.0f));
|
||||
emit energyChanged(currentEnergy);
|
||||
|
||||
|
||||
}
|
||||
|
||||
extern QByteArray avatarStateToFrame(const AvatarData* _avatar);
|
||||
|
@ -1311,21 +1323,22 @@ void MyAvatar::preRender(RenderArgs* renderArgs) {
|
|||
_prevShouldDrawHead = shouldDrawHead;
|
||||
}
|
||||
|
||||
const float RENDER_HEAD_CUTOFF_DISTANCE = 0.50f;
|
||||
const float RENDER_HEAD_CUTOFF_DISTANCE = 0.6f;
|
||||
|
||||
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) {
|
||||
|
@ -1360,6 +1373,37 @@ void MyAvatar::updateOrientation(float deltaTime) {
|
|||
totalBodyYaw += _driveKeys[STEP_YAW];
|
||||
}
|
||||
|
||||
// use head/HMD orientation to turn while flying
|
||||
if (getCharacterController()->getState() == CharacterController::State::Hover) {
|
||||
|
||||
// This is the direction the user desires to fly in.
|
||||
glm::vec3 desiredFacing = getHead()->getCameraOrientation() * Vectors::UNIT_Z;
|
||||
desiredFacing.y = 0.0f;
|
||||
|
||||
// This is our reference frame, it is captured when the user begins to move.
|
||||
glm::vec3 referenceFacing = transformVector(_sensorToWorldMatrix, _hoverReferenceCameraFacing);
|
||||
referenceFacing.y = 0.0f;
|
||||
referenceFacing = glm::normalize(referenceFacing);
|
||||
glm::vec3 referenceRight(referenceFacing.z, 0.0f, -referenceFacing.x);
|
||||
const float HOVER_FLY_ROTATION_PERIOD = 0.5f;
|
||||
float tau = glm::clamp(deltaTime / HOVER_FLY_ROTATION_PERIOD, 0.0f, 1.0f);
|
||||
|
||||
// new facing is a linear interpolation between the desired and reference vectors.
|
||||
glm::vec3 newFacing = glm::normalize((1.0f - tau) * referenceFacing + tau * desiredFacing);
|
||||
|
||||
// calcualte the signed delta yaw angle to apply so that we match our newFacing.
|
||||
float sign = copysignf(1.0f, glm::dot(desiredFacing, referenceRight));
|
||||
float deltaAngle = sign * acosf(glm::clamp(glm::dot(referenceFacing, newFacing), -1.0f, 1.0f));
|
||||
|
||||
// speedFactor is 0 when we are at rest adn 1.0 when we are at max flying speed.
|
||||
const float MAX_FLYING_SPEED = 30.0f;
|
||||
float speedFactor = glm::min(glm::length(getVelocity()) / MAX_FLYING_SPEED, 1.0f);
|
||||
|
||||
// apply our delta, but scale it by the speed factor, so we turn faster when we are flying faster.
|
||||
totalBodyYaw += (speedFactor * deltaAngle * (180.0f / PI));
|
||||
}
|
||||
|
||||
|
||||
// update body orientation by movement inputs
|
||||
setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f))));
|
||||
|
||||
|
@ -1510,7 +1554,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
|
||||
|
@ -1535,6 +1580,16 @@ void MyAvatar::updatePosition(float deltaTime) {
|
|||
// update _moving flag based on speed
|
||||
const float MOVING_SPEED_THRESHOLD = 0.01f;
|
||||
_moving = speed > MOVING_SPEED_THRESHOLD;
|
||||
|
||||
|
||||
// capture the head rotation, in sensor space, when the user first indicates they would like to move/fly.
|
||||
if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) {
|
||||
_hoverReferenceCameraFacingIsCaptured = true;
|
||||
// transform the camera facing vector into sensor space.
|
||||
_hoverReferenceCameraFacing = transformVector(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z);
|
||||
} else if (_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) <= 0.1f && fabs(_driveKeys[TRANSLATE_X]) <= 0.1f)) {
|
||||
_hoverReferenceCameraFacingIsCaptured = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MyAvatar::updateCollisionSound(const glm::vec3 &penetration, float deltaTime, float frequency) {
|
||||
|
@ -1579,10 +1634,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);
|
||||
|
@ -1853,3 +1904,31 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co
|
|||
}
|
||||
}
|
||||
|
||||
float MyAvatar::getAccelerationEnergy() {
|
||||
glm::vec3 velocity = getVelocity();
|
||||
int changeInVelocity = abs(velocity.length() - priorVelocity.length());
|
||||
float changeInEnergy = priorVelocity.length() * changeInVelocity * AVATAR_MOVEMENT_ENERGY_CONSTANT;
|
||||
priorVelocity = velocity;
|
||||
|
||||
return changeInEnergy;
|
||||
}
|
||||
|
||||
float MyAvatar::getEnergy() {
|
||||
return currentEnergy;
|
||||
}
|
||||
|
||||
void MyAvatar::setEnergy(float value) {
|
||||
currentEnergy = value;
|
||||
}
|
||||
|
||||
float MyAvatar::getAudioEnergy() {
|
||||
return getAudioLoudness() * AUDIO_ENERGY_CONSTANT;
|
||||
}
|
||||
|
||||
bool MyAvatar::didTeleport() {
|
||||
glm::vec3 pos = getPosition();
|
||||
glm::vec3 changeInPosition = pos - lastPosition;
|
||||
lastPosition = pos;
|
||||
return (changeInPosition.length() > MAX_AVATAR_MOVEMENT_PER_FRAME);
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ class MyAvatar : public Avatar {
|
|||
Q_PROPERTY(controller::Pose rightHandPose READ getRightHandPose)
|
||||
Q_PROPERTY(controller::Pose leftHandTipPose READ getLeftHandTipPose)
|
||||
Q_PROPERTY(controller::Pose rightHandTipPose READ getRightHandTipPose)
|
||||
Q_PROPERTY(float energy READ getEnergy WRITE setEnergy)
|
||||
|
||||
public:
|
||||
MyAvatar(RigPointer rig);
|
||||
|
@ -239,8 +240,6 @@ public:
|
|||
glm::quat getCustomListenOrientation() { return _customListenOrientation; }
|
||||
void setCustomListenOrientation(glm::quat customListenOrientation) { _customListenOrientation = customListenOrientation; }
|
||||
|
||||
bool isHovering() const;
|
||||
|
||||
public slots:
|
||||
void increaseSize();
|
||||
void decreaseSize();
|
||||
|
@ -278,8 +277,10 @@ signals:
|
|||
void transformChanged();
|
||||
void newCollisionSoundURL(const QUrl& url);
|
||||
void collisionWithEntity(const Collision& collision);
|
||||
void energyChanged(float newEnergy);
|
||||
void positionGoneTo();
|
||||
|
||||
|
||||
private:
|
||||
|
||||
glm::vec3 getWorldBodyPosition() const;
|
||||
|
@ -415,6 +416,21 @@ private:
|
|||
|
||||
AtRestDetector _hmdAtRestDetector;
|
||||
bool _lastIsMoving { false };
|
||||
bool _hoverReferenceCameraFacingIsCaptured { false };
|
||||
glm::vec3 _hoverReferenceCameraFacing; // hmd sensor space
|
||||
|
||||
float AVATAR_MOVEMENT_ENERGY_CONSTANT { 0.001f };
|
||||
float AUDIO_ENERGY_CONSTANT { 0.000001f };
|
||||
float MAX_AVATAR_MOVEMENT_PER_FRAME { 30.0f };
|
||||
float currentEnergy { 0.0f };
|
||||
float energyChargeRate { 0.003f };
|
||||
glm::vec3 priorVelocity;
|
||||
glm::vec3 lastPosition;
|
||||
float getAudioEnergy();
|
||||
float getAccelerationEnergy();
|
||||
float getEnergy();
|
||||
void setEnergy(float value);
|
||||
bool didTeleport();
|
||||
};
|
||||
|
||||
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -9,29 +9,20 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <QDir>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QtCore/QDir>
|
||||
#include <QMessageBox>
|
||||
#include <QScriptValue>
|
||||
#include <QScrollArea>
|
||||
|
||||
#include "Application.h"
|
||||
#include "DomainHandler.h"
|
||||
#include "MainWindow.h"
|
||||
#include "Menu.h"
|
||||
#include "OffscreenUi.h"
|
||||
#include "ui/ModelsBrowser.h"
|
||||
#include "WebWindowClass.h"
|
||||
|
||||
#include "WindowScriptingInterface.h"
|
||||
|
||||
WindowScriptingInterface::WindowScriptingInterface() :
|
||||
_editDialog(NULL),
|
||||
_nonBlockingFormActive(false),
|
||||
_formResult(QDialog::Rejected)
|
||||
{
|
||||
WindowScriptingInterface::WindowScriptingInterface() {
|
||||
const DomainHandler& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
|
||||
connect(&domainHandler, &DomainHandler::connectedToDomain, this, &WindowScriptingInterface::domainChanged);
|
||||
connect(qApp, &Application::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused);
|
||||
|
@ -85,544 +76,31 @@ QScriptValue WindowScriptingInterface::getCursorPositionY() {
|
|||
return QCursor::pos().y();
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::alert(const QString& message) {
|
||||
QScriptValue retVal;
|
||||
QMetaObject::invokeMethod(this, "showAlert", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QScriptValue, retVal), Q_ARG(const QString&, message));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::confirm(const QString& message) {
|
||||
QScriptValue retVal;
|
||||
QMetaObject::invokeMethod(this, "showConfirm", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QScriptValue, retVal), Q_ARG(const QString&, message));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::form(const QString& title, QScriptValue form) {
|
||||
QScriptValue retVal;
|
||||
QMetaObject::invokeMethod(this, "showForm", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QScriptValue, retVal),
|
||||
Q_ARG(const QString&, title), Q_ARG(QScriptValue, form));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) {
|
||||
QScriptValue retVal;
|
||||
QMetaObject::invokeMethod(this, "showPrompt", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QScriptValue, retVal),
|
||||
Q_ARG(const QString&, message), Q_ARG(const QString&, defaultText));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) {
|
||||
QScriptValue retVal;
|
||||
QMetaObject::invokeMethod(this, "showBrowse", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QScriptValue, retVal),
|
||||
Q_ARG(const QString&, title), Q_ARG(const QString&, directory), Q_ARG(const QString&, nameFilter));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) {
|
||||
QScriptValue retVal;
|
||||
QMetaObject::invokeMethod(this, "showBrowse", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QScriptValue, retVal),
|
||||
Q_ARG(const QString&, title), Q_ARG(const QString&, directory), Q_ARG(const QString&, nameFilter),
|
||||
Q_ARG(QFileDialog::AcceptMode, QFileDialog::AcceptSave));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::s3Browse(const QString& nameFilter) {
|
||||
QScriptValue retVal;
|
||||
QMetaObject::invokeMethod(this, "showS3Browse", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QScriptValue, retVal),
|
||||
Q_ARG(const QString&, nameFilter));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::nonBlockingForm(const QString& title, QScriptValue form) {
|
||||
QMetaObject::invokeMethod(this, "showNonBlockingForm", Qt::BlockingQueuedConnection,
|
||||
Q_ARG(const QString&, title), Q_ARG(QScriptValue, form));
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::reloadNonBlockingForm(QScriptValue newValues) {
|
||||
QMetaObject::invokeMethod(this, "doReloadNonBlockingForm", Qt::BlockingQueuedConnection,
|
||||
Q_ARG(QScriptValue, newValues));
|
||||
}
|
||||
|
||||
|
||||
QScriptValue WindowScriptingInterface::getNonBlockingFormResult(QScriptValue form) {
|
||||
QScriptValue retVal;
|
||||
QMetaObject::invokeMethod(this, "doGetNonBlockingFormResult", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QScriptValue, retVal),
|
||||
Q_ARG(QScriptValue, form));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::peekNonBlockingFormResult(QScriptValue form) {
|
||||
QScriptValue retVal;
|
||||
QMetaObject::invokeMethod(this, "doPeekNonBlockingFormResult", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QScriptValue, retVal),
|
||||
Q_ARG(QScriptValue, form));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/// Display an alert box
|
||||
/// \param const QString& message message to display
|
||||
/// \return QScriptValue::UndefinedValue
|
||||
QScriptValue WindowScriptingInterface::showAlert(const QString& message) {
|
||||
void WindowScriptingInterface::alert(const QString& message) {
|
||||
OffscreenUi::warning("", message);
|
||||
return QScriptValue::UndefinedValue;
|
||||
}
|
||||
|
||||
/// Display a confirmation box with the options 'Yes' and 'No'
|
||||
/// \param const QString& message message to display
|
||||
/// \return QScriptValue `true` if 'Yes' was clicked, `false` otherwise
|
||||
QScriptValue WindowScriptingInterface::showConfirm(const QString& message) {
|
||||
bool confirm = false;
|
||||
if (QMessageBox::Yes == OffscreenUi::question("", message)) {
|
||||
confirm = true;
|
||||
}
|
||||
return QScriptValue(confirm);
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::chooseDirectory() {
|
||||
QPushButton* button = reinterpret_cast<QPushButton*>(sender());
|
||||
|
||||
QString title = button->property("title").toString();
|
||||
QString path = button->property("path").toString();
|
||||
QRegExp displayAs = button->property("displayAs").toRegExp();
|
||||
QRegExp validateAs = button->property("validateAs").toRegExp();
|
||||
QString errorMessage = button->property("errorMessage").toString();
|
||||
|
||||
QString directory = QFileDialog::getExistingDirectory(button, title, path);
|
||||
if (directory.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateAs.exactMatch(directory)) {
|
||||
OffscreenUi::warning(NULL, "Invalid Directory", errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
button->setProperty("path", directory);
|
||||
|
||||
displayAs.indexIn(directory);
|
||||
QString buttonText = displayAs.cap(1) != "" ? displayAs.cap(1) : ".";
|
||||
button->setText(buttonText);
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::inlineButtonClicked() {
|
||||
QPushButton* button = reinterpret_cast<QPushButton*>(sender());
|
||||
QString name = button->property("name").toString();
|
||||
emit inlineButtonClicked(name);
|
||||
}
|
||||
|
||||
QString WindowScriptingInterface::jsRegExp2QtRegExp(QString string) {
|
||||
// Converts string representation of RegExp from JavaScript format to Qt format.
|
||||
return string.mid(1, string.length() - 2) // No enclosing slashes.
|
||||
.replace("\\/", "/"); // No escaping of forward slash.
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::showNonBlockingForm(const QString& title, QScriptValue form) {
|
||||
if (!form.isArray() || (form.isArray() && form.property("length").toInt32() <= 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// what should we do if someone calls us while we still think we have a dialog showing???
|
||||
if (_nonBlockingFormActive) {
|
||||
qDebug() << "Show Non-Blocking Form called when form already active.";
|
||||
return;
|
||||
}
|
||||
|
||||
_form = form;
|
||||
_editDialog = createForm(title, _form);
|
||||
_nonBlockingFormActive = true;
|
||||
|
||||
connect(_editDialog, SIGNAL(accepted()), this, SLOT(nonBlockingFormAccepted()));
|
||||
connect(_editDialog, SIGNAL(rejected()), this, SLOT(nonBlockingFormRejected()));
|
||||
|
||||
_editDialog->setModal(true);
|
||||
_editDialog->show();
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::doReloadNonBlockingForm(QScriptValue newValues) {
|
||||
if (!newValues.isArray() || (newValues.isArray() && newValues.property("length").toInt32() <= 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// what should we do if someone calls us while we still think we have a dialog showing???
|
||||
if (!_editDialog) {
|
||||
qDebug() << "Reload Non-Blocking Form called when no form is active.";
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < newValues.property("length").toInt32(); ++i) {
|
||||
QScriptValue item = newValues.property(i);
|
||||
|
||||
if (item.property("oldIndex").isValid()) {
|
||||
int oldIndex = item.property("oldIndex").toInt32();
|
||||
QScriptValue oldItem = _form.property(oldIndex);
|
||||
if (oldItem.isValid()) {
|
||||
QLineEdit* originalEdit = _edits[oldItem.property("editIndex").toInt32()];
|
||||
originalEdit->setText(item.property("value").toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool WindowScriptingInterface::nonBlockingFormActive() {
|
||||
return _nonBlockingFormActive;
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::doPeekNonBlockingFormResult(QScriptValue array) {
|
||||
QScriptValue retVal;
|
||||
|
||||
int e = -1;
|
||||
int d = -1;
|
||||
int c = -1;
|
||||
int h = -1;
|
||||
for (int i = 0; i < _form.property("length").toInt32(); ++i) {
|
||||
QScriptValue item = _form.property(i);
|
||||
QScriptValue value = item.property("value");
|
||||
|
||||
if (item.property("button").toString() != "") {
|
||||
// Nothing to do
|
||||
} else if (item.property("type").toString() == "inlineButton") {
|
||||
// Nothing to do
|
||||
} else if (item.property("type").toString() == "header") {
|
||||
// Nothing to do
|
||||
} else if (item.property("directory").toString() != "") {
|
||||
d += 1;
|
||||
value = _directories.at(d)->property("path").toString();
|
||||
item.setProperty("directory", value);
|
||||
_form.setProperty(i, item);
|
||||
} else if (item.property("options").isArray()) {
|
||||
c += 1;
|
||||
item.setProperty("value",
|
||||
_combos.at(c)->currentIndex() < item.property("options").property("length").toInt32() ?
|
||||
item.property("options").property(_combos.at(c)->currentIndex()) :
|
||||
array.engine()->undefinedValue()
|
||||
);
|
||||
_form.setProperty(i, item);
|
||||
} else if (item.property("type").toString() == "checkbox") {
|
||||
h++;
|
||||
value = _checks.at(h)->checkState() == Qt::Checked;
|
||||
item.setProperty("value", value);
|
||||
_form.setProperty(i, item);
|
||||
} else {
|
||||
e += 1;
|
||||
bool ok = true;
|
||||
if (value.isNumber()) {
|
||||
value = _edits.at(e)->text().toDouble(&ok);
|
||||
} else if (value.isString()) {
|
||||
value = _edits.at(e)->text();
|
||||
} else if (value.isBool()) {
|
||||
if (_edits.at(e)->text() == "true") {
|
||||
value = true;
|
||||
} else if (_edits.at(e)->text() == "false") {
|
||||
value = false;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
item.setProperty("value", value);
|
||||
_form.setProperty(i, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
array = _form;
|
||||
return (_formResult == QDialog::Accepted);
|
||||
}
|
||||
|
||||
QScriptValue WindowScriptingInterface::doGetNonBlockingFormResult(QScriptValue array) {
|
||||
QScriptValue retVal;
|
||||
|
||||
if (_formResult == QDialog::Accepted) {
|
||||
int e = -1;
|
||||
int d = -1;
|
||||
int c = -1;
|
||||
int h = -1;
|
||||
for (int i = 0; i < _form.property("length").toInt32(); ++i) {
|
||||
QScriptValue item = _form.property(i);
|
||||
QScriptValue value = item.property("value");
|
||||
|
||||
if (item.property("button").toString() != "") {
|
||||
// Nothing to do
|
||||
} else if (item.property("type").toString() == "inlineButton") {
|
||||
// Nothing to do
|
||||
} else if (item.property("type").toString() == "header") {
|
||||
// Nothing to do
|
||||
} else if (item.property("directory").toString() != "") {
|
||||
d += 1;
|
||||
value = _directories.at(d)->property("path").toString();
|
||||
item.setProperty("directory", value);
|
||||
_form.setProperty(i, item);
|
||||
} else if (item.property("options").isArray()) {
|
||||
c += 1;
|
||||
item.setProperty("value",
|
||||
_combos.at(c)->currentIndex() < item.property("options").property("length").toInt32() ?
|
||||
item.property("options").property(_combos.at(c)->currentIndex()) :
|
||||
array.engine()->undefinedValue()
|
||||
);
|
||||
_form.setProperty(i, item);
|
||||
} else if (item.property("type").toString() == "checkbox") {
|
||||
h++;
|
||||
value = _checks.at(h)->checkState() == Qt::Checked;
|
||||
item.setProperty("value", value);
|
||||
_form.setProperty(i, item);
|
||||
} else {
|
||||
e += 1;
|
||||
bool ok = true;
|
||||
if (value.isNumber()) {
|
||||
value = _edits.at(e)->text().toDouble(&ok);
|
||||
} else if (value.isString()) {
|
||||
value = _edits.at(e)->text();
|
||||
} else if (value.isBool()) {
|
||||
if (_edits.at(e)->text() == "true") {
|
||||
value = true;
|
||||
} else if (_edits.at(e)->text() == "false") {
|
||||
value = false;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
item.setProperty("value", value);
|
||||
_form.setProperty(i, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete _editDialog;
|
||||
_editDialog = NULL;
|
||||
_form = QScriptValue();
|
||||
_edits.clear();
|
||||
_directories.clear();
|
||||
_combos.clear();
|
||||
_checks.clear();
|
||||
|
||||
array = _form;
|
||||
return (_formResult == QDialog::Accepted);
|
||||
}
|
||||
|
||||
|
||||
/// Display a form layout with an edit box
|
||||
/// \param const QString& title title to display
|
||||
/// \param const QScriptValue form to display as an array of objects:
|
||||
/// - label, value
|
||||
/// - label, directory, title, display regexp, validate regexp, error message
|
||||
/// - button ("Cancel")
|
||||
/// \return QScriptValue `true` if 'OK' was clicked, `false` otherwise
|
||||
QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptValue form) {
|
||||
if (form.isArray() && form.property("length").toInt32() <= 0) {
|
||||
return false;
|
||||
}
|
||||
QDialog* editDialog = createForm(title, form);
|
||||
|
||||
int result = editDialog->exec();
|
||||
|
||||
if (result == QDialog::Accepted) {
|
||||
int e = -1;
|
||||
int d = -1;
|
||||
int c = -1;
|
||||
int h = -1;
|
||||
for (int i = 0; i < form.property("length").toInt32(); ++i) {
|
||||
QScriptValue item = form.property(i);
|
||||
QScriptValue value = item.property("value");
|
||||
|
||||
if (item.property("button").toString() != "") {
|
||||
// Nothing to do
|
||||
} else if (item.property("type").toString() == "inlineButton") {
|
||||
// Nothing to do
|
||||
} else if (item.property("type").toString() == "header") {
|
||||
// Nothing to do
|
||||
} else if (item.property("directory").toString() != "") {
|
||||
d += 1;
|
||||
value = _directories.at(d)->property("path").toString();
|
||||
item.setProperty("directory", value);
|
||||
form.setProperty(i, item);
|
||||
} else if (item.property("options").isArray()) {
|
||||
c += 1;
|
||||
item.setProperty("value",
|
||||
_combos.at(c)->currentIndex() < item.property("options").property("length").toInt32() ?
|
||||
item.property("options").property(_combos.at(c)->currentIndex()) :
|
||||
form.engine()->undefinedValue()
|
||||
);
|
||||
form.setProperty(i, item);
|
||||
} else if (item.property("type").toString() == "checkbox") {
|
||||
h++;
|
||||
value = _checks.at(h)->checkState() == Qt::Checked;
|
||||
item.setProperty("value", value);
|
||||
form.setProperty(i, item);
|
||||
} else {
|
||||
e += 1;
|
||||
bool ok = true;
|
||||
if (value.isNumber()) {
|
||||
value = _edits.at(e)->text().toDouble(&ok);
|
||||
} else if (value.isString()) {
|
||||
value = _edits.at(e)->text();
|
||||
} else if (value.isBool()) {
|
||||
if (_edits.at(e)->text() == "true") {
|
||||
value = true;
|
||||
} else if (_edits.at(e)->text() == "false") {
|
||||
value = false;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
item.setProperty("value", value);
|
||||
form.setProperty(i, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete editDialog;
|
||||
_combos.clear();
|
||||
_checks.clear();
|
||||
_edits.clear();
|
||||
_directories.clear();
|
||||
return (result == QDialog::Accepted);
|
||||
}
|
||||
|
||||
|
||||
|
||||
QDialog* WindowScriptingInterface::createForm(const QString& title, QScriptValue form) {
|
||||
QDialog* editDialog = new QDialog(qApp->getWindow());
|
||||
editDialog->setWindowTitle(title);
|
||||
|
||||
bool cancelButton = false;
|
||||
|
||||
QVBoxLayout* layout = new QVBoxLayout();
|
||||
editDialog->setLayout(layout);
|
||||
|
||||
QScrollArea* area = new QScrollArea();
|
||||
layout->addWidget(area);
|
||||
area->setWidgetResizable(true);
|
||||
QWidget* container = new QWidget();
|
||||
QFormLayout* formLayout = new QFormLayout();
|
||||
container->setLayout(formLayout);
|
||||
container->sizePolicy().setHorizontalStretch(1);
|
||||
formLayout->setRowWrapPolicy(QFormLayout::DontWrapRows);
|
||||
formLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
formLayout->setFormAlignment(Qt::AlignHCenter | Qt::AlignTop);
|
||||
formLayout->setLabelAlignment(Qt::AlignLeft);
|
||||
|
||||
area->setWidget(container);
|
||||
|
||||
for (int i = 0; i < form.property("length").toInt32(); ++i) {
|
||||
QScriptValue item = form.property(i);
|
||||
|
||||
if (item.property("button").toString() != "") {
|
||||
cancelButton = cancelButton || item.property("button").toString().toLower() == "cancel";
|
||||
|
||||
} else if (item.property("directory").toString() != "") {
|
||||
QString path = item.property("directory").toString();
|
||||
QString title = item.property("title").toString();
|
||||
if (title == "") {
|
||||
title = "Choose Directory";
|
||||
}
|
||||
QString displayAsString = item.property("displayAs").toString();
|
||||
QRegExp displayAs = QRegExp(displayAsString != "" ? jsRegExp2QtRegExp(displayAsString) : "^(.*)$");
|
||||
QString validateAsString = item.property("validateAs").toString();
|
||||
QRegExp validateAs = QRegExp(validateAsString != "" ? jsRegExp2QtRegExp(validateAsString) : ".*");
|
||||
QString errorMessage = item.property("errorMessage").toString();
|
||||
if (errorMessage == "") {
|
||||
errorMessage = "Invalid directory";
|
||||
}
|
||||
|
||||
QPushButton* directory = new QPushButton(displayAs.cap(1));
|
||||
directory->setProperty("title", title);
|
||||
directory->setProperty("path", path);
|
||||
directory->setProperty("displayAs", displayAs);
|
||||
directory->setProperty("validateAs", validateAs);
|
||||
directory->setProperty("errorMessage", errorMessage);
|
||||
displayAs.indexIn(path);
|
||||
directory->setText(displayAs.cap(1) != "" ? displayAs.cap(1) : ".");
|
||||
|
||||
directory->setMinimumWidth(200);
|
||||
_directories.push_back(directory);
|
||||
|
||||
formLayout->addRow(new QLabel(item.property("label").toString()), directory);
|
||||
connect(directory, SIGNAL(clicked(bool)), SLOT(chooseDirectory()));
|
||||
|
||||
} else if (item.property("type").toString() == "inlineButton") {
|
||||
QString buttonLabel = item.property("buttonLabel").toString();
|
||||
|
||||
QPushButton* inlineButton = new QPushButton(buttonLabel);
|
||||
inlineButton->setMinimumWidth(200);
|
||||
inlineButton->setProperty("name", item.property("name").toString());
|
||||
formLayout->addRow(new QLabel(item.property("label").toString()), inlineButton);
|
||||
connect(inlineButton, SIGNAL(clicked(bool)), SLOT(inlineButtonClicked()));
|
||||
|
||||
} else if (item.property("type").toString() == "header") {
|
||||
formLayout->addRow(new QLabel(item.property("label").toString()));
|
||||
} else if (item.property("options").isArray()) {
|
||||
QComboBox* combo = new QComboBox();
|
||||
combo->setMinimumWidth(200);
|
||||
qint32 options_count = item.property("options").property("length").toInt32();
|
||||
for (qint32 i = 0; i < options_count; i++) {
|
||||
combo->addItem(item.property("options").property(i).toString());
|
||||
}
|
||||
_combos.push_back(combo);
|
||||
formLayout->addRow(new QLabel(item.property("label").toString()), combo);
|
||||
} else if (item.property("type").toString() == "checkbox") {
|
||||
QCheckBox* check = new QCheckBox();
|
||||
check->setTristate(false);
|
||||
check->setCheckState(item.property("value").toString() == "true" ? Qt::Checked : Qt::Unchecked);
|
||||
_checks.push_back(check);
|
||||
formLayout->addRow(new QLabel(item.property("label").toString()), check);
|
||||
} else {
|
||||
QLineEdit* edit = new QLineEdit(item.property("value").toString());
|
||||
edit->setMinimumWidth(200);
|
||||
int editIndex = _edits.size();
|
||||
_edits.push_back(edit);
|
||||
item.setProperty("editIndex", editIndex);
|
||||
formLayout->addRow(new QLabel(item.property("label").toString()), edit);
|
||||
}
|
||||
}
|
||||
|
||||
QDialogButtonBox* buttons = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok
|
||||
| (cancelButton ? QDialogButtonBox::Cancel : QDialogButtonBox::NoButton)
|
||||
);
|
||||
connect(buttons, SIGNAL(accepted()), editDialog, SLOT(accept()));
|
||||
connect(buttons, SIGNAL(rejected()), editDialog, SLOT(reject()));
|
||||
layout->addWidget(buttons);
|
||||
|
||||
return editDialog;
|
||||
QScriptValue WindowScriptingInterface::confirm(const QString& message) {
|
||||
return QScriptValue((QMessageBox::Yes == OffscreenUi::question("", message)));
|
||||
}
|
||||
|
||||
/// Display a prompt with a text box
|
||||
/// \param const QString& message message to display
|
||||
/// \param const QString& defaultText default text in the text box
|
||||
/// \return QScriptValue string text value in text box if the dialog was accepted, `null` otherwise.
|
||||
QScriptValue WindowScriptingInterface::showPrompt(const QString& message, const QString& defaultText) {
|
||||
QInputDialog promptDialog(qApp->getWindow());
|
||||
promptDialog.setWindowTitle("");
|
||||
promptDialog.setLabelText(message);
|
||||
promptDialog.setTextValue(defaultText);
|
||||
promptDialog.setFixedSize(600, 200);
|
||||
|
||||
if (promptDialog.exec() == QDialog::Accepted) {
|
||||
return QScriptValue(promptDialog.textValue());
|
||||
}
|
||||
return QScriptValue::NullValue;
|
||||
QScriptValue WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) {
|
||||
bool ok = false;
|
||||
QString result = OffscreenUi::getText(nullptr, "", message, QLineEdit::Normal, defaultText, &ok);
|
||||
return ok ? QScriptValue(result) : QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
/// Display a file dialog. If `directory` is an invalid file or directory the browser will start at the current
|
||||
/// working directory.
|
||||
/// \param const QString& title title of the window
|
||||
/// \param const QString& directory directory to start the file browser at
|
||||
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
|
||||
/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue`
|
||||
QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QString& directory, const QString& nameFilter,
|
||||
QFileDialog::AcceptMode acceptMode) {
|
||||
QString fixupPathForMac(const QString& directory) {
|
||||
// On OS X `directory` does not work as expected unless a file is included in the path, so we append a bogus
|
||||
// filename if the directory is valid.
|
||||
QString path = "";
|
||||
|
@ -631,35 +109,31 @@ QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QS
|
|||
fileInfo.setFile(directory, "__HIFI_INVALID_FILE__");
|
||||
path = fileInfo.filePath();
|
||||
}
|
||||
|
||||
QFileDialog fileDialog(qApp->getWindow(), title, path, nameFilter);
|
||||
fileDialog.setAcceptMode(acceptMode);
|
||||
QUrl fileUrl(directory);
|
||||
if (acceptMode == QFileDialog::AcceptSave) {
|
||||
// TODO -- Setting this breaks the dialog on Linux. Does it help something on other platforms?
|
||||
// fileDialog.setFileMode(QFileDialog::Directory);
|
||||
fileDialog.selectFile(fileUrl.fileName());
|
||||
}
|
||||
if (fileDialog.exec()) {
|
||||
return QScriptValue(fileDialog.selectedFiles().first());
|
||||
}
|
||||
return QScriptValue::NullValue;
|
||||
return path;
|
||||
}
|
||||
|
||||
/// Display a browse window for S3 models
|
||||
/// \param const QString& nameFilter filter to filter filenames
|
||||
/// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current
|
||||
/// working directory.
|
||||
/// \param const QString& title title of the window
|
||||
/// \param const QString& directory directory to start the file browser at
|
||||
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
|
||||
/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue`
|
||||
QScriptValue WindowScriptingInterface::showS3Browse(const QString& nameFilter) {
|
||||
ModelsBrowser browser(FSTReader::ENTITY_MODEL);
|
||||
if (nameFilter != "") {
|
||||
browser.setNameFilter(nameFilter);
|
||||
}
|
||||
QEventLoop loop;
|
||||
connect(&browser, &ModelsBrowser::selected, &loop, &QEventLoop::quit);
|
||||
QMetaObject::invokeMethod(&browser, "browse", Qt::QueuedConnection);
|
||||
loop.exec();
|
||||
|
||||
return browser.getSelectedFile();
|
||||
QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) {
|
||||
QString path = fixupPathForMac(directory);
|
||||
QString result = OffscreenUi::getOpenFileName(nullptr, title, path, nameFilter);
|
||||
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
|
||||
}
|
||||
|
||||
/// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current
|
||||
/// working directory.
|
||||
/// \param const QString& title title of the window
|
||||
/// \param const QString& directory directory to start the file browser at
|
||||
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
|
||||
/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue`
|
||||
QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) {
|
||||
QString path = fixupPathForMac(directory);
|
||||
QString result = OffscreenUi::getSaveFileName(nullptr, title, path, nameFilter);
|
||||
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
|
||||
}
|
||||
|
||||
int WindowScriptingInterface::getInnerWidth() {
|
||||
|
|
|
@ -12,12 +12,9 @@
|
|||
#ifndef hifi_WindowScriptingInterface_h
|
||||
#define hifi_WindowScriptingInterface_h
|
||||
|
||||
#include <QObject>
|
||||
#include <QScriptValue>
|
||||
#include <QString>
|
||||
#include <QFileDialog>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QString>
|
||||
#include <QtScript/QScriptValue>
|
||||
|
||||
class WebWindowClass;
|
||||
|
||||
|
@ -41,61 +38,19 @@ public slots:
|
|||
QScriptValue hasFocus();
|
||||
void setFocus();
|
||||
void raiseMainWindow();
|
||||
QScriptValue alert(const QString& message = "");
|
||||
void alert(const QString& message = "");
|
||||
QScriptValue confirm(const QString& message = "");
|
||||
QScriptValue form(const QString& title, QScriptValue array);
|
||||
QScriptValue prompt(const QString& message = "", const QString& defaultText = "");
|
||||
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
QScriptValue s3Browse(const QString& nameFilter = "");
|
||||
|
||||
void nonBlockingForm(const QString& title, QScriptValue array);
|
||||
void reloadNonBlockingForm(QScriptValue array);
|
||||
QScriptValue getNonBlockingFormResult(QScriptValue array);
|
||||
QScriptValue peekNonBlockingFormResult(QScriptValue array);
|
||||
|
||||
signals:
|
||||
void domainChanged(const QString& domainHostname);
|
||||
void inlineButtonClicked(const QString& name);
|
||||
void nonBlockingFormClosed();
|
||||
void svoImportRequested(const QString& url);
|
||||
void domainConnectionRefused(const QString& reason);
|
||||
|
||||
private slots:
|
||||
QScriptValue showAlert(const QString& message);
|
||||
QScriptValue showConfirm(const QString& message);
|
||||
QScriptValue showForm(const QString& title, QScriptValue form);
|
||||
QScriptValue showPrompt(const QString& message, const QString& defaultText);
|
||||
QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter,
|
||||
QFileDialog::AcceptMode acceptMode = QFileDialog::AcceptOpen);
|
||||
QScriptValue showS3Browse(const QString& nameFilter);
|
||||
|
||||
void showNonBlockingForm(const QString& title, QScriptValue array);
|
||||
void doReloadNonBlockingForm(QScriptValue array);
|
||||
bool nonBlockingFormActive();
|
||||
QScriptValue doGetNonBlockingFormResult(QScriptValue array);
|
||||
QScriptValue doPeekNonBlockingFormResult(QScriptValue array);
|
||||
|
||||
void chooseDirectory();
|
||||
void inlineButtonClicked();
|
||||
|
||||
void nonBlockingFormAccepted() { _nonBlockingFormActive = false; _formResult = QDialog::Accepted; emit nonBlockingFormClosed(); }
|
||||
void nonBlockingFormRejected() { _nonBlockingFormActive = false; _formResult = QDialog::Rejected; emit nonBlockingFormClosed(); }
|
||||
|
||||
WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height);
|
||||
|
||||
private:
|
||||
QString jsRegExp2QtRegExp(QString string);
|
||||
QDialog* createForm(const QString& title, QScriptValue form);
|
||||
|
||||
QDialog* _editDialog;
|
||||
QScriptValue _form;
|
||||
bool _nonBlockingFormActive;
|
||||
int _formResult;
|
||||
QVector<QComboBox*> _combos;
|
||||
QVector<QCheckBox*> _checks;
|
||||
QVector<QLineEdit*> _edits;
|
||||
QVector<QPushButton*> _directories;
|
||||
};
|
||||
|
||||
#endif // hifi_WindowScriptingInterface_h
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -15,12 +15,13 @@
|
|||
|
||||
bool AnimClip::usePreAndPostPoseFromAnim = false;
|
||||
|
||||
AnimClip::AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag) :
|
||||
AnimClip::AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag, bool mirrorFlag) :
|
||||
AnimNode(AnimNode::Type::Clip, id),
|
||||
_startFrame(startFrame),
|
||||
_endFrame(endFrame),
|
||||
_timeScale(timeScale),
|
||||
_loopFlag(loopFlag),
|
||||
_mirrorFlag(mirrorFlag),
|
||||
_frame(startFrame)
|
||||
{
|
||||
loadURL(url);
|
||||
|
@ -37,6 +38,7 @@ const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt,
|
|||
_endFrame = animVars.lookup(_endFrameVar, _endFrame);
|
||||
_timeScale = animVars.lookup(_timeScaleVar, _timeScale);
|
||||
_loopFlag = animVars.lookup(_loopFlagVar, _loopFlag);
|
||||
_mirrorFlag = animVars.lookup(_mirrorFlagVar, _mirrorFlag);
|
||||
float frame = animVars.lookup(_frameVar, _frame);
|
||||
|
||||
_frame = ::accumulateTime(_startFrame, _endFrame, _timeScale, frame, dt, _loopFlag, _id, triggersOut);
|
||||
|
@ -49,6 +51,12 @@ const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt,
|
|||
}
|
||||
|
||||
if (_anim.size()) {
|
||||
|
||||
// lazy creation of mirrored animation frames.
|
||||
if (_mirrorFlag && _anim.size() != _mirrorAnim.size()) {
|
||||
buildMirrorAnim();
|
||||
}
|
||||
|
||||
int prevIndex = (int)glm::floor(_frame);
|
||||
int nextIndex;
|
||||
if (_loopFlag && _frame >= _endFrame) {
|
||||
|
@ -63,8 +71,8 @@ const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt,
|
|||
prevIndex = std::min(std::max(0, prevIndex), frameCount - 1);
|
||||
nextIndex = std::min(std::max(0, nextIndex), frameCount - 1);
|
||||
|
||||
const AnimPoseVec& prevFrame = _anim[prevIndex];
|
||||
const AnimPoseVec& nextFrame = _anim[nextIndex];
|
||||
const AnimPoseVec& prevFrame = _mirrorFlag ? _mirrorAnim[prevIndex] : _anim[prevIndex];
|
||||
const AnimPoseVec& nextFrame = _mirrorFlag ? _mirrorAnim[nextIndex] : _anim[nextIndex];
|
||||
float alpha = glm::fract(_frame);
|
||||
|
||||
::blend(_poses.size(), &prevFrame[0], &nextFrame[0], alpha, &_poses[0]);
|
||||
|
@ -162,9 +170,22 @@ void AnimClip::copyFromNetworkAnim() {
|
|||
}
|
||||
}
|
||||
|
||||
// mirrorAnim will be re-built on demand, if needed.
|
||||
_mirrorAnim.clear();
|
||||
|
||||
_poses.resize(skeletonJointCount);
|
||||
}
|
||||
|
||||
void AnimClip::buildMirrorAnim() {
|
||||
assert(_skeleton);
|
||||
|
||||
_mirrorAnim.clear();
|
||||
_mirrorAnim.reserve(_anim.size());
|
||||
for (auto& relPoses : _anim) {
|
||||
_mirrorAnim.push_back(relPoses);
|
||||
_skeleton->mirrorRelativePoses(_mirrorAnim.back());
|
||||
}
|
||||
}
|
||||
|
||||
const AnimPoseVec& AnimClip::getPosesInternal() const {
|
||||
return _poses;
|
||||
|
|
|
@ -27,7 +27,7 @@ public:
|
|||
|
||||
static bool usePreAndPostPoseFromAnim;
|
||||
|
||||
AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag);
|
||||
AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag, bool mirrorFlag);
|
||||
virtual ~AnimClip() override;
|
||||
|
||||
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override;
|
||||
|
@ -36,6 +36,7 @@ public:
|
|||
void setEndFrameVar(const QString& endFrameVar) { _endFrameVar = endFrameVar; }
|
||||
void setTimeScaleVar(const QString& timeScaleVar) { _timeScaleVar = timeScaleVar; }
|
||||
void setLoopFlagVar(const QString& loopFlagVar) { _loopFlagVar = loopFlagVar; }
|
||||
void setMirrorFlagVar(const QString& mirrorFlagVar) { _mirrorFlagVar = mirrorFlagVar; }
|
||||
void setFrameVar(const QString& frameVar) { _frameVar = frameVar; }
|
||||
|
||||
float getStartFrame() const { return _startFrame; }
|
||||
|
@ -49,12 +50,16 @@ public:
|
|||
bool getLoopFlag() const { return _loopFlag; }
|
||||
void setLoopFlag(bool loopFlag) { _loopFlag = loopFlag; }
|
||||
|
||||
bool getMirrorFlag() const { return _mirrorFlag; }
|
||||
void setMirrorFlag(bool mirrorFlag) { _mirrorFlag = mirrorFlag; }
|
||||
|
||||
void loadURL(const QString& url);
|
||||
protected:
|
||||
|
||||
virtual void setCurrentFrameInternal(float frame) override;
|
||||
|
||||
void copyFromNetworkAnim();
|
||||
void buildMirrorAnim();
|
||||
|
||||
// for AnimDebugDraw rendering
|
||||
virtual const AnimPoseVec& getPosesInternal() const override;
|
||||
|
@ -64,18 +69,21 @@ protected:
|
|||
|
||||
// _anim[frame][joint]
|
||||
std::vector<AnimPoseVec> _anim;
|
||||
std::vector<AnimPoseVec> _mirrorAnim;
|
||||
|
||||
QString _url;
|
||||
float _startFrame;
|
||||
float _endFrame;
|
||||
float _timeScale;
|
||||
bool _loopFlag;
|
||||
bool _mirrorFlag;
|
||||
float _frame;
|
||||
|
||||
QString _startFrameVar;
|
||||
QString _endFrameVar;
|
||||
QString _timeScaleVar;
|
||||
QString _loopFlagVar;
|
||||
QString _mirrorFlagVar;
|
||||
QString _frameVar;
|
||||
|
||||
// no copies
|
||||
|
|
|
@ -590,17 +590,24 @@ void AnimInverseKinematics::initConstraints() {
|
|||
stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot);
|
||||
stConstraint->setTwistLimits(-PI / 4.0f, PI / 4.0f);
|
||||
|
||||
// these directions are approximate swing limits in root-frame
|
||||
// NOTE: they don't need to be normalized
|
||||
std::vector<glm::vec3> swungDirections;
|
||||
swungDirections.push_back(glm::vec3(mirror * 0.25f, 0.0f, 1.0f));
|
||||
swungDirections.push_back(glm::vec3(mirror * -0.5f, 0.0f, 1.0f));
|
||||
swungDirections.push_back(glm::vec3(mirror * -1.0f, 0.0f, 1.0f));
|
||||
swungDirections.push_back(glm::vec3(mirror * -1.0f, 0.0f, 0.0f));
|
||||
swungDirections.push_back(glm::vec3(mirror * -0.5f, -0.5f, -1.0f));
|
||||
swungDirections.push_back(glm::vec3(mirror * 0.0f, -0.75f, -1.0f));
|
||||
swungDirections.push_back(glm::vec3(mirror * 0.25f, -1.0f, 0.0f));
|
||||
swungDirections.push_back(glm::vec3(mirror * 0.25f, -1.0f, 1.0f));
|
||||
float deltaTheta = PI / 4.0f;
|
||||
float theta = 0.0f;
|
||||
swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.25f, sinf(theta)));
|
||||
theta += deltaTheta;
|
||||
swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta)));
|
||||
theta += deltaTheta;
|
||||
swungDirections.push_back(glm::vec3(mirror * cosf(theta), -0.25f, sinf(theta))); // posterior
|
||||
theta += deltaTheta;
|
||||
swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta)));
|
||||
theta += deltaTheta;
|
||||
swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.25f, sinf(theta)));
|
||||
theta += deltaTheta;
|
||||
swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.5f, sinf(theta)));
|
||||
theta += deltaTheta;
|
||||
swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.5f, sinf(theta))); // anterior
|
||||
theta += deltaTheta;
|
||||
swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.5f, sinf(theta)));
|
||||
|
||||
// rotate directions into joint-frame
|
||||
glm::quat invAbsoluteRotation = glm::inverse(absolutePoses[i].rot);
|
||||
|
@ -755,7 +762,7 @@ void AnimInverseKinematics::initConstraints() {
|
|||
// we determine the max/min angles by rotating the swing limit lines from parent- to child-frame
|
||||
// then measure the angles to swing the yAxis into alignment
|
||||
const float MIN_KNEE_ANGLE = 0.0f;
|
||||
const float MAX_KNEE_ANGLE = 3.0f * PI / 4.0f;
|
||||
const float MAX_KNEE_ANGLE = 7.0f * PI / 8.0f;
|
||||
glm::quat invReferenceRotation = glm::inverse(referenceRotation);
|
||||
glm::vec3 minSwingAxis = invReferenceRotation * glm::angleAxis(MIN_KNEE_ANGLE, hingeAxis) * Vectors::UNIT_Y;
|
||||
glm::vec3 maxSwingAxis = invReferenceRotation * glm::angleAxis(MAX_KNEE_ANGLE, hingeAxis) * Vectors::UNIT_Y;
|
||||
|
|
|
@ -67,6 +67,16 @@ static AnimNode::Type stringToAnimNodeType(const QString& str) {
|
|||
return AnimNode::Type::NumTypes;
|
||||
}
|
||||
|
||||
static AnimStateMachine::InterpType stringToInterpType(const QString& str) {
|
||||
if (str == "snapshotBoth") {
|
||||
return AnimStateMachine::InterpType::SnapshotBoth;
|
||||
} else if (str == "snapshotPrev") {
|
||||
return AnimStateMachine::InterpType::SnapshotPrev;
|
||||
} else {
|
||||
return AnimStateMachine::InterpType::NumTypes;
|
||||
}
|
||||
}
|
||||
|
||||
static const char* animManipulatorJointVarTypeToString(AnimManipulator::JointVar::Type type) {
|
||||
switch (type) {
|
||||
case AnimManipulator::JointVar::Type::AbsoluteRotation: return "absoluteRotation";
|
||||
|
@ -145,6 +155,14 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) {
|
|||
} \
|
||||
bool NAME = NAME##_VAL.toBool()
|
||||
|
||||
#define READ_OPTIONAL_BOOL(NAME, JSON_OBJ, DEFAULT) \
|
||||
auto NAME##_VAL = JSON_OBJ.value(#NAME); \
|
||||
bool NAME = DEFAULT; \
|
||||
if (NAME##_VAL.isBool()) { \
|
||||
NAME = NAME##_VAL.toBool(); \
|
||||
} \
|
||||
do {} while (0)
|
||||
|
||||
#define READ_FLOAT(NAME, JSON_OBJ, ID, URL, ERROR_RETURN) \
|
||||
auto NAME##_VAL = JSON_OBJ.value(#NAME); \
|
||||
if (!NAME##_VAL.isDouble()) { \
|
||||
|
@ -222,13 +240,15 @@ static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString&
|
|||
READ_FLOAT(endFrame, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_FLOAT(timeScale, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_BOOL(loopFlag, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_OPTIONAL_BOOL(mirrorFlag, jsonObj, false);
|
||||
|
||||
READ_OPTIONAL_STRING(startFrameVar, jsonObj);
|
||||
READ_OPTIONAL_STRING(endFrameVar, jsonObj);
|
||||
READ_OPTIONAL_STRING(timeScaleVar, jsonObj);
|
||||
READ_OPTIONAL_STRING(loopFlagVar, jsonObj);
|
||||
READ_OPTIONAL_STRING(mirrorFlagVar, jsonObj);
|
||||
|
||||
auto node = std::make_shared<AnimClip>(id, url, startFrame, endFrame, timeScale, loopFlag);
|
||||
auto node = std::make_shared<AnimClip>(id, url, startFrame, endFrame, timeScale, loopFlag, mirrorFlag);
|
||||
|
||||
if (!startFrameVar.isEmpty()) {
|
||||
node->setStartFrameVar(startFrameVar);
|
||||
|
@ -242,6 +262,9 @@ static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString&
|
|||
if (!loopFlagVar.isEmpty()) {
|
||||
node->setLoopFlagVar(loopFlagVar);
|
||||
}
|
||||
if (!mirrorFlagVar.isEmpty()) {
|
||||
node->setMirrorFlagVar(mirrorFlagVar);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
@ -465,9 +488,11 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj,
|
|||
READ_STRING(id, stateObj, nodeId, jsonUrl, false);
|
||||
READ_FLOAT(interpTarget, stateObj, nodeId, jsonUrl, false);
|
||||
READ_FLOAT(interpDuration, stateObj, nodeId, jsonUrl, false);
|
||||
READ_OPTIONAL_STRING(interpType, stateObj);
|
||||
|
||||
READ_OPTIONAL_STRING(interpTargetVar, stateObj);
|
||||
READ_OPTIONAL_STRING(interpDurationVar, stateObj);
|
||||
READ_OPTIONAL_STRING(interpTypeVar, stateObj);
|
||||
|
||||
auto iter = childMap.find(id);
|
||||
if (iter == childMap.end()) {
|
||||
|
@ -475,7 +500,16 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj,
|
|||
return false;
|
||||
}
|
||||
|
||||
auto statePtr = std::make_shared<AnimStateMachine::State>(id, iter->second, interpTarget, interpDuration);
|
||||
AnimStateMachine::InterpType interpTypeEnum = AnimStateMachine::InterpType::SnapshotBoth; // default value
|
||||
if (!interpType.isEmpty()) {
|
||||
interpTypeEnum = stringToInterpType(interpType);
|
||||
if (interpTypeEnum == AnimStateMachine::InterpType::NumTypes) {
|
||||
qCCritical(animation) << "AnimNodeLoader, bad interpType on stateMachine state, nodeId = " << nodeId << "stateId =" << id << "url = " << jsonUrl.toDisplayString();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto statePtr = std::make_shared<AnimStateMachine::State>(id, iter->second, interpTarget, interpDuration, interpTypeEnum);
|
||||
assert(statePtr);
|
||||
|
||||
if (!interpTargetVar.isEmpty()) {
|
||||
|
@ -484,6 +518,9 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj,
|
|||
if (!interpDurationVar.isEmpty()) {
|
||||
statePtr->setInterpDurationVar(interpDurationVar);
|
||||
}
|
||||
if (!interpTypeVar.isEmpty()) {
|
||||
statePtr->setInterpTypeVar(interpTypeVar);
|
||||
}
|
||||
|
||||
smNode->addState(statePtr);
|
||||
stateMap.insert(StateMap::value_type(statePtr->getID(), statePtr));
|
||||
|
|
|
@ -51,6 +51,11 @@ AnimPose AnimPose::inverse() const {
|
|||
return AnimPose(glm::inverse(static_cast<glm::mat4>(*this)));
|
||||
}
|
||||
|
||||
// mirror about x-axis without applying negative scale.
|
||||
AnimPose AnimPose::mirror() const {
|
||||
return AnimPose(scale, glm::quat(rot.w, rot.x, -rot.y, -rot.z), glm::vec3(-trans.x, trans.y, trans.z));
|
||||
}
|
||||
|
||||
AnimPose::operator glm::mat4() const {
|
||||
glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f);
|
||||
glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f);
|
||||
|
|
|
@ -30,6 +30,7 @@ struct AnimPose {
|
|||
AnimPose operator*(const AnimPose& rhs) const;
|
||||
|
||||
AnimPose inverse() const;
|
||||
AnimPose mirror() const;
|
||||
operator glm::mat4() const;
|
||||
|
||||
glm::vec3 scale;
|
||||
|
|
|
@ -87,7 +87,8 @@ AnimPose AnimSkeleton::getAbsolutePose(int jointIndex, const AnimPoseVec& poses)
|
|||
|
||||
void AnimSkeleton::convertRelativePosesToAbsolute(AnimPoseVec& poses) const {
|
||||
// poses start off relative and leave in absolute frame
|
||||
for (int i = 0; i < (int)poses.size() && i < (int)_joints.size(); ++i) {
|
||||
int lastIndex = std::min((int)poses.size(), (int)_joints.size());
|
||||
for (int i = 0; i < lastIndex; ++i) {
|
||||
int parentIndex = _joints[i].parentIndex;
|
||||
if (parentIndex != -1) {
|
||||
poses[i] = poses[parentIndex] * poses[i];
|
||||
|
@ -95,6 +96,30 @@ void AnimSkeleton::convertRelativePosesToAbsolute(AnimPoseVec& poses) const {
|
|||
}
|
||||
}
|
||||
|
||||
void AnimSkeleton::convertAbsolutePosesToRelative(AnimPoseVec& poses) const {
|
||||
// poses start off absolute and leave in relative frame
|
||||
int lastIndex = std::min((int)poses.size(), (int)_joints.size());
|
||||
for (int i = lastIndex - 1; i >= 0; --i) {
|
||||
int parentIndex = _joints[i].parentIndex;
|
||||
if (parentIndex != -1) {
|
||||
poses[i] = poses[parentIndex].inverse() * poses[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnimSkeleton::mirrorRelativePoses(AnimPoseVec& poses) const {
|
||||
convertRelativePosesToAbsolute(poses);
|
||||
mirrorAbsolutePoses(poses);
|
||||
convertAbsolutePosesToRelative(poses);
|
||||
}
|
||||
|
||||
void AnimSkeleton::mirrorAbsolutePoses(AnimPoseVec& poses) const {
|
||||
AnimPoseVec temp = poses;
|
||||
for (int i = 0; i < (int)poses.size(); i++) {
|
||||
poses[_mirrorMap[i]] = temp[i].mirror();
|
||||
}
|
||||
}
|
||||
|
||||
void AnimSkeleton::buildSkeletonFromJoints(const std::vector<FBXJoint>& joints) {
|
||||
_joints = joints;
|
||||
|
||||
|
@ -150,6 +175,24 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector<FBXJoint>& joints)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// build mirror map.
|
||||
_mirrorMap.reserve(_joints.size());
|
||||
for (int i = 0; i < (int)joints.size(); i++) {
|
||||
int mirrorJointIndex = -1;
|
||||
if (_joints[i].name.startsWith("Left")) {
|
||||
QString mirrorJointName = QString(_joints[i].name).replace(0, 4, "Right");
|
||||
mirrorJointIndex = nameToJointIndex(mirrorJointName);
|
||||
} else if (_joints[i].name.startsWith("Right")) {
|
||||
QString mirrorJointName = QString(_joints[i].name).replace(0, 5, "Left");
|
||||
mirrorJointIndex = nameToJointIndex(mirrorJointName);
|
||||
}
|
||||
if (mirrorJointIndex >= 0) {
|
||||
_mirrorMap.push_back(mirrorJointIndex);
|
||||
} else {
|
||||
_mirrorMap.push_back(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
|
|
|
@ -53,6 +53,10 @@ public:
|
|||
AnimPose getAbsolutePose(int jointIndex, const AnimPoseVec& poses) const;
|
||||
|
||||
void convertRelativePosesToAbsolute(AnimPoseVec& poses) const;
|
||||
void convertAbsolutePosesToRelative(AnimPoseVec& poses) const;
|
||||
|
||||
void mirrorRelativePoses(AnimPoseVec& poses) const;
|
||||
void mirrorAbsolutePoses(AnimPoseVec& poses) const;
|
||||
|
||||
#ifndef NDEBUG
|
||||
void dump() const;
|
||||
|
@ -69,6 +73,7 @@ protected:
|
|||
AnimPoseVec _absoluteDefaultPoses;
|
||||
AnimPoseVec _relativePreRotationPoses;
|
||||
AnimPoseVec _relativePostRotationPoses;
|
||||
std::vector<int> _mirrorMap;
|
||||
|
||||
// no copies
|
||||
AnimSkeleton(const AnimSkeleton&) = delete;
|
||||
|
|
|
@ -52,8 +52,25 @@ const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, fl
|
|||
if (_duringInterp) {
|
||||
_alpha += _alphaVel * dt;
|
||||
if (_alpha < 1.0f) {
|
||||
if (_poses.size() > 0 && _nextPoses.size() > 0 && _prevPoses.size() > 0) {
|
||||
::blend(_poses.size(), &_prevPoses[0], &_nextPoses[0], _alpha, &_poses[0]);
|
||||
AnimPoseVec* nextPoses = nullptr;
|
||||
AnimPoseVec* prevPoses = nullptr;
|
||||
AnimPoseVec localNextPoses;
|
||||
if (_interpType == InterpType::SnapshotBoth) {
|
||||
// interp between both snapshots
|
||||
prevPoses = &_prevPoses;
|
||||
nextPoses = &_nextPoses;
|
||||
} else if (_interpType == InterpType::SnapshotPrev) {
|
||||
// interp between the prev snapshot and evaluated next target.
|
||||
// this is useful for interping into a blend
|
||||
localNextPoses = currentStateNode->evaluate(animVars, dt, triggersOut);
|
||||
prevPoses = &_prevPoses;
|
||||
nextPoses = &localNextPoses;
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
if (_poses.size() > 0 && nextPoses && prevPoses && nextPoses->size() > 0 && prevPoses->size() > 0) {
|
||||
::blend(_poses.size(), &(prevPoses->at(0)), &(nextPoses->at(0)), _alpha, &_poses[0]);
|
||||
}
|
||||
} else {
|
||||
_duringInterp = false;
|
||||
|
@ -86,16 +103,32 @@ void AnimStateMachine::switchState(const AnimVariantMap& animVars, State::Pointe
|
|||
_alpha = 0.0f;
|
||||
float duration = std::max(0.001f, animVars.lookup(desiredState->_interpDurationVar, desiredState->_interpDuration));
|
||||
_alphaVel = FRAMES_PER_SECOND / duration;
|
||||
_prevPoses = _poses;
|
||||
nextStateNode->setCurrentFrame(desiredState->_interpTarget);
|
||||
_interpType = (InterpType)animVars.lookup(desiredState->_interpTypeVar, (int)desiredState->_interpType);
|
||||
|
||||
// because dt is 0, we should not encounter any triggers
|
||||
const float dt = 0.0f;
|
||||
Triggers triggers;
|
||||
_nextPoses = nextStateNode->evaluate(animVars, dt, triggers);
|
||||
|
||||
if (_interpType == InterpType::SnapshotBoth) {
|
||||
// snapshot previous pose.
|
||||
_prevPoses = _poses;
|
||||
// snapshot next pose at the target frame.
|
||||
nextStateNode->setCurrentFrame(desiredState->_interpTarget);
|
||||
_nextPoses = nextStateNode->evaluate(animVars, dt, triggers);
|
||||
} else if (_interpType == InterpType::SnapshotPrev) {
|
||||
// snapshot previoius pose
|
||||
_prevPoses = _poses;
|
||||
// no need to evaluate _nextPoses we will do it dynamically during the interp,
|
||||
// however we need to set the current frame.
|
||||
nextStateNode->setCurrentFrame(desiredState->_interpTarget - duration);
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
#if WANT_DEBUG
|
||||
qCDebug(animation) << "AnimStateMachine::switchState:" << _currentState->getID() << "->" << desiredState->getID() << "duration =" << duration << "targetFrame =" << desiredState->_interpTarget;
|
||||
qCDebug(animation) << "AnimStateMachine::switchState:" << _currentState->getID() << "->" << desiredState->getID() << "duration =" << duration << "targetFrame =" << desiredState->_interpTarget << "interpType = " << (int)_interpType;
|
||||
#endif
|
||||
|
||||
_currentState = desiredState;
|
||||
}
|
||||
|
||||
|
|
|
@ -31,13 +31,27 @@
|
|||
// visible after interpolation is complete.
|
||||
// * interpDuration - (frames) The total length of time it will take to interp between the current pose and the
|
||||
// interpTarget frame.
|
||||
// * interpType - How the interpolation is performed.
|
||||
// * SnapshotBoth: Stores two snapshots, the previous animation before interpolation begins and the target state at the
|
||||
// interTarget frame. Then during the interpolation period the two snapshots are interpolated to produce smooth motion between them.
|
||||
// * SnapshotPrev: Stores a snapshot of the previous animation before interpolation begins. However the target state is
|
||||
// evaluated dynamically. During the interpolation period the previous snapshot is interpolated with the target pose
|
||||
// to produce smooth motion between them. This mode is useful for interping into a blended animation where the actual
|
||||
// blend factor is not known at the start of the interp or is might change dramatically during the interp.
|
||||
|
||||
class AnimStateMachine : public AnimNode {
|
||||
public:
|
||||
friend class AnimNodeLoader;
|
||||
friend bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl);
|
||||
|
||||
enum class InterpType {
|
||||
SnapshotBoth = 0,
|
||||
SnapshotPrev,
|
||||
NumTypes
|
||||
};
|
||||
|
||||
protected:
|
||||
|
||||
class State {
|
||||
public:
|
||||
friend AnimStateMachine;
|
||||
|
@ -55,14 +69,16 @@ protected:
|
|||
State::Pointer _state;
|
||||
};
|
||||
|
||||
State(const QString& id, int childIndex, float interpTarget, float interpDuration) :
|
||||
State(const QString& id, int childIndex, float interpTarget, float interpDuration, InterpType interpType) :
|
||||
_id(id),
|
||||
_childIndex(childIndex),
|
||||
_interpTarget(interpTarget),
|
||||
_interpDuration(interpDuration) {}
|
||||
_interpDuration(interpDuration),
|
||||
_interpType(interpType) {}
|
||||
|
||||
void setInterpTargetVar(const QString& interpTargetVar) { _interpTargetVar = interpTargetVar; }
|
||||
void setInterpDurationVar(const QString& interpDurationVar) { _interpDurationVar = interpDurationVar; }
|
||||
void setInterpTypeVar(const QString& interpTypeVar) { _interpTypeVar = interpTypeVar; }
|
||||
|
||||
int getChildIndex() const { return _childIndex; }
|
||||
const QString& getID() const { return _id; }
|
||||
|
@ -78,9 +94,11 @@ protected:
|
|||
int _childIndex;
|
||||
float _interpTarget; // frames
|
||||
float _interpDuration; // frames
|
||||
InterpType _interpType;
|
||||
|
||||
QString _interpTargetVar;
|
||||
QString _interpDurationVar;
|
||||
QString _interpTypeVar;
|
||||
|
||||
std::vector<Transition> _transitions;
|
||||
|
||||
|
@ -115,6 +133,7 @@ protected:
|
|||
|
||||
// interpolation state
|
||||
bool _duringInterp = false;
|
||||
InterpType _interpType { InterpType::SnapshotBoth };
|
||||
float _alphaVel = 0.0f;
|
||||
float _alpha = 0.0f;
|
||||
AnimPoseVec _prevPoses;
|
||||
|
|
|
@ -120,7 +120,7 @@ void Rig::overrideRoleAnimation(const QString& role, const QString& url, float f
|
|||
_origRoleAnimations[role] = node;
|
||||
const float REFERENCE_FRAMES_PER_SECOND = 30.0f;
|
||||
float timeScale = fps / REFERENCE_FRAMES_PER_SECOND;
|
||||
auto clipNode = std::make_shared<AnimClip>(role, url, firstFrame, lastFrame, timeScale, loop);
|
||||
auto clipNode = std::make_shared<AnimClip>(role, url, firstFrame, lastFrame, timeScale, loop, false);
|
||||
AnimNode::Pointer parent = node->getParent();
|
||||
parent->replaceChild(node, clipNode);
|
||||
} else {
|
||||
|
@ -152,7 +152,7 @@ void Rig::prefetchAnimation(const QString& url) {
|
|||
|
||||
// This will begin loading the NetworkGeometry for the given URL.
|
||||
// which should speed us up if we request it later via overrideAnimation.
|
||||
auto clipNode = std::make_shared<AnimClip>("prefetch", url, 0, 0, 1.0, false);
|
||||
auto clipNode = std::make_shared<AnimClip>("prefetch", url, 0, 0, 1.0, false, false);
|
||||
_prefetchedAnimations.push_back(clipNode);
|
||||
}
|
||||
|
||||
|
@ -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,15 @@ 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::Takeoff && _desiredState == RigRole::InAir) {
|
||||
_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 +681,12 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
_animVars.set("isNotTurning", true);
|
||||
_animVars.set("isFlying", false);
|
||||
_animVars.set("isNotFlying", true);
|
||||
_animVars.set("isTakeoffStand", false);
|
||||
_animVars.set("isTakeoffRun", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
_animVars.set("isInAirStand", false);
|
||||
_animVars.set("isInAirRun", false);
|
||||
_animVars.set("isNotInAir", true);
|
||||
}
|
||||
} else if (_state == RigRole::Turn) {
|
||||
if (turningSpeed > 0.0f) {
|
||||
|
@ -682,6 +707,13 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
_animVars.set("isNotMoving", true);
|
||||
_animVars.set("isFlying", false);
|
||||
_animVars.set("isNotFlying", true);
|
||||
_animVars.set("isTakeoffStand", false);
|
||||
_animVars.set("isTakeoffRun", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
_animVars.set("isInAirStand", false);
|
||||
_animVars.set("isInAirRun", false);
|
||||
_animVars.set("isNotInAir", true);
|
||||
|
||||
} else if (_state == RigRole::Idle ) {
|
||||
// default anim vars to notMoving and notTurning
|
||||
_animVars.set("isMovingForward", false);
|
||||
|
@ -694,7 +726,14 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
_animVars.set("isNotTurning", true);
|
||||
_animVars.set("isFlying", false);
|
||||
_animVars.set("isNotFlying", true);
|
||||
} else {
|
||||
_animVars.set("isTakeoffStand", false);
|
||||
_animVars.set("isTakeoffRun", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
_animVars.set("isInAirStand", false);
|
||||
_animVars.set("isInAirRun", false);
|
||||
_animVars.set("isNotInAir", true);
|
||||
|
||||
} else if (_state == RigRole::Hover) {
|
||||
// flying.
|
||||
_animVars.set("isMovingForward", false);
|
||||
_animVars.set("isMovingBackward", false);
|
||||
|
@ -706,15 +745,82 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
_animVars.set("isNotTurning", true);
|
||||
_animVars.set("isFlying", true);
|
||||
_animVars.set("isNotFlying", false);
|
||||
_animVars.set("isTakeoffStand", false);
|
||||
_animVars.set("isTakeoffRun", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
_animVars.set("isInAirStand", false);
|
||||
_animVars.set("isInAirRun", 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);
|
||||
|
||||
bool takeOffRun = forwardSpeed > 0.1f;
|
||||
if (takeOffRun) {
|
||||
_animVars.set("isTakeoffStand", false);
|
||||
_animVars.set("isTakeoffRun", true);
|
||||
} else {
|
||||
_animVars.set("isTakeoffStand", true);
|
||||
_animVars.set("isTakeoffRun", false);
|
||||
}
|
||||
|
||||
_animVars.set("isNotTakeoff", false);
|
||||
_animVars.set("isInAirStand", false);
|
||||
_animVars.set("isInAirRun", false);
|
||||
_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("isTakeoffStand", false);
|
||||
_animVars.set("isTakeoffRun", false);
|
||||
_animVars.set("isNotTakeoff", true);
|
||||
|
||||
bool inAirRun = forwardSpeed > 0.1f;
|
||||
if (inAirRun) {
|
||||
_animVars.set("isInAirStand", false);
|
||||
_animVars.set("isInAirRun", true);
|
||||
} else {
|
||||
_animVars.set("isInAirStand", true);
|
||||
_animVars.set("isInAirRun", false);
|
||||
}
|
||||
_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 { true };
|
||||
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,6 +22,16 @@ AvatarHashMap::AvatarHashMap() {
|
|||
connect(DependencyManager::get<NodeList>().data(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged);
|
||||
}
|
||||
|
||||
QVector<QUuid> AvatarHashMap::getAvatarIdentifiers() {
|
||||
QReadLocker locker(&_hashLock);
|
||||
return _avatarHash.keys().toVector();
|
||||
}
|
||||
|
||||
AvatarData* AvatarHashMap::getAvatar(QUuid avatarID) {
|
||||
// Null/Default-constructed QUuids will return MyAvatar
|
||||
return getAvatarBySessionID(avatarID).get();
|
||||
}
|
||||
|
||||
bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range) {
|
||||
auto hashCopy = getHashCopy();
|
||||
foreach(const AvatarSharedPointer& sharedAvatar, hashCopy) {
|
||||
|
|
|
@ -34,6 +34,12 @@ public:
|
|||
AvatarHash getHashCopy() { QReadLocker lock(&_hashLock); return _avatarHash; }
|
||||
int size() { return _avatarHash.size(); }
|
||||
|
||||
// Currently, your own avatar will be included as the null avatar id.
|
||||
Q_INVOKABLE QVector<QUuid> getAvatarIdentifiers();
|
||||
Q_INVOKABLE AvatarData* getAvatar(QUuid avatarID);
|
||||
|
||||
virtual AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) { return findAvatar(sessionID); }
|
||||
|
||||
signals:
|
||||
void avatarAddedEvent(const QUuid& sessionUUID);
|
||||
void avatarRemovedEvent(const QUuid& sessionUUID);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -652,6 +652,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
|||
}
|
||||
if (_simulationOwner.set(newSimOwner)) {
|
||||
_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
{ // When we own the simulation we don't accept updates to the entity's transform/velocities
|
||||
|
@ -987,7 +988,7 @@ EntityTreePointer EntityItem::getTree() const {
|
|||
return tree;
|
||||
}
|
||||
|
||||
bool EntityItem::wantTerseEditLogging() {
|
||||
bool EntityItem::wantTerseEditLogging() const {
|
||||
EntityTreePointer tree = getTree();
|
||||
return tree ? tree->wantTerseEditLogging() : false;
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ public:
|
|||
quint64 getLastBroadcast() const { return _lastBroadcast; }
|
||||
void setLastBroadcast(quint64 lastBroadcast) { _lastBroadcast = lastBroadcast; }
|
||||
|
||||
void markAsChangedOnServer() { _changedOnServer = usecTimestampNow(); }
|
||||
void markAsChangedOnServer() { _changedOnServer = usecTimestampNow(); }
|
||||
quint64 getLastChangedOnServer() const { return _changedOnServer; }
|
||||
|
||||
// TODO: eventually only include properties changed since the params.lastViewFrustumSent time
|
||||
|
@ -351,14 +351,14 @@ public:
|
|||
void setPhysicsInfo(void* data) { _physicsInfo = data; }
|
||||
EntityTreeElementPointer getElement() const { return _element; }
|
||||
EntityTreePointer getTree() const;
|
||||
bool wantTerseEditLogging();
|
||||
bool wantTerseEditLogging() const;
|
||||
|
||||
glm::mat4 getEntityToWorldMatrix() const;
|
||||
glm::mat4 getWorldToEntityMatrix() const;
|
||||
glm::vec3 worldToEntity(const glm::vec3& point) const;
|
||||
glm::vec3 entityToWorld(const glm::vec3& point) const;
|
||||
|
||||
quint64 getLastEditedFromRemote() { return _lastEditedFromRemote; }
|
||||
quint64 getLastEditedFromRemote() const { return _lastEditedFromRemote; }
|
||||
|
||||
void getAllTerseUpdateProperties(EntityItemProperties& properties) const;
|
||||
|
||||
|
|
|
@ -910,7 +910,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem
|
|||
success = packetData->startSubTree(octcode);
|
||||
delete[] octcode;
|
||||
|
||||
// assuming we have rome to fit our octalCode, proceed...
|
||||
// assuming we have room to fit our octalCode, proceed...
|
||||
if (success) {
|
||||
|
||||
// Now add our edit content details...
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "EntityScriptingInterface.h"
|
||||
|
||||
#include "EntityItemID.h"
|
||||
|
@ -25,8 +24,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);
|
||||
|
@ -122,6 +122,20 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
|
|||
EntityItemProperties propertiesWithSimID = convertLocationFromScriptSemantics(properties);
|
||||
propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged());
|
||||
|
||||
auto dimensions = propertiesWithSimID.getDimensions();
|
||||
float volume = dimensions.x * dimensions.y * dimensions.z;
|
||||
auto density = propertiesWithSimID.getDensity();
|
||||
auto newVelocity = propertiesWithSimID.getVelocity().length();
|
||||
float cost = calculateCost(density * volume, 0, newVelocity);
|
||||
cost *= costMultiplier;
|
||||
|
||||
if(cost > _currentAvatarEnergy) {
|
||||
return QUuid();
|
||||
} else {
|
||||
//debit the avatar energy and continue
|
||||
emit debitEnergySource(cost);
|
||||
}
|
||||
|
||||
EntityItemID id = EntityItemID(QUuid::createUuid());
|
||||
|
||||
// If we have a local entity tree set, then also update it.
|
||||
|
@ -130,17 +144,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 {
|
||||
|
@ -211,9 +228,28 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit
|
|||
|
||||
QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& scriptSideProperties) {
|
||||
EntityItemProperties properties = scriptSideProperties;
|
||||
|
||||
auto dimensions = properties.getDimensions();
|
||||
float volume = dimensions.x * dimensions.y * dimensions.z;
|
||||
auto density = properties.getDensity();
|
||||
auto newVelocity = properties.getVelocity().length();
|
||||
float oldVelocity = { 0.0f };
|
||||
|
||||
EntityItemID entityID(id);
|
||||
if (!_entityTree) {
|
||||
queueEntityMessage(PacketType::EntityEdit, entityID, properties);
|
||||
|
||||
//if there is no local entity entity tree, no existing velocity, use 0.
|
||||
float cost = calculateCost(density * volume, oldVelocity, newVelocity);
|
||||
cost *= costMultiplier;
|
||||
|
||||
if(cost > _currentAvatarEnergy) {
|
||||
return QUuid();
|
||||
} else {
|
||||
//debit the avatar energy and continue
|
||||
emit debitEnergySource(cost);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
// If we have a local entity tree set, then also update it.
|
||||
|
@ -227,6 +263,9 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
|
|||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
//existing entity, retrieve old velocity for check down below
|
||||
oldVelocity = entity->getVelocity().length();
|
||||
|
||||
if (!scriptSideProperties.parentIDChanged()) {
|
||||
properties.setParentID(entity->getParentID());
|
||||
}
|
||||
|
@ -242,6 +281,16 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
|
|||
}
|
||||
properties = convertLocationFromScriptSemantics(properties);
|
||||
updatedEntity = _entityTree->updateEntity(entityID, properties);
|
||||
|
||||
float cost = calculateCost(density * volume, oldVelocity, newVelocity);
|
||||
cost *= costMultiplier;
|
||||
|
||||
if(cost > _currentAvatarEnergy) {
|
||||
updatedEntity = false;
|
||||
} else {
|
||||
//debit the avatar energy and continue
|
||||
emit debitEnergySource(cost);
|
||||
}
|
||||
});
|
||||
|
||||
if (!updatedEntity) {
|
||||
|
@ -255,7 +304,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();
|
||||
|
||||
|
@ -316,6 +365,21 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
|
|||
_entityTree->withWriteLock([&] {
|
||||
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
|
||||
if (entity) {
|
||||
|
||||
auto dimensions = entity->getDimensions();
|
||||
float volume = dimensions.x * dimensions.y * dimensions.z;
|
||||
auto density = entity->getDensity();
|
||||
auto velocity = entity->getVelocity().length();
|
||||
float cost = calculateCost(density * volume, velocity, 0);
|
||||
cost *= costMultiplier;
|
||||
|
||||
if(cost > _currentAvatarEnergy) {
|
||||
return;
|
||||
} else {
|
||||
//debit the avatar energy and continue
|
||||
emit debitEnergySource(cost);
|
||||
}
|
||||
|
||||
if (entity->getLocked()) {
|
||||
shouldDelete = false;
|
||||
} else {
|
||||
|
@ -992,3 +1056,20 @@ QStringList EntityScriptingInterface::getJointNames(const QUuid& entityID) {
|
|||
Q_RETURN_ARG(QStringList, result), Q_ARG(QUuid, entityID));
|
||||
return result;
|
||||
}
|
||||
|
||||
float EntityScriptingInterface::calculateCost(float mass, float oldVelocity, float newVelocity) {
|
||||
return std::abs(mass * (newVelocity - oldVelocity));
|
||||
}
|
||||
|
||||
void EntityScriptingInterface::setCurrentAvatarEnergy(float energy) {
|
||||
// qCDebug(entities) << "NEW AVATAR ENERGY IN ENTITY SCRIPTING INTERFACE: " << energy;
|
||||
_currentAvatarEnergy = energy;
|
||||
}
|
||||
|
||||
float EntityScriptingInterface::getCostMultiplier() {
|
||||
return costMultiplier;
|
||||
}
|
||||
|
||||
void EntityScriptingInterface::setCostMultiplier(float value) {
|
||||
costMultiplier = value;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QStringList>
|
||||
#include <QtQml/QJSValue>
|
||||
#include <QtQml/QJSValueList>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
#include <Octree.h>
|
||||
|
@ -57,8 +59,11 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra
|
|||
/// handles scripting of Entity commands from JS passed to assigned clients
|
||||
class EntityScriptingInterface : public OctreeScriptingInterface, public Dependency {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(float currentAvatarEnergy READ getCurrentAvatarEnergy WRITE setCurrentAvatarEnergy)
|
||||
Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier)
|
||||
public:
|
||||
EntityScriptingInterface();
|
||||
EntityScriptingInterface(bool bidOnSimulationOwnership);
|
||||
|
||||
EntityEditPacketSender* getEntityPacketSender() const { return (EntityEditPacketSender*)getPacketSender(); }
|
||||
virtual NodeType_t getServerNodeType() const { return NodeType::EntityServer; }
|
||||
|
@ -67,7 +72,7 @@ public:
|
|||
void setEntityTree(EntityTreePointer modelTree);
|
||||
EntityTreePointer getEntityTree() { return _entityTree; }
|
||||
void setEntitiesScriptEngine(EntitiesScriptEngineProvider* engine) { _entitiesScriptEngine = engine; }
|
||||
|
||||
float calculateCost(float mass, float oldVelocity, float newVelocity);
|
||||
public slots:
|
||||
|
||||
// returns true if the DomainServer will allow this Node/Avatar to make changes
|
||||
|
@ -163,6 +168,7 @@ public slots:
|
|||
|
||||
Q_INVOKABLE int getJointIndex(const QUuid& entityID, const QString& name);
|
||||
Q_INVOKABLE QStringList getJointNames(const QUuid& entityID);
|
||||
|
||||
|
||||
signals:
|
||||
void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision);
|
||||
|
@ -188,6 +194,7 @@ signals:
|
|||
void deletingEntity(const EntityItemID& entityID);
|
||||
void addingEntity(const EntityItemID& entityID);
|
||||
void clearingEntities();
|
||||
void debitEnergySource(float value);
|
||||
|
||||
private:
|
||||
bool actionWorker(const QUuid& entityID, std::function<bool(EntitySimulation*, EntityItemPointer)> actor);
|
||||
|
@ -204,7 +211,16 @@ private:
|
|||
bool precisionPicking, const QVector<EntityItemID>& entityIdsToInclude, const QVector<EntityItemID>& entityIdsToDiscard);
|
||||
|
||||
EntityTreePointer _entityTree;
|
||||
EntitiesScriptEngineProvider* _entitiesScriptEngine = nullptr;
|
||||
EntitiesScriptEngineProvider* _entitiesScriptEngine { nullptr };
|
||||
|
||||
bool _bidOnSimulationOwnership { false };
|
||||
float _currentAvatarEnergy = { FLT_MAX };
|
||||
float getCurrentAvatarEnergy() { return _currentAvatarEnergy; }
|
||||
void setCurrentAvatarEnergy(float energy);
|
||||
|
||||
float costMultiplier = { 0.01f };
|
||||
float getCostMultiplier();
|
||||
void setCostMultiplier(float value);
|
||||
};
|
||||
|
||||
#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) {
|
||||
|
|
|
@ -11,38 +11,58 @@
|
|||
|
||||
//#include <PerfStat.h>
|
||||
|
||||
#include "EntityItem.h"
|
||||
#include "SimpleEntitySimulation.h"
|
||||
|
||||
#include <DirtyOctreeElementOperator.h>
|
||||
|
||||
#include "EntityItem.h"
|
||||
#include "EntitiesLogging.h"
|
||||
|
||||
const quint64 AUTO_REMOVE_SIMULATION_OWNER_USEC = 2 * USECS_PER_SECOND;
|
||||
const quint64 MIN_SIMULATION_OWNERSHIP_UPDATE_PERIOD = 2 * USECS_PER_SECOND;
|
||||
|
||||
void SimpleEntitySimulation::updateEntitiesInternal(const quint64& now) {
|
||||
// If an Entity has a simulation owner and we don't get an update for some amount of time,
|
||||
// clear the owner. This guards against an interface failing to release the Entity when it
|
||||
// has finished simulating it.
|
||||
auto nodeList = DependencyManager::get<LimitedNodeList>();
|
||||
if (_entitiesWithSimulator.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (now < _nextSimulationExpiry) {
|
||||
// nothing has expired yet
|
||||
return;
|
||||
}
|
||||
|
||||
// If an Entity has a simulation owner but there has been no update for a while: clear the owner.
|
||||
// If an Entity goes ownerless for too long: zero velocity and remove from _entitiesWithSimulator.
|
||||
_nextSimulationExpiry = now + MIN_SIMULATION_OWNERSHIP_UPDATE_PERIOD;
|
||||
|
||||
QMutexLocker lock(&_mutex);
|
||||
SetOfEntities::iterator itemItr = _entitiesWithSimulator.begin();
|
||||
while (itemItr != _entitiesWithSimulator.end()) {
|
||||
EntityItemPointer entity = *itemItr;
|
||||
if (entity->getSimulatorID().isNull()) {
|
||||
itemItr = _entitiesWithSimulator.erase(itemItr);
|
||||
} else if (now - entity->getLastChangedOnServer() >= AUTO_REMOVE_SIMULATION_OWNER_USEC) {
|
||||
SharedNodePointer ownerNode = nodeList->nodeWithUUID(entity->getSimulatorID());
|
||||
if (ownerNode.isNull() || !ownerNode->isAlive()) {
|
||||
qCDebug(entities) << "auto-removing simulation owner" << entity->getSimulatorID();
|
||||
entity->clearSimulationOwnership();
|
||||
itemItr = _entitiesWithSimulator.erase(itemItr);
|
||||
quint64 expiry = entity->getLastChangedOnServer() + MIN_SIMULATION_OWNERSHIP_UPDATE_PERIOD;
|
||||
if (expiry < now) {
|
||||
if (entity->getSimulatorID().isNull()) {
|
||||
// no simulators are volunteering
|
||||
// zero the velocity on this entity so that it doesn't drift far away
|
||||
entity->setVelocity(glm::vec3(0.0f));
|
||||
entity->setVelocity(Vectors::ZERO);
|
||||
entity->setAngularVelocity(Vectors::ZERO);
|
||||
entity->setAcceleration(Vectors::ZERO);
|
||||
// remove from list
|
||||
itemItr = _entitiesWithSimulator.erase(itemItr);
|
||||
continue;
|
||||
} else {
|
||||
++itemItr;
|
||||
// the simulator has stopped updating this object
|
||||
// clear ownership and restart timer, giving nearby simulators time to volunteer
|
||||
qCDebug(entities) << "auto-removing simulation owner " << entity->getSimulatorID();
|
||||
entity->clearSimulationOwnership();
|
||||
}
|
||||
} else {
|
||||
++itemItr;
|
||||
entity->markAsChangedOnServer();
|
||||
// dirty all the tree elements that contain the entity
|
||||
DirtyOctreeElementOperator op(entity->getElement());
|
||||
getEntityTree()->recurseTreeWithOperator(&op);
|
||||
} else if (expiry < _nextSimulationExpiry) {
|
||||
_nextSimulationExpiry = expiry;
|
||||
}
|
||||
++itemItr;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ protected:
|
|||
virtual void clearEntitiesInternal() override;
|
||||
|
||||
SetOfEntities _entitiesWithSimulator;
|
||||
quint64 _nextSimulationExpiry { 0 };
|
||||
};
|
||||
|
||||
#endif // hifi_SimpleEntitySimulation_h
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
|
||||
#include <NumericalConstants.h>
|
||||
|
||||
// static
|
||||
// static
|
||||
const int SimulationOwner::NUM_BYTES_ENCODED = NUM_BYTES_RFC4122_UUID + 1;
|
||||
|
||||
|
||||
SimulationOwner::SimulationOwner(const SimulationOwner& other)
|
||||
SimulationOwner::SimulationOwner(const SimulationOwner& other)
|
||||
: _id(other._id), _priority(other._priority), _expiry(other._expiry) {
|
||||
}
|
||||
|
||||
|
@ -48,11 +48,6 @@ void SimulationOwner::clear() {
|
|||
|
||||
void SimulationOwner::setPriority(quint8 priority) {
|
||||
_priority = priority;
|
||||
if (_priority == 0) {
|
||||
// when priority is zero we clear everything
|
||||
_expiry = 0;
|
||||
_id = QUuid();
|
||||
}
|
||||
}
|
||||
|
||||
void SimulationOwner::promotePriority(quint8 priority) {
|
||||
|
|
|
@ -18,10 +18,10 @@
|
|||
#include <SharedUtil.h>
|
||||
#include <UUID.h>
|
||||
|
||||
const quint8 NO_PRORITY = 0x00;
|
||||
const quint8 ZERO_SIMULATION_PRIORITY = 0x00;
|
||||
|
||||
// Simulation observers will bid to simulate unowned active objects at the lowest possible priority
|
||||
// which is VOLUNTEER. If the server accepts a VOLUNTEER bid it will automatically bump it
|
||||
// which is VOLUNTEER. If the server accepts a VOLUNTEER bid it will automatically bump it
|
||||
// to RECRUIT priority so that other volunteers don't accidentally take over.
|
||||
const quint8 VOLUNTEER_SIMULATION_PRIORITY = 0x01;
|
||||
const quint8 RECRUIT_SIMULATION_PRIORITY = VOLUNTEER_SIMULATION_PRIORITY + 1;
|
||||
|
|
|
@ -82,8 +82,12 @@ void Connection::resetRTT() {
|
|||
|
||||
SendQueue& Connection::getSendQueue() {
|
||||
if (!_sendQueue) {
|
||||
|
||||
// we may have a sequence number from the previous inactive queue - re-use that so that the
|
||||
// receiver is getting the sequence numbers it expects (given that the connection must still be active)
|
||||
|
||||
// Lasily create send queue
|
||||
_sendQueue = SendQueue::create(_parentSocket, _destination);
|
||||
_sendQueue = SendQueue::create(_parentSocket, _destination, _inactiveSendQueueSequenceNumber);
|
||||
|
||||
#ifdef UDT_CONNECTION_DEBUG
|
||||
qCDebug(networking) << "Created SendQueue for connection to" << _destination;
|
||||
|
@ -105,6 +109,10 @@ SendQueue& Connection::getSendQueue() {
|
|||
}
|
||||
|
||||
void Connection::queueInactive() {
|
||||
// get the current sequence number from the send queue, this is to be re-used if the send
|
||||
// queue is re-activated for this connection
|
||||
_inactiveSendQueueSequenceNumber = _sendQueue->getCurrentSequenceNumber();
|
||||
|
||||
// tell our current send queue to go down and reset our ptr to it to null
|
||||
stopSendQueue();
|
||||
|
||||
|
|
|
@ -139,6 +139,8 @@ private:
|
|||
|
||||
SequenceNumber _lastSentACK; // The last sent ACK
|
||||
SequenceNumber _lastSentACK2; // The last sent ACK sub-sequence number in an ACK2
|
||||
|
||||
SequenceNumber _inactiveSendQueueSequenceNumber { 0 };
|
||||
|
||||
int _acksDuringSYN { 1 }; // The number of non-SYN ACKs sent during SYN
|
||||
int _lightACKsDuringSYN { 1 }; // The number of lite ACKs sent during SYN interval
|
||||
|
|
|
@ -52,11 +52,11 @@ private:
|
|||
Mutex2& _mutex2;
|
||||
};
|
||||
|
||||
std::unique_ptr<SendQueue> SendQueue::create(Socket* socket, HifiSockAddr destination) {
|
||||
std::unique_ptr<SendQueue> SendQueue::create(Socket* socket, HifiSockAddr destination, SequenceNumber currentSequenceNumber) {
|
||||
Q_ASSERT_X(socket, "SendQueue::create", "Must be called with a valid Socket*");
|
||||
|
||||
auto queue = std::unique_ptr<SendQueue>(new SendQueue(socket, destination));
|
||||
|
||||
auto queue = std::unique_ptr<SendQueue>(new SendQueue(socket, destination, currentSequenceNumber));
|
||||
|
||||
// Setup queue private thread
|
||||
QThread* thread = new QThread;
|
||||
thread->setObjectName("Networking: SendQueue " + destination.objectName()); // Name thread for easier debug
|
||||
|
@ -74,10 +74,12 @@ std::unique_ptr<SendQueue> SendQueue::create(Socket* socket, HifiSockAddr destin
|
|||
return queue;
|
||||
}
|
||||
|
||||
SendQueue::SendQueue(Socket* socket, HifiSockAddr dest) :
|
||||
SendQueue::SendQueue(Socket* socket, HifiSockAddr dest, SequenceNumber currentSequenceNumber) :
|
||||
_socket(socket),
|
||||
_destination(dest)
|
||||
_destination(dest),
|
||||
_currentSequenceNumber(currentSequenceNumber)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void SendQueue::queuePacket(std::unique_ptr<Packet> packet) {
|
||||
|
@ -389,6 +391,7 @@ bool SendQueue::isInactive(bool sentAPacket) {
|
|||
static const int NUM_TIMEOUTS_BEFORE_INACTIVE = 16;
|
||||
static const int MIN_SECONDS_BEFORE_INACTIVE_MS = 5 * 1000;
|
||||
if (_timeoutExpiryCount >= NUM_TIMEOUTS_BEFORE_INACTIVE &&
|
||||
_lastReceiverResponse > 0 &&
|
||||
(QDateTime::currentMSecsSinceEpoch() - _lastReceiverResponse) > MIN_SECONDS_BEFORE_INACTIVE_MS) {
|
||||
// If the flow window has been full for over CONSIDER_INACTIVE_AFTER,
|
||||
// then signal the queue is inactive and return so it can be cleaned up
|
||||
|
|
|
@ -50,7 +50,8 @@ public:
|
|||
Stopped
|
||||
};
|
||||
|
||||
static std::unique_ptr<SendQueue> create(Socket* socket, HifiSockAddr destination);
|
||||
static std::unique_ptr<SendQueue> create(Socket* socket, HifiSockAddr destination,
|
||||
SequenceNumber currentSequenceNumber = SequenceNumber());
|
||||
|
||||
void queuePacket(std::unique_ptr<Packet> packet);
|
||||
void queuePacketList(std::unique_ptr<PacketList> packetList);
|
||||
|
@ -83,7 +84,7 @@ private slots:
|
|||
void run();
|
||||
|
||||
private:
|
||||
SendQueue(Socket* socket, HifiSockAddr dest);
|
||||
SendQueue(Socket* socket, HifiSockAddr dest, SequenceNumber currentSequenceNumber);
|
||||
SendQueue(SendQueue& other) = delete;
|
||||
SendQueue(SendQueue&& other) = delete;
|
||||
|
||||
|
@ -108,7 +109,7 @@ private:
|
|||
|
||||
std::atomic<uint32_t> _lastACKSequenceNumber { 0 }; // Last ACKed sequence number
|
||||
|
||||
SequenceNumber _currentSequenceNumber; // Last sequence number sent out
|
||||
SequenceNumber _currentSequenceNumber { 0 }; // Last sequence number sent out
|
||||
std::atomic<uint32_t> _atomicCurrentSequenceNumber { 0 }; // Atomic for last sequence number sent out
|
||||
|
||||
std::atomic<int> _packetSendPeriod { 0 }; // Interval between two packet send event in microseconds, set from CC
|
||||
|
|
30
libraries/octree/src/DirtyOctreeElementOperator.cpp
Normal file
30
libraries/octree/src/DirtyOctreeElementOperator.cpp
Normal file
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// DirtyOctreeElementOperator.cpp
|
||||
// libraries/entities/src
|
||||
//
|
||||
// Created by Andrew Meawdows 2016.02.04
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "DirtyOctreeElementOperator.h"
|
||||
|
||||
DirtyOctreeElementOperator::DirtyOctreeElementOperator(OctreeElementPointer element)
|
||||
: _element(element) {
|
||||
assert(_element.get());
|
||||
_point = _element->getAACube().calcCenter();
|
||||
}
|
||||
|
||||
bool DirtyOctreeElementOperator::preRecursion(OctreeElementPointer element) {
|
||||
if (element == _element) {
|
||||
return false;
|
||||
}
|
||||
return element->getAACube().contains(_point);
|
||||
}
|
||||
|
||||
bool DirtyOctreeElementOperator::postRecursion(OctreeElementPointer element) {
|
||||
element->markWithChangedTime();
|
||||
return true;
|
||||
}
|
30
libraries/octree/src/DirtyOctreeElementOperator.h
Normal file
30
libraries/octree/src/DirtyOctreeElementOperator.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// DirtyOctreeElementOperator.h
|
||||
// libraries/entities/src
|
||||
//
|
||||
// Created by Andrew Meawdows 2016.02.04
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#ifndef hifi_DirtyOctreeElementOperator_h
|
||||
#define hifi_DirtyOctreeElementOperator_h
|
||||
|
||||
#include "Octree.h"
|
||||
|
||||
class DirtyOctreeElementOperator : public RecurseOctreeOperator {
|
||||
public:
|
||||
DirtyOctreeElementOperator(OctreeElementPointer element);
|
||||
|
||||
~DirtyOctreeElementOperator() {}
|
||||
|
||||
virtual bool preRecursion(OctreeElementPointer element);
|
||||
virtual bool postRecursion(OctreeElementPointer element);
|
||||
private:
|
||||
glm::vec3 _point;
|
||||
OctreeElementPointer _element;
|
||||
};
|
||||
|
||||
#endif // hifi_DirtyOctreeElementOperator_h
|
|
@ -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,18 @@ 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;
|
||||
_rayHitStartTime = 0;
|
||||
_takeoffToInAirStartTime = 0;
|
||||
_jumpButtonDownStartTime = 0;
|
||||
_jumpButtonDownCount = 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 +113,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 +127,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 +163,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 +253,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 +261,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 +346,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 +355,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);
|
||||
|
@ -408,8 +418,14 @@ glm::vec3 CharacterController::getLinearVelocity() const {
|
|||
|
||||
void CharacterController::preSimulation() {
|
||||
if (_enabled && _dynamicsWorld) {
|
||||
quint64 now = usecTimestampNow();
|
||||
|
||||
// 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
|
||||
|
@ -419,37 +435,82 @@ void CharacterController::preSimulation() {
|
|||
btScalar rayLength = _radius + MAX_FALL_HEIGHT;
|
||||
btVector3 rayEnd = rayStart - rayLength * _currentUp;
|
||||
|
||||
const btScalar JUMP_PROXIMITY_THRESHOLD = 0.1f * _radius;
|
||||
const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND;
|
||||
const btScalar MIN_HOVER_HEIGHT = 2.5f;
|
||||
const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND;
|
||||
const btScalar MAX_WALKING_SPEED = 2.5f;
|
||||
const quint64 RAY_HIT_START_PERIOD = 500 * MSECS_PER_SECOND;
|
||||
|
||||
ClosestNotMe rayCallback(_rigidBody);
|
||||
rayCallback.m_closestHitFraction = 1.0f;
|
||||
_dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback);
|
||||
if (rayCallback.hasHit()) {
|
||||
bool rayHasHit = rayCallback.hasHit();
|
||||
if (rayHasHit) {
|
||||
_rayHitStartTime = now;
|
||||
_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 if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) {
|
||||
rayHasHit = true;
|
||||
} else {
|
||||
_floorDistance = FLT_MAX;
|
||||
setHovering(true);
|
||||
}
|
||||
|
||||
if (_pendingFlags & PENDING_FLAG_JUMP) {
|
||||
_pendingFlags &= ~ PENDING_FLAG_JUMP;
|
||||
if (onGround()) {
|
||||
_isJumping = true;
|
||||
btVector3 velocity = _rigidBody->getLinearVelocity();
|
||||
// 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) {
|
||||
_jumpButtonDownStartTime = 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 (!rayHasHit && !_hasSupport) {
|
||||
SET_STATE(State::Hover, "no ground detected");
|
||||
} else if (_pendingFlags & PENDING_FLAG_JUMP && _jumpButtonDownCount != _takeoffJumpButtonID) {
|
||||
_takeoffJumpButtonID = _jumpButtonDownCount;
|
||||
_takeoffToInAirStartTime = now;
|
||||
SET_STATE(State::Takeoff, "jump pressed");
|
||||
} else if (rayHasHit && !_hasSupport && _floorDistance > JUMP_PROXIMITY_THRESHOLD) {
|
||||
SET_STATE(State::InAir, "falling");
|
||||
}
|
||||
break;
|
||||
case State::Takeoff:
|
||||
if (!rayHasHit && !_hasSupport) {
|
||||
SET_STATE(State::Hover, "no ground");
|
||||
} else if ((now - _takeoffToInAirStartTime) > TAKE_OFF_TO_IN_AIR_PERIOD) {
|
||||
SET_STATE(State::InAir, "takeoff done");
|
||||
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 - _jumpButtonDownStartTime) > 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,11 @@ protected:
|
|||
|
||||
glm::vec3 _boxScale; // used to compute capsule shape
|
||||
|
||||
quint64 _jumpToHoverStart;
|
||||
quint64 _rayHitStartTime;
|
||||
quint64 _takeoffToInAirStartTime;
|
||||
quint64 _jumpButtonDownStartTime;
|
||||
quint32 _jumpButtonDownCount;
|
||||
quint32 _takeoffJumpButtonID;
|
||||
|
||||
btScalar _halfHeight;
|
||||
btScalar _radius;
|
||||
|
@ -116,16 +134,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
|
||||
|
|
|
@ -26,10 +26,7 @@
|
|||
#include "EntityTree.h"
|
||||
#endif
|
||||
|
||||
static const float ACCELERATION_EQUIVALENT_EPSILON_RATIO = 0.1f;
|
||||
static const quint8 STEPS_TO_DECIDE_BALLISTIC = 4;
|
||||
|
||||
const uint32_t LOOPS_FOR_SIMULATION_ORPHAN = 50;
|
||||
const uint8_t LOOPS_FOR_SIMULATION_ORPHAN = 50;
|
||||
const quint64 USECS_BETWEEN_OWNERSHIP_BIDS = USECS_PER_SECOND / 5;
|
||||
|
||||
#ifdef WANT_DEBUG_ENTITY_TREE_LOCKS
|
||||
|
@ -52,8 +49,6 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer
|
|||
ObjectMotionState(shape),
|
||||
_entityPtr(entity),
|
||||
_entity(entity.get()),
|
||||
_sentInactive(true),
|
||||
_lastStep(0),
|
||||
_serverPosition(0.0f),
|
||||
_serverRotation(),
|
||||
_serverVelocity(0.0f),
|
||||
|
@ -61,13 +56,16 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer
|
|||
_serverGravity(0.0f),
|
||||
_serverAcceleration(0.0f),
|
||||
_serverActionData(QByteArray()),
|
||||
_lastMeasureStep(0),
|
||||
_lastVelocity(glm::vec3(0.0f)),
|
||||
_measuredAcceleration(glm::vec3(0.0f)),
|
||||
_measuredDeltaTime(0.0f),
|
||||
_accelerationNearlyGravityCount(0),
|
||||
_nextOwnershipBid(0),
|
||||
_loopsWithoutOwner(0)
|
||||
_measuredDeltaTime(0.0f),
|
||||
_lastMeasureStep(0),
|
||||
_lastStep(0),
|
||||
_loopsWithoutOwner(0),
|
||||
_accelerationNearlyGravityCount(0),
|
||||
_numInactiveUpdates(1),
|
||||
_outgoingPriority(ZERO_SIMULATION_PRIORITY)
|
||||
{
|
||||
_type = MOTIONSTATE_TYPE_ENTITY;
|
||||
assert(_entity);
|
||||
|
@ -102,27 +100,35 @@ bool EntityMotionState::handleEasyChanges(uint32_t& flags) {
|
|||
ObjectMotionState::handleEasyChanges(flags);
|
||||
|
||||
if (flags & Simulation::DIRTY_SIMULATOR_ID) {
|
||||
_loopsWithoutOwner = 0;
|
||||
if (_entity->getSimulatorID().isNull()) {
|
||||
// simulation ownership is being removed
|
||||
// remove the ACTIVATION flag because this object is coming to rest
|
||||
// according to a remote simulation and we don't want to wake it up again
|
||||
flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION;
|
||||
// hint to Bullet that the object is deactivating
|
||||
_body->setActivationState(WANTS_DEACTIVATION);
|
||||
_outgoingPriority = NO_PRORITY;
|
||||
} else {
|
||||
// simulation ownership has been removed by an external simulator
|
||||
if (glm::length2(_entity->getVelocity()) == 0.0f) {
|
||||
// this object is coming to rest --> clear the ACTIVATION flag and outgoing priority
|
||||
flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION;
|
||||
_body->setActivationState(WANTS_DEACTIVATION);
|
||||
_outgoingPriority = ZERO_SIMULATION_PRIORITY;
|
||||
_loopsWithoutOwner = 0;
|
||||
} else {
|
||||
// unowned object is still moving --> we should volunteer to own it
|
||||
// TODO? put a delay in here proportional to distance from object?
|
||||
setOutgoingPriority(VOLUNTEER_SIMULATION_PRIORITY);
|
||||
_loopsWithoutOwner = LOOPS_FOR_SIMULATION_ORPHAN;
|
||||
_nextOwnershipBid = 0;
|
||||
}
|
||||
} else {
|
||||
// this entity's simulation is owned by someone, so we push its ownership expiry into the future
|
||||
_nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS;
|
||||
if (Physics::getSessionUUID() == _entity->getSimulatorID() || _entity->getSimulationPriority() >= _outgoingPriority) {
|
||||
// we own the simulation or our priority looses to (or ties with) remote
|
||||
_outgoingPriority = NO_PRORITY;
|
||||
// either we already own the simulation or our old outgoing priority momentarily looses to current owner
|
||||
// so we clear it
|
||||
_outgoingPriority = ZERO_SIMULATION_PRIORITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (flags & Simulation::DIRTY_SIMULATOR_OWNERSHIP) {
|
||||
// (DIRTY_SIMULATOR_OWNERSHIP really means "we should bid for ownership with SCRIPT priority")
|
||||
// we're manipulating this object directly via script, so we artificially
|
||||
// manipulate the logic to trigger an immediate bid for ownership
|
||||
// The DIRTY_SIMULATOR_OWNERSHIP bit really means "we should bid for ownership at SCRIPT priority".
|
||||
// Since that bit is set there must be a local script that is updating the physics properties of the objects
|
||||
// therefore we upgrade _outgoingPriority to trigger a bid for ownership.
|
||||
setOutgoingPriority(SCRIPT_EDIT_SIMULATION_PRIORITY);
|
||||
}
|
||||
if ((flags & Simulation::DIRTY_PHYSICS_ACTIVATION) && !_body->isActive()) {
|
||||
|
@ -203,7 +209,6 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) {
|
|||
_loopsWithoutOwner++;
|
||||
|
||||
if (_loopsWithoutOwner > LOOPS_FOR_SIMULATION_ORPHAN && usecTimestampNow() > _nextOwnershipBid) {
|
||||
//qDebug() << "Warning -- claiming something I saw moving." << getName();
|
||||
setOutgoingPriority(VOLUNTEER_SIMULATION_PRIORITY);
|
||||
}
|
||||
}
|
||||
|
@ -235,14 +240,14 @@ btCollisionShape* EntityMotionState::computeNewShape() {
|
|||
}
|
||||
|
||||
bool EntityMotionState::isCandidateForOwnership(const QUuid& sessionID) const {
|
||||
if (!_body || !_entity) {
|
||||
return false;
|
||||
}
|
||||
assert(_body);
|
||||
assert(_entity);
|
||||
assert(entityTreeIsLocked());
|
||||
return _outgoingPriority != NO_PRORITY || sessionID == _entity->getSimulatorID() || _entity->actionDataNeedsTransmit();
|
||||
return _outgoingPriority != ZERO_SIMULATION_PRIORITY || sessionID == _entity->getSimulatorID() || _entity->actionDataNeedsTransmit();
|
||||
}
|
||||
|
||||
bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) {
|
||||
// NOTE: we only get here if we think we own the simulation
|
||||
assert(_body);
|
||||
// if we've never checked before, our _lastStep will be 0, and we need to initialize our state
|
||||
if (_lastStep == 0) {
|
||||
|
@ -253,7 +258,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) {
|
|||
_serverAngularVelocity = bulletToGLM(_body->getAngularVelocity());
|
||||
_lastStep = simulationStep;
|
||||
_serverActionData = _entity->getActionData();
|
||||
_sentInactive = true;
|
||||
_numInactiveUpdates = 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -266,16 +271,21 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) {
|
|||
int numSteps = simulationStep - _lastStep;
|
||||
float dt = (float)(numSteps) * PHYSICS_ENGINE_FIXED_SUBSTEP;
|
||||
|
||||
const float INACTIVE_UPDATE_PERIOD = 0.5f;
|
||||
if (_sentInactive) {
|
||||
if (_numInactiveUpdates > 0) {
|
||||
const uint8_t MAX_NUM_INACTIVE_UPDATES = 3;
|
||||
if (_numInactiveUpdates > MAX_NUM_INACTIVE_UPDATES) {
|
||||
// clear local ownership (stop sending updates) and let the server clear itself
|
||||
_entity->clearSimulationOwnership();
|
||||
return false;
|
||||
}
|
||||
// we resend the inactive update every INACTIVE_UPDATE_PERIOD
|
||||
// until it is removed from the outgoing updates
|
||||
// (which happens when we don't own the simulation and it isn't touching our simulation)
|
||||
const float INACTIVE_UPDATE_PERIOD = 0.5f;
|
||||
return (dt > INACTIVE_UPDATE_PERIOD);
|
||||
}
|
||||
|
||||
bool isActive = _body->isActive();
|
||||
if (!isActive) {
|
||||
if (!_body->isActive()) {
|
||||
// object has gone inactive but our last send was moving --> send non-moving update immediately
|
||||
return true;
|
||||
}
|
||||
|
@ -374,11 +384,12 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep, const QUuid& s
|
|||
}
|
||||
|
||||
if (_entity->getSimulatorID() != sessionID) {
|
||||
// we don't own the simulation, but maybe we should...
|
||||
if (_outgoingPriority != NO_PRORITY) {
|
||||
// we don't own the simulation
|
||||
if (_outgoingPriority != ZERO_SIMULATION_PRIORITY) {
|
||||
// but we would like to own it
|
||||
if (_outgoingPriority < _entity->getSimulationPriority()) {
|
||||
// our priority loses to remote, so we don't bother to bid
|
||||
_outgoingPriority = NO_PRORITY;
|
||||
// but our priority loses to remote, so we don't bother trying
|
||||
_outgoingPriority = ZERO_SIMULATION_PRIORITY;
|
||||
return false;
|
||||
}
|
||||
return usecTimestampNow() > _nextOwnershipBid;
|
||||
|
@ -400,10 +411,12 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q
|
|||
_entity->setVelocity(zero);
|
||||
_entity->setAngularVelocity(zero);
|
||||
_entity->setAcceleration(zero);
|
||||
_sentInactive = true;
|
||||
_numInactiveUpdates++;
|
||||
} else {
|
||||
const uint8_t STEPS_TO_DECIDE_BALLISTIC = 4;
|
||||
float gravityLength = glm::length(_entity->getGravity());
|
||||
float accVsGravity = glm::abs(glm::length(_measuredAcceleration) - gravityLength);
|
||||
const float ACCELERATION_EQUIVALENT_EPSILON_RATIO = 0.1f;
|
||||
if (accVsGravity < ACCELERATION_EQUIVALENT_EPSILON_RATIO * gravityLength) {
|
||||
// acceleration measured during the most recent simulation step was close to gravity.
|
||||
if (getAccelerationNearlyGravityCount() < STEPS_TO_DECIDE_BALLISTIC) {
|
||||
|
@ -440,7 +453,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q
|
|||
_entity->setVelocity(zero);
|
||||
_entity->setAngularVelocity(zero);
|
||||
}
|
||||
_sentInactive = false;
|
||||
_numInactiveUpdates = 0;
|
||||
}
|
||||
|
||||
// remember properties for local server prediction
|
||||
|
@ -488,12 +501,12 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q
|
|||
// we own the simulation but the entity has stopped, so we tell the server that we're clearing simulatorID
|
||||
// but we remember that we do still own it... and rely on the server to tell us that we don't
|
||||
properties.clearSimulationOwner();
|
||||
_outgoingPriority = NO_PRORITY;
|
||||
_outgoingPriority = ZERO_SIMULATION_PRIORITY;
|
||||
}
|
||||
// else the ownership is not changing so we don't bother to pack it
|
||||
} else {
|
||||
// we don't own the simulation for this entity yet, but we're sending a bid for it
|
||||
properties.setSimulationOwner(sessionID, glm::max<quint8>(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY));
|
||||
properties.setSimulationOwner(sessionID, glm::max<uint8_t>(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY));
|
||||
_nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS;
|
||||
}
|
||||
|
||||
|
@ -558,7 +571,7 @@ void EntityMotionState::clearIncomingDirtyFlags() {
|
|||
}
|
||||
|
||||
// virtual
|
||||
quint8 EntityMotionState::getSimulationPriority() const {
|
||||
uint8_t EntityMotionState::getSimulationPriority() const {
|
||||
return _entity->getSimulationPriority();
|
||||
}
|
||||
|
||||
|
@ -568,7 +581,7 @@ QUuid EntityMotionState::getSimulatorID() const {
|
|||
return _entity->getSimulatorID();
|
||||
}
|
||||
|
||||
void EntityMotionState::bump(quint8 priority) {
|
||||
void EntityMotionState::bump(uint8_t priority) {
|
||||
setOutgoingPriority(glm::max(VOLUNTEER_SIMULATION_PRIORITY, --priority));
|
||||
}
|
||||
|
||||
|
@ -601,7 +614,7 @@ void EntityMotionState::measureBodyAcceleration() {
|
|||
if (numSubsteps > PHYSICS_ENGINE_MAX_NUM_SUBSTEPS) {
|
||||
_loopsWithoutOwner = 0;
|
||||
_lastStep = ObjectMotionState::getWorldSimulationStep();
|
||||
_sentInactive = false;
|
||||
_numInactiveUpdates = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -631,6 +644,6 @@ void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& ma
|
|||
_entity->computeCollisionGroupAndFinalMask(group, mask);
|
||||
}
|
||||
|
||||
void EntityMotionState::setOutgoingPriority(quint8 priority) {
|
||||
_outgoingPriority = glm::max<quint8>(_outgoingPriority, priority);
|
||||
void EntityMotionState::setOutgoingPriority(uint8_t priority) {
|
||||
_outgoingPriority = glm::max<uint8_t>(_outgoingPriority, priority);
|
||||
}
|
||||
|
|
|
@ -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,12 +48,12 @@ 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; }
|
||||
quint8 getAccelerationNearlyGravityCount() { return _accelerationNearlyGravityCount; }
|
||||
uint8_t getAccelerationNearlyGravityCount() { return _accelerationNearlyGravityCount; }
|
||||
|
||||
virtual float getObjectRestitution() const override { return _entity->getRestitution(); }
|
||||
virtual float getObjectFriction() const override { return _entity->getFriction(); }
|
||||
|
@ -69,9 +69,9 @@ public:
|
|||
|
||||
virtual const QUuid getObjectID() const override { return _entity->getID(); }
|
||||
|
||||
virtual quint8 getSimulationPriority() const override;
|
||||
virtual uint8_t getSimulationPriority() const override;
|
||||
virtual QUuid getSimulatorID() const override;
|
||||
virtual void bump(quint8 priority) override;
|
||||
virtual void bump(uint8_t priority) override;
|
||||
|
||||
EntityItemPointer getEntity() const { return _entityPtr.lock(); }
|
||||
|
||||
|
@ -80,10 +80,10 @@ 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);
|
||||
void setOutgoingPriority(uint8_t priority);
|
||||
|
||||
friend class PhysicalEntitySimulation;
|
||||
|
||||
|
@ -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
|
||||
|
@ -106,10 +106,6 @@ protected:
|
|||
// Meanwhile we also keep a raw EntityItem* for internal stuff where the pointer is guaranteed valid.
|
||||
EntityItem* _entity;
|
||||
|
||||
bool _sentInactive; // true if body was inactive when we sent last update
|
||||
|
||||
// these are for the prediction of the remote server's simple extrapolation
|
||||
uint32_t _lastStep; // last step of server extrapolation
|
||||
glm::vec3 _serverPosition; // in simulation-frame (not world-frame)
|
||||
glm::quat _serverRotation;
|
||||
glm::vec3 _serverVelocity;
|
||||
|
@ -118,15 +114,18 @@ protected:
|
|||
glm::vec3 _serverAcceleration;
|
||||
QByteArray _serverActionData;
|
||||
|
||||
uint32_t _lastMeasureStep;
|
||||
glm::vec3 _lastVelocity;
|
||||
glm::vec3 _measuredAcceleration;
|
||||
float _measuredDeltaTime;
|
||||
quint64 _nextOwnershipBid { 0 };
|
||||
|
||||
quint8 _accelerationNearlyGravityCount;
|
||||
quint64 _nextOwnershipBid = NO_PRORITY;
|
||||
uint32_t _loopsWithoutOwner;
|
||||
quint8 _outgoingPriority = NO_PRORITY;
|
||||
float _measuredDeltaTime;
|
||||
uint32_t _lastMeasureStep;
|
||||
uint32_t _lastStep; // last step of server extrapolation
|
||||
|
||||
uint8_t _loopsWithoutOwner;
|
||||
uint8_t _accelerationNearlyGravityCount;
|
||||
uint8_t _numInactiveUpdates { 1 };
|
||||
uint8_t _outgoingPriority { ZERO_SIMULATION_PRIORITY };
|
||||
};
|
||||
|
||||
#endif // hifi_EntityMotionState_h
|
||||
|
|
|
@ -249,6 +249,7 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result)
|
|||
|
||||
void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates, const QUuid& sessionID) {
|
||||
QMutexLocker lock(&_mutex);
|
||||
|
||||
// walk the motionStates looking for those that correspond to entities
|
||||
for (auto stateItr : motionStates) {
|
||||
ObjectMotionState* state = &(*stateItr);
|
||||
|
@ -273,13 +274,15 @@ void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates&
|
|||
return;
|
||||
}
|
||||
|
||||
// send outgoing packets
|
||||
// look for entities to prune or update
|
||||
QSet<EntityMotionState*>::iterator stateItr = _outgoingChanges.begin();
|
||||
while (stateItr != _outgoingChanges.end()) {
|
||||
EntityMotionState* state = *stateItr;
|
||||
if (!state->isCandidateForOwnership(sessionID)) {
|
||||
// prune
|
||||
stateItr = _outgoingChanges.erase(stateItr);
|
||||
} else if (state->shouldSendUpdate(numSubsteps, sessionID)) {
|
||||
// update
|
||||
state->sendUpdate(_entityPacketSender, sessionID, numSubsteps);
|
||||
++stateItr;
|
||||
} else {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
// render-utils/src/
|
||||
//
|
||||
// Created by Sam Gateau on 5/29/15.
|
||||
// Copyright 20154 High Fidelity, Inc.
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
@ -15,7 +15,6 @@
|
|||
#include <RenderArgs.h>
|
||||
#include <ViewFrustum.h>
|
||||
#include <gpu/Context.h>
|
||||
#include <gpu/StandardShaderLib.h>
|
||||
|
||||
#include "DebugDeferredBuffer.h"
|
||||
#include "DeferredLightingEffect.h"
|
||||
|
@ -31,34 +30,11 @@
|
|||
|
||||
#include "RenderDeferredTask.h"
|
||||
|
||||
#include "model_vert.h"
|
||||
#include "model_shadow_vert.h"
|
||||
#include "model_normal_map_vert.h"
|
||||
#include "model_lightmap_vert.h"
|
||||
#include "model_lightmap_normal_map_vert.h"
|
||||
#include "skin_model_vert.h"
|
||||
#include "skin_model_shadow_vert.h"
|
||||
#include "skin_model_normal_map_vert.h"
|
||||
|
||||
#include "model_frag.h"
|
||||
#include "model_shadow_frag.h"
|
||||
#include "model_normal_map_frag.h"
|
||||
#include "model_normal_specular_map_frag.h"
|
||||
#include "model_specular_map_frag.h"
|
||||
#include "model_lightmap_frag.h"
|
||||
#include "model_lightmap_normal_map_frag.h"
|
||||
#include "model_lightmap_normal_specular_map_frag.h"
|
||||
#include "model_lightmap_specular_map_frag.h"
|
||||
#include "model_translucent_frag.h"
|
||||
|
||||
#include "overlay3D_vert.h"
|
||||
#include "overlay3D_frag.h"
|
||||
|
||||
#include "drawOpaqueStencil_frag.h"
|
||||
|
||||
using namespace render;
|
||||
|
||||
void initDeferredPipelines(render::ShapePlumber& plumber);
|
||||
extern void initStencilPipeline(gpu::PipelinePointer& pipeline);
|
||||
extern void initOverlay3DPipelines(render::ShapePlumber& plumber);
|
||||
extern void initDeferredPipelines(render::ShapePlumber& plumber);
|
||||
|
||||
void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) {
|
||||
DependencyManager::get<DeferredLightingEffect>()->prepare(renderContext->args);
|
||||
|
@ -128,7 +104,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) {
|
|||
addJob<DrawStatus>("DrawStatus", opaques, DrawStatus(statusIconMap));
|
||||
}
|
||||
|
||||
addJob<DrawOverlay3D>("DrawOverlay3D", shapePlumber);
|
||||
addJob<DrawOverlay3D>("DrawOverlay3D");
|
||||
|
||||
addJob<HitEffect>("HitEffect");
|
||||
|
||||
|
@ -180,22 +156,8 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont
|
|||
});
|
||||
}
|
||||
|
||||
// TODO: Move this to the shapePlumber
|
||||
gpu::PipelinePointer DrawOverlay3D::_opaquePipeline;
|
||||
const gpu::PipelinePointer& DrawOverlay3D::getOpaquePipeline() {
|
||||
if (!_opaquePipeline) {
|
||||
auto vs = gpu::Shader::createVertex(std::string(overlay3D_vert));
|
||||
auto ps = gpu::Shader::createPixel(std::string(overlay3D_frag));
|
||||
auto program = gpu::Shader::createProgram(vs, ps);
|
||||
|
||||
auto state = std::make_shared<gpu::State>();
|
||||
state->setDepthTest(false);
|
||||
// additive blending
|
||||
state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
|
||||
|
||||
_opaquePipeline = gpu::Pipeline::create(program, state);
|
||||
}
|
||||
return _opaquePipeline;
|
||||
DrawOverlay3D::DrawOverlay3D() : _shapePlumber{ std::make_shared<ShapePlumber>() } {
|
||||
initOverlay3DPipelines(*_shapePlumber);
|
||||
}
|
||||
|
||||
void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) {
|
||||
|
@ -246,9 +208,8 @@ void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderCon
|
|||
batch.setViewTransform(viewMat);
|
||||
batch.setViewportTransform(args->_viewport);
|
||||
batch.setStateScissorRect(args->_viewport);
|
||||
|
||||
batch.setPipeline(getOpaquePipeline());
|
||||
batch.setResourceTexture(0, args->_whiteTexture);
|
||||
|
||||
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn);
|
||||
});
|
||||
args->_batch = nullptr;
|
||||
|
@ -259,20 +220,7 @@ void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderCon
|
|||
gpu::PipelinePointer DrawStencilDeferred::_opaquePipeline;
|
||||
const gpu::PipelinePointer& DrawStencilDeferred::getOpaquePipeline() {
|
||||
if (!_opaquePipeline) {
|
||||
const gpu::int8 STENCIL_OPAQUE = 1;
|
||||
auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS();
|
||||
auto ps = gpu::Shader::createPixel(std::string(drawOpaqueStencil_frag));
|
||||
auto program = gpu::Shader::createProgram(vs, ps);
|
||||
|
||||
|
||||
gpu::Shader::makeProgram((*program));
|
||||
|
||||
auto state = std::make_shared<gpu::State>();
|
||||
state->setDepthTest(true, false, gpu::LESS_EQUAL);
|
||||
state->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_OPAQUE, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_REPLACE));
|
||||
state->setColorWriteMask(0);
|
||||
|
||||
_opaquePipeline = gpu::Pipeline::create(program, state);
|
||||
initStencilPipeline(_opaquePipeline);
|
||||
}
|
||||
return _opaquePipeline;
|
||||
}
|
||||
|
@ -413,166 +361,3 @@ void Blit::run(const SceneContextPointer& sceneContext, const RenderContextPoint
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
void pipelineBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch) {
|
||||
if (pipeline.locations->normalFittingMapUnit > -1) {
|
||||
batch.setResourceTexture(pipeline.locations->normalFittingMapUnit,
|
||||
DependencyManager::get<TextureCache>()->getNormalFittingTexture());
|
||||
}
|
||||
}
|
||||
|
||||
void initDeferredPipelines(render::ShapePlumber& plumber) {
|
||||
using Key = render::ShapeKey;
|
||||
using ShaderPointer = gpu::ShaderPointer;
|
||||
|
||||
auto addPipeline = [&plumber](const Key& key, const ShaderPointer& vertexShader, const ShaderPointer& pixelShader) {
|
||||
auto state = std::make_shared<gpu::State>();
|
||||
|
||||
// Cull backface
|
||||
state->setCullMode(gpu::State::CULL_BACK);
|
||||
|
||||
// Z test depends on transparency
|
||||
state->setDepthTest(true, !key.isTranslucent(), gpu::LESS_EQUAL);
|
||||
|
||||
// Blend if transparent
|
||||
state->setBlendFunction(key.isTranslucent(),
|
||||
// For transparency, keep the highlight intensity
|
||||
gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
|
||||
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
|
||||
|
||||
ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader);
|
||||
plumber.addPipeline(key, program, state, &pipelineBatchSetter);
|
||||
|
||||
// Add a wireframe version
|
||||
if (!key.isWireFrame()) {
|
||||
auto wireFrameKey = Key::Builder(key).withWireframe();
|
||||
auto wireFrameState = std::make_shared<gpu::State>(state->getValues());
|
||||
|
||||
wireFrameState->setFillMode(gpu::State::FILL_LINE);
|
||||
|
||||
plumber.addPipeline(wireFrameKey, program, wireFrameState, &pipelineBatchSetter);
|
||||
}
|
||||
};
|
||||
|
||||
// Vertex shaders
|
||||
auto modelVertex = gpu::Shader::createVertex(std::string(model_vert));
|
||||
auto modelNormalMapVertex = gpu::Shader::createVertex(std::string(model_normal_map_vert));
|
||||
auto modelLightmapVertex = gpu::Shader::createVertex(std::string(model_lightmap_vert));
|
||||
auto modelLightmapNormalMapVertex = gpu::Shader::createVertex(std::string(model_lightmap_normal_map_vert));
|
||||
auto modelShadowVertex = gpu::Shader::createVertex(std::string(model_shadow_vert));
|
||||
auto skinModelVertex = gpu::Shader::createVertex(std::string(skin_model_vert));
|
||||
auto skinModelNormalMapVertex = gpu::Shader::createVertex(std::string(skin_model_normal_map_vert));
|
||||
auto skinModelShadowVertex = gpu::Shader::createVertex(std::string(skin_model_shadow_vert));
|
||||
|
||||
// Pixel shaders
|
||||
auto modelPixel = gpu::Shader::createPixel(std::string(model_frag));
|
||||
auto modelNormalMapPixel = gpu::Shader::createPixel(std::string(model_normal_map_frag));
|
||||
auto modelSpecularMapPixel = gpu::Shader::createPixel(std::string(model_specular_map_frag));
|
||||
auto modelNormalSpecularMapPixel = gpu::Shader::createPixel(std::string(model_normal_specular_map_frag));
|
||||
auto modelTranslucentPixel = gpu::Shader::createPixel(std::string(model_translucent_frag));
|
||||
auto modelShadowPixel = gpu::Shader::createPixel(std::string(model_shadow_frag));
|
||||
auto modelLightmapPixel = gpu::Shader::createPixel(std::string(model_lightmap_frag));
|
||||
auto modelLightmapNormalMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_normal_map_frag));
|
||||
auto modelLightmapSpecularMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_specular_map_frag));
|
||||
auto modelLightmapNormalSpecularMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_normal_specular_map_frag));
|
||||
|
||||
// Fill the pipelineLib
|
||||
addPipeline(
|
||||
Key::Builder(),
|
||||
modelVertex, modelPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTangents(),
|
||||
modelNormalMapVertex, modelNormalMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSpecular(),
|
||||
modelVertex, modelSpecularMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTangents().withSpecular(),
|
||||
modelNormalMapVertex, modelNormalSpecularMapPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTranslucent(),
|
||||
modelVertex, modelTranslucentPixel);
|
||||
// FIXME Ignore lightmap for translucents meshpart
|
||||
addPipeline(
|
||||
Key::Builder().withTranslucent().withLightmap(),
|
||||
modelVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTangents().withTranslucent(),
|
||||
modelNormalMapVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSpecular().withTranslucent(),
|
||||
modelVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTangents().withSpecular().withTranslucent(),
|
||||
modelNormalMapVertex, modelTranslucentPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withLightmap(),
|
||||
modelLightmapVertex, modelLightmapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withLightmap().withTangents(),
|
||||
modelLightmapNormalMapVertex, modelLightmapNormalMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withLightmap().withSpecular(),
|
||||
modelLightmapVertex, modelLightmapSpecularMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withLightmap().withTangents().withSpecular(),
|
||||
modelLightmapNormalMapVertex, modelLightmapNormalSpecularMapPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned(),
|
||||
skinModelVertex, modelPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTangents(),
|
||||
skinModelNormalMapVertex, modelNormalMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withSpecular(),
|
||||
skinModelVertex, modelSpecularMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTangents().withSpecular(),
|
||||
skinModelNormalMapVertex, modelNormalSpecularMapPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTranslucent(),
|
||||
skinModelVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTangents().withTranslucent(),
|
||||
skinModelNormalMapVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withSpecular().withTranslucent(),
|
||||
skinModelVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTangents().withSpecular().withTranslucent(),
|
||||
skinModelNormalMapVertex, modelTranslucentPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withDepthOnly(),
|
||||
modelShadowVertex, modelShadowPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withDepthOnly(),
|
||||
skinModelShadowVertex, modelShadowPixel);
|
||||
}
|
||||
|
||||
|
|
|
@ -68,11 +68,10 @@ protected:
|
|||
|
||||
class DrawStencilDeferred {
|
||||
public:
|
||||
static const gpu::PipelinePointer& getOpaquePipeline();
|
||||
using JobModel = render::Job::Model<DrawStencilDeferred>;
|
||||
|
||||
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext);
|
||||
|
||||
using JobModel = render::Job::Model<DrawStencilDeferred>;
|
||||
static const gpu::PipelinePointer& getOpaquePipeline();
|
||||
|
||||
protected:
|
||||
static gpu::PipelinePointer _opaquePipeline; //lazy evaluation hence mutable
|
||||
|
@ -106,15 +105,12 @@ public:
|
|||
using Config = DrawOverlay3DConfig;
|
||||
using JobModel = render::Job::Model<DrawOverlay3D, Config>;
|
||||
|
||||
DrawOverlay3D(render::ShapePlumberPointer shapePlumber) : _shapePlumber{ shapePlumber } {}
|
||||
DrawOverlay3D();
|
||||
|
||||
void configure(const Config& config) { _maxDrawn = config.maxDrawn; }
|
||||
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext);
|
||||
|
||||
static const gpu::PipelinePointer& getOpaquePipeline();
|
||||
|
||||
protected:
|
||||
static gpu::PipelinePointer _opaquePipeline; //lazy evaluation hence mutable
|
||||
render::ShapePlumberPointer _shapePlumber;
|
||||
int _maxDrawn; // initialized by Config
|
||||
};
|
||||
|
|
234
libraries/render-utils/src/RenderPipelines.cpp
Normal file
234
libraries/render-utils/src/RenderPipelines.cpp
Normal file
|
@ -0,0 +1,234 @@
|
|||
|
||||
//
|
||||
// RenderPipelines.cpp
|
||||
// render-utils/src/
|
||||
//
|
||||
// Created by Zach Pomerantz on 1/28/2016.
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <gpu/Context.h>
|
||||
#include <gpu/StandardShaderLib.h>
|
||||
|
||||
#include "TextureCache.h"
|
||||
#include "render/DrawTask.h"
|
||||
|
||||
#include "model_vert.h"
|
||||
#include "model_shadow_vert.h"
|
||||
#include "model_normal_map_vert.h"
|
||||
#include "model_lightmap_vert.h"
|
||||
#include "model_lightmap_normal_map_vert.h"
|
||||
#include "skin_model_vert.h"
|
||||
#include "skin_model_shadow_vert.h"
|
||||
#include "skin_model_normal_map_vert.h"
|
||||
|
||||
#include "model_frag.h"
|
||||
#include "model_shadow_frag.h"
|
||||
#include "model_normal_map_frag.h"
|
||||
#include "model_normal_specular_map_frag.h"
|
||||
#include "model_specular_map_frag.h"
|
||||
#include "model_lightmap_frag.h"
|
||||
#include "model_lightmap_normal_map_frag.h"
|
||||
#include "model_lightmap_normal_specular_map_frag.h"
|
||||
#include "model_lightmap_specular_map_frag.h"
|
||||
#include "model_translucent_frag.h"
|
||||
|
||||
#include "overlay3D_vert.h"
|
||||
#include "overlay3D_frag.h"
|
||||
|
||||
#include "drawOpaqueStencil_frag.h"
|
||||
|
||||
using namespace render;
|
||||
|
||||
void initStencilPipeline(gpu::PipelinePointer& pipeline) {
|
||||
const gpu::int8 STENCIL_OPAQUE = 1;
|
||||
auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS();
|
||||
auto ps = gpu::Shader::createPixel(std::string(drawOpaqueStencil_frag));
|
||||
auto program = gpu::Shader::createProgram(vs, ps);
|
||||
gpu::Shader::makeProgram((*program));
|
||||
|
||||
auto state = std::make_shared<gpu::State>();
|
||||
state->setDepthTest(true, false, gpu::LESS_EQUAL);
|
||||
state->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_OPAQUE, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_REPLACE));
|
||||
state->setColorWriteMask(0);
|
||||
|
||||
pipeline = gpu::Pipeline::create(program, state);
|
||||
}
|
||||
|
||||
void initOverlay3DPipelines(ShapePlumber& plumber) {
|
||||
auto vs = gpu::Shader::createVertex(std::string(overlay3D_vert));
|
||||
auto ps = gpu::Shader::createPixel(std::string(overlay3D_frag));
|
||||
auto program = gpu::Shader::createProgram(vs, ps);
|
||||
|
||||
auto opaqueState = std::make_shared<gpu::State>();
|
||||
opaqueState->setDepthTest(false);
|
||||
opaqueState->setBlendFunction(false);
|
||||
|
||||
plumber.addPipeline(ShapeKey::Filter::Builder().withOpaque(), program, opaqueState);
|
||||
}
|
||||
|
||||
void pipelineBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch) {
|
||||
if (pipeline.locations->normalFittingMapUnit > -1) {
|
||||
batch.setResourceTexture(pipeline.locations->normalFittingMapUnit,
|
||||
DependencyManager::get<TextureCache>()->getNormalFittingTexture());
|
||||
}
|
||||
}
|
||||
|
||||
void initDeferredPipelines(render::ShapePlumber& plumber) {
|
||||
using Key = render::ShapeKey;
|
||||
using ShaderPointer = gpu::ShaderPointer;
|
||||
|
||||
auto addPipeline = [&plumber](const Key& key, const ShaderPointer& vertexShader, const ShaderPointer& pixelShader) {
|
||||
auto state = std::make_shared<gpu::State>();
|
||||
|
||||
// Cull backface
|
||||
state->setCullMode(gpu::State::CULL_BACK);
|
||||
|
||||
// Z test depends on transparency
|
||||
state->setDepthTest(true, !key.isTranslucent(), gpu::LESS_EQUAL);
|
||||
|
||||
// Blend if transparent
|
||||
state->setBlendFunction(key.isTranslucent(),
|
||||
// For transparency, keep the highlight intensity
|
||||
gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
|
||||
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
|
||||
|
||||
ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader);
|
||||
plumber.addPipeline(key, program, state, &pipelineBatchSetter);
|
||||
|
||||
// Add a wireframe version
|
||||
if (!key.isWireFrame()) {
|
||||
auto wireFrameKey = Key::Builder(key).withWireframe();
|
||||
auto wireFrameState = std::make_shared<gpu::State>(state->getValues());
|
||||
|
||||
wireFrameState->setFillMode(gpu::State::FILL_LINE);
|
||||
|
||||
plumber.addPipeline(wireFrameKey, program, wireFrameState, &pipelineBatchSetter);
|
||||
}
|
||||
};
|
||||
|
||||
// Vertex shaders
|
||||
auto modelVertex = gpu::Shader::createVertex(std::string(model_vert));
|
||||
auto modelNormalMapVertex = gpu::Shader::createVertex(std::string(model_normal_map_vert));
|
||||
auto modelLightmapVertex = gpu::Shader::createVertex(std::string(model_lightmap_vert));
|
||||
auto modelLightmapNormalMapVertex = gpu::Shader::createVertex(std::string(model_lightmap_normal_map_vert));
|
||||
auto modelShadowVertex = gpu::Shader::createVertex(std::string(model_shadow_vert));
|
||||
auto skinModelVertex = gpu::Shader::createVertex(std::string(skin_model_vert));
|
||||
auto skinModelNormalMapVertex = gpu::Shader::createVertex(std::string(skin_model_normal_map_vert));
|
||||
auto skinModelShadowVertex = gpu::Shader::createVertex(std::string(skin_model_shadow_vert));
|
||||
|
||||
// Pixel shaders
|
||||
auto modelPixel = gpu::Shader::createPixel(std::string(model_frag));
|
||||
auto modelNormalMapPixel = gpu::Shader::createPixel(std::string(model_normal_map_frag));
|
||||
auto modelSpecularMapPixel = gpu::Shader::createPixel(std::string(model_specular_map_frag));
|
||||
auto modelNormalSpecularMapPixel = gpu::Shader::createPixel(std::string(model_normal_specular_map_frag));
|
||||
auto modelTranslucentPixel = gpu::Shader::createPixel(std::string(model_translucent_frag));
|
||||
auto modelShadowPixel = gpu::Shader::createPixel(std::string(model_shadow_frag));
|
||||
auto modelLightmapPixel = gpu::Shader::createPixel(std::string(model_lightmap_frag));
|
||||
auto modelLightmapNormalMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_normal_map_frag));
|
||||
auto modelLightmapSpecularMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_specular_map_frag));
|
||||
auto modelLightmapNormalSpecularMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_normal_specular_map_frag));
|
||||
|
||||
// Fill the pipelineLib
|
||||
addPipeline(
|
||||
Key::Builder(),
|
||||
modelVertex, modelPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTangents(),
|
||||
modelNormalMapVertex, modelNormalMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSpecular(),
|
||||
modelVertex, modelSpecularMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTangents().withSpecular(),
|
||||
modelNormalMapVertex, modelNormalSpecularMapPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTranslucent(),
|
||||
modelVertex, modelTranslucentPixel);
|
||||
// FIXME Ignore lightmap for translucents meshpart
|
||||
addPipeline(
|
||||
Key::Builder().withTranslucent().withLightmap(),
|
||||
modelVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTangents().withTranslucent(),
|
||||
modelNormalMapVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSpecular().withTranslucent(),
|
||||
modelVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withTangents().withSpecular().withTranslucent(),
|
||||
modelNormalMapVertex, modelTranslucentPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withLightmap(),
|
||||
modelLightmapVertex, modelLightmapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withLightmap().withTangents(),
|
||||
modelLightmapNormalMapVertex, modelLightmapNormalMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withLightmap().withSpecular(),
|
||||
modelLightmapVertex, modelLightmapSpecularMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withLightmap().withTangents().withSpecular(),
|
||||
modelLightmapNormalMapVertex, modelLightmapNormalSpecularMapPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned(),
|
||||
skinModelVertex, modelPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTangents(),
|
||||
skinModelNormalMapVertex, modelNormalMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withSpecular(),
|
||||
skinModelVertex, modelSpecularMapPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTangents().withSpecular(),
|
||||
skinModelNormalMapVertex, modelNormalSpecularMapPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTranslucent(),
|
||||
skinModelVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTangents().withTranslucent(),
|
||||
skinModelNormalMapVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withSpecular().withTranslucent(),
|
||||
skinModelVertex, modelTranslucentPixel);
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withTangents().withSpecular().withTranslucent(),
|
||||
skinModelNormalMapVertex, modelTranslucentPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withDepthOnly(),
|
||||
modelShadowVertex, modelShadowPixel);
|
||||
|
||||
|
||||
addPipeline(
|
||||
Key::Builder().withSkinned().withDepthOnly(),
|
||||
skinModelShadowVertex, modelShadowPixel);
|
||||
}
|
||||
|
|
@ -257,7 +257,13 @@ public:
|
|||
QConfigPointer config = _jobs.back().getConfiguration();
|
||||
config->setParent(_config.get());
|
||||
config->setObjectName(name.c_str());
|
||||
QObject::connect(config.get(), SIGNAL(dirty()), _config.get(), SLOT(refresh()));
|
||||
|
||||
// Connect dirty->refresh if defined
|
||||
static const char* DIRTY_SIGNAL = "dirty()";
|
||||
if (config->metaObject()->indexOfSignal(DIRTY_SIGNAL) != -1) {
|
||||
QObject::connect(config.get(), SIGNAL(dirty()), _config.get(), SLOT(refresh()));
|
||||
}
|
||||
|
||||
return _jobs.back().getOutput();
|
||||
}
|
||||
template <class T, class... A> const Varying addJob(std::string name, A&&... args) {
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
|
||||
QUrl FileDialogHelper::home() {
|
||||
|
@ -26,7 +28,11 @@ QString FileDialogHelper::urlToPath(const QUrl& url) {
|
|||
return url.toLocalFile();
|
||||
}
|
||||
|
||||
bool FileDialogHelper::validPath(const QString& path) {
|
||||
bool FileDialogHelper::fileExists(const QString& path) {
|
||||
return QFile(path).exists();
|
||||
}
|
||||
|
||||
bool FileDialogHelper::validPath(const QString& path) {
|
||||
return QFile(path).exists();
|
||||
}
|
||||
|
||||
|
@ -38,3 +44,62 @@ QUrl FileDialogHelper::pathToUrl(const QString& path) {
|
|||
return QUrl::fromLocalFile(path);
|
||||
}
|
||||
|
||||
|
||||
QUrl FileDialogHelper::saveHelper(const QString& saveText, const QUrl& currentFolder, const QStringList& selectionFilters) {
|
||||
qDebug() << "Calling save helper with " << saveText << " " << currentFolder << " " << selectionFilters;
|
||||
|
||||
QFileInfo fileInfo(saveText);
|
||||
|
||||
// Check if it's a relative path and if it is resolve to the absolute path
|
||||
{
|
||||
if (fileInfo.isRelative()) {
|
||||
fileInfo = QFileInfo(currentFolder.toLocalFile() + "/" + fileInfo.filePath());
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to append an extension, but only if the current resolved path isn't a directory
|
||||
if (!fileInfo.isDir()) {
|
||||
QString fileName = fileInfo.fileName();
|
||||
if (!fileName.contains(".") && selectionFilters.size() == 1) {
|
||||
const QRegularExpression extensionRe{ ".*(\\.[a-zA-Z0-9]+)$" };
|
||||
QString filter = selectionFilters[0];
|
||||
auto match = extensionRe.match(filter);
|
||||
if (match.hasMatch()) {
|
||||
fileInfo = QFileInfo(fileInfo.filePath() + match.captured(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QUrl::fromLocalFile(fileInfo.absoluteFilePath());
|
||||
}
|
||||
|
||||
bool FileDialogHelper::urlIsDir(const QUrl& url) {
|
||||
return QFileInfo(url.toLocalFile()).isDir();
|
||||
}
|
||||
|
||||
bool FileDialogHelper::urlIsFile(const QUrl& url) {
|
||||
return QFileInfo(url.toLocalFile()).isFile();
|
||||
}
|
||||
|
||||
bool FileDialogHelper::urlExists(const QUrl& url) {
|
||||
return QFileInfo(url.toLocalFile()).exists();
|
||||
}
|
||||
|
||||
bool FileDialogHelper::urlIsWritable(const QUrl& url) {
|
||||
QFileInfo fileInfo(url.toLocalFile());
|
||||
// Is the file writable?
|
||||
if (fileInfo.exists()) {
|
||||
return fileInfo.isWritable();
|
||||
}
|
||||
|
||||
// No file, get the parent directory and check if writable
|
||||
return QFileInfo(fileInfo.absoluteDir().absolutePath()).isWritable();
|
||||
}
|
||||
|
||||
QStringList FileDialogHelper::drives() {
|
||||
QStringList result;
|
||||
for (const auto& drive : QDir::drives()) {
|
||||
result << drive.absolutePath();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -47,10 +47,17 @@ public:
|
|||
|
||||
Q_INVOKABLE QUrl home();
|
||||
Q_INVOKABLE QStringList standardPath(StandardLocation location);
|
||||
Q_INVOKABLE QStringList drives();
|
||||
Q_INVOKABLE QString urlToPath(const QUrl& url);
|
||||
Q_INVOKABLE bool urlIsDir(const QUrl& url);
|
||||
Q_INVOKABLE bool urlIsFile(const QUrl& url);
|
||||
Q_INVOKABLE bool urlExists(const QUrl& url);
|
||||
Q_INVOKABLE bool urlIsWritable(const QUrl& url);
|
||||
Q_INVOKABLE bool fileExists(const QString& path);
|
||||
Q_INVOKABLE bool validPath(const QString& path);
|
||||
Q_INVOKABLE bool validFolder(const QString& path);
|
||||
Q_INVOKABLE QUrl pathToUrl(const QString& path);
|
||||
Q_INVOKABLE QUrl saveHelper(const QString& saveText, const QUrl& currentFolder, const QStringList& selectionFilters);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -104,6 +105,7 @@ void OffscreenUi::create(QOpenGLContext* context) {
|
|||
OffscreenQmlSurface::create(context);
|
||||
auto rootContext = getRootContext();
|
||||
|
||||
rootContext->setContextProperty("OffscreenUi", this);
|
||||
rootContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags());
|
||||
rootContext->setContextProperty("urlHandler", new UrlHandler());
|
||||
rootContext->setContextProperty("fileDialogHelper", new FileDialogHelper());
|
||||
|
@ -218,7 +220,7 @@ QQuickItem* OffscreenUi::createMessageBox(QMessageBox::Icon icon, const QString&
|
|||
return qvariant_cast<QQuickItem*>(result);
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton OffscreenUi::waitForMessageBoxResult(QQuickItem* messageBox) {
|
||||
int OffscreenUi::waitForMessageBoxResult(QQuickItem* messageBox) {
|
||||
if (!messageBox) {
|
||||
return QMessageBox::NoButton;
|
||||
}
|
||||
|
@ -240,7 +242,7 @@ QMessageBox::StandardButton OffscreenUi::messageBox(QMessageBox::Icon icon, cons
|
|||
return result;
|
||||
}
|
||||
|
||||
return waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton));
|
||||
return static_cast<QMessageBox::StandardButton>(waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton)));
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton OffscreenUi::critical(const QString& title, const QString& text,
|
||||
|
@ -477,6 +479,26 @@ private slots:
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
QString OffscreenUi::fileDialog(const QVariantMap& properties) {
|
||||
QVariant buildDialogResult;
|
||||
bool invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog",
|
||||
Q_RETURN_ARG(QVariant, buildDialogResult),
|
||||
Q_ARG(QVariant, QVariant::fromValue(properties)));
|
||||
|
||||
if (!invokeResult) {
|
||||
qWarning() << "Failed to create file open dialog";
|
||||
return QString();
|
||||
}
|
||||
|
||||
QVariant result = FileDialogListener(qvariant_cast<QQuickItem*>(buildDialogResult)).waitForResult();
|
||||
if (!result.isValid()) {
|
||||
return QString();
|
||||
}
|
||||
qDebug() << result.toString();
|
||||
return result.toUrl().toLocalFile();
|
||||
}
|
||||
|
||||
QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QString result;
|
||||
|
@ -496,28 +518,40 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir,
|
|||
map.insert("dir", QUrl::fromLocalFile(dir));
|
||||
map.insert("filter", filter);
|
||||
map.insert("options", static_cast<int>(options));
|
||||
return fileDialog(map);
|
||||
}
|
||||
|
||||
QVariant buildDialogResult;
|
||||
bool invokeResult = QMetaObject::invokeMethod(_desktop, "fileOpenDialog",
|
||||
Q_RETURN_ARG(QVariant, buildDialogResult),
|
||||
Q_ARG(QVariant, QVariant::fromValue(map)));
|
||||
|
||||
if (!invokeResult) {
|
||||
qWarning() << "Failed to create file open dialog";
|
||||
return QString();
|
||||
QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QString result;
|
||||
QMetaObject::invokeMethod(this, "fileSaveDialog", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QString, result),
|
||||
Q_ARG(QString, caption),
|
||||
Q_ARG(QString, dir),
|
||||
Q_ARG(QString, filter),
|
||||
Q_ARG(QString*, selectedFilter),
|
||||
Q_ARG(QFileDialog::Options, options));
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariant result = FileDialogListener(qvariant_cast<QQuickItem*>(buildDialogResult)).waitForResult();
|
||||
if (!result.isValid()) {
|
||||
return QString();
|
||||
}
|
||||
qDebug() << result.toString();
|
||||
return result.toUrl().toLocalFile();
|
||||
// FIXME support returning the selected filter... somehow?
|
||||
QVariantMap map;
|
||||
map.insert("caption", caption);
|
||||
map.insert("dir", QUrl::fromLocalFile(dir));
|
||||
map.insert("filter", filter);
|
||||
map.insert("options", static_cast<int>(options));
|
||||
map.insert("saveDialog", true);
|
||||
|
||||
return fileDialog(map);
|
||||
}
|
||||
|
||||
QString OffscreenUi::getOpenFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) {
|
||||
return DependencyManager::get<OffscreenUi>()->fileOpenDialog(caption, dir, filter, selectedFilter, options);
|
||||
}
|
||||
|
||||
QString OffscreenUi::getSaveFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) {
|
||||
return DependencyManager::get<OffscreenUi>()->fileSaveDialog(caption, dir, filter, selectedFilter, options);
|
||||
}
|
||||
|
||||
|
||||
#include "OffscreenUi.moc"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue