diff --git a/BUILD.md b/BUILD.md index b8bc1cd14c..674f0d24cc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -63,7 +63,7 @@ If `libgnutls28-dev` 3.2.12 or higher is available via your package manager, it [Homebrew](http://brew.sh/) is an excellent package manager for OS X. It makes install of all hifi dependencies very simple. brew tap highfidelity/homebrew-formulas - brew install cmake glm zlib gnutls + brew install cmake glm gnutls brew install highfidelity/formulas/qt5 brew link qt5 --force brew install highfidelity/formulas/qxmpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a399e11168..b7fa55d4a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ if (WIN32) elseif (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) #SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -pedantic") #SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-unknown-pragmas") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fno-strict-aliasing") endif(WIN32) if (NOT QT_CMAKE_PREFIX_PATH) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index fcc2288356..5720ecaaf5 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -147,6 +147,15 @@ void Agent::readPendingDatagrams() { } } else if (datagramPacketType == PacketTypeMixedAudio) { + + QUuid senderUUID = uuidFromPacketHeader(receivedPacket); + + // parse sequence number for this packet + int numBytesPacketHeader = numBytesForPacketHeader(receivedPacket); + const char* sequenceAt = receivedPacket.constData() + numBytesPacketHeader; + quint16 sequence = *(reinterpret_cast<const quint16*>(sequenceAt)); + _incomingMixedAudioSequenceNumberStats.sequenceNumberReceived(sequence, senderUUID); + // parse the data and grab the average loudness _receivedAudioBuffer.parseData(receivedPacket); @@ -213,8 +222,6 @@ void Agent::run() { loop.exec(); - - // let the AvatarData and ResourceCache classes use our QNetworkAccessManager AvatarData::setNetworkAccessManager(networkManager); ResourceCache::setNetworkAccessManager(networkManager); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 9af95e757c..ec8f7c88cb 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -71,6 +71,8 @@ private: ModelTreeHeadlessViewer _modelViewer; MixedAudioRingBuffer _receivedAudioBuffer; + SequenceNumberStats _incomingMixedAudioSequenceNumberStats; + AvatarHashMap _avatarHashMap; }; diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 2dc51b44a0..663aef81a7 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -38,6 +38,9 @@ #include <QtCore/QJsonObject> #include <QtCore/QJsonValue> #include <QtCore/QTimer> +#include <QtNetwork/QNetworkAccessManager> +#include <QtNetwork/QNetworkRequest> +#include <QtNetwork/QNetworkReply> #include <Logging.h> #include <NodeList.h> @@ -54,9 +57,6 @@ #include "AudioMixer.h" -const short JITTER_BUFFER_MSECS = 12; -const short JITTER_BUFFER_SAMPLES = JITTER_BUFFER_MSECS * (SAMPLE_RATE / 1000.0); - const float LOUDNESS_TO_DISTANCE_RATIO = 0.00001f; const QString AUDIO_MIXER_LOGGING_TARGET_NAME = "audio-mixer"; @@ -67,6 +67,8 @@ void attachNewBufferToNode(Node *newNode) { } } +bool AudioMixer::_useDynamicJitterBuffers = false; + AudioMixer::AudioMixer(const QByteArray& packet) : ThreadedAssignment(packet), _trailingSleepRatio(1.0f), @@ -76,7 +78,8 @@ AudioMixer::AudioMixer(const QByteArray& packet) : _sumListeners(0), _sumMixes(0), _sourceUnattenuatedZone(NULL), - _listenerUnattenuatedZone(NULL) + _listenerUnattenuatedZone(NULL), + _lastSendAudioStreamStatsTime(usecTimestampNow()) { } @@ -427,12 +430,46 @@ void AudioMixer::sendStatsPacket() { } else { statsObject["average_mixes_per_listener"] = 0.0; } - + ThreadedAssignment::addPacketStatsAndSendStatsPacket(statsObject); - _sumListeners = 0; _sumMixes = 0; _numStatFrames = 0; + + + // NOTE: These stats can be too large to fit in an MTU, so we break it up into multiple packts... + QJsonObject statsObject2; + + // add stats for each listerner + bool somethingToSend = false; + int sizeOfStats = 0; + int TOO_BIG_FOR_MTU = 1200; // some extra space for JSONification + + NodeList* nodeList = NodeList::getInstance(); + int clientNumber = 0; + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { + clientNumber++; + AudioMixerClientData* clientData = static_cast<AudioMixerClientData*>(node->getLinkedData()); + if (clientData) { + QString property = "jitterStats." + node->getUUID().toString(); + QString value = clientData->getAudioStreamStatsString(); + statsObject2[qPrintable(property)] = value; + somethingToSend = true; + sizeOfStats += property.size() + value.size(); + } + + // if we're too large, send the packet + if (sizeOfStats > TOO_BIG_FOR_MTU) { + nodeList->sendStatsToDomainServer(statsObject2); + sizeOfStats = 0; + statsObject2 = QJsonObject(); // clear it + somethingToSend = false; + } + } + + if (somethingToSend) { + nodeList->sendStatsToDomainServer(statsObject2); + } } void AudioMixer::run() { @@ -445,37 +482,88 @@ void AudioMixer::run() { nodeList->linkedDataCreateCallback = attachNewBufferToNode; - // check the payload to see if we have any unattenuated zones - const QString UNATTENUATED_ZONE_REGEX_STRING = "--unattenuated-zone ([\\d.,-]+)"; - QRegExp unattenuatedZoneMatch(UNATTENUATED_ZONE_REGEX_STRING); + // setup a QNetworkAccessManager to ask the domain-server for our settings + QNetworkAccessManager *networkManager = new QNetworkAccessManager(this); - if (unattenuatedZoneMatch.indexIn(_payload) != -1) { - QString unattenuatedZoneString = unattenuatedZoneMatch.cap(1); - QStringList zoneStringList = unattenuatedZoneString.split(','); + QUrl settingsJSONURL; + settingsJSONURL.setScheme("http"); + settingsJSONURL.setHost(nodeList->getDomainHandler().getHostname()); + settingsJSONURL.setPort(DOMAIN_SERVER_HTTP_PORT); + settingsJSONURL.setPath("/settings.json"); + settingsJSONURL.setQuery(QString("type=%1").arg(_type)); + + QNetworkReply *reply = NULL; + + int failedAttempts = 0; + const int MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS = 5; + + qDebug() << "Requesting settings for assignment from domain-server at" << settingsJSONURL.toString(); + + while (!reply || reply->error() != QNetworkReply::NoError) { + reply = networkManager->get(QNetworkRequest(settingsJSONURL)); - glm::vec3 sourceCorner(zoneStringList[0].toFloat(), zoneStringList[1].toFloat(), zoneStringList[2].toFloat()); - glm::vec3 sourceDimensions(zoneStringList[3].toFloat(), zoneStringList[4].toFloat(), zoneStringList[5].toFloat()); - - glm::vec3 listenerCorner(zoneStringList[6].toFloat(), zoneStringList[7].toFloat(), zoneStringList[8].toFloat()); - glm::vec3 listenerDimensions(zoneStringList[9].toFloat(), zoneStringList[10].toFloat(), zoneStringList[11].toFloat()); + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - _sourceUnattenuatedZone = new AABox(sourceCorner, sourceDimensions); - _listenerUnattenuatedZone = new AABox(listenerCorner, listenerDimensions); + loop.exec(); - glm::vec3 sourceCenter = _sourceUnattenuatedZone->calcCenter(); - glm::vec3 destinationCenter = _listenerUnattenuatedZone->calcCenter(); + ++failedAttempts; - qDebug() << "There is an unattenuated zone with source center at" - << QString("%1, %2, %3").arg(sourceCenter.x).arg(sourceCenter.y).arg(sourceCenter.z); - qDebug() << "Buffers inside this zone will not be attenuated inside a box with center at" - << QString("%1, %2, %3").arg(destinationCenter.x).arg(destinationCenter.y).arg(destinationCenter.z); + if (failedAttempts == MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS) { + qDebug() << "Failed to get settings from domain-server. Bailing on assignment."; + setFinished(true); + return; + } } - + + QJsonObject settingsObject = QJsonDocument::fromJson(reply->readAll()).object(); + + // check the settings object to see if we have anything we can parse out + const QString AUDIO_GROUP_KEY = "audio"; + + if (settingsObject.contains(AUDIO_GROUP_KEY)) { + QJsonObject audioGroupObject = settingsObject[AUDIO_GROUP_KEY].toObject(); + + const QString UNATTENUATED_ZONE_KEY = "unattenuated-zone"; + + QString unattenuatedZoneString = audioGroupObject[UNATTENUATED_ZONE_KEY].toString(); + if (!unattenuatedZoneString.isEmpty()) { + QStringList zoneStringList = unattenuatedZoneString.split(','); + + glm::vec3 sourceCorner(zoneStringList[0].toFloat(), zoneStringList[1].toFloat(), zoneStringList[2].toFloat()); + glm::vec3 sourceDimensions(zoneStringList[3].toFloat(), zoneStringList[4].toFloat(), zoneStringList[5].toFloat()); + + glm::vec3 listenerCorner(zoneStringList[6].toFloat(), zoneStringList[7].toFloat(), zoneStringList[8].toFloat()); + glm::vec3 listenerDimensions(zoneStringList[9].toFloat(), zoneStringList[10].toFloat(), zoneStringList[11].toFloat()); + + _sourceUnattenuatedZone = new AABox(sourceCorner, sourceDimensions); + _listenerUnattenuatedZone = new AABox(listenerCorner, listenerDimensions); + + glm::vec3 sourceCenter = _sourceUnattenuatedZone->calcCenter(); + glm::vec3 destinationCenter = _listenerUnattenuatedZone->calcCenter(); + + qDebug() << "There is an unattenuated zone with source center at" + << QString("%1, %2, %3").arg(sourceCenter.x).arg(sourceCenter.y).arg(sourceCenter.z); + qDebug() << "Buffers inside this zone will not be attenuated inside a box with center at" + << QString("%1, %2, %3").arg(destinationCenter.x).arg(destinationCenter.y).arg(destinationCenter.z); + } + + // check the payload to see if we have asked for dynamicJitterBuffer support + const QString DYNAMIC_JITTER_BUFFER_JSON_KEY = "dynamic-jitter-buffer"; + bool shouldUseDynamicJitterBuffers = audioGroupObject[DYNAMIC_JITTER_BUFFER_JSON_KEY].toBool(); + if (shouldUseDynamicJitterBuffers) { + qDebug() << "Enable dynamic jitter buffers."; + _useDynamicJitterBuffers = true; + } else { + qDebug() << "Dynamic jitter buffers disabled, using old behavior."; + } + } + int nextFrame = 0; QElapsedTimer timer; timer.start(); - char* clientMixBuffer = new char[NETWORK_BUFFER_LENGTH_BYTES_STEREO + char* clientMixBuffer = new char[NETWORK_BUFFER_LENGTH_BYTES_STEREO + sizeof(quint16) + numBytesForPacketHeaderGivenPacketType(PacketTypeMixedAudio)]; int usecToSleep = BUFFER_SEND_INTERVAL_USECS; @@ -487,8 +575,7 @@ void AudioMixer::run() { foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { if (node->getLinkedData()) { - ((AudioMixerClientData*) node->getLinkedData())->checkBuffersBeforeFrameSend(JITTER_BUFFER_SAMPLES, - _sourceUnattenuatedZone, + ((AudioMixerClientData*) node->getLinkedData())->checkBuffersBeforeFrameSend(_sourceUnattenuatedZone, _listenerUnattenuatedZone); } } @@ -545,20 +632,50 @@ void AudioMixer::run() { ++framesSinceCutoffEvent; } + + const quint64 TOO_LONG_SINCE_LAST_SEND_AUDIO_STREAM_STATS = 1 * USECS_PER_SECOND; + + bool sendAudioStreamStats = false; + quint64 now = usecTimestampNow(); + if (now - _lastSendAudioStreamStatsTime > TOO_LONG_SINCE_LAST_SEND_AUDIO_STREAM_STATS) { + _lastSendAudioStreamStatsTime = now; + sendAudioStreamStats = true; + } + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { if (node->getType() == NodeType::Agent && node->getActiveSocket() && node->getLinkedData() && ((AudioMixerClientData*) node->getLinkedData())->getAvatarAudioRingBuffer()) { + + AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData(); + prepareMixForListeningNode(node.data()); + // pack header int numBytesPacketHeader = populatePacketHeader(clientMixBuffer, PacketTypeMixedAudio); + char* dataAt = clientMixBuffer + numBytesPacketHeader; - memcpy(clientMixBuffer + numBytesPacketHeader, _clientSamples, NETWORK_BUFFER_LENGTH_BYTES_STEREO); - nodeList->writeDatagram(clientMixBuffer, NETWORK_BUFFER_LENGTH_BYTES_STEREO + numBytesPacketHeader, node); + // pack sequence number + quint16 sequence = nodeData->getOutgoingSequenceNumber(); + memcpy(dataAt, &sequence, sizeof(quint16)); + dataAt += sizeof(quint16); + + // pack mixed audio samples + memcpy(dataAt, _clientSamples, NETWORK_BUFFER_LENGTH_BYTES_STEREO); + dataAt += NETWORK_BUFFER_LENGTH_BYTES_STEREO; + + // send mixed audio packet + nodeList->writeDatagram(clientMixBuffer, dataAt - clientMixBuffer, node); + nodeData->incrementOutgoingMixedAudioSequenceNumber(); + // send an audio stream stats packet if it's time + if (sendAudioStreamStats) { + nodeData->sendAudioStreamStatsPackets(node); + } + ++_sumListeners; } } - + // push forward the next output pointers for any audio buffers we used foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { if (node->getLinkedData()) { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 39f8cf63ae..2c94f32edc 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -34,6 +34,9 @@ public slots: void readPendingDatagrams(); void sendStatsPacket(); + + static bool getUseDynamicJitterBuffers() { return _useDynamicJitterBuffers; } + private: /// adds one buffer to the mix for a listening node void addBufferToMixForListeningNodeWithBuffer(PositionalAudioRingBuffer* bufferToAdd, @@ -54,6 +57,9 @@ private: int _sumMixes; AABox* _sourceUnattenuatedZone; AABox* _listenerUnattenuatedZone; + static bool _useDynamicJitterBuffers; + + quint64 _lastSendAudioStreamStatsTime; }; #endif // hifi_AudioMixer_h diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 2f78a4ac78..9b14ecfd19 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -16,10 +16,13 @@ #include "InjectedAudioRingBuffer.h" +#include "AudioMixer.h" #include "AudioMixerClientData.h" AudioMixerClientData::AudioMixerClientData() : - _ringBuffers() + _ringBuffers(), + _outgoingMixedAudioSequenceNumber(0), + _incomingAvatarAudioSequenceNumberStats() { } @@ -43,16 +46,24 @@ AvatarAudioRingBuffer* AudioMixerClientData::getAvatarAudioRingBuffer() const { } int AudioMixerClientData::parseData(const QByteArray& packet) { + + // parse sequence number for this packet + int numBytesPacketHeader = numBytesForPacketHeader(packet); + const char* sequenceAt = packet.constData() + numBytesPacketHeader; + quint16 sequence = *(reinterpret_cast<const quint16*>(sequenceAt)); + PacketType packetType = packetTypeForPacket(packet); if (packetType == PacketTypeMicrophoneAudioWithEcho || packetType == PacketTypeMicrophoneAudioNoEcho || packetType == PacketTypeSilentAudioFrame) { + _incomingAvatarAudioSequenceNumberStats.sequenceNumberReceived(sequence); + // grab the AvatarAudioRingBuffer from the vector (or create it if it doesn't exist) AvatarAudioRingBuffer* avatarRingBuffer = getAvatarAudioRingBuffer(); // read the first byte after the header to see if this is a stereo or mono buffer - quint8 channelFlag = packet.at(numBytesForPacketHeader(packet)); + quint8 channelFlag = packet.at(numBytesForPacketHeader(packet) + sizeof(quint16)); bool isStereo = channelFlag == 1; if (avatarRingBuffer && avatarRingBuffer->isStereo() != isStereo) { @@ -65,7 +76,7 @@ int AudioMixerClientData::parseData(const QByteArray& packet) { if (!avatarRingBuffer) { // we don't have an AvatarAudioRingBuffer yet, so add it - avatarRingBuffer = new AvatarAudioRingBuffer(isStereo); + avatarRingBuffer = new AvatarAudioRingBuffer(isStereo, AudioMixer::getUseDynamicJitterBuffers()); _ringBuffers.push_back(avatarRingBuffer); } @@ -75,7 +86,9 @@ int AudioMixerClientData::parseData(const QByteArray& packet) { // this is injected audio // grab the stream identifier for this injected audio - QUuid streamIdentifier = QUuid::fromRfc4122(packet.mid(numBytesForPacketHeader(packet), NUM_BYTES_RFC4122_UUID)); + QUuid streamIdentifier = QUuid::fromRfc4122(packet.mid(numBytesForPacketHeader(packet) + sizeof(quint16), NUM_BYTES_RFC4122_UUID)); + + _incomingInjectedAudioSequenceNumberStatsMap[streamIdentifier].sequenceNumberReceived(sequence); InjectedAudioRingBuffer* matchingInjectedRingBuffer = NULL; @@ -88,7 +101,8 @@ int AudioMixerClientData::parseData(const QByteArray& packet) { if (!matchingInjectedRingBuffer) { // we don't have a matching injected audio ring buffer, so add it - matchingInjectedRingBuffer = new InjectedAudioRingBuffer(streamIdentifier); + matchingInjectedRingBuffer = new InjectedAudioRingBuffer(streamIdentifier, + AudioMixer::getUseDynamicJitterBuffers()); _ringBuffers.push_back(matchingInjectedRingBuffer); } @@ -98,10 +112,9 @@ int AudioMixerClientData::parseData(const QByteArray& packet) { return 0; } -void AudioMixerClientData::checkBuffersBeforeFrameSend(int jitterBufferLengthSamples, - AABox* checkSourceZone, AABox* listenerZone) { +void AudioMixerClientData::checkBuffersBeforeFrameSend(AABox* checkSourceZone, AABox* listenerZone) { for (int i = 0; i < _ringBuffers.size(); i++) { - if (_ringBuffers[i]->shouldBeAddedToMix(jitterBufferLengthSamples)) { + if (_ringBuffers[i]->shouldBeAddedToMix()) { // this is a ring buffer that is ready to go // set its flag so we know to push its buffer when all is said and done _ringBuffers[i]->setWillBeAddedToMix(true); @@ -120,20 +133,146 @@ void AudioMixerClientData::checkBuffersBeforeFrameSend(int jitterBufferLengthSam } void AudioMixerClientData::pushBuffersAfterFrameSend() { - for (int i = 0; i < _ringBuffers.size(); i++) { + + QList<PositionalAudioRingBuffer*>::iterator i = _ringBuffers.begin(); + while (i != _ringBuffers.end()) { // this was a used buffer, push the output pointer forwards - PositionalAudioRingBuffer* audioBuffer = _ringBuffers[i]; + PositionalAudioRingBuffer* audioBuffer = *i; if (audioBuffer->willBeAddedToMix()) { - audioBuffer->shiftReadPosition(audioBuffer->isStereo() - ? NETWORK_BUFFER_LENGTH_SAMPLES_STEREO : NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL); - + audioBuffer->shiftReadPosition(audioBuffer->getSamplesPerFrame()); audioBuffer->setWillBeAddedToMix(false); } else if (audioBuffer->getType() == PositionalAudioRingBuffer::Injector && audioBuffer->hasStarted() && audioBuffer->isStarved()) { // this is an empty audio buffer that has starved, safe to delete + // also delete its sequence number stats + QUuid streamIdentifier = ((InjectedAudioRingBuffer*)audioBuffer)->getStreamIdentifier(); + _incomingInjectedAudioSequenceNumberStatsMap.remove(streamIdentifier); delete audioBuffer; - _ringBuffers.erase(_ringBuffers.begin() + i); + i = _ringBuffers.erase(i); + continue; } + i++; } } + +AudioStreamStats AudioMixerClientData::getAudioStreamStatsOfStream(const PositionalAudioRingBuffer* ringBuffer) const { + AudioStreamStats streamStats; + SequenceNumberStats streamSequenceNumberStats; + + streamStats._streamType = ringBuffer->getType(); + if (streamStats._streamType == PositionalAudioRingBuffer::Injector) { + streamStats._streamIdentifier = ((InjectedAudioRingBuffer*)ringBuffer)->getStreamIdentifier(); + streamSequenceNumberStats = _incomingInjectedAudioSequenceNumberStatsMap.value(streamStats._streamIdentifier); + } else { + streamSequenceNumberStats = _incomingAvatarAudioSequenceNumberStats; + } + streamStats._jitterBufferFrames = ringBuffer->getCurrentJitterBufferFrames(); + + streamStats._packetsReceived = streamSequenceNumberStats.getNumReceived(); + streamStats._packetsUnreasonable = streamSequenceNumberStats.getNumUnreasonable(); + streamStats._packetsEarly = streamSequenceNumberStats.getNumEarly(); + streamStats._packetsLate = streamSequenceNumberStats.getNumLate(); + streamStats._packetsLost = streamSequenceNumberStats.getNumLost(); + streamStats._packetsRecovered = streamSequenceNumberStats.getNumRecovered(); + streamStats._packetsDuplicate = streamSequenceNumberStats.getNumDuplicate(); + + return streamStats; +} + +void AudioMixerClientData::sendAudioStreamStatsPackets(const SharedNodePointer& destinationNode) const { + + char packet[MAX_PACKET_SIZE]; + NodeList* nodeList = NodeList::getInstance(); + + // The append flag is a boolean value that will be packed right after the header. The first packet sent + // inside this method will have 0 for this flag, while every subsequent packet will have 1 for this flag. + // The sole purpose of this flag is so the client can clear its map of injected audio stream stats when + // it receives a packet with an appendFlag of 0. This prevents the buildup of dead audio stream stats in the client. + quint8 appendFlag = 0; + + // pack header + int numBytesPacketHeader = populatePacketHeader(packet, PacketTypeAudioStreamStats); + char* headerEndAt = packet + numBytesPacketHeader; + + // calculate how many stream stat structs we can fit in each packet + const int numStreamStatsRoomFor = (MAX_PACKET_SIZE - numBytesPacketHeader - sizeof(quint8) - sizeof(quint16)) / sizeof(AudioStreamStats); + + // pack and send stream stats packets until all ring buffers' stats are sent + int numStreamStatsRemaining = _ringBuffers.size(); + QList<PositionalAudioRingBuffer*>::ConstIterator ringBuffersIterator = _ringBuffers.constBegin(); + while (numStreamStatsRemaining > 0) { + + char* dataAt = headerEndAt; + + // pack the append flag + memcpy(dataAt, &appendFlag, sizeof(quint8)); + appendFlag = 1; + dataAt += sizeof(quint8); + + // calculate and pack the number of stream stats to follow + quint16 numStreamStatsToPack = std::min(numStreamStatsRemaining, numStreamStatsRoomFor); + memcpy(dataAt, &numStreamStatsToPack, sizeof(quint16)); + dataAt += sizeof(quint16); + + // pack the calculated number of stream stats + for (int i = 0; i < numStreamStatsToPack; i++) { + AudioStreamStats streamStats = getAudioStreamStatsOfStream(*ringBuffersIterator); + memcpy(dataAt, &streamStats, sizeof(AudioStreamStats)); + dataAt += sizeof(AudioStreamStats); + + ringBuffersIterator++; + } + numStreamStatsRemaining -= numStreamStatsToPack; + + // send the current packet + nodeList->writeDatagram(packet, dataAt - packet, destinationNode); + } +} + +QString AudioMixerClientData::getAudioStreamStatsString() const { + QString result; + AvatarAudioRingBuffer* avatarRingBuffer = getAvatarAudioRingBuffer(); + if (avatarRingBuffer) { + int desiredJitterBuffer = avatarRingBuffer->getDesiredJitterBufferFrames(); + int calculatedJitterBuffer = avatarRingBuffer->getCalculatedDesiredJitterBufferFrames(); + int currentJitterBuffer = avatarRingBuffer->getCurrentJitterBufferFrames(); + int overflowCount = avatarRingBuffer->getOverflowCount(); + int samplesAvailable = avatarRingBuffer->samplesAvailable(); + int framesAvailable = (samplesAvailable / avatarRingBuffer->getSamplesPerFrame()); + AudioStreamStats streamStats = getAudioStreamStatsOfStream(avatarRingBuffer); + result += "mic.desired:" + QString::number(desiredJitterBuffer) + + " calculated:" + QString::number(calculatedJitterBuffer) + + " current:" + QString::number(currentJitterBuffer) + + " available:" + QString::number(framesAvailable) + + " samples:" + QString::number(samplesAvailable) + + " overflows:" + QString::number(overflowCount) + + " early:" + QString::number(streamStats._packetsEarly) + + " late:" + QString::number(streamStats._packetsLate) + + " lost:" + QString::number(streamStats._packetsLost); + } else { + result = "mic unknown"; + } + + for (int i = 0; i < _ringBuffers.size(); i++) { + if (_ringBuffers[i]->getType() == PositionalAudioRingBuffer::Injector) { + int desiredJitterBuffer = _ringBuffers[i]->getDesiredJitterBufferFrames(); + int calculatedJitterBuffer = _ringBuffers[i]->getCalculatedDesiredJitterBufferFrames(); + int currentJitterBuffer = _ringBuffers[i]->getCurrentJitterBufferFrames(); + int overflowCount = _ringBuffers[i]->getOverflowCount(); + int samplesAvailable = _ringBuffers[i]->samplesAvailable(); + int framesAvailable = (samplesAvailable / _ringBuffers[i]->getSamplesPerFrame()); + AudioStreamStats streamStats = getAudioStreamStatsOfStream(_ringBuffers[i]); + result += "| injected[" + QString::number(i) + "].desired:" + QString::number(desiredJitterBuffer) + + " calculated:" + QString::number(calculatedJitterBuffer) + + " current:" + QString::number(currentJitterBuffer) + + " available:" + QString::number(framesAvailable) + + " samples:" + QString::number(samplesAvailable) + + " overflows:" + QString::number(overflowCount) + + " early:" + QString::number(streamStats._packetsEarly) + + " late:" + QString::number(streamStats._packetsLate) + + " lost:" + QString::number(streamStats._packetsLost); + } + } + return result; +} diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index e52b09e134..65fd4b3da3 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -17,6 +17,8 @@ #include <PositionalAudioRingBuffer.h> #include "AvatarAudioRingBuffer.h" +#include "AudioStreamStats.h" +#include "SequenceNumberStats.h" class AudioMixerClientData : public NodeData { public: @@ -27,11 +29,23 @@ public: AvatarAudioRingBuffer* getAvatarAudioRingBuffer() const; int parseData(const QByteArray& packet); - void checkBuffersBeforeFrameSend(int jitterBufferLengthSamples, - AABox* checkSourceZone = NULL, AABox* listenerZone = NULL); + void checkBuffersBeforeFrameSend(AABox* checkSourceZone = NULL, AABox* listenerZone = NULL); void pushBuffersAfterFrameSend(); + + AudioStreamStats getAudioStreamStatsOfStream(const PositionalAudioRingBuffer* ringBuffer) const; + QString getAudioStreamStatsString() const; + + void sendAudioStreamStatsPackets(const SharedNodePointer& destinationNode) const; + + void incrementOutgoingMixedAudioSequenceNumber() { _outgoingMixedAudioSequenceNumber++; } + quint16 getOutgoingSequenceNumber() const { return _outgoingMixedAudioSequenceNumber; } + private: QList<PositionalAudioRingBuffer*> _ringBuffers; + + quint16 _outgoingMixedAudioSequenceNumber; + SequenceNumberStats _incomingAvatarAudioSequenceNumberStats; + QHash<QUuid, SequenceNumberStats> _incomingInjectedAudioSequenceNumberStatsMap; }; #endif // hifi_AudioMixerClientData_h diff --git a/assignment-client/src/audio/AvatarAudioRingBuffer.cpp b/assignment-client/src/audio/AvatarAudioRingBuffer.cpp index 5613a64cc4..9c6cc32f57 100644 --- a/assignment-client/src/audio/AvatarAudioRingBuffer.cpp +++ b/assignment-client/src/audio/AvatarAudioRingBuffer.cpp @@ -13,12 +13,15 @@ #include "AvatarAudioRingBuffer.h" -AvatarAudioRingBuffer::AvatarAudioRingBuffer(bool isStereo) : - PositionalAudioRingBuffer(PositionalAudioRingBuffer::Microphone, isStereo) { +AvatarAudioRingBuffer::AvatarAudioRingBuffer(bool isStereo, bool dynamicJitterBuffer) : + PositionalAudioRingBuffer(PositionalAudioRingBuffer::Microphone, isStereo, dynamicJitterBuffer) { } int AvatarAudioRingBuffer::parseData(const QByteArray& packet) { + _interframeTimeGapStats.frameReceived(); + updateDesiredJitterBufferFrames(); + _shouldLoopbackForNode = (packetTypeForPacket(packet) == PacketTypeMicrophoneAudioWithEcho); return PositionalAudioRingBuffer::parseData(packet); } diff --git a/assignment-client/src/audio/AvatarAudioRingBuffer.h b/assignment-client/src/audio/AvatarAudioRingBuffer.h index f842c2aa33..e227e70958 100644 --- a/assignment-client/src/audio/AvatarAudioRingBuffer.h +++ b/assignment-client/src/audio/AvatarAudioRingBuffer.h @@ -18,7 +18,7 @@ class AvatarAudioRingBuffer : public PositionalAudioRingBuffer { public: - AvatarAudioRingBuffer(bool isStereo = false); + AvatarAudioRingBuffer(bool isStereo = false, bool dynamicJitterBuffer = false); int parseData(const QByteArray& packet); private: diff --git a/assignment-client/src/metavoxels/MetavoxelServer.cpp b/assignment-client/src/metavoxels/MetavoxelServer.cpp index 3bf1632074..d0c0d4c781 100644 --- a/assignment-client/src/metavoxels/MetavoxelServer.cpp +++ b/assignment-client/src/metavoxels/MetavoxelServer.cpp @@ -69,7 +69,7 @@ void MetavoxelServer::readPendingDatagrams() { void MetavoxelServer::maybeAttachSession(const SharedNodePointer& node) { if (node->getType() == NodeType::Agent) { QMutexLocker locker(&node->getMutex()); - node->setLinkedData(new MetavoxelSession(this, NodeList::getInstance()->nodeWithUUID(node->getUUID()))); + node->setLinkedData(new MetavoxelSession(node, this)); } } @@ -77,7 +77,7 @@ void MetavoxelServer::sendDeltas() { // send deltas for all sessions foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { if (node->getType() == NodeType::Agent) { - static_cast<MetavoxelSession*>(node->getLinkedData())->sendDelta(); + static_cast<MetavoxelSession*>(node->getLinkedData())->update(); } } @@ -89,59 +89,34 @@ void MetavoxelServer::sendDeltas() { _sendTimer.start(qMax(0, 2 * SEND_INTERVAL - elapsed)); } -MetavoxelSession::MetavoxelSession(MetavoxelServer* server, const SharedNodePointer& node) : - _server(server), - _sequencer(byteArrayWithPopulatedHeader(PacketTypeMetavoxelData)), - _node(node) { +MetavoxelSession::MetavoxelSession(const SharedNodePointer& node, MetavoxelServer* server) : + Endpoint(node, new PacketRecord(), NULL), + _server(server) { - connect(&_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendData(const QByteArray&))); - connect(&_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readPacket(Bitstream&))); - connect(&_sequencer, SIGNAL(sendAcknowledged(int)), SLOT(clearSendRecordsBefore(int))); connect(&_sequencer, SIGNAL(receivedHighPriorityMessage(const QVariant&)), SLOT(handleMessage(const QVariant&))); connect(_sequencer.getReliableInputChannel(), SIGNAL(receivedMessage(const QVariant&)), SLOT(handleMessage(const QVariant&))); - - // insert the baseline send record - SendRecord record = { 0 }; - _sendRecords.append(record); } -MetavoxelSession::~MetavoxelSession() { -} - -int MetavoxelSession::parseData(const QByteArray& packet) { - // process through sequencer - _sequencer.receivedDatagram(packet); - return packet.size(); -} - -void MetavoxelSession::sendDelta() { +void MetavoxelSession::update() { // wait until we have a valid lod - if (!_lod.isValid()) { - return; + if (_lod.isValid()) { + Endpoint::update(); } - Bitstream& out = _sequencer.startPacket(); +} + +void MetavoxelSession::writeUpdateMessage(Bitstream& out) { out << QVariant::fromValue(MetavoxelDeltaMessage()); - _server->getData().writeDelta(_sendRecords.first().data, _sendRecords.first().lod, out, _lod); - _sequencer.endPacket(); - - // record the send - SendRecord record = { _sequencer.getOutgoingPacketNumber(), _server->getData(), _lod }; - _sendRecords.append(record); + PacketRecord* sendRecord = getLastAcknowledgedSendRecord(); + _server->getData().writeDelta(sendRecord->getData(), sendRecord->getLOD(), out, _lod); } -void MetavoxelSession::sendData(const QByteArray& data) { - NodeList::getInstance()->writeDatagram(data, _node); -} - -void MetavoxelSession::readPacket(Bitstream& in) { - QVariant message; - in >> message; +void MetavoxelSession::handleMessage(const QVariant& message, Bitstream& in) { handleMessage(message); } -void MetavoxelSession::clearSendRecordsBefore(int index) { - _sendRecords.erase(_sendRecords.begin(), _sendRecords.begin() + index + 1); +PacketRecord* MetavoxelSession::maybeCreateSendRecord() const { + return new PacketRecord(_lod, _server->getData()); } void MetavoxelSession::handleMessage(const QVariant& message) { diff --git a/assignment-client/src/metavoxels/MetavoxelServer.h b/assignment-client/src/metavoxels/MetavoxelServer.h index b01bb9b412..d9b010e282 100644 --- a/assignment-client/src/metavoxels/MetavoxelServer.h +++ b/assignment-client/src/metavoxels/MetavoxelServer.h @@ -17,8 +17,7 @@ #include <ThreadedAssignment.h> -#include <DatagramSequencer.h> -#include <MetavoxelData.h> +#include <Endpoint.h> class MetavoxelEditMessage; class MetavoxelSession; @@ -53,46 +52,31 @@ private: }; /// Contains the state of a single client session. -class MetavoxelSession : public NodeData { +class MetavoxelSession : public Endpoint { Q_OBJECT public: - MetavoxelSession(MetavoxelServer* server, const SharedNodePointer& node); - virtual ~MetavoxelSession(); + MetavoxelSession(const SharedNodePointer& node, MetavoxelServer* server); - virtual int parseData(const QByteArray& packet); + virtual void update(); - void sendDelta(); +protected: + + virtual void writeUpdateMessage(Bitstream& out); + virtual void handleMessage(const QVariant& message, Bitstream& in); + + virtual PacketRecord* maybeCreateSendRecord() const; private slots: - void sendData(const QByteArray& data); - - void readPacket(Bitstream& in); - - void clearSendRecordsBefore(int index); - void handleMessage(const QVariant& message); private: - class SendRecord { - public: - int packetNumber; - MetavoxelData data; - MetavoxelLOD lod; - }; - MetavoxelServer* _server; - DatagramSequencer _sequencer; - - SharedNodePointer _node; - MetavoxelLOD _lod; - - QList<SendRecord> _sendRecords; }; #endif // hifi_MetavoxelServer_h diff --git a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp index 76a6845342..3d2ca6ddc3 100644 --- a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp +++ b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp @@ -28,7 +28,8 @@ OctreeInboundPacketProcessor::OctreeInboundPacketProcessor(OctreeServer* myServe _totalLockWaitTime(0), _totalElementsInPacket(0), _totalPackets(0), - _lastNackTime(usecTimestampNow()) + _lastNackTime(usecTimestampNow()), + _shuttingDown(false) { } @@ -72,6 +73,10 @@ void OctreeInboundPacketProcessor::midProcess() { } void OctreeInboundPacketProcessor::processPacket(const SharedNodePointer& sendingNode, const QByteArray& packet) { + if (_shuttingDown) { + qDebug() << "OctreeInboundPacketProcessor::processPacket() while shutting down... ignoring incoming packet"; + return; + } bool debugProcessPacket = _myServer->wantsVerboseDebug(); @@ -182,8 +187,13 @@ void OctreeInboundPacketProcessor::trackInboundPacket(const QUuid& nodeUUID, uns } int OctreeInboundPacketProcessor::sendNackPackets() { - int packetsSent = 0; + + if (_shuttingDown) { + qDebug() << "OctreeInboundPacketProcessor::sendNackPackets() while shutting down... ignore"; + return packetsSent; + } + char packet[MAX_PACKET_SIZE]; NodeToSenderStatsMapIterator i = _singleSenderStats.begin(); @@ -206,7 +216,7 @@ int OctreeInboundPacketProcessor::sendNackPackets() { } const SharedNodePointer& destinationNode = NodeList::getInstance()->getNodeHash().value(nodeUUID); - const QSet<unsigned short int>& missingSequenceNumbers = nodeStats.getMissingSequenceNumbers(); + const QSet<unsigned short int>& missingSequenceNumbers = nodeStats.getIncomingEditSequenceNumberStats().getMissingSet(); // construct nack packet(s) for this node int numSequenceNumbersAvailable = missingSequenceNumbers.size(); @@ -241,6 +251,8 @@ int OctreeInboundPacketProcessor::sendNackPackets() { // send it NodeList::getInstance()->writeUnverifiedDatagram(packet, dataAt - packet, destinationNode); packetsSent++; + + qDebug() << "NACK Sent back to editor/client... destinationNode=" << nodeUUID; } i++; } @@ -254,8 +266,7 @@ SingleSenderStats::SingleSenderStats() _totalLockWaitTime(0), _totalElementsInPacket(0), _totalPackets(0), - _incomingLastSequence(0), - _missingSequenceNumbers() + _incomingEditSequenceNumberStats() { } @@ -263,74 +274,8 @@ SingleSenderStats::SingleSenderStats() void SingleSenderStats::trackInboundPacket(unsigned short int incomingSequence, quint64 transitTime, int editsInPacket, quint64 processTime, quint64 lockWaitTime) { - const int UINT16_RANGE = std::numeric_limits<uint16_t>::max() + 1; - const int MAX_REASONABLE_SEQUENCE_GAP = 1000; // this must be less than UINT16_RANGE / 2 for rollover handling to work - const int MAX_MISSING_SEQUENCE_SIZE = 100; - - unsigned short int expectedSequence = _totalPackets == 0 ? incomingSequence : _incomingLastSequence + (unsigned short int)1; - - if (incomingSequence == expectedSequence) { // on time - _incomingLastSequence = incomingSequence; - } else { // out of order - int incoming = (int)incomingSequence; - int expected = (int)expectedSequence; - - // check if the gap between incoming and expected is reasonable, taking possible rollover into consideration - int absGap = std::abs(incoming - expected); - if (absGap >= UINT16_RANGE - MAX_REASONABLE_SEQUENCE_GAP) { - // rollover likely occurred between incoming and expected. - // correct the larger of the two so that it's within [-UINT16_RANGE, -1] while the other remains within [0, UINT16_RANGE-1] - if (incoming > expected) { - incoming -= UINT16_RANGE; - } else { - expected -= UINT16_RANGE; - } - } else if (absGap > MAX_REASONABLE_SEQUENCE_GAP) { - // ignore packet if gap is unreasonable - qDebug() << "ignoring unreasonable packet... sequence:" << incomingSequence - << "_incomingLastSequence:" << _incomingLastSequence; - return; - } - - // now that rollover has been corrected for (if it occurred), incoming and expected can be - // compared to each other directly, though one of them might be negative - if (incoming > expected) { // early - // add all sequence numbers that were skipped to the missing sequence numbers list - for (int missingSequence = expected; missingSequence < incoming; missingSequence++) { - _missingSequenceNumbers.insert(missingSequence < 0 ? missingSequence + UINT16_RANGE : missingSequence); - } - _incomingLastSequence = incomingSequence; - } else { // late - // remove this from missing sequence number if it's in there - _missingSequenceNumbers.remove(incomingSequence); - - // do not update _incomingLastSequence; it shouldn't become smaller - } - } - - // prune missing sequence list if it gets too big; sequence numbers that are older than MAX_REASONABLE_SEQUENCE_GAP - // will be removed. - if (_missingSequenceNumbers.size() > MAX_MISSING_SEQUENCE_SIZE) { - // some older sequence numbers may be from before a rollover point; this must be handled. - // some sequence numbers in this list may be larger than _incomingLastSequence, indicating that they were received - // before the most recent rollover. - int cutoff = (int)_incomingLastSequence - MAX_REASONABLE_SEQUENCE_GAP; - if (cutoff >= 0) { - foreach(unsigned short int missingSequence, _missingSequenceNumbers) { - unsigned short int nonRolloverCutoff = (unsigned short int)cutoff; - if (missingSequence > _incomingLastSequence || missingSequence <= nonRolloverCutoff) { - _missingSequenceNumbers.remove(missingSequence); - } - } - } else { - unsigned short int rolloverCutoff = (unsigned short int)(cutoff + UINT16_RANGE); - foreach(unsigned short int missingSequence, _missingSequenceNumbers) { - if (missingSequence > _incomingLastSequence && missingSequence <= rolloverCutoff) { - _missingSequenceNumbers.remove(missingSequence); - } - } - } - } + // track sequence number + _incomingEditSequenceNumberStats.sequenceNumberReceived(incomingSequence); // update other stats _totalTransitTime += transitTime; diff --git a/assignment-client/src/octree/OctreeInboundPacketProcessor.h b/assignment-client/src/octree/OctreeInboundPacketProcessor.h index 46a57205cb..960282384b 100644 --- a/assignment-client/src/octree/OctreeInboundPacketProcessor.h +++ b/assignment-client/src/octree/OctreeInboundPacketProcessor.h @@ -14,9 +14,10 @@ #ifndef hifi_OctreeInboundPacketProcessor_h #define hifi_OctreeInboundPacketProcessor_h -#include <map> - #include <ReceivedPacketProcessor.h> + +#include "SequenceNumberStats.h" + class OctreeServer; class SingleSenderStats { @@ -32,7 +33,8 @@ public: { return _totalElementsInPacket == 0 ? 0 : _totalProcessTime / _totalElementsInPacket; } quint64 getAverageLockWaitTimePerElement() const { return _totalElementsInPacket == 0 ? 0 : _totalLockWaitTime / _totalElementsInPacket; } - const QSet<unsigned short int>& getMissingSequenceNumbers() const { return _missingSequenceNumbers; } + + const SequenceNumberStats& getIncomingEditSequenceNumberStats() const { return _incomingEditSequenceNumberStats; } void trackInboundPacket(unsigned short int incomingSequence, quint64 transitTime, int editsInPacket, quint64 processTime, quint64 lockWaitTime); @@ -42,9 +44,7 @@ public: quint64 _totalLockWaitTime; quint64 _totalElementsInPacket; quint64 _totalPackets; - - unsigned short int _incomingLastSequence; - QSet<unsigned short int> _missingSequenceNumbers; + SequenceNumberStats _incomingEditSequenceNumberStats; }; typedef QHash<QUuid, SingleSenderStats> NodeToSenderStatsMap; @@ -73,6 +73,8 @@ public: NodeToSenderStatsMap& getSingleSenderStats() { return _singleSenderStats; } + void shuttingDown() { _shuttingDown = true;} + protected: virtual void processPacket(const SharedNodePointer& sendingNode, const QByteArray& packet); @@ -100,5 +102,6 @@ private: NodeToSenderStatsMap _singleSenderStats; quint64 _lastNackTime; + bool _shuttingDown; }; #endif // hifi_OctreeInboundPacketProcessor_h diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 48c8674c03..23719b86cf 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1097,6 +1097,8 @@ void OctreeServer::forceNodeShutdown(SharedNodePointer node) { void OctreeServer::aboutToFinish() { qDebug() << qPrintable(_safeServerName) << "server STARTING about to finish..."; + qDebug() << qPrintable(_safeServerName) << "inform Octree Inbound Packet Processor that we are shutting down..."; + _octreeInboundPacketProcessor->shuttingDown(); foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { qDebug() << qPrintable(_safeServerName) << "server about to finish while node still connected node:" << *node; forceNodeShutdown(node); diff --git a/cmake/modules/FindLibOVR.cmake b/cmake/modules/FindLibOVR.cmake index f65088e817..91714e8a59 100644 --- a/cmake/modules/FindLibOVR.cmake +++ b/cmake/modules/FindLibOVR.cmake @@ -42,14 +42,11 @@ else (LIBOVR_LIBRARIES AND LIBOVR_INCLUDE_DIRS) if (UDEV_LIBRARY AND XINERAMA_LIBRARY AND OVR_LIBRARY) set(LIBOVR_LIBRARIES "${OVR_LIBRARY};${UDEV_LIBRARY};${XINERAMA_LIBRARY}" CACHE INTERNAL "Oculus libraries") endif (UDEV_LIBRARY AND XINERAMA_LIBRARY AND OVR_LIBRARY) - elseif (WIN32) - if (CMAKE_BUILD_TYPE MATCHES DEBUG) - set(WINDOWS_LIBOVR_NAME "libovrd.lib") - else() - set(WINDOWS_LIBOVR_NAME "libovr.lib") - endif() + elseif (WIN32) + find_library(LIBOVR_RELEASE_LIBRARIES "Lib/Win32/libovr.lib" HINTS ${LIBOVR_SEARCH_DIRS}) + find_library(LIBOVR_DEBUG_LIBRARIES "Lib/Win32/libovrd.lib" HINTS ${LIBOVR_SEARCH_DIRS}) - find_library(LIBOVR_LIBRARIES "Lib/Win32/${WINDOWS_LIBOVR_NAME}" HINTS ${LIBOVR_SEARCH_DIRS}) + set(LIBOVR_LIBRARIES "${LIBOVR_RELEASE_LIBRARIES} ${LIBOVR_DEBUG_LIBRARIES}") endif () if (LIBOVR_INCLUDE_DIRS AND LIBOVR_LIBRARIES) diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index ff33cc206b..3b60ada78b 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -1,8 +1,8 @@ -#nodes-lead { +#nodes-lead, #settings-lead { color: #66CCCC; } -#nodes-lead .lead-line { +#nodes-lead .lead-line, #settings-lead .lead-line { background-color: #66CCCC; } diff --git a/domain-server/resources/web/index.shtml b/domain-server/resources/web/index.shtml index b6ba8f67db..f0315a113f 100644 --- a/domain-server/resources/web/index.shtml +++ b/domain-server/resources/web/index.shtml @@ -18,7 +18,20 @@ </tr> </thead> <tbody> - </tbody> + <script id="nodes-template" type="text/template"> + <% _.each(nodes, function(node, node_index){ %> + <tr> + <td><%- node.type %></td> + <td><a href="stats/?uuid=<%- node.uuid %>"><%- node.uuid %></a></td> + <td><%- node.pool %></td> + <td><%- node.public.ip %><span class='port'><%- node.public.port %></span></td> + <td><%- node.local.ip %><span class='port'><%- node.local.port %></span></td> + <td><%- ((Date.now() - node.wake_timestamp) / 1000).toLocaleString() %></td> + <td><%- (typeof node.pending_credits == 'number' ? node.pending_credits.toLocaleString() : 'N/A') %></td> + <td><span class='glyphicon glyphicon-remove' data-uuid="<%- node.uuid %>"></span></td> + </tr> + <% }); %> + </script> </table> <div id="queued-lead" class="table-lead"><h3>Queued Assignments</h3><div class="lead-line"></div></div> @@ -31,8 +44,18 @@ </tr> </thead> <tbody> + <script id="queued-template" type="text/template"> + <% _.each(queued, function(assignment, uuid){ %> + <tr> + <td><%- assignment.type %></td> + <td><%- uuid %></td> + <td><%- assignment.pool %></td> + </tr> + <% }); %> + </script> </tbody> </table> <!--#include file="footer.html"--> <script src='js/tables.js'></script> +<script src='js/underscore-1.5.0.min.js'></script> <!--#include file="page-end.html"--> \ No newline at end of file diff --git a/domain-server/resources/web/js/form2js.min.js b/domain-server/resources/web/js/form2js.min.js new file mode 100755 index 0000000000..f1e610f7c3 --- /dev/null +++ b/domain-server/resources/web/js/form2js.min.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010 Maxim Vasiliev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author Maxim Vasiliev + * Date: 09.09.2010 + * Time: 19:02:33 + */ +(function(e,t){if(typeof define==="function"&&define.amd){define(t)}else{e.form2js=t()}})(this,function(){"use strict";function e(e,r,i,s,o,u){u=u?true:false;if(typeof i=="undefined"||i==null)i=true;if(typeof r=="undefined"||r==null)r=".";if(arguments.length<5)o=false;e=typeof e=="string"?document.getElementById(e):e;var a=[],f,l=0;if(e.constructor==Array||typeof NodeList!="undefined"&&e.constructor==NodeList){while(f=e[l++]){a=a.concat(n(f,s,o,u))}}else{a=n(e,s,o,u)}return t(a,i,r)}function t(e,t,n){var r={},i={},s,o,u,a,f,l,c,h,p,d,v,m,g;for(s=0;s<e.length;s++){f=e[s].value;if(t&&(f===""||f===null))continue;m=e[s].name;g=m.split(n);l=[];c=r;h="";for(o=0;o<g.length;o++){v=g[o].split("][");if(v.length>1){for(u=0;u<v.length;u++){if(u==0){v[u]=v[u]+"]"}else if(u==v.length-1){v[u]="["+v[u]}else{v[u]="["+v[u]+"]"}d=v[u].match(/([a-z_]+)?\[([a-z_][a-z0-9_]+?)\]/i);if(d){for(a=1;a<d.length;a++){if(d[a])l.push(d[a])}}else{l.push(v[u])}}}else l=l.concat(v)}for(o=0;o<l.length;o++){v=l[o];if(v.indexOf("[]")>-1&&o==l.length-1){p=v.substr(0,v.indexOf("["));h+=p;if(!c[p])c[p]=[];c[p].push(f)}else if(v.indexOf("[")>-1){p=v.substr(0,v.indexOf("["));d=v.replace(/(^([a-z_]+)?\[)|(\]$)/gi,"");h+="_"+p+"_"+d;if(!i[h])i[h]={};if(p!=""&&!c[p])c[p]=[];if(o==l.length-1){if(p==""){c.push(f);i[h][d]=c[c.length-1]}else{c[p].push(f);i[h][d]=c[p][c[p].length-1]}}else{if(!i[h][d]){if(/^[0-9a-z_]+\[?/i.test(l[o+1]))c[p].push({});else c[p].push([]);i[h][d]=c[p][c[p].length-1]}}c=i[h][d]}else{h+=v;if(o<l.length-1){if(!c[v])c[v]={};c=c[v]}else{c[v]=f}}}}return r}function n(e,t,n,s){var o=i(e,t,n,s);return o.length>0?o:r(e,t,n,s)}function r(e,t,n,r){var s=[],o=e.firstChild;while(o){s=s.concat(i(o,t,n,r));o=o.nextSibling}return s}function i(e,t,n,i){if(e.disabled&&!i)return[];var u,a,f,l=s(e,n);u=t&&t(e);if(u&&u.name){f=[u]}else if(l!=""&&e.nodeName.match(/INPUT|TEXTAREA/i)){a=o(e,i);if(null===a){f=[]}else{f=[{name:l,value:a}]}}else if(l!=""&&e.nodeName.match(/SELECT/i)){a=o(e,i);f=[{name:l.replace(/\[\]$/,""),value:a}]}else{f=r(e,t,n,i)}return f}function s(e,t){if(e.name&&e.name!="")return e.name;else if(t&&e.id&&e.id!="")return e.id;else return""}function o(e,t){if(e.disabled&&!t)return null;switch(e.nodeName){case"INPUT":case"TEXTAREA":switch(e.type.toLowerCase()){case"radio":if(e.checked&&e.value==="false")return false;case"checkbox":if(e.checked&&e.value==="true")return true;if(!e.checked&&e.value==="true")return false;if(e.checked)return e.value;break;case"button":case"reset":case"submit":case"image":return"";break;default:return e.value;break}break;case"SELECT":return u(e);break;default:break}return null}function u(e){var t=e.multiple,n=[],r,i,s;if(!t)return e.value;for(r=e.getElementsByTagName("option"),i=0,s=r.length;i<s;i++){if(r[i].selected)n.push(r[i].value)}return n}return e}) \ No newline at end of file diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js new file mode 100644 index 0000000000..487ec5b296 --- /dev/null +++ b/domain-server/resources/web/js/settings.js @@ -0,0 +1,72 @@ +var Settings = {}; + +$(document).ready(function(){ + var source = $('#settings-template').html(); + Settings.template = _.template(source); + + reloadSettings(); +}); + +function reloadSettings() { + $.getJSON('/settings.json', function(data){ + $('#settings').html(Settings.template(data)); + }); +} + +var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!"; + +$('#settings').on('click', 'button', function(e){ + // disable any inputs not changed + $("input:not([data-changed])").each(function(){ + $(this).prop('disabled', true); + }); + + // grab a JSON representation of the form via form2js + var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); + + // re-enable all inputs + $("input").each(function(){ + $(this).prop('disabled', false); + }); + + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved + $.ajax('/settings.json', { + data: JSON.stringify(formJSON), + contentType: 'application/json', + type: 'POST' + }).done(function(data){ + if (data.status == "success") { + showAlertMessage("Domain settings saved.", true); + } else { + showAlertMessage(SETTINGS_ERROR_MESSAGE, false); + } + + reloadSettings(); + }).fail(function(){ + showAlertMessage(SETTINGS_ERROR_MESSAGE, false); + reloadSettings(); + }); + + return false; +}); + +$('#settings').on('change', 'input', function(){ + // this input was changed, add the changed data attribute to it + $(this).attr('data-changed', true); +}); + +function cleanupFormValues(node) { + if (node.type && node.type === 'checkbox') { + return { name: node.id, value: node.checked ? true : false }; + } else { + return false; + } +} + +function showAlertMessage(message, isSuccess) { + var alertBox = $('.alert'); + alertBox.attr('class', 'alert'); + alertBox.addClass(isSuccess ? 'alert-success' : 'alert-danger'); + alertBox.html(message); + alertBox.fadeIn(); +} \ No newline at end of file diff --git a/domain-server/resources/web/js/tables.js b/domain-server/resources/web/js/tables.js index b564d9392f..0b29d4e6c9 100644 --- a/domain-server/resources/web/js/tables.js +++ b/domain-server/resources/web/js/tables.js @@ -1,4 +1,8 @@ $(document).ready(function(){ + // setup the underscore templates + var nodeTemplate = _.template($('#nodes-template').html()); + var queuedTemplate = _.template($('#queued-template').html()); + // setup a function to grab the assignments function getNodesAndAssignments() { $.getJSON("nodes.json", function(json){ @@ -29,40 +33,11 @@ $(document).ready(function(){ } }); - nodesTableBody = ""; - - $.each(json.nodes, function(index, data) { - nodesTableBody += "<tr>"; - nodesTableBody += "<td>" + data.type + "</td>"; - nodesTableBody += "<td><a href='stats/?uuid=" + data.uuid + "'>" + data.uuid + "</a></td>"; - nodesTableBody += "<td>" + (data.pool ? data.pool : "") + "</td>"; - nodesTableBody += "<td>" + data.public.ip + "<span class='port'>:" + data.public.port + "</span></td>"; - nodesTableBody += "<td>" + data.local.ip + "<span class='port'>:" + data.local.port + "</span></td>"; - - var uptimeSeconds = (Date.now() - data.wake_timestamp) / 1000; - nodesTableBody += "<td>" + uptimeSeconds.toLocaleString() + "</td>"; - - nodesTableBody += "<td>" + (typeof data.pending_credits == 'number' ? data.pending_credits.toLocaleString() : 'N/A') + "</td>"; - - nodesTableBody += "<td><span class='glyphicon glyphicon-remove' data-uuid=" + data.uuid + "></span></td>"; - nodesTableBody += "</tr>"; - }); - - $('#nodes-table tbody').html(nodesTableBody); + $('#nodes-table tbody').html(nodeTemplate(json)); }); $.getJSON("assignments.json", function(json){ - queuedTableBody = ""; - - $.each(json.queued, function (uuid, data) { - queuedTableBody += "<tr>"; - queuedTableBody += "<td>" + data.type + "</td>"; - queuedTableBody += "<td>" + uuid + "</td>"; - queuedTableBody += "<td>" + (data.pool ? data.pool : "") + "</td>"; - queuedTableBody += "</tr>"; - }); - - $('#assignments-table tbody').html(queuedTableBody); + $('#assignments-table tbody').html(queuedTemplate(json)); }); } diff --git a/domain-server/resources/web/js/underscore-1.5.0.min.js b/domain-server/resources/web/js/underscore-1.5.0.min.js new file mode 100644 index 0000000000..4db9729997 --- /dev/null +++ b/domain-server/resources/web/js/underscore-1.5.0.min.js @@ -0,0 +1,7 @@ +// Underscore.js 1.5.0 +// http://underscorejs.org +// (c) 2009-2011 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. +!function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,w=i.bind,j=function(n){return n instanceof j?n:this instanceof j?(this._wrapped=n,void 0):new j(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=j),exports._=j):n._=j,j.VERSION="1.5.0";var A=j.each=j.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(j.has(n,a)&&t.call(e,n[a],a,n)===r)return};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var E="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(E);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(E);return r},j.find=j.detect=function(n,t,r){var e;return O(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var O=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:O(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,function(n){return n[t]})},j.where=function(n,t,r){return j.isEmpty(t)?r?void 0:[]:j[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},j.findWhere=function(n,t){return j.where(n,t,!0)},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);if(!t&&j.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>e.computed&&(e={value:n,computed:a})}),e.value},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);if(!t&&j.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a<e.computed&&(e={value:n,computed:a})}),e.value},j.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=j.random(r++),e[r-1]=e[t],e[t]=n}),e};var F=function(n){return j.isFunction(n)?n:function(t){return t[n]}};j.sortBy=function(n,t,r){var e=F(t);return j.pluck(j.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index<t.index?-1:1}),"value")};var k=function(n,t,r,e){var u={},i=F(null==t?j.identity:t);return A(n,function(t,a){var o=i.call(r,t,a,n);e(u,o,t)}),u};j.groupBy=function(n,t,r){return k(n,t,r,function(n,t,r){(j.has(n,t)?n[t]:n[t]=[]).push(r)})},j.countBy=function(n,t,r){return k(n,t,r,function(n,t){j.has(n,t)||(n[t]=0),n[t]++})},j.sortedIndex=function(n,t,r,e){r=null==r?j.identity:F(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;r.call(e,n[o])<u?i=o+1:a=o}return i},j.toArray=function(n){return n?j.isArray(n)?o.call(n):n.length===+n.length?j.map(n,j.identity):j.values(n):[]},j.size=function(n){return null==n?0:n.length===+n.length?n.length:j.keys(n).length},j.first=j.head=j.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},j.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},j.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},j.rest=j.tail=j.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},j.compact=function(n){return j.filter(n,j.identity)};var R=function(n,t,r){return t&&j.every(n,j.isArray)?c.apply(r,n):(A(n,function(n){j.isArray(n)||j.isArguments(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r)};j.flatten=function(n,t){return R(n,t,[])},j.without=function(n){return j.difference(n,o.call(arguments,1))},j.uniq=j.unique=function(n,t,r,e){j.isFunction(t)&&(e=r,r=t,t=!1);var u=r?j.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:j.contains(a,r))||(a.push(r),i.push(n[e]))}),i},j.union=function(){return j.uniq(j.flatten(arguments,!0))},j.intersection=function(n){var t=o.call(arguments,1);return j.filter(j.uniq(n),function(n){return j.every(t,function(t){return j.indexOf(t,n)>=0})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){return j.unzip.apply(j,o.call(arguments))},j.unzip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var M=function(){};j.bind=function(n,t){var r,e;if(w&&n.bind===w)return w.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));M.prototype=n.prototype;var u=new M;M.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},j.bindAll=function(n){var t=o.call(arguments,1);if(0===t.length)throw new Error("bindAll must be passed function names");return A(t,function(t){n[t]=j.bind(n[t],n)}),n},j.memoize=function(n,t){var r={};return t||(t=j.identity),function(){var e=t.apply(this,arguments);return j.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},j.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},j.defer=function(n){return j.delay.apply(j,[n,1].concat(o.call(arguments,1)))},j.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var c=function(){o=new Date,a=null,i=n.apply(e,u)};return function(){var l=new Date;o||r.leading!==!1||(o=l);var f=t-(l-o);return e=this,u=arguments,0>=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u)):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u=null;return function(){var i=this,a=arguments,o=function(){u=null,r||(e=n.apply(i,a))},c=r&&!u;return clearTimeout(u),u=setTimeout(o,t),c&&(e=n.apply(i,a)),e}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push(n[r]);return t},j.pairs=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push([r,n[r]]);return t},j.invert=function(n){var t={};for(var r in n)j.has(n,r)&&(t[n[r]]=r);return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var I={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};I.unescape=j.invert(I.escape);var T={escape:new RegExp("["+j.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(I.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(T[n],function(t){return I[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),D.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},z=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(z,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var D=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],D.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return D.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}.call(this); +//# sourceMappingURL=underscore-min.map \ No newline at end of file diff --git a/domain-server/resources/web/settings/describe.json b/domain-server/resources/web/settings/describe.json new file mode 100644 index 0000000000..227b6bf0cd --- /dev/null +++ b/domain-server/resources/web/settings/describe.json @@ -0,0 +1,20 @@ +{ + "audio": { + "label": "Audio", + "assignment-types": [0], + "settings": { + "unattenuated-zone": { + "label": "Unattenuated Zone", + "help": "Boxes for source and listener (corner x, corner y, corner z, size x, size y, size z, corner x, corner y, corner z, size x, size y, size z)", + "placeholder": "no zone", + "default": "" + }, + "dynamic-jitter-buffer": { + "type": "checkbox", + "label": "Dynamic Jitter Buffers", + "help": "Dynamically buffer client audio based on perceived jitter in packet receipt timing", + "default": false + } + } + } +} \ No newline at end of file diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml new file mode 100644 index 0000000000..3bb669b32e --- /dev/null +++ b/domain-server/resources/web/settings/index.shtml @@ -0,0 +1,46 @@ +<!--#include virtual="header.html"--> +<div id="settings-lead" class="table-lead"><h3>Settings</h3><div class="lead-line"></div></div> +<div style="clear: both;"></div> +<div class="alert" style="display:none;"></div> +<form class="form-horizontal" id="settings-form" role="form"> + +<script id="settings-template" type="text/template"> + <% _.each(descriptions, function(group, group_key){ %> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="panel-title"><%- group.label %></h3> + </div> + <div class="panel-body"> + <% _.each(group.settings, function(setting, setting_key){ %> + <div class="form-group"> + <% var setting_id = group_key + "." + setting_key %> + <label for="<%- setting_id %>" class="col-sm-2 control-label"><%- setting.label %></label> + <div class="col-sm-10"> + <% if(setting.type) %> + <% if (setting.type === "checkbox") { %> + <% var checked_box = (values[group_key] || {})[setting_key] || setting.default %> + <input type="checkbox" id="<%- setting_id %>" <%- checked_box ? "checked" : "" %>> + <% } else { %> + <input type="text" class="form-control" id="<%- setting_id %>" + placeholder="<%- setting.placeholder %>" + value="<%- (values[group_key] || {})[setting_key] %>"> + <% } %> + + </div> + <p class="help-block col-sm-offset-2 col-sm-10"><%- setting.help %></p> + </div> + <% }); %> + </div> + </div> + <% }); %> + <button type="submit" class="btn btn-default">Save</button> +</script> + +<div id="settings"></div> + </form> + +<!--#include virtual="footer.html"--> +<script src='/js/settings.js'></script> +<script src='/js/form2js.min.js'></script> +<script src='/js/underscore-1.5.0.min.js'></script> +<!--#include virtual="page-end.html"--> \ No newline at end of file diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d55a9b52ca..7a2d5f4f99 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -41,7 +41,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : _oauthClientID(), _hostname(), _networkReplyUUIDMap(), - _sessionAuthenticationHash() + _sessionAuthenticationHash(), + _settingsManager() { setOrganizationName("High Fidelity"); setOrganizationDomain("highfidelity.io"); @@ -362,7 +363,7 @@ void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const Q QString dashes = payloadKey.size() == 1 ? "-" : "--"; payloadStringList << QString("%1%2 %3").arg(dashes).arg(payloadKey).arg(jsonObject[payloadKey].toString()); } - + configAssignment->setPayload(payloadStringList.join(' ').toUtf8()); addStaticAssignmentToAssignmentHash(configAssignment); @@ -1162,12 +1163,13 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url } } - // didn't process the request, let the HTTPManager try and handle - return false; + // didn't process the request, let our DomainServerSettingsManager or HTTPManager handle + return _settingsManager.handleHTTPRequest(connection, url); } bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url) { const QString URI_OAUTH = "/oauth"; + qDebug() << "HTTPS request received at" << url.toString(); if (url.path() == URI_OAUTH) { QUrlQuery codeURLQuery(url); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index b038850b3d..01f44b698e 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -24,6 +24,7 @@ #include <HTTPSConnection.h> #include <LimitedNodeList.h> +#include "DomainServerSettingsManager.h" #include "WalletTransaction.h" #include "PendingAssignedNodeData.h" @@ -110,6 +111,8 @@ private: QString _hostname; QMap<QNetworkReply*, QUuid> _networkReplyUUIDMap; QHash<QUuid, bool> _sessionAuthenticationHash; + + DomainServerSettingsManager _settingsManager; }; #endif // hifi_DomainServer_h diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp new file mode 100644 index 0000000000..d7e2e05ca8 --- /dev/null +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -0,0 +1,179 @@ +// +// DomainServerSettingsManager.cpp +// domain-server/src +// +// Created by Stephen Birarda on 2014-06-24. +// Copyright 2014 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 <QtCore/QCoreApplication> +#include <QtCore/QFile> +#include <QtCore/QJsonArray> +#include <QtCore/QJsonObject> +#include <QtCore/QUrl> +#include <QtCore/QUrlQuery> + +#include <Assignment.h> +#include <HTTPConnection.h> + +#include "DomainServerSettingsManager.h" + +const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/web/settings/describe.json"; +const QString SETTINGS_CONFIG_FILE_RELATIVE_PATH = "/resources/config.json"; + +DomainServerSettingsManager::DomainServerSettingsManager() : + _descriptionObject(), + _settingsMap() +{ + // load the description object from the settings description + QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH); + descriptionFile.open(QIODevice::ReadOnly); + + _descriptionObject = QJsonDocument::fromJson(descriptionFile.readAll()).object(); + + // load the existing config file to get the current values + QFile configFile(QCoreApplication::applicationDirPath() + SETTINGS_CONFIG_FILE_RELATIVE_PATH); + configFile.open(QIODevice::ReadOnly); + + _settingsMap = QJsonDocument::fromJson(configFile.readAll()).toVariant().toMap(); +} + +const QString DESCRIPTION_SETTINGS_KEY = "settings"; +const QString SETTING_DEFAULT_KEY = "default"; + +bool DomainServerSettingsManager::handleHTTPRequest(HTTPConnection* connection, const QUrl &url) { + if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == "/settings.json") { + // this is a POST operation to change one or more settings + QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent()); + QJsonObject postedObject = postedDocument.object(); + + // we recurse one level deep below each group for the appropriate setting + recurseJSONObjectAndOverwriteSettings(postedObject, _settingsMap, _descriptionObject); + + // store whatever the current _settingsMap is to file + persistToFile(); + + // return success to the caller + QString jsonSuccess = "{\"status\": \"success\"}"; + connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); + + return true; + } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == "/settings.json") { + // this is a GET operation for our settings + + // check if there is a query parameter for settings affecting a particular type of assignment + const QString SETTINGS_TYPE_QUERY_KEY = "type"; + QUrlQuery settingsQuery(url); + QString typeValue = settingsQuery.queryItemValue(SETTINGS_TYPE_QUERY_KEY); + + QJsonObject responseObject; + + if (typeValue.isEmpty()) { + // combine the description object and our current settings map + responseObject["descriptions"] = _descriptionObject; + responseObject["values"] = QJsonDocument::fromVariant(_settingsMap).object(); + } else { + // convert the string type value to a QJsonValue + QJsonValue queryType = QJsonValue(typeValue.toInt()); + + const QString AFFECTED_TYPES_JSON_KEY = "assignment-types"; + + // enumerate the groups in the description object to find which settings to pass + foreach(const QString& group, _descriptionObject.keys()) { + QJsonObject groupObject = _descriptionObject[group].toObject(); + QJsonObject groupSettingsObject = groupObject[DESCRIPTION_SETTINGS_KEY].toObject(); + + QJsonObject groupResponseObject; + + + foreach(const QString& settingKey, groupSettingsObject.keys()) { + QJsonObject settingObject = groupSettingsObject[settingKey].toObject(); + + QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray(); + if (affectedTypesArray.isEmpty()) { + affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray(); + } + + if (affectedTypesArray.contains(queryType)) { + // this is a setting we should include in the responseObject + + // we need to check if the settings map has a value for this setting + QVariant variantValue; + QVariant settingsMapGroupValue = _settingsMap.value(group); + + if (!settingsMapGroupValue.isNull()) { + variantValue = settingsMapGroupValue.toMap().value(settingKey); + } + + if (variantValue.isNull()) { + // no value for this setting, pass the default + groupResponseObject[settingKey] = settingObject[SETTING_DEFAULT_KEY]; + } else { + groupResponseObject[settingKey] = QJsonValue::fromVariant(variantValue); + } + } + } + + if (!groupResponseObject.isEmpty()) { + // set this group's object to the constructed object + responseObject[group] = groupResponseObject; + } + } + + } + + connection->respond(HTTPConnection::StatusCode200, QJsonDocument(responseObject).toJson(), "application/json"); + return true; + } + + return false; +} + +void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, + QVariantMap& settingsVariant, + QJsonObject descriptionObject) { + foreach(const QString& key, postedObject.keys()) { + + QJsonValue rootValue = postedObject[key]; + + // we don't continue if this key is not present in our descriptionObject + if (descriptionObject.contains(key)) { + if (rootValue.isString()) { + settingsVariant[key] = rootValue.toString(); + } else if (rootValue.isBool()) { + settingsVariant[key] = rootValue.toBool(); + } else if (rootValue.isObject()) { + // there's a JSON Object to explore, so attempt to recurse into it + QJsonObject nextDescriptionObject = descriptionObject[key].toObject(); + + if (nextDescriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) { + if (!settingsVariant.contains(key)) { + // we don't have a map below this key yet, so set it up now + settingsVariant[key] = QVariantMap(); + } + + recurseJSONObjectAndOverwriteSettings(rootValue.toObject(), + *reinterpret_cast<QVariantMap*>(settingsVariant[key].data()), + nextDescriptionObject[DESCRIPTION_SETTINGS_KEY].toObject()); + } + } + } + } +} + +QByteArray DomainServerSettingsManager::getJSONSettingsMap() const { + return QJsonDocument::fromVariant(_settingsMap).toJson(); +} + +void DomainServerSettingsManager::persistToFile() { + QFile settingsFile(QCoreApplication::applicationDirPath() + SETTINGS_CONFIG_FILE_RELATIVE_PATH); + + if (settingsFile.open(QIODevice::WriteOnly)) { + settingsFile.write(getJSONSettingsMap()); + } else { + qCritical("Could not write to JSON settings file. Unable to persist settings."); + } +} \ No newline at end of file diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h new file mode 100644 index 0000000000..8b80cad280 --- /dev/null +++ b/domain-server/src/DomainServerSettingsManager.h @@ -0,0 +1,35 @@ +// +// DomainServerSettingsManager.h +// domain-server/src +// +// Created by Stephen Birarda on 2014-06-24. +// Copyright 2014 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_DomainServerSettingsManager_h +#define hifi_DomainServerSettingsManager_h + +#include <QtCore/QJsonDocument> + +#include <HTTPManager.h> + +class DomainServerSettingsManager : public QObject, HTTPRequestHandler { + Q_OBJECT +public: + DomainServerSettingsManager(); + bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url); + + QByteArray getJSONSettingsMap() const; +private: + void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant, + QJsonObject descriptionObject); + void persistToFile(); + + QJsonObject _descriptionObject; + QVariantMap _settingsMap; +}; + +#endif // hifi_DomainServerSettingsManager_h \ No newline at end of file diff --git a/examples/concertCamera.js b/examples/concertCamera.js new file mode 100644 index 0000000000..03908d0b57 --- /dev/null +++ b/examples/concertCamera.js @@ -0,0 +1,72 @@ +// +// concertCamera.js +// +// Created by Philip Rosedale on June 24, 2014 +// Copyright 2014 High Fidelity, Inc. +// +// Move a camera through a series of pre-set locations by pressing number keys +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var oldMode; +var avatarPosition; + +var cameraNumber = 0; +var freeCamera = false; + +var cameraLocations = [ {x: 7971.9, y: 241.3, z: 7304.1}, {x: 7973.0, y: 241.3, z: 7304.1}, {x: 7975.5, y: 241.3, z: 7304.1}, {x: 7972.3, y: 241.3, z: 7303.3}, {x: 7971.0, y: 241.3, z: 7304.3}, {x: 7973.5, y: 240.7, z: 7302.5} ]; +var cameraLookAts = [ {x: 7971.1, y: 241.3, z: 7304.1}, {x: 7972.1, y: 241.3, z: 7304.1}, {x: 7972.1, y: 241.3, z: 7304.1}, {x: 7972.1, y: 241.3, z: 7304.1}, {x: 7972.1, y: 241.3, z: 7304.1}, {x: 7971.3, y: 241.3, z: 7304.2} ]; + +function saveCameraState() { + oldMode = Camera.getMode(); + avatarPosition = MyAvatar.position; + Camera.setModeShiftPeriod(0.0); + Camera.setMode("independent"); +} + +function restoreCameraState() { + Camera.stopLooking(); + Camera.setMode(oldMode); +} + +function update(deltaTime) { + if (freeCamera) { + var delta = Vec3.subtract(MyAvatar.position, avatarPosition); + if (Vec3.length(delta) > 0.05) { + cameraNumber = 0; + freeCamera = false; + restoreCameraState(); + } + } +} + +function keyPressEvent(event) { + + var choice = parseInt(event.text); + + if ((choice > 0) && (choice <= cameraLocations.length)) { + print("camera " + choice); + if (!freeCamera) { + saveCameraState(); + freeCamera = true; + } + Camera.setMode("independent"); + Camera.setPosition(cameraLocations[choice - 1]); + Camera.keepLookingAt(cameraLookAts[choice - 1]); + } + if (event.text == "ESC") { + cameraNumber = 0; + freeCamera = false; + restoreCameraState(); + } + if (event.text == "0") { + // Show camera location in log + var cameraLocation = Camera.getPosition(); + print(cameraLocation.x + ", " + cameraLocation.y + ", " + cameraLocation.z); + } +} + +Script.update.connect(update); +Controller.keyPressEvent.connect(keyPressEvent); diff --git a/examples/editModels.js b/examples/editModels.js index eebcd075fa..64c203534c 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -34,6 +34,7 @@ var LASER_COLOR = { red: 255, green: 0, blue: 0 }; var LASER_LENGTH_FACTOR = 500 ; +var MIN_ANGULAR_SIZE = 2; var MAX_ANGULAR_SIZE = 45; var LEFT = 0; @@ -277,8 +278,9 @@ function controller(wichSide) { var X = Vec3.sum(A, Vec3.multiply(B, x)); var d = Vec3.length(Vec3.subtract(P, X)); - if (0 < x && x < LASER_LENGTH_FACTOR) { - if (2 * Math.atan(properties.radius / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / 3.14 > MAX_ANGULAR_SIZE) { + var angularSize = 2 * Math.atan(properties.radius / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / 3.14; + if (0 < x && angularSize > MIN_ANGULAR_SIZE) { + if (angularSize > MAX_ANGULAR_SIZE) { print("Angular size too big: " + 2 * Math.atan(properties.radius / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / 3.14); return { valid: false }; } @@ -326,7 +328,8 @@ function controller(wichSide) { origin: this.palmPosition, direction: this.front }); - if (intersection.accurate && intersection.modelID.isKnownID) { + var angularSize = 2 * Math.atan(intersection.modelProperties.radius / Vec3.distance(Camera.getPosition(), intersection.modelProperties.position)) * 180 / 3.14; + if (intersection.accurate && intersection.modelID.isKnownID && angularSize > MIN_ANGULAR_SIZE && angularSize < MAX_ANGULAR_SIZE) { this.glowedIntersectingModel = intersection.modelID; Models.editModel(this.glowedIntersectingModel, { glowLevel: 0.25 }); } @@ -749,7 +752,16 @@ function Tooltip() { text += "ID: " + properties.id + "\n" text += "model url: " + properties.modelURL + "\n" text += "animation url: " + properties.animationURL + "\n" - + if (properties.sittingPoints.length > 0) { + text += properties.sittingPoints.length + " sitting points: " + for (var i = 0; i < properties.sittingPoints.length; ++i) { + text += properties.sittingPoints[i].name + " " + } + } else { + text += "No sitting points" + } + + Overlays.editOverlay(this.textOverlay, { text: text }); } @@ -828,8 +840,9 @@ function mousePressEvent(event) { var X = Vec3.sum(A, Vec3.multiply(B, x)); var d = Vec3.length(Vec3.subtract(P, X)); - if (0 < x && x < LASER_LENGTH_FACTOR) { - if (2 * Math.atan(properties.radius / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / 3.14 < MAX_ANGULAR_SIZE) { + var angularSize = 2 * Math.atan(properties.radius / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / 3.14; + if (0 < x && angularSize > MIN_ANGULAR_SIZE) { + if (angularSize < MAX_ANGULAR_SIZE) { modelSelected = true; selectedModelID = foundModel; selectedModelProperties = properties; @@ -884,7 +897,8 @@ function mouseMoveEvent(event) { glowedModelID.isKnownID = false; } - if (modelIntersection.modelID.isKnownID) { + var angularSize = 2 * Math.atan(modelIntersection.modelProperties.radius / Vec3.distance(Camera.getPosition(), modelIntersection.modelProperties.position)) * 180 / 3.14; + if (modelIntersection.modelID.isKnownID && angularSize > MIN_ANGULAR_SIZE && angularSize < MAX_ANGULAR_SIZE) { Models.editModel(modelIntersection.modelID, { glowLevel: 0.25 }); glowedModelID = modelIntersection.modelID; } @@ -1114,8 +1128,8 @@ function handeMenuEvent(menuItem){ Models.editModel(editModelID, properties); } } - tooltip.show(false); } + tooltip.show(false); } Menu.menuItemEvent.connect(handeMenuEvent); diff --git a/examples/editVoxels.js b/examples/editVoxels.js index cff0d65743..412612fdad 100644 --- a/examples/editVoxels.js +++ b/examples/editVoxels.js @@ -1196,7 +1196,7 @@ function menuItemEvent(menuItem) { print("deleting..."); if (isImporting) { cancelImport(); - } else { + } else if (voxelToolSelected) { Clipboard.deleteVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); } } diff --git a/examples/inWorldTestTone.js b/examples/inWorldTestTone.js new file mode 100644 index 0000000000..e4f34d87cd --- /dev/null +++ b/examples/inWorldTestTone.js @@ -0,0 +1,38 @@ +// +// inWorldTestTone.js +// +// +// Created by Philip Rosedale on 5/29/14. +// Copyright 2014 High Fidelity, Inc. +// +// This example script plays a test tone that is useful for debugging audio dropout. 220Hz test tone played at the domain origin. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var sound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/220Sine.wav"); + +var soundPlaying = false; + +function update(deltaTime) { + if (!Audio.isInjectorPlaying(soundPlaying)) { + var options = new AudioInjectionOptions(); + options.position = { x:0, y:0, z:0 }; + options.volume = 1.0; + options.loop = true; + soundPlaying = Audio.playSound(sound, options); + print("Started sound loop"); + } +} + +function scriptEnding() { + if (Audio.isInjectorPlaying(soundPlaying)) { + Audio.stopInjector(soundPlaying); + print("Stopped sound loop"); + } +} + +Script.update.connect(update); +Script.scriptEnding.connect(scriptEnding); + diff --git a/examples/inspect.js b/examples/inspect.js index b292d5f609..a4ff405c3f 100644 --- a/examples/inspect.js +++ b/examples/inspect.js @@ -195,6 +195,8 @@ function keyReleaseEvent(event) { } } + + function mousePressEvent(event) { if (alt && !isActive) { mouseLastX = event.x; diff --git a/examples/sit.js b/examples/sit.js index d10c08c95a..056a65fbf1 100644 --- a/examples/sit.js +++ b/examples/sit.js @@ -19,7 +19,7 @@ var buttonHeight = 46; var buttonPadding = 10; var buttonPositionX = windowDimensions.x - buttonPadding - buttonWidth; -var buttonPositionY = (windowDimensions.y - buttonHeight) / 2 ; +var buttonPositionY = (windowDimensions.y - buttonHeight) / 2 - (buttonHeight + buttonPadding); var sitDownButton = Overlays.addOverlay("image", { x: buttonPositionX, y: buttonPositionY, width: buttonWidth, height: buttonHeight, @@ -38,9 +38,15 @@ var standUpButton = Overlays.addOverlay("image", { var passedTime = 0.0; var startPosition = null; +var startRotation = null; var animationLenght = 2.0; -var sitting = false; +var avatarOldPosition = { x: 0, y: 0, z: 0 }; + +var sitting = false; + +var seat = new Object(); +var hiddingSeats = false; // This is the pose we would like to end up var pose = [ @@ -49,13 +55,7 @@ var pose = [ {joint:"RightFoot", rotation: {x:30, y:15.0, z:0.0}}, {joint:"LeftUpLeg", rotation: {x:100.0, y:-15.0, z:0.0}}, {joint:"LeftLeg", rotation: {x:-130.0, y:-15.0, z:0.0}}, - {joint:"LeftFoot", rotation: {x:30, y:15.0, z:0.0}}, - - {joint:"Spine2", rotation: {x:20, y:0.0, z:0.0}}, - - {joint:"RightShoulder", rotation: {x:0.0, y:40.0, z:0.0}}, - {joint:"LeftShoulder", rotation: {x:0.0, y:-40.0, z:0.0}} - + {joint:"LeftFoot", rotation: {x:30, y:15.0, z:0.0}} ]; var startPoseAndTransition = []; @@ -89,7 +89,7 @@ var sittingDownAnimation = function(deltaTime) { } } -var standingUpAnimation = function(deltaTime){ +var standingUpAnimation = function(deltaTime) { passedTime += deltaTime; var factor = 1 - passedTime/animationLenght; @@ -103,6 +103,24 @@ var standingUpAnimation = function(deltaTime){ } } +var goToSeatAnimation = function(deltaTime) { + passedTime += deltaTime; + var factor = passedTime/animationLenght; + + if (passedTime <= animationLenght) { + var targetPosition = Vec3.sum(seat.position, { x: 0.3, y: 0.5, z: 0 }); + MyAvatar.position = Vec3.sum(Vec3.multiply(startPosition, 1 - factor), Vec3.multiply(targetPosition, factor)); + } else if (passedTime <= 2 * animationLenght) { + Quat.print("MyAvatar: ", MyAvatar.orientation); + Quat.print("Seat: ", seat.rotation); + MyAvatar.orientation = Quat.mix(startRotation, seat.rotation, factor - 1); + } else { + Script.update.disconnect(goToSeatAnimation); + sitDown(); + showIndicators(false); + } +} + function sitDown() { sitting = true; passedTime = 0.0; @@ -130,15 +148,104 @@ function standUp() { Overlays.editOverlay(sitDownButton, { visible: true }); } -Controller.mousePressEvent.connect(function(event){ +var models = new Object(); +function SeatIndicator(modelProperties, seatIndex) { + this.position = Vec3.sum(modelProperties.position, + Vec3.multiply(Vec3.multiplyQbyV(modelProperties.modelRotation, + modelProperties.sittingPoints[seatIndex].position), + modelProperties.radius)); + + this.orientation = Quat.multiply(modelProperties.modelRotation, + modelProperties.sittingPoints[seatIndex].rotation); + this.scale = MyAvatar.scale / 12; + + this.sphere = Overlays.addOverlay("sphere", { + position: this.position, + size: this.scale, + solid: true, + color: { red: 0, green: 0, blue: 255 }, + alpha: 0.3, + visible: true + }); + + this.show = function(doShow) { + Overlays.editOverlay(this.sphere, { visible: doShow }); + } + + this.update = function() { + Overlays.editOverlay(this.sphere, { + position: this.position, + size: this.scale + }); + } + + this.cleanup = function() { + Overlays.deleteOverlay(this.sphere); + } +} +Controller.mousePressEvent.connect(function(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); if (clickedOverlay == sitDownButton) { sitDown(); } else if (clickedOverlay == standUpButton) { standUp(); - } + } else { + var pickRay = Camera.computePickRay(event.x, event.y); + + var clickedOnSeat = false; + + for (index in models) { + var model = models[index]; + + for (var i = 0; i < model.properties.sittingPoints.length; ++i) { + if (raySphereIntersection(pickRay.origin, + pickRay.direction, + model.properties.sittingPoints[i].indicator.position, + model.properties.sittingPoints[i].indicator.scale / 2)) { + clickedOnSeat = true; + seat.position = model.properties.sittingPoints[i].indicator.position; + seat.rotation = model.properties.sittingPoints[i].indicator.orientation; + } + } + } + if (clickedOnSeat) { + passedTime = 0.0; + startPosition = MyAvatar.position; + startRotation = MyAvatar.orientation; + try{ Script.update.disconnect(standingUpAnimation); } catch(e){} + try{ Script.update.disconnect(sittingDownAnimation); } catch(e){} + Script.update.connect(goToSeatAnimation); + } + + + + return; + var intersection = Models.findRayIntersection(pickRay); + + if (intersection.accurate && intersection.intersects && false) { + var properties = intersection.modelProperties; + print("Intersecting with model, let's check for seats."); + + if (properties.sittingPoints.length > 0) { + print("Available seats, going to the first one: " + properties.sittingPoints[0].name); + seat.position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.modelRotation, properties.sittingPoints[0].position)); + Vec3.print("Seat position: ", seat.position); + seat.rotation = Quat.multiply(properties.modelRotation, properties.sittingPoints[0].rotation); + Quat.print("Seat rotation: ", seat.rotation); + + passedTime = 0.0; + startPosition = MyAvatar.position; + startRotation = MyAvatar.orientation; + try{ Script.update.disconnect(standingUpAnimation); } catch(e){} + try{ Script.update.disconnect(sittingDownAnimation); } catch(e){} + Script.update.connect(goToSeatAnimation); + } else { + print ("Sorry, no seats here."); + } + } + } }) function update(deltaTime){ @@ -149,7 +256,76 @@ function update(deltaTime){ var newY = (windowDimensions.y - buttonHeight) / 2 ; Overlays.editOverlay( standUpButton, {x: newX, y: newY} ); Overlays.editOverlay( sitDownButton, {x: newX, y: newY} ); - } + } + + if (MyAvatar.position.x != avatarOldPosition.x && + MyAvatar.position.y != avatarOldPosition.y && + MyAvatar.position.z != avatarOldPosition.z) { + avatarOldPosition = MyAvatar.position; + + var SEARCH_RADIUS = 5; + var foundModels = Models.findModels(MyAvatar.position, SEARCH_RADIUS); + // Let's remove indicator that got out of radius + for (model in models) { + if (Vec3.distance(models[model].properties.position, MyAvatar.position) > SEARCH_RADIUS) { + removeIndicators(models[model]); + } + } + + // Let's add indicators to new seats in radius + for (var i = 0; i < foundModels.length; ++i) { + var model = foundModels[i]; + if (typeof(models[model.id]) == "undefined") { + addIndicators(model); + } + } + + if (hiddingSeats && passedTime >= animationLenght) { + showIndicators(true); + } + } +} + +function addIndicators(modelID) { + modelID.properties = Models.getModelProperties(modelID); + if (modelID.properties.sittingPoints.length > 0) { + for (var i = 0; i < modelID.properties.sittingPoints.length; ++i) { + modelID.properties.sittingPoints[i].indicator = new SeatIndicator(modelID.properties, i); + } + + models[modelID.id] = modelID; + } else { + Models.editModel(modelID, { glowLevel: 0.0 }); + } +} + +function removeIndicators(modelID) { + for (var i = 0; i < modelID.properties.sittingPoints.length; ++i) { + modelID.properties.sittingPoints[i].indicator.cleanup(); + } + delete models[modelID.id]; +} + +function showIndicators(doShow) { + for (model in models) { + var modelID = models[model]; + for (var i = 0; i < modelID.properties.sittingPoints.length; ++i) { + modelID.properties.sittingPoints[i].indicator.show(doShow); + } + } + hiddingSeats = !doShow; +} + +function raySphereIntersection(origin, direction, center, radius) { + var A = origin; + var B = Vec3.normalize(direction); + var P = center; + + var x = Vec3.dot(Vec3.subtract(P, A), B); + var X = Vec3.sum(A, Vec3.multiply(B, x)); + var d = Vec3.length(Vec3.subtract(P, X)); + + return (x > 0 && d <= radius); } function keyPressEvent(event) { @@ -167,11 +343,15 @@ Script.update.connect(update); Controller.keyPressEvent.connect(keyPressEvent); Script.scriptEnding.connect(function() { - for (var i = 0; i < pose.length; i++){ MyAvatar.clearJointData(pose[i].joint); - } + } Overlays.deleteOverlay(sitDownButton); Overlays.deleteOverlay(standUpButton); + for (model in models){ + for (var i = 0; i < models[model].properties.sittingPoints.length; ++i) { + models[model].properties.sittingPoints[i].indicator.cleanup(); + } + } }); diff --git a/examples/squeezeHands.js b/examples/squeezeHands.js index e53dd9569c..da720734e1 100644 --- a/examples/squeezeHands.js +++ b/examples/squeezeHands.js @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var rightHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/RightHandAnim.fbx"; -var leftHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/LeftHandAnim.fbx"; +var rightHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/RightHandAnimPhilip.fbx"; +var leftHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/LeftHandAnimPhilip.fbx"; var LEFT = 0; var RIGHT = 1; diff --git a/examples/toyball.js b/examples/toyball.js index d312c1bc94..e03fd67a5d 100644 --- a/examples/toyball.js +++ b/examples/toyball.js @@ -26,14 +26,21 @@ var RIGHT_TIP = 3; var RIGHT_BUTTON_FWD = 11; var RIGHT_BUTTON_3 = 9; +var BALL_RADIUS = 0.08; +var GRAVITY_STRENGTH = 0.5; + +var HELD_COLOR = { red: 240, green: 0, blue: 0 }; +var THROWN_COLOR = { red: 128, green: 0, blue: 0 }; + var leftBallAlreadyInHand = false; var rightBallAlreadyInHand = false; var leftHandParticle; var rightHandParticle; -var throwSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/throw.raw"); +var newSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/throw.raw"); var catchSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/catch.raw"); -var targetRadius = 0.25; +var throwSound = new Sound("http://highfidelity-public.s3-us-west-1.amazonaws.com/sounds/Switches%20and%20sliders/slider%20-%20whoosh1.raw"); +var targetRadius = 1.0; var wantDebugging = false; @@ -44,31 +51,19 @@ function debugPrint(message) { } function getBallHoldPosition(whichSide) { - var normal; - var tipPosition; if (whichSide == LEFT_PALM) { - normal = Controller.getSpatialControlNormal(LEFT_PALM); - tipPosition = Controller.getSpatialControlPosition(LEFT_TIP); + position = MyAvatar.getLeftPalmPosition(); } else { - normal = Controller.getSpatialControlNormal(RIGHT_PALM); - tipPosition = Controller.getSpatialControlPosition(RIGHT_TIP); + position = MyAvatar.getRightPalmPosition(); } - var BALL_FORWARD_OFFSET = 0.08; // put the ball a bit forward of fingers - position = { x: BALL_FORWARD_OFFSET * normal.x, - y: BALL_FORWARD_OFFSET * normal.y, - z: BALL_FORWARD_OFFSET * normal.z }; - - position.x += tipPosition.x; - position.y += tipPosition.y; - position.z += tipPosition.z; - return position; } function checkControllerSide(whichSide) { var BUTTON_FWD; var BUTTON_3; + var TRIGGER; var palmPosition; var ballAlreadyInHand; var handMessage; @@ -76,18 +71,20 @@ function checkControllerSide(whichSide) { if (whichSide == LEFT_PALM) { BUTTON_FWD = LEFT_BUTTON_FWD; BUTTON_3 = LEFT_BUTTON_3; + TRIGGER = 0; palmPosition = Controller.getSpatialControlPosition(LEFT_PALM); ballAlreadyInHand = leftBallAlreadyInHand; handMessage = "LEFT"; } else { BUTTON_FWD = RIGHT_BUTTON_FWD; BUTTON_3 = RIGHT_BUTTON_3; + TRIGGER = 1; palmPosition = Controller.getSpatialControlPosition(RIGHT_PALM); ballAlreadyInHand = rightBallAlreadyInHand; handMessage = "RIGHT"; } - - var grabButtonPressed = (Controller.isButtonPressed(BUTTON_FWD) || Controller.isButtonPressed(BUTTON_3)); + + var grabButtonPressed = (Controller.isButtonPressed(BUTTON_FWD) || Controller.isButtonPressed(BUTTON_3) || (Controller.getTriggerValue(TRIGGER) > 0.5)); // If I don't currently have a ball in my hand, then try to catch closest one if (!ballAlreadyInHand && grabButtonPressed) { @@ -107,8 +104,11 @@ function checkControllerSide(whichSide) { var ballPosition = getBallHoldPosition(whichSide); var properties = { position: { x: ballPosition.x, y: ballPosition.y, - z: ballPosition.z }, - velocity : { x: 0, y: 0, z: 0}, inHand: true }; + z: ballPosition.z }, + color: HELD_COLOR, + velocity : { x: 0, y: 0, z: 0}, + lifetime : 600, + inHand: true }; Particles.editParticle(closestParticle, properties); var options = new AudioInjectionOptions(); @@ -127,7 +127,7 @@ function checkControllerSide(whichSide) { //} // If '3' is pressed, and not holding a ball, make a new one - if (Controller.isButtonPressed(BUTTON_3) && !ballAlreadyInHand) { + if (grabButtonPressed && !ballAlreadyInHand) { var ballPosition = getBallHoldPosition(whichSide); var properties = { position: { x: ballPosition.x, y: ballPosition.y, @@ -135,11 +135,11 @@ function checkControllerSide(whichSide) { velocity: { x: 0, y: 0, z: 0}, gravity: { x: 0, y: 0, z: 0}, inHand: true, - radius: 0.05, + radius: BALL_RADIUS, damping: 0.999, - color: { red: 255, green: 0, blue: 0 }, + color: HELD_COLOR, - lifetime: 10 // 10 seconds - same as default, not needed but here as an example + lifetime: 600 // 10 seconds - same as default, not needed but here as an example }; newParticle = Particles.addParticle(properties); @@ -155,7 +155,7 @@ function checkControllerSide(whichSide) { var options = new AudioInjectionOptions(); options.position = ballPosition; options.volume = 1.0; - Audio.playSound(catchSound, options); + Audio.playSound(newSound, options); return; // exit early } @@ -188,7 +188,9 @@ function checkControllerSide(whichSide) { y: tipVelocity.y * THROWN_VELOCITY_SCALING, z: tipVelocity.z * THROWN_VELOCITY_SCALING } , inHand: false, - gravity: { x: 0, y: -2, z: 0}, + color: THROWN_COLOR, + lifetime: 10, + gravity: { x: 0, y: -GRAVITY_STRENGTH, z: 0}, }; Particles.editParticle(handParticle, properties); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 44fd8fd5d1..1821216f28 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -185,7 +185,11 @@ if (LIBOVR_FOUND AND NOT DISABLE_LIBOVR) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${LIBOVR_INCLUDE_DIRS}") endif () - target_link_libraries(${TARGET_NAME} "${LIBOVR_LIBRARIES}") + if (WIN32) + target_link_libraries(${TARGET_NAME} optimized "${LIBOVR_RELEASE_LIBRARIES}" debug "${LIBOVR_DEBUG_LIBRARIES}") + else() + target_link_libraries(${TARGET_NAME} "${LIBOVR_LIBRARIES}") + endif() endif (LIBOVR_FOUND AND NOT DISABLE_LIBOVR) # and with PrioVR library diff --git a/interface/external/oculus/readme.txt b/interface/external/oculus/readme.txt index 002047b73f..f689f81478 100644 --- a/interface/external/oculus/readme.txt +++ b/interface/external/oculus/readme.txt @@ -2,18 +2,12 @@ Instructions for adding the Oculus library (LibOVR) to Interface Stephen Birarda, March 6, 2014 -You can download the Oculus SDK from https://developer.oculusvr.com/ (account creation required). Interface has been tested with SDK version 0.2.5. +You can download the Oculus SDK from https://developer.oculusvr.com/ (account creation required). Interface has been tested with SDK version 0.3.2. 1. Copy the Oculus SDK folders from the LibOVR directory (Lib, Include, Src) into the interface/externals/oculus folder. This readme.txt should be there as well. You may optionally choose to copy the SDK folders to a location outside the repository (so you can re-use with different checkouts and different projects). If so our CMake find module expects you to set the ENV variable 'HIFI_LIB_DIR' to a directory containing a subfolder 'oculus' that contains the three folders mentioned above. - - NOTE: On OS X there is a linker error with version 0.2.5c of the Oculus SDK. - It must be re-built (from the included LibOVR_With_Samples.xcodeproj) with RRTI support. - In XCode Build Settings for the ovr target, set "Enable C++ Runtime Types" to yes. - Then, Archive and use the organizer to save a copy of the built products. - In the exported directory you will have a new libovr.a to copy into the oculus directory from above. 2. Clear your build directory, run cmake and build, and you should be all set. \ No newline at end of file diff --git a/interface/resources/shaders/oculus.frag b/interface/resources/shaders/oculus.frag index f2b066a974..8e96428e17 100644 --- a/interface/resources/shaders/oculus.frag +++ b/interface/resources/shaders/oculus.frag @@ -4,12 +4,11 @@ // oculus.frag // fragment shader // -// Created by Andrzej Kapolka on 11/26/13. -// Copyright 2013 High Fidelity, Inc. +// Created by Ben Arnold on 6/24/14. +// Copyright 2014 High Fidelity, Inc. // -// this shader is an adaptation (HLSL -> GLSL, removed conditional) of the one in the Oculus sample -// code (Samples/OculusRoomTiny/RenderTiny_D3D1X_Device.cpp), which is under the Apache license -// (http://www.apache.org/licenses/LICENSE-2.0) +// this shader is an adaptation (HLSL -> GLSL) of the one in the +// Oculus_SDK_Overview.pdf for the 3.2 SDK. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -17,23 +16,16 @@ uniform sampler2D texture; -uniform vec2 lensCenter; -uniform vec2 screenCenter; -uniform vec2 scale; -uniform vec2 scaleIn; -uniform vec4 hmdWarpParam; - -vec2 hmdWarp(vec2 in01) { - vec2 theta = (in01 - lensCenter) * scaleIn; - float rSq = theta.x * theta.x + theta.y * theta.y; - vec2 theta1 = theta * (hmdWarpParam.x + hmdWarpParam.y * rSq + - hmdWarpParam.z * rSq * rSq + hmdWarpParam.w * rSq * rSq * rSq); - return lensCenter + scale * theta1; -} +varying float vFade; +varying vec2 oTexCoord0; +varying vec2 oTexCoord1; +varying vec2 oTexCoord2; void main(void) { - vec2 tc = hmdWarp(gl_TexCoord[0].st); - vec2 below = step(screenCenter.st + vec2(-0.25, -0.5), tc.st); - vec2 above = vec2(1.0, 1.0) - step(screenCenter.st + vec2(0.25, 0.5), tc.st); - gl_FragColor = mix(vec4(0.0, 0.0, 0.0, 1.0), texture2D(texture, tc), above.s * above.t * below.s * below.t); + // 3 samples for fixing chromatic aberrations + float r = texture2D(texture, oTexCoord0.xy).r; + float g = texture2D(texture, oTexCoord1.xy).g; + float b = texture2D(texture, oTexCoord2.xy).b; + + gl_FragColor = vec4(r * vFade, g * vFade, b * vFade, 1.0); } diff --git a/interface/resources/shaders/oculus.vert b/interface/resources/shaders/oculus.vert new file mode 100644 index 0000000000..800fbd9317 --- /dev/null +++ b/interface/resources/shaders/oculus.vert @@ -0,0 +1,63 @@ +#version 120 + +// +// oculus.vert +// vertex shader +// +// Created by Ben Arnold on 6/24/14. +// Copyright 2014 High Fidelity, Inc. +// +// this shader is an adaptation (HLSL -> GLSL) of the one in the +// Oculus_SDK_Overview.pdf for the 3.2 SDK. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +uniform vec2 EyeToSourceUVScale; +uniform vec2 EyeToSourceUVOffset; +uniform mat4 EyeRotationStart; +uniform mat4 EyeRotationEnd; + +attribute vec2 position; +attribute vec4 color; +attribute vec2 texCoord0; +attribute vec2 texCoord1; +attribute vec2 texCoord2; + +varying float vFade; +varying vec2 oTexCoord0; +varying vec2 oTexCoord1; +varying vec2 oTexCoord2; + +vec2 TimewarpTexCoord(vec2 texCoord, mat4 rotMat) +{ + // Vertex inputs are in TanEyeAngle space for the R,G,B channels (i.e. after chromatic + // aberration and distortion). These are now "real world" vectors in direction (x,y,1) + // relative to the eye of the HMD. Apply the 3x3 timewarp rotation to these vectors. + vec3 transformed = vec3( rotMat * vec4(texCoord.xy, 1, 1) ); + + // Project them back onto the Z=1 plane of the rendered images. + vec2 flattened = (transformed.xy / transformed.z); + + // Scale them into ([0,0.5],[0,1]) or ([0.5,0],[0,1]) UV lookup space (depending on eye) + return (EyeToSourceUVScale * flattened + EyeToSourceUVOffset); +} + +void main() +{ + float timewarpMixFactor = color.a; + mat4 mixedEyeRot = EyeRotationStart * (1.0 - timewarpMixFactor) + EyeRotationEnd * (timewarpMixFactor); + + oTexCoord0 = TimewarpTexCoord(texCoord0, mixedEyeRot); + oTexCoord1 = TimewarpTexCoord(texCoord1, mixedEyeRot); + oTexCoord2 = TimewarpTexCoord(texCoord2, mixedEyeRot); + + //Flip y texture coordinates + oTexCoord0.y = 1.0 - oTexCoord0.y; + oTexCoord1.y = 1.0 - oTexCoord1.y; + oTexCoord2.y = 1.0 - oTexCoord2.y; + + gl_Position = vec4(position.xy, 0.5, 1.0); + vFade = color.r; // For vignette fade +} \ No newline at end of file diff --git a/interface/resources/styles/preferences.qss b/interface/resources/styles/preferences.qss index e678acd0c9..40e35c8e52 100644 --- a/interface/resources/styles/preferences.qss +++ b/interface/resources/styles/preferences.qss @@ -12,7 +12,8 @@ QLabel#advancedTuningLabel { QPushButton#buttonBrowseHead, QPushButton#buttonBrowseBody, -QPushButton#buttonBrowseLocation { +QPushButton#buttonBrowseLocation, +QPushButton#buttonBrowseScriptsLocation { background-image: url(styles/search.svg); background-repeat: no-repeat; background-position: center center; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8e7f20b76f..b9e70b0854 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -60,6 +60,7 @@ #include <ParticlesScriptingInterface.h> #include <PerfStat.h> #include <ResourceCache.h> +#include <UserActivityLogger.h> #include <UUID.h> #include <OctreeSceneStats.h> #include <LocalVoxelsList.h> @@ -136,7 +137,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _nodeThread(new QThread(this)), _datagramProcessor(), _frameCount(0), - _fps(120.0f), + _fps(60.0f), _justStarted(true), _voxelImporter(NULL), _importSucceded(false), @@ -169,7 +170,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _nodeBoundsDisplay(this), _previousScriptLocation(), _applicationOverlay(), - _runningScriptsWidget(new RunningScriptsWidget(_window)), + _runningScriptsWidget(NULL), _runningScriptsWidgetWasVisible(false), _trayIcon(new QSystemTrayIcon(_window)), _lastNackTime(usecTimestampNow()) @@ -203,6 +204,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // call Menu getInstance static method to set up the menu _window->setMenuBar(Menu::getInstance()); + _runningScriptsWidget = new RunningScriptsWidget(_window); + unsigned int listenPort = 0; // bind to an ephemeral port by default const char** constArgv = const_cast<const char**>(argv); const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); @@ -268,6 +271,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // set the account manager's root URL and trigger a login request if we don't have the access token accountManager.setAuthURL(DEFAULT_NODE_AUTH_URL); + UserActivityLogger::getInstance().launch(applicationVersion()); // once the event loop has started, check and signal for an access token QMetaObject::invokeMethod(&accountManager, "checkAndSignalForAccessToken", Qt::QueuedConnection); @@ -396,7 +400,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : } Application::~Application() { - + int DELAY_TIME = 1000; + UserActivityLogger::getInstance().close(DELAY_TIME); + qInstallMessageHandler(NULL); // make sure we don't call the idle timer any more @@ -553,7 +559,7 @@ void Application::initializeGL() { } // update before the first render - update(0.0f); + update(1.f / _fps); InfoView::showFirstTime(); } @@ -565,6 +571,16 @@ void Application::paintGL() { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::paintGL()"); + const bool glowEnabled = Menu::getInstance()->isOptionChecked(MenuOption::EnableGlowEffect); + + // Set the desired FBO texture size. If it hasn't changed, this does nothing. + // Otherwise, it must rebuild the FBOs + if (OculusManager::isConnected()) { + _textureCache.setFrameBufferSize(OculusManager::getRenderTargetSize()); + } else { + _textureCache.setFrameBufferSize(_glWidget->size()); + } + glEnable(GL_LINE_SMOOTH); if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { @@ -573,28 +589,16 @@ void Application::paintGL() { _myCamera.setTargetRotation(_myAvatar->getHead()->getCameraOrientation()); } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { + //Note, the camera distance is set in Camera::setMode() so we dont have to do it here. _myCamera.setTightness(0.0f); // Camera is directly connected to head without smoothing _myCamera.setTargetPosition(_myAvatar->getUprightHeadPosition()); - _myCamera.setTargetRotation(_myAvatar->getHead()->getCameraOrientation()); + _myCamera.setTargetRotation(_myAvatar->getWorldAlignedOrientation()); } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { _myCamera.setTightness(0.0f); - glm::vec3 eyePosition = _myAvatar->getHead()->calculateAverageEyePosition(); - float headHeight = eyePosition.y - _myAvatar->getPosition().y; _myCamera.setDistance(MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); - _myCamera.setTargetPosition(_myAvatar->getPosition() + glm::vec3(0, headHeight + (_raiseMirror * _myAvatar->getScale()), 0)); _myCamera.setTargetRotation(_myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); - } - - if (OculusManager::isConnected()) { - // Oculus in third person causes nausea, so only allow it if option is checked in dev menu - if (!Menu::getInstance()->isOptionChecked(MenuOption::AllowOculusCameraModeChange) || _myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { - _myCamera.setDistance(0.0f); - _myCamera.setTargetPosition(_myAvatar->getHead()->calculateAverageEyePosition()); - _myCamera.setTargetRotation(_myAvatar->getHead()->getCameraOrientation()); - } - _myCamera.setUpShift(0.0f); - _myCamera.setTightness(0.0f); // Camera is directly connected to head without smoothing + _myCamera.setTargetPosition(_myAvatar->getHead()->calculateAverageEyePosition()); } // Update camera position @@ -629,16 +633,32 @@ void Application::paintGL() { updateShadowMap(); } + //If we aren't using the glow shader, we have to clear the color and depth buffer + if (!glowEnabled) { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + if (OculusManager::isConnected()) { - OculusManager::display(whichCamera); + //When in mirror mode, use camera rotation. Otherwise, use body rotation + if (whichCamera.getMode() == CAMERA_MODE_MIRROR) { + OculusManager::display(whichCamera.getRotation(), whichCamera.getPosition(), whichCamera); + } else { + OculusManager::display(_myAvatar->getWorldAlignedOrientation(), whichCamera.getPosition(), whichCamera); + } } else if (TV3DManager::isConnected()) { - _glowEffect.prepare(); + if (glowEnabled) { + _glowEffect.prepare(); + } TV3DManager::display(whichCamera); - _glowEffect.render(); + if (glowEnabled) { + _glowEffect.render(); + } } else { - _glowEffect.prepare(); + if (glowEnabled) { + _glowEffect.prepare(); + } glMatrixMode(GL_MODELVIEW); glPushMatrix(); @@ -646,7 +666,9 @@ void Application::paintGL() { displaySide(whichCamera); glPopMatrix(); - _glowEffect.render(); + if (glowEnabled) { + _glowEffect.render(); + } if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { renderRearViewMirror(_mirrorViewRect); @@ -2178,7 +2200,8 @@ int Application::sendNackPackets() { OctreeSceneStats& stats = _octreeServerSceneStats[nodeUUID]; // make copy of missing sequence numbers from stats - const QSet<OCTREE_PACKET_SEQUENCE> missingSequenceNumbers = stats.getMissingSequenceNumbers(); + const QSet<OCTREE_PACKET_SEQUENCE> missingSequenceNumbers = + stats.getIncomingOctreeSequenceNumberStats().getMissingSet(); _octreeSceneStatsLock.unlock(); @@ -3148,9 +3171,7 @@ void Application::resetSensors() { _faceshift.reset(); _visage.reset(); - if (OculusManager::isConnected()) { - OculusManager::reset(); - } + OculusManager::reset(); _prioVR.reset(); //_leapmotion.reset(); @@ -3303,6 +3324,10 @@ void Application::nodeKilled(SharedNodePointer node) { _particleEditSender.nodeKilled(node); _modelEditSender.nodeKilled(node); + if (node->getType() == NodeType::AudioMixer) { + QMetaObject::invokeMethod(&_audio, "resetIncomingMixedAudioSequenceNumberStats"); + } + if (node->getType() == NodeType::VoxelServer) { QUuid nodeUUID = node->getUUID(); // see if this is the first we've heard of this node... @@ -3547,10 +3572,12 @@ void Application::saveScripts() { _settings->endArray(); } -ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScriptFromEditor) { +ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScriptFromEditor, bool activateMainWindow) { QUrl scriptUrl(scriptName); const QString& scriptURLString = scriptUrl.toString(); - if(loadScriptFromEditor && _scriptEnginesHash.contains(scriptURLString) && !_scriptEnginesHash[scriptURLString]->isFinished()){ + if (_scriptEnginesHash.contains(scriptURLString) && loadScriptFromEditor + && !_scriptEnginesHash[scriptURLString]->isFinished()) { + return _scriptEnginesHash[scriptURLString]; } @@ -3563,11 +3590,13 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript if (!scriptEngine->hasScript()) { qDebug() << "Application::loadScript(), script failed to load..."; + QMessageBox::warning(getWindow(), "Error Loading Script", scriptURLString + " failed to load."); return NULL; } - _scriptEnginesHash.insert(scriptURLString, scriptEngine); + _scriptEnginesHash.insertMulti(scriptURLString, scriptEngine); _runningScriptsWidget->setRunningScripts(getRunningScripts()); + UserActivityLogger::getInstance().loadedScript(scriptURLString); } // setup the packet senders and jurisdiction listeners of the script engine's scripting interfaces so @@ -3623,13 +3652,16 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript // when the application is about to quit, stop our script engine so it unwinds properly connect(this, SIGNAL(aboutToQuit()), scriptEngine, SLOT(stop())); + NodeList* nodeList = NodeList::getInstance(); + connect(nodeList, &NodeList::nodeKilled, scriptEngine, &ScriptEngine::nodeKilled); + scriptEngine->moveToThread(workerThread); // Starts an event loop, and emits workerThread->started() workerThread->start(); // restore the main window's active state - if (!loadScriptFromEditor) { + if (activateMainWindow && !loadScriptFromEditor) { _window->activateWindow(); } bumpSettings(); @@ -3638,7 +3670,10 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript } void Application::scriptFinished(const QString& scriptName) { - if (_scriptEnginesHash.remove(scriptName)) { + const QString& scriptURLString = QUrl(scriptName).toString(); + QHash<QString, ScriptEngine*>::iterator it = _scriptEnginesHash.find(scriptURLString); + if (it != _scriptEnginesHash.end()) { + _scriptEnginesHash.erase(it); _runningScriptsWidget->scriptStopped(scriptName); _runningScriptsWidget->setRunningScripts(getRunningScripts()); bumpSettings(); @@ -3658,8 +3693,9 @@ void Application::stopAllScripts(bool restart) { } void Application::stopScript(const QString &scriptName) { - if (_scriptEnginesHash.contains(scriptName)) { - _scriptEnginesHash.value(scriptName)->stop(); + const QString& scriptURLString = QUrl(scriptName).toString(); + if (_scriptEnginesHash.contains(scriptURLString)) { + _scriptEnginesHash.value(scriptURLString)->stop(); qDebug() << "stopping script..." << scriptName; } } diff --git a/interface/src/Application.h b/interface/src/Application.h index af0dfe9d15..56b6f673ae 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -324,7 +324,7 @@ public slots: void loadScriptURLDialog(); void toggleLogDialog(); void initAvatarAndViewFrustum(); - ScriptEngine* loadScript(const QString& fileNameString = QString(), bool loadScriptFromEditor = false); + ScriptEngine* loadScript(const QString& fileNameString = QString(), bool loadScriptFromEditor = false, bool activateMainWindow = false); void scriptFinished(const QString& scriptName); void stopAllScripts(bool restart = false); void stopScript(const QString& scriptName); diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 271bcd5279..f5f148969c 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -102,7 +102,9 @@ Audio::Audio(int16_t initialJitterBufferSamples, QObject* parent) : _samplesPerScope(NETWORK_SAMPLES_PER_FRAME * _framesPerScope), _scopeInput(0), _scopeOutputLeft(0), - _scopeOutputRight(0) + _scopeOutputRight(0), + _audioMixerAvatarStreamStats(), + _outgoingAvatarAudioSequenceNumber(0) { // clear the array of locally injected samples memset(_localProceduralSamples, 0, NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL); @@ -118,6 +120,9 @@ void Audio::init(QGLWidget *parent) { void Audio::reset() { _ringBuffer.reset(); + _outgoingAvatarAudioSequenceNumber = 0; + _audioMixerInjectedStreamStatsMap.clear(); + _incomingMixedAudioSequenceNumberStats.reset(); } QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& deviceName) { @@ -218,9 +223,14 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { hr = pPropertyStore->GetValue(PKEY_Device_FriendlyName, &pv); pPropertyStore->Release(); pPropertyStore = NULL; - //QAudio devices seems to only take the 31 first characters of the Friendly Device Name. - const DWORD QT_WIN_MAX_AUDIO_DEVICENAME_LEN = 31; - deviceName = QString::fromWCharArray((wchar_t*)pv.pwszVal).left(QT_WIN_MAX_AUDIO_DEVICENAME_LEN); + deviceName = QString::fromWCharArray((wchar_t*)pv.pwszVal); + const DWORD WINDOWS7_MAJOR_VERSION = 6; + const DWORD WINDOWS7_MINOR_VERSION = 1; + if (osvi.dwMajorVersion <= WINDOWS7_MAJOR_VERSION && osvi.dwMinorVersion <= WINDOWS7_MINOR_VERSION) { + // Windows 7 provides only the 31 first characters of the device name. + const DWORD QT_WIN7_MAX_AUDIO_DEVICENAME_LEN = 31; + deviceName = deviceName.left(QT_WIN7_MAX_AUDIO_DEVICENAME_LEN); + } qDebug() << (mode == QAudio::AudioOutput ? "output" : "input") << " device:" << deviceName; PropVariantClear(&pv); } @@ -416,7 +426,7 @@ void Audio::handleAudioInput() { static char audioDataPacket[MAX_PACKET_SIZE]; static int numBytesPacketHeader = numBytesForPacketHeaderGivenPacketType(PacketTypeMicrophoneAudioNoEcho); - static int leadingBytes = numBytesPacketHeader + sizeof(glm::vec3) + sizeof(glm::quat) + sizeof(quint8); + static int leadingBytes = numBytesPacketHeader + sizeof(quint16) + sizeof(glm::vec3) + sizeof(glm::quat) + sizeof(quint8); static int16_t* networkAudioSamples = (int16_t*) (audioDataPacket + leadingBytes); @@ -461,8 +471,8 @@ void Audio::handleAudioInput() { int16_t* inputAudioSamples = new int16_t[inputSamplesRequired]; _inputRingBuffer.readSamples(inputAudioSamples, inputSamplesRequired); - int numNetworkBytes = _isStereoInput ? NETWORK_BUFFER_LENGTH_BYTES_STEREO : NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL; - int numNetworkSamples = _isStereoInput ? NETWORK_BUFFER_LENGTH_SAMPLES_STEREO : NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL; + const int numNetworkBytes = _isStereoInput ? NETWORK_BUFFER_LENGTH_BYTES_STEREO : NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL; + const int numNetworkSamples = _isStereoInput ? NETWORK_BUFFER_LENGTH_SAMPLES_STEREO : NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL; // zero out the monoAudioSamples array and the locally injected audio memset(networkAudioSamples, 0, numNetworkBytes); @@ -634,12 +644,10 @@ void Audio::handleAudioInput() { packetType = PacketTypeSilentAudioFrame; // we need to indicate how many silent samples this is to the audio mixer - audioDataPacket[0] = _isStereoInput - ? NETWORK_BUFFER_LENGTH_SAMPLES_STEREO - : NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL; + networkAudioSamples[0] = numNetworkSamples; numAudioBytes = sizeof(int16_t); } else { - numAudioBytes = _isStereoInput ? NETWORK_BUFFER_LENGTH_BYTES_STEREO : NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL; + numAudioBytes = numNetworkBytes; if (Menu::getInstance()->isOptionChecked(MenuOption::EchoServerAudio)) { packetType = PacketTypeMicrophoneAudioWithEcho; @@ -650,6 +658,10 @@ void Audio::handleAudioInput() { char* currentPacketPtr = audioDataPacket + populatePacketHeader(audioDataPacket, packetType); + // pack sequence number + memcpy(currentPacketPtr, &_outgoingAvatarAudioSequenceNumber, sizeof(quint16)); + currentPacketPtr += sizeof(quint16); + // set the mono/stereo byte *currentPacketPtr++ = isStereo; @@ -662,6 +674,7 @@ void Audio::handleAudioInput() { currentPacketPtr += sizeof(headOrientation); nodeList->writeDatagram(audioDataPacket, numAudioBytes + leadingBytes, audioMixer); + _outgoingAvatarAudioSequenceNumber++; Application::getInstance()->getBandwidthMeter()->outputStream(BandwidthMeter::AUDIO) .updateValue(numAudioBytes + leadingBytes); @@ -704,6 +717,36 @@ void Audio::addReceivedAudioToBuffer(const QByteArray& audioByteArray) { Application::getInstance()->getBandwidthMeter()->inputStream(BandwidthMeter::AUDIO).updateValue(audioByteArray.size()); } +void Audio::parseAudioStreamStatsPacket(const QByteArray& packet) { + + int numBytesPacketHeader = numBytesForPacketHeader(packet); + const char* dataAt = packet.constData() + numBytesPacketHeader; + + // parse the appendFlag, clear injected audio stream stats if 0 + quint8 appendFlag = *(reinterpret_cast<const quint16*>(dataAt)); + dataAt += sizeof(quint8); + if (!appendFlag) { + _audioMixerInjectedStreamStatsMap.clear(); + } + + // parse the number of stream stats structs to follow + quint16 numStreamStats = *(reinterpret_cast<const quint16*>(dataAt)); + dataAt += sizeof(quint16); + + // parse the stream stats + AudioStreamStats streamStats; + for (quint16 i = 0; i < numStreamStats; i++) { + memcpy(&streamStats, dataAt, sizeof(AudioStreamStats)); + dataAt += sizeof(AudioStreamStats); + + if (streamStats._streamType == PositionalAudioRingBuffer::Microphone) { + _audioMixerAvatarStreamStats = streamStats; + } else { + _audioMixerInjectedStreamStatsMap[streamStats._streamIdentifier] = streamStats; + } + } +} + // NOTE: numSamples is the total number of single channel samples, since callers will always call this with stereo // data we know that we will have 2x samples for each stereo time sample at the format's sample rate void Audio::addSpatialAudioToBuffer(unsigned int sampleTime, const QByteArray& spatialAudio, unsigned int numSamples) { @@ -803,6 +846,16 @@ void Audio::toggleStereoInput() { } void Audio::processReceivedAudio(const QByteArray& audioByteArray) { + + QUuid senderUUID = uuidFromPacketHeader(audioByteArray); + + // parse sequence number for this packet + int numBytesPacketHeader = numBytesForPacketHeader(audioByteArray); + const char* sequenceAt = audioByteArray.constData() + numBytesPacketHeader; + quint16 sequence = *((quint16*)sequenceAt); + _incomingMixedAudioSequenceNumberStats.sequenceNumberReceived(sequence, senderUUID); + + // parse audio data _ringBuffer.parseData(audioByteArray); float networkOutputToOutputRatio = (_desiredOutputFormat.sampleRate() / (float) _outputFormat.sampleRate()) @@ -825,7 +878,8 @@ void Audio::processReceivedAudio(const QByteArray& audioByteArray) { QByteArray outputBuffer; outputBuffer.resize(numDeviceOutputSamples * sizeof(int16_t)); - int numSamplesNeededToStartPlayback = NETWORK_BUFFER_LENGTH_SAMPLES_STEREO + (_jitterBufferSamples * 2); + int numSamplesNeededToStartPlayback = std::min(NETWORK_BUFFER_LENGTH_SAMPLES_STEREO + (_jitterBufferSamples * 2), + _ringBuffer.getSampleCapacity()); if (!_ringBuffer.isNotStarvedOrHasMinimumSamples(numSamplesNeededToStartPlayback)) { // We are still waiting for enough samples to begin playback @@ -842,6 +896,7 @@ void Audio::processReceivedAudio(const QByteArray& audioByteArray) { buffer.resize(numNetworkOutputSamples * sizeof(int16_t)); _ringBuffer.readSamples((int16_t*)buffer.data(), numNetworkOutputSamples); + // Accumulate direct transmission of audio from sender to receiver if (Menu::getInstance()->isOptionChecked(MenuOption::AudioSpatialProcessingIncludeOriginal)) { emit preProcessOriginalInboundAudio(sampleTime, buffer, _desiredOutputFormat); diff --git a/interface/src/Audio.h b/interface/src/Audio.h index 74fc373cb0..9f04e5cb03 100644 --- a/interface/src/Audio.h +++ b/interface/src/Audio.h @@ -16,6 +16,7 @@ #include <vector> #include "InterfaceConfig.h" +#include "AudioStreamStats.h" #include <QAudio> #include <QAudioInput> @@ -72,13 +73,17 @@ public: bool getProcessSpatialAudio() const { return _processSpatialAudio; } + const SequenceNumberStats& getIncomingMixedAudioSequenceNumberStats() const { return _incomingMixedAudioSequenceNumberStats; } + public slots: void start(); void stop(); void addReceivedAudioToBuffer(const QByteArray& audioByteArray); + void parseAudioStreamStatsPacket(const QByteArray& packet); void addSpatialAudioToBuffer(unsigned int sampleTime, const QByteArray& spatialAudio, unsigned int numSamples); void handleAudioInput(); void reset(); + void resetIncomingMixedAudioSequenceNumberStats() { _incomingMixedAudioSequenceNumberStats.reset(); } void toggleMute(); void toggleAudioNoiseReduction(); void toggleToneInjection(); @@ -102,6 +107,9 @@ public slots: float getInputVolume() const { return (_audioInput) ? _audioInput->volume() : 0.0f; } void setInputVolume(float volume) { if (_audioInput) _audioInput->setVolume(volume); } + const AudioStreamStats& getAudioMixerAvatarStreamStats() const { return _audioMixerAvatarStreamStats; } + const QHash<QUuid, AudioStreamStats>& getAudioMixerInjectedStreamStatsMap() const { return _audioMixerInjectedStreamStatsMap; } + signals: bool muteToggled(); void preProcessOriginalInboundAudio(unsigned int sampleTime, QByteArray& samples, const QAudioFormat& format); @@ -233,6 +241,11 @@ private: QByteArray* _scopeOutputLeft; QByteArray* _scopeOutputRight; + AudioStreamStats _audioMixerAvatarStreamStats; + QHash<QUuid, AudioStreamStats> _audioMixerInjectedStreamStatsMap; + + quint16 _outgoingAvatarAudioSequenceNumber; + SequenceNumberStats _incomingMixedAudioSequenceNumberStats; }; diff --git a/interface/src/Camera.cpp b/interface/src/Camera.cpp index 0e33e14f32..4490b60fc9 100644 --- a/interface/src/Camera.cpp +++ b/interface/src/Camera.cpp @@ -317,8 +317,6 @@ void CameraScriptableObject::setMode(const QString& mode) { } if (currentMode != targetMode) { _camera->setMode(targetMode); - const float DEFAULT_MODE_SHIFT_PERIOD = 0.5f; // half second - _camera->setModeShiftPeriod(DEFAULT_MODE_SHIFT_PERIOD); } } diff --git a/interface/src/Camera.h b/interface/src/Camera.h index 5e189c1111..2bbbf0e751 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -42,9 +42,8 @@ public: void setTargetPosition(const glm::vec3& t); void setTightness(float t) { _tightness = t; } void setTargetRotation(const glm::quat& rotation); - - void setMode(CameraMode m); void setModeShiftPeriod(float r); + void setMode(CameraMode m); void setFieldOfView(float f); void setAspectRatio(float a); void setNearClip(float n); @@ -130,6 +129,7 @@ public: public slots: QString getMode() const; void setMode(const QString& mode); + void setModeShiftPeriod(float r) {_camera->setModeShiftPeriod(r); } void setPosition(const glm::vec3& value) { _camera->setTargetPosition(value);} glm::vec3 getPosition() const { return _camera->getPosition(); } diff --git a/interface/src/DatagramProcessor.cpp b/interface/src/DatagramProcessor.cpp index 29528da126..a159af7be5 100644 --- a/interface/src/DatagramProcessor.cpp +++ b/interface/src/DatagramProcessor.cpp @@ -51,7 +51,10 @@ void DatagramProcessor::processDatagrams() { QMetaObject::invokeMethod(&application->_audio, "addReceivedAudioToBuffer", Qt::QueuedConnection, Q_ARG(QByteArray, incomingPacket)); break; - + case PacketTypeAudioStreamStats: + QMetaObject::invokeMethod(&application->_audio, "parseAudioStreamStatsPacket", Qt::QueuedConnection, + Q_ARG(QByteArray, incomingPacket)); + break; case PacketTypeParticleAddResponse: // this will keep creatorTokenIDs to IDs mapped correctly Particle::handleAddParticleResponse(incomingPacket); diff --git a/interface/src/GLCanvas.cpp b/interface/src/GLCanvas.cpp index 49085e63df..17026b5d5c 100644 --- a/interface/src/GLCanvas.cpp +++ b/interface/src/GLCanvas.cpp @@ -12,6 +12,7 @@ #include "Application.h" #include "GLCanvas.h" +#include "devices/OculusManager.h" #include <QMimeData> #include <QUrl> #include <QMainWindow> @@ -41,8 +42,17 @@ void GLCanvas::initializeGL() { void GLCanvas::paintGL() { if (!_throttleRendering && !Application::getInstance()->getWindow()->isMinimized()) { + //Need accurate frame timing for the oculus rift + if (OculusManager::isConnected()) { + OculusManager::beginFrameTiming(); + } + Application::getInstance()->paintGL(); swapBuffers(); + + if (OculusManager::isConnected()) { + OculusManager::endFrameTiming(); + } } } @@ -102,8 +112,17 @@ void GLCanvas::activeChanged(Qt::ApplicationState state) { void GLCanvas::throttleRender() { _frameTimer.start(_idleRenderInterval); if (!Application::getInstance()->getWindow()->isMinimized()) { + //Need accurate frame timing for the oculus rift + if (OculusManager::isConnected()) { + OculusManager::beginFrameTiming(); + } + Application::getInstance()->paintGL(); swapBuffers(); + + if (OculusManager::isConnected()) { + OculusManager::endFrameTiming(); + } } } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 5c8c2e97aa..402347c5d4 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -109,7 +109,8 @@ Menu::Menu() : _loginAction(NULL), _preferencesDialog(NULL), _loginDialog(NULL), - _snapshotsLocation() + _snapshotsLocation(), + _scriptsLocation() { Application *appInstance = Application::getInstance(); @@ -249,12 +250,21 @@ Menu::Menu() : QMenu* viewMenu = addMenu("View"); +#ifdef Q_OS_MAC addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Fullscreen, Qt::CTRL | Qt::META | Qt::Key_F, false, appInstance, SLOT(setFullscreen(bool))); +#else + addCheckableActionToQMenuAndActionHash(viewMenu, + MenuOption::Fullscreen, + Qt::CTRL | Qt::Key_F, + false, + appInstance, + SLOT(setFullscreen(bool))); +#endif addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::FirstPerson, Qt::Key_P, true, appInstance,SLOT(cameraMenuChanged())); addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Mirror, Qt::SHIFT | Qt::Key_H, true); @@ -322,6 +332,8 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Stars, Qt::Key_Asterisk, true); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Atmosphere, Qt::SHIFT | Qt::Key_A, true); + + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::EnableGlowEffect, 0, true); addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::GlowMode, 0, @@ -336,6 +348,7 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, false); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::StringHair, 0, false); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true); addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, Qt::SHIFT | Qt::Key_L, this, SLOT(lodTools())); @@ -366,7 +379,7 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::RenderSkeletonCollisionShapes); addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::RenderHeadCollisionShapes); addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::RenderBoundingCollisionShapes); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::CollideAsRagDoll); + addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::CollideAsRagdoll); addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::LookAtVectors, 0, false); addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, @@ -389,7 +402,6 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::ChatCircling, 0, false); QMenu* oculusOptionsMenu = developerMenu->addMenu("Oculus Options"); - addCheckableActionToQMenuAndActionHash(oculusOptionsMenu, MenuOption::AllowOculusCameraModeChange, 0, false); addCheckableActionToQMenuAndActionHash(oculusOptionsMenu, MenuOption::DisplayOculusOverlays, 0, true); QMenu* sixenseOptionsMenu = developerMenu->addMenu("Sixense Options"); @@ -598,6 +610,7 @@ void Menu::loadSettings(QSettings* settings) { _boundaryLevelAdjust = loadSetting(settings, "boundaryLevelAdjust", 0); _snapshotsLocation = settings->value("snapshotsLocation", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)).toString(); + setScriptsLocation(settings->value("scriptsLocation", QString()).toString()); settings->beginGroup("View Frustum Offset Camera"); // in case settings is corrupt or missing loadSetting() will check for NaN @@ -642,6 +655,7 @@ void Menu::saveSettings(QSettings* settings) { settings->setValue("avatarLODDistanceMultiplier", _avatarLODDistanceMultiplier); settings->setValue("boundaryLevelAdjust", _boundaryLevelAdjust); settings->setValue("snapshotsLocation", _snapshotsLocation); + settings->setValue("scriptsLocation", _scriptsLocation); settings->beginGroup("View Frustum Offset Camera"); settings->setValue("viewFrustumOffsetYaw", _viewFrustumOffset.yaw); settings->setValue("viewFrustumOffsetPitch", _viewFrustumOffset.pitch); @@ -1293,6 +1307,7 @@ void Menu::showChat() { if (_chatWindow->isHidden()) { _chatWindow->show(); } + _chatWindow->activateWindow(); } else { Application::getInstance()->getTrayIcon()->showMessage("Interface", "You need to login to be able to chat with others on this domain."); } @@ -1776,3 +1791,8 @@ QString Menu::getSnapshotsLocation() const { } return _snapshotsLocation; } + +void Menu::setScriptsLocation(const QString& scriptsLocation) { + _scriptsLocation = scriptsLocation; + emit scriptLocationChanged(scriptsLocation); +} diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 4d2174a448..a15d3712f1 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -102,6 +102,9 @@ public: QString getSnapshotsLocation() const; void setSnapshotsLocation(QString snapshotsLocation) { _snapshotsLocation = snapshotsLocation; } + const QString& getScriptsLocation() const { return _scriptsLocation; } + void setScriptsLocation(const QString& scriptsLocation); + BandwidthDialog* getBandwidthDialog() const { return _bandwidthDialog; } FrustumDrawMode getFrustumDrawMode() const { return _frustumDrawMode; } ViewFrustumOffset getViewFrustumOffset() const { return _viewFrustumOffset; } @@ -156,6 +159,9 @@ public: void static goToDomain(const QString newDomain); void static goTo(QString destination); +signals: + void scriptLocationChanged(const QString& newPath); + public slots: void loginForCurrentDomain(); @@ -283,12 +289,12 @@ private: QPointer<LoginDialog> _loginDialog; QAction* _chatAction; QString _snapshotsLocation; + QString _scriptsLocation; }; namespace MenuOption { const QString AboutApp = "About Interface"; const QString AlignForearmsWithWrists = "Align Forearms with Wrists"; - const QString AllowOculusCameraModeChange = "Allow Oculus Camera Mode Change (Nausea)"; const QString AlternateIK = "Alternate IK"; const QString AmbientOcclusion = "Ambient Occlusion"; const QString Animations = "Animations..."; @@ -319,10 +325,11 @@ namespace MenuOption { const QString Bandwidth = "Bandwidth Display"; const QString BandwidthDetails = "Bandwidth Details"; const QString BuckyBalls = "Bucky Balls"; + const QString StringHair = "String Hair"; const QString CascadedShadows = "Cascaded"; const QString Chat = "Chat..."; const QString ChatCircling = "Chat Circling"; - const QString CollideAsRagDoll = "Collide As RagDoll"; + const QString CollideAsRagdoll = "Collide As Ragdoll"; const QString CollideWithAvatars = "Collide With Avatars"; const QString CollideWithEnvironment = "Collide With World Boundaries"; const QString CollideWithParticles = "Collide With Particles"; @@ -344,6 +351,7 @@ namespace MenuOption { const QString DontFadeOnVoxelServerChanges = "Don't Fade In/Out on Voxel Server Changes"; const QString EchoLocalAudio = "Echo Local Audio"; const QString EchoServerAudio = "Echo Server Audio"; + const QString EnableGlowEffect = "Enable Glow Effect (Warning: Poor Oculus Performance)"; const QString Enable3DTVMode = "Enable 3DTV Mode"; const QString EnableVRMode = "Enable VR Mode"; const QString ExpandMiscAvatarTiming = "Expand Misc MyAvatar Timing"; diff --git a/interface/src/MetavoxelSystem.cpp b/interface/src/MetavoxelSystem.cpp index 78c97c1703..7a5119a62d 100644 --- a/interface/src/MetavoxelSystem.cpp +++ b/interface/src/MetavoxelSystem.cpp @@ -35,6 +35,8 @@ MetavoxelSystem::MetavoxelSystem() : } void MetavoxelSystem::init() { + MetavoxelClientManager::init(); + if (!_program.isLinked()) { _program.addShaderFromSourceFile(QGLShader::Vertex, Application::resourcesPath() + "shaders/metavoxel_point.vert"); _program.link(); @@ -43,62 +45,19 @@ void MetavoxelSystem::init() { } _buffer.setUsagePattern(QOpenGLBuffer::DynamicDraw); _buffer.create(); - - connect(NodeList::getInstance(), SIGNAL(nodeAdded(SharedNodePointer)), SLOT(maybeAttachClient(const SharedNodePointer&))); } -SharedObjectPointer MetavoxelSystem::findFirstRaySpannerIntersection( - const glm::vec3& origin, const glm::vec3& direction, const AttributePointer& attribute, float& distance) { - SharedObjectPointer closestSpanner; - float closestDistance = FLT_MAX; - foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { - if (node->getType() == NodeType::MetavoxelServer) { - QMutexLocker locker(&node->getMutex()); - MetavoxelClient* client = static_cast<MetavoxelClient*>(node->getLinkedData()); - if (client) { - float clientDistance; - SharedObjectPointer clientSpanner = client->getData().findFirstRaySpannerIntersection( - origin, direction, attribute, clientDistance); - if (clientSpanner && clientDistance < closestDistance) { - closestSpanner = clientSpanner; - closestDistance = clientDistance; - } - } - } - } - if (closestSpanner) { - distance = closestDistance; - } - return closestSpanner; -} - -void MetavoxelSystem::applyEdit(const MetavoxelEditMessage& edit, bool reliable) { - foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { - if (node->getType() == NodeType::MetavoxelServer) { - QMutexLocker locker(&node->getMutex()); - MetavoxelClient* client = static_cast<MetavoxelClient*>(node->getLinkedData()); - if (client) { - client->applyEdit(edit, reliable); - } - } - } +MetavoxelLOD MetavoxelSystem::getLOD() const { + const float FIXED_LOD_THRESHOLD = 0.01f; + return MetavoxelLOD(Application::getInstance()->getCamera()->getPosition(), FIXED_LOD_THRESHOLD); } void MetavoxelSystem::simulate(float deltaTime) { - // simulate the clients + // update the clients _points.clear(); _simulateVisitor.setDeltaTime(deltaTime); _simulateVisitor.setOrder(-Application::getInstance()->getViewFrustum()->getDirection()); - foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { - if (node->getType() == NodeType::MetavoxelServer) { - QMutexLocker locker(&node->getMutex()); - MetavoxelClient* client = static_cast<MetavoxelClient*>(node->getLinkedData()); - if (client) { - client->simulate(deltaTime); - client->guide(_simulateVisitor); - } - } - } + update(); _buffer.bind(); int bytes = _points.size() * sizeof(Point); @@ -153,7 +112,7 @@ void MetavoxelSystem::render() { foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { if (node->getType() == NodeType::MetavoxelServer) { QMutexLocker locker(&node->getMutex()); - MetavoxelClient* client = static_cast<MetavoxelClient*>(node->getLinkedData()); + MetavoxelSystemClient* client = static_cast<MetavoxelSystemClient*>(node->getLinkedData()); if (client) { client->guide(_renderVisitor); } @@ -161,11 +120,13 @@ void MetavoxelSystem::render() { } } -void MetavoxelSystem::maybeAttachClient(const SharedNodePointer& node) { - if (node->getType() == NodeType::MetavoxelServer) { - QMutexLocker locker(&node->getMutex()); - node->setLinkedData(new MetavoxelClient(NodeList::getInstance()->nodeWithUUID(node->getUUID()))); - } +MetavoxelClient* MetavoxelSystem::createClient(const SharedNodePointer& node) { + return new MetavoxelSystemClient(node, this); +} + +void MetavoxelSystem::updateClient(MetavoxelClient* client) { + MetavoxelClientManager::updateClient(client); + client->guide(_simulateVisitor); } MetavoxelSystem::SimulateVisitor::SimulateVisitor(QVector<Point>& points) : @@ -235,115 +196,22 @@ bool MetavoxelSystem::RenderVisitor::visit(Spanner* spanner, const glm::vec3& cl return true; } -MetavoxelClient::MetavoxelClient(const SharedNodePointer& node) : - _node(node), - _sequencer(byteArrayWithPopulatedHeader(PacketTypeMetavoxelData)) { - - connect(&_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendData(const QByteArray&))); - connect(&_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readPacket(Bitstream&))); - connect(&_sequencer, SIGNAL(sendAcknowledged(int)), SLOT(clearSendRecordsBefore(int))); - connect(&_sequencer, SIGNAL(receiveAcknowledged(int)), SLOT(clearReceiveRecordsBefore(int))); - - // insert the baseline send record - SendRecord sendRecord = { 0 }; - _sendRecords.append(sendRecord); - - // insert the baseline receive record - ReceiveRecord receiveRecord = { 0, _data }; - _receiveRecords.append(receiveRecord); +MetavoxelSystemClient::MetavoxelSystemClient(const SharedNodePointer& node, MetavoxelSystem* system) : + MetavoxelClient(node, system) { } -MetavoxelClient::~MetavoxelClient() { - // close the session - Bitstream& out = _sequencer.startPacket(); - out << QVariant::fromValue(CloseSessionMessage()); - _sequencer.endPacket(); -} - -static MetavoxelLOD getLOD() { - const float FIXED_LOD_THRESHOLD = 0.01f; - return MetavoxelLOD(Application::getInstance()->getCamera()->getPosition(), FIXED_LOD_THRESHOLD); -} - -void MetavoxelClient::guide(MetavoxelVisitor& visitor) { - visitor.setLOD(getLOD()); - _data.guide(visitor); -} - -void MetavoxelClient::applyEdit(const MetavoxelEditMessage& edit, bool reliable) { - if (reliable) { - _sequencer.getReliableOutputChannel()->sendMessage(QVariant::fromValue(edit)); - - } else { - // apply immediately to local tree - edit.apply(_data, _sequencer.getWeakSharedObjectHash()); - - // start sending it out - _sequencer.sendHighPriorityMessage(QVariant::fromValue(edit)); - } -} - -void MetavoxelClient::simulate(float deltaTime) { - Bitstream& out = _sequencer.startPacket(); - - ClientStateMessage state = { getLOD() }; - out << QVariant::fromValue(state); - _sequencer.endPacket(); - - // record the send - SendRecord record = { _sequencer.getOutgoingPacketNumber(), state.lod }; - _sendRecords.append(record); -} - -int MetavoxelClient::parseData(const QByteArray& packet) { +int MetavoxelSystemClient::parseData(const QByteArray& packet) { // process through sequencer QMetaObject::invokeMethod(&_sequencer, "receivedDatagram", Q_ARG(const QByteArray&, packet)); Application::getInstance()->getBandwidthMeter()->inputStream(BandwidthMeter::METAVOXELS).updateValue(packet.size()); return packet.size(); } -void MetavoxelClient::sendData(const QByteArray& data) { +void MetavoxelSystemClient::sendDatagram(const QByteArray& data) { NodeList::getInstance()->writeDatagram(data, _node); Application::getInstance()->getBandwidthMeter()->outputStream(BandwidthMeter::METAVOXELS).updateValue(data.size()); } -void MetavoxelClient::readPacket(Bitstream& in) { - QVariant message; - in >> message; - handleMessage(message, in); - - // record the receipt - ReceiveRecord record = { _sequencer.getIncomingPacketNumber(), _data, _sendRecords.first().lod }; - _receiveRecords.append(record); - - // reapply local edits - foreach (const DatagramSequencer::HighPriorityMessage& message, _sequencer.getHighPriorityMessages()) { - if (message.data.userType() == MetavoxelEditMessage::Type) { - message.data.value<MetavoxelEditMessage>().apply(_data, _sequencer.getWeakSharedObjectHash()); - } - } -} - -void MetavoxelClient::clearSendRecordsBefore(int index) { - _sendRecords.erase(_sendRecords.begin(), _sendRecords.begin() + index + 1); -} - -void MetavoxelClient::clearReceiveRecordsBefore(int index) { - _receiveRecords.erase(_receiveRecords.begin(), _receiveRecords.begin() + index + 1); -} - -void MetavoxelClient::handleMessage(const QVariant& message, Bitstream& in) { - int userType = message.userType(); - if (userType == MetavoxelDeltaMessage::Type) { - _data.readDelta(_receiveRecords.first().data, _receiveRecords.first().lod, in, _sendRecords.first().lod); - - } else if (userType == QMetaType::QVariantList) { - foreach (const QVariant& element, message.toList()) { - handleMessage(element, in); - } - } -} - static void enableClipPlane(GLenum plane, float x, float y, float z, float w) { GLdouble coefficients[] = { x, y, z, w }; glClipPlane(plane, coefficients); diff --git a/interface/src/MetavoxelSystem.h b/interface/src/MetavoxelSystem.h index f98a260ab1..f3a4fd4412 100644 --- a/interface/src/MetavoxelSystem.h +++ b/interface/src/MetavoxelSystem.h @@ -18,37 +18,31 @@ #include <glm/glm.hpp> -#include <NodeList.h> - -#include <DatagramSequencer.h> -#include <MetavoxelData.h> -#include <MetavoxelMessages.h> +#include <MetavoxelClientManager.h> #include "renderer/ProgramObject.h" class Model; /// Renders a metavoxel tree. -class MetavoxelSystem : public QObject { +class MetavoxelSystem : public MetavoxelClientManager { Q_OBJECT public: MetavoxelSystem(); - void init(); - - SharedObjectPointer findFirstRaySpannerIntersection(const glm::vec3& origin, const glm::vec3& direction, - const AttributePointer& attribute, float& distance); - - Q_INVOKABLE void applyEdit(const MetavoxelEditMessage& edit, bool reliable = false); + virtual void init(); + + virtual MetavoxelLOD getLOD() const; void simulate(float deltaTime); void render(); - -private slots: - void maybeAttachClient(const SharedNodePointer& node); +protected: + + virtual MetavoxelClient* createClient(const SharedNodePointer& node); + virtual void updateClient(MetavoxelClient* client); private: @@ -89,59 +83,18 @@ private: }; /// A client session associated with a single server. -class MetavoxelClient : public NodeData { +class MetavoxelSystemClient : public MetavoxelClient { Q_OBJECT public: - MetavoxelClient(const SharedNodePointer& node); - virtual ~MetavoxelClient(); - - MetavoxelData& getData() { return _data; } - - void guide(MetavoxelVisitor& visitor); - - void applyEdit(const MetavoxelEditMessage& edit, bool reliable = false); - - void simulate(float deltaTime); - + MetavoxelSystemClient(const SharedNodePointer& node, MetavoxelSystem* system); + virtual int parseData(const QByteArray& packet); -private slots: +protected: - void sendData(const QByteArray& data); - - void readPacket(Bitstream& in); - - void clearSendRecordsBefore(int index); - - void clearReceiveRecordsBefore(int index); - -private: - - void handleMessage(const QVariant& message, Bitstream& in); - - class SendRecord { - public: - int packetNumber; - MetavoxelLOD lod; - }; - - class ReceiveRecord { - public: - int packetNumber; - MetavoxelData data; - MetavoxelLOD lod; - }; - - SharedNodePointer _node; - - DatagramSequencer _sequencer; - - MetavoxelData _data; - - QList<SendRecord> _sendRecords; - QList<ReceiveRecord> _receiveRecords; + virtual void sendDatagram(const QByteArray& data); }; /// Base class for spanner renderers; provides clipping. diff --git a/interface/src/ScriptsModel.cpp b/interface/src/ScriptsModel.cpp new file mode 100644 index 0000000000..f9ed94f3fa --- /dev/null +++ b/interface/src/ScriptsModel.cpp @@ -0,0 +1,209 @@ +// +// ScriptsModel.cpp +// interface/src +// +// Created by Ryan Huffman on 05/12/14. +// Copyright 2014 High Fidelity, Inc. +// +// S3 request code written with ModelBrowser as a reference. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include <QNetworkAccessManager> +#include <QUrl> +#include <QUrlQuery> +#include <QXmlStreamReader> + +#include "ScriptsModel.h" +#include "Menu.h" + + +static const QString S3_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com"; +static const QString PUBLIC_URL = "http://public.highfidelity.io"; +static const QString MODELS_LOCATION = "scripts/"; + +static const QString PREFIX_PARAMETER_NAME = "prefix"; +static const QString MARKER_PARAMETER_NAME = "marker"; +static const QString IS_TRUNCATED_NAME = "IsTruncated"; +static const QString CONTAINER_NAME = "Contents"; +static const QString KEY_NAME = "Key"; + +static const int SCRIPT_PATH = Qt::UserRole; + +ScriptItem::ScriptItem(const QString& filename, const QString& fullPath) : + _filename(filename), + _fullPath(fullPath) { +}; + +ScriptsModel::ScriptsModel(QObject* parent) : + QAbstractListModel(parent), + _loadingScripts(false), + _localDirectory(), + _fsWatcher(), + _localFiles(), + _remoteFiles() { + + QString scriptPath = Menu::getInstance()->getScriptsLocation(); + + _localDirectory.setPath(scriptPath); + _localDirectory.setFilter(QDir::Files | QDir::Readable); + _localDirectory.setNameFilters(QStringList("*.js")); + + _fsWatcher.addPath(_localDirectory.absolutePath()); + connect(&_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &ScriptsModel::reloadLocalFiles); + + connect(Menu::getInstance(), &Menu::scriptLocationChanged, this, &ScriptsModel::updateScriptsLocation); + + reloadLocalFiles(); + reloadRemoteFiles(); +} + +QVariant ScriptsModel::data(const QModelIndex& index, int role) const { + const QList<ScriptItem*>* files = NULL; + int row = 0; + bool isLocal = index.row() < _localFiles.size(); + if (isLocal) { + files = &_localFiles; + row = index.row(); + } else { + files = &_remoteFiles; + row = index.row() - _localFiles.size(); + } + + if (role == Qt::DisplayRole) { + return QVariant((*files)[row]->getFilename() + (isLocal ? " (local)" : "")); + } else if (role == ScriptPath) { + return QVariant((*files)[row]->getFullPath()); + } + return QVariant(); +} + +int ScriptsModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + return _localFiles.length() + _remoteFiles.length(); +} + +void ScriptsModel::updateScriptsLocation(const QString& newPath) { + _fsWatcher.removePath(_localDirectory.absolutePath()); + _localDirectory.setPath(newPath); + _fsWatcher.addPath(_localDirectory.absolutePath()); + reloadLocalFiles(); +} + +void ScriptsModel::reloadRemoteFiles() { + if (!_loadingScripts) { + _loadingScripts = true; + while (!_remoteFiles.isEmpty()) { + delete _remoteFiles.takeFirst(); + } + requestRemoteFiles(); + } +} + +void ScriptsModel::requestRemoteFiles(QString marker) { + QUrl url(S3_URL); + QUrlQuery query; + query.addQueryItem(PREFIX_PARAMETER_NAME, MODELS_LOCATION); + if (!marker.isEmpty()) { + query.addQueryItem(MARKER_PARAMETER_NAME, marker); + } + url.setQuery(query); + + QNetworkAccessManager* accessManager = new QNetworkAccessManager(this); + connect(accessManager, SIGNAL(finished(QNetworkReply*)), SLOT(downloadFinished(QNetworkReply*))); + + QNetworkRequest request(url); + accessManager->get(request); +} + +void ScriptsModel::downloadFinished(QNetworkReply* reply) { + bool finished = true; + + if (reply->error() == QNetworkReply::NoError) { + QByteArray data = reply->readAll(); + + if (!data.isEmpty()) { + finished = parseXML(data); + } else { + qDebug() << "Error: Received no data when loading remote scripts"; + } + } + + reply->deleteLater(); + sender()->deleteLater(); + + if (finished) { + _loadingScripts = false; + } +} + +bool ScriptsModel::parseXML(QByteArray xmlFile) { + beginResetModel(); + + QXmlStreamReader xml(xmlFile); + QRegExp jsRegex(".*\\.js"); + bool truncated = false; + QString lastKey; + + while (!xml.atEnd() && !xml.hasError()) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == IS_TRUNCATED_NAME) { + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == IS_TRUNCATED_NAME)) { + xml.readNext(); + if (xml.text().toString() == "true") { + truncated = true; + } + } + } + + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == CONTAINER_NAME) { + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == CONTAINER_NAME)) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == KEY_NAME) { + xml.readNext(); + lastKey = xml.text().toString(); + if (jsRegex.exactMatch(xml.text().toString())) { + _remoteFiles.append(new ScriptItem(lastKey.mid(MODELS_LOCATION.length()), S3_URL + "/" + lastKey)); + } + } + xml.readNext(); + } + } + xml.readNext(); + } + + endResetModel(); + + // Error handling + if (xml.hasError()) { + qDebug() << "Error loading remote scripts: " << xml.errorString(); + return true; + } + + if (truncated) { + requestRemoteFiles(lastKey); + } + + // If this request was not truncated, we are done. + return !truncated; +} + +void ScriptsModel::reloadLocalFiles() { + beginResetModel(); + + while (!_localFiles.isEmpty()) { + delete _localFiles.takeFirst(); + } + + _localDirectory.refresh(); + + const QFileInfoList localFiles = _localDirectory.entryInfoList(); + for (int i = 0; i < localFiles.size(); i++) { + QFileInfo file = localFiles[i]; + _localFiles.append(new ScriptItem(file.fileName(), file.absoluteFilePath())); + } + + endResetModel(); +} diff --git a/interface/src/ScriptsModel.h b/interface/src/ScriptsModel.h new file mode 100644 index 0000000000..250c7eb9a8 --- /dev/null +++ b/interface/src/ScriptsModel.h @@ -0,0 +1,62 @@ +// +// ScriptsModel.h +// interface/src +// +// Created by Ryan Huffman on 05/12/14. +// Copyright 2014 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_ScriptsModel_h +#define hifi_ScriptsModel_h + +#include <QAbstractListModel> +#include <QDir> +#include <QNetworkReply> +#include <QFileSystemWatcher> + +class ScriptItem { +public: + ScriptItem(const QString& filename, const QString& fullPath); + + const QString& getFilename() { return _filename; }; + const QString& getFullPath() { return _fullPath; }; + +private: + QString _filename; + QString _fullPath; +}; + +class ScriptsModel : public QAbstractListModel { + Q_OBJECT +public: + ScriptsModel(QObject* parent = NULL); + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const; + + enum Role { + ScriptPath = Qt::UserRole, + }; + +protected slots: + void updateScriptsLocation(const QString& newPath); + void downloadFinished(QNetworkReply* reply); + void reloadLocalFiles(); + void reloadRemoteFiles(); + +protected: + void requestRemoteFiles(QString marker = QString()); + bool parseXML(QByteArray xmlFile); + +private: + bool _loadingScripts; + QDir _localDirectory; + QFileSystemWatcher _fsWatcher; + QList<ScriptItem*> _localFiles; + QList<ScriptItem*> _remoteFiles; +}; + +#endif // hifi_ScriptsModel_h diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index 5ce1435bd6..4dbb015459 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -68,6 +68,7 @@ void printVector(glm::vec3 vec) { qDebug("%4.2f, %4.2f, %4.2f", vec.x, vec.y, vec.z); } + // Return the azimuth angle (in radians) between two points. float azimuth_to(glm::vec3 head_pos, glm::vec3 source_pos) { return atan2(head_pos.x - source_pos.x, head_pos.z - source_pos.z); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index baf46605fd..9b136980f4 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -48,6 +48,10 @@ Avatar::Avatar() : _skeletonModel(this), _bodyYawDelta(0.0f), _velocity(0.0f, 0.0f, 0.0f), + _lastVelocity(0.0f, 0.0f, 0.0f), + _acceleration(0.0f, 0.0f, 0.0f), + _angularVelocity(0.0f, 0.0f, 0.0f), + _lastOrientation(), _leanScale(0.5f), _scale(1.0f), _worldUpDirection(DEFAULT_UP_DIRECTION), @@ -76,6 +80,7 @@ void Avatar::init() { _skeletonModel.init(); _initialized = true; _shouldRenderBillboard = (getLODDistance() >= BILLBOARD_LOD_DISTANCE); + initializeHair(); } glm::vec3 Avatar::getChestPosition() const { @@ -134,10 +139,15 @@ void Avatar::simulate(float deltaTime) { head->setPosition(headPosition); head->setScale(_scale); head->simulate(deltaTime, false, _shouldRenderBillboard); + + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + simulateHair(deltaTime); + } } // update position by velocity, and subtract the change added earlier for gravity _position += _velocity * deltaTime; + updateAcceleration(deltaTime); // update animation for display name fade in/out if ( _displayNameTargetAlpha != _displayNameAlpha) { @@ -157,6 +167,17 @@ void Avatar::simulate(float deltaTime) { } } +void Avatar::updateAcceleration(float deltaTime) { + // Linear Component of Acceleration + _acceleration = (_velocity - _lastVelocity) * (1.f / deltaTime); + _lastVelocity = _velocity; + // Angular Component of Acceleration + glm::quat orientation = getOrientation(); + glm::quat delta = glm::inverse(_lastOrientation) * orientation; + _angularVelocity = safeEulerAngles(delta) * (1.f / deltaTime); + _lastOrientation = getOrientation(); +} + void Avatar::setMouseRay(const glm::vec3 &origin, const glm::vec3 &direction) { _mouseRayOrigin = origin; _mouseRayDirection = direction; @@ -218,9 +239,6 @@ void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { bool renderSkeleton = Menu::getInstance()->isOptionChecked(MenuOption::RenderSkeletonCollisionShapes); bool renderHead = Menu::getInstance()->isOptionChecked(MenuOption::RenderHeadCollisionShapes); bool renderBounding = Menu::getInstance()->isOptionChecked(MenuOption::RenderBoundingCollisionShapes); - if (renderSkeleton || renderHead || renderBounding) { - updateShapePositions(); - } if (renderSkeleton) { _skeletonModel.renderJointCollisionShapes(0.7f); @@ -230,7 +248,6 @@ void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { getHead()->getFaceModel().renderJointCollisionShapes(0.7f); } if (renderBounding && shouldRenderHead(cameraPosition, renderMode)) { - getHead()->getFaceModel().renderBoundingCollisionShapes(0.7f); _skeletonModel.renderBoundingCollisionShapes(0.7f); } @@ -361,6 +378,232 @@ void Avatar::renderBody(RenderMode renderMode, float glowLevel) { getHand()->render(false, modelRenderMode); } getHead()->render(1.0f, modelRenderMode); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + renderHair(); + } +} + +// +// Constants for the Hair Simulation +// + +const float HAIR_LENGTH = 0.2f; +const float HAIR_LINK_LENGTH = HAIR_LENGTH / HAIR_LINKS; +const float HAIR_DAMPING = 0.99f; +const float HEAD_RADIUS = 0.21f; +const float CONSTRAINT_RELAXATION = 10.0f; +const glm::vec3 HAIR_GRAVITY(0.0f, -0.007f, 0.0f); +const float HAIR_ACCELERATION_COUPLING = 0.025f; +const float HAIR_ANGULAR_VELOCITY_COUPLING = 0.10f; +const float HAIR_MAX_LINEAR_ACCELERATION = 4.0f; +const float HAIR_THICKNESS = 0.015f; +const float HAIR_STIFFNESS = 0.0000f; +const glm::vec3 HAIR_COLOR1(0.98f, 0.92f, 0.843f); +const glm::vec3 HAIR_COLOR2(0.545f, 0.533f, 0.47f); +const glm::vec3 WIND_DIRECTION(0.5f, -1.0f, 0.0f); +const float MAX_WIND_STRENGTH = 0.02f; +const float FINGER_LENGTH = 0.25f; +const float FINGER_RADIUS = 0.10f; + +void Avatar::renderHair() { + // + // Render the avatar's moveable hair + // + + glm::vec3 headPosition = getHead()->getPosition(); + glPushMatrix(); + glTranslatef(headPosition.x, headPosition.y, headPosition.z); + const glm::quat& rotation = getHead()->getFinalOrientationInWorldFrame(); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); + + glBegin(GL_QUADS); + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + for (int link = 0; link < HAIR_LINKS - 1; link++) { + int vertexIndex = strand * HAIR_LINKS + link; + glColor3fv(&_hairColors[vertexIndex].x); + glNormal3fv(&_hairNormals[vertexIndex].x); + glVertex3f(_hairPosition[vertexIndex].x - _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex].y - _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex].z - _hairQuadDelta[vertexIndex].z); + glVertex3f(_hairPosition[vertexIndex].x + _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex].y + _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex].z + _hairQuadDelta[vertexIndex].z); + + glVertex3f(_hairPosition[vertexIndex + 1].x + _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex + 1].y + _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex + 1].z + _hairQuadDelta[vertexIndex].z); + glVertex3f(_hairPosition[vertexIndex + 1].x - _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex + 1].y - _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex + 1].z - _hairQuadDelta[vertexIndex].z); + } + } + glEnd(); + + glPopMatrix(); + +} + +void Avatar::simulateHair(float deltaTime) { + + deltaTime = glm::clamp(deltaTime, 0.0f, 1.0f / 30.0f); + glm::vec3 acceleration = getAcceleration(); + if (glm::length(acceleration) > HAIR_MAX_LINEAR_ACCELERATION) { + acceleration = glm::normalize(acceleration) * HAIR_MAX_LINEAR_ACCELERATION; + } + const glm::quat& rotation = getHead()->getFinalOrientationInWorldFrame(); + acceleration = acceleration * rotation; + glm::vec3 angularVelocity = getAngularVelocity() + getHead()->getAngularVelocity(); + + // Get hand positions to allow touching hair + glm::vec3 leftHandPosition, rightHandPosition; + getSkeletonModel().getLeftHandPosition(leftHandPosition); + getSkeletonModel().getRightHandPosition(rightHandPosition); + leftHandPosition -= getHead()->getPosition(); + rightHandPosition -= getHead()->getPosition(); + glm::quat leftRotation, rightRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftRotation); + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightRotation); + leftHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.0f) * glm::inverse(leftRotation); + rightHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.0f) * glm::inverse(rightRotation); + leftHandPosition = leftHandPosition * rotation; + rightHandPosition = rightHandPosition * rotation; + + float windIntensity = randFloat() * MAX_WIND_STRENGTH; + + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + for (int link = 0; link < HAIR_LINKS; link++) { + int vertexIndex = strand * HAIR_LINKS + link; + if (vertexIndex % HAIR_LINKS == 0) { + // Base Joint - no integration + } else { + // + // Vertlet Integration + // + // Add velocity from last position, with damping + glm::vec3 thisPosition = _hairPosition[vertexIndex]; + glm::vec3 diff = thisPosition - _hairLastPosition[vertexIndex]; + _hairPosition[vertexIndex] += diff * HAIR_DAMPING; + // Resolve collision with head sphere + if (glm::length(_hairPosition[vertexIndex]) < HEAD_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex]) * + (HEAD_RADIUS - glm::length(_hairPosition[vertexIndex])); + } + // Resolve collision with hands + if (glm::length(_hairPosition[vertexIndex] - leftHandPosition) < FINGER_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex] - leftHandPosition) * + (FINGER_RADIUS - glm::length(_hairPosition[vertexIndex] - leftHandPosition)); + } + if (glm::length(_hairPosition[vertexIndex] - rightHandPosition) < FINGER_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex] - rightHandPosition) * + (FINGER_RADIUS - glm::length(_hairPosition[vertexIndex] - rightHandPosition)); + } + + + // Add a little gravity + _hairPosition[vertexIndex] += HAIR_GRAVITY * rotation * deltaTime; + + // Add linear acceleration of the avatar body + _hairPosition[vertexIndex] -= acceleration * HAIR_ACCELERATION_COUPLING * deltaTime; + + // Add stiffness (like hair care products do) + _hairPosition[vertexIndex] += (_hairOriginalPosition[vertexIndex] - _hairPosition[vertexIndex]) + * powf(1.f - link / HAIR_LINKS, 2.f) * HAIR_STIFFNESS; + + // Add some wind + glm::vec3 wind = WIND_DIRECTION * windIntensity; + _hairPosition[vertexIndex] += wind * deltaTime; + + const float ANGULAR_VELOCITY_MIN = 0.001f; + // Add angular acceleration of the avatar body + if (glm::length(angularVelocity) > ANGULAR_VELOCITY_MIN) { + glm::vec3 yawVector = _hairPosition[vertexIndex]; + yawVector.y = 0.f; + if (glm::length(yawVector) > EPSILON) { + float radius = glm::length(yawVector); + yawVector = glm::normalize(yawVector); + float angle = atan2f(yawVector.x, -yawVector.z) + PI; + glm::vec3 delta = glm::vec3(-1.f, 0.f, 0.f) * glm::angleAxis(angle, glm::vec3(0, 1, 0)); + _hairPosition[vertexIndex] -= delta * radius * angularVelocity.y * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; + } + glm::vec3 pitchVector = _hairPosition[vertexIndex]; + pitchVector.x = 0.f; + if (glm::length(pitchVector) > EPSILON) { + float radius = glm::length(pitchVector); + pitchVector = glm::normalize(pitchVector); + float angle = atan2f(pitchVector.y, -pitchVector.z) + PI; + glm::vec3 delta = glm::vec3(0.0f, 1.0f, 0.f) * glm::angleAxis(angle, glm::vec3(1, 0, 0)); + _hairPosition[vertexIndex] -= delta * radius * angularVelocity.x * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; + } + glm::vec3 rollVector = _hairPosition[vertexIndex]; + rollVector.z = 0.f; + if (glm::length(rollVector) > EPSILON) { + float radius = glm::length(rollVector); + pitchVector = glm::normalize(rollVector); + float angle = atan2f(rollVector.x, rollVector.y) + PI; + glm::vec3 delta = glm::vec3(-1.0f, 0.0f, 0.f) * glm::angleAxis(angle, glm::vec3(0, 0, 1)); + _hairPosition[vertexIndex] -= delta * radius * angularVelocity.z * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; + } + } + + // Iterate length constraints to other links + for (int link = 0; link < HAIR_MAX_CONSTRAINTS; link++) { + if (_hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + link] > -1) { + // If there is a constraint, try to enforce it + glm::vec3 vectorBetween = _hairPosition[_hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + link]] - _hairPosition[vertexIndex]; + _hairPosition[vertexIndex] += glm::normalize(vectorBetween) * (glm::length(vectorBetween) - HAIR_LINK_LENGTH) * CONSTRAINT_RELAXATION * deltaTime; + } + } + // Store start position for next vertlet pass + _hairLastPosition[vertexIndex] = thisPosition; + } + } + } +} + +void Avatar::initializeHair() { + const float FACE_WIDTH = PI / 4.0f; + glm::vec3 thisVertex; + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + float strandAngle = randFloat() * PI; + float azimuth = FACE_WIDTH / 2.0f + (randFloat() * (2.0 * PI - FACE_WIDTH)); + float elevation = PI_OVER_TWO - (randFloat() * 0.75 * PI); + glm::vec3 thisStrand(sinf(azimuth) * cosf(elevation), sinf(elevation), -cosf(azimuth) * cosf(elevation)); + thisStrand *= HEAD_RADIUS; + + for (int link = 0; link < HAIR_LINKS; link++) { + int vertexIndex = strand * HAIR_LINKS + link; + // Clear constraints + for (int link2 = 0; link2 < HAIR_MAX_CONSTRAINTS; link2++) { + _hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + link2] = -1; + } + if (vertexIndex % HAIR_LINKS == 0) { + // start of strand + thisVertex = thisStrand; + } else { + thisVertex+= glm::normalize(thisStrand) * HAIR_LINK_LENGTH; + // Set constraints to vertex before and maybe vertex after in strand + _hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS] = vertexIndex - 1; + if (link < (HAIR_LINKS - 1)) { + _hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + 1] = vertexIndex + 1; + } + } + _hairPosition[vertexIndex] = thisVertex; + _hairLastPosition[vertexIndex] = _hairPosition[vertexIndex]; + _hairOriginalPosition[vertexIndex] = _hairPosition[vertexIndex]; + + _hairQuadDelta[vertexIndex] = glm::vec3(cos(strandAngle) * HAIR_THICKNESS, 0.f, sin(strandAngle) * HAIR_THICKNESS); + _hairQuadDelta[vertexIndex] *= 1.f - (link / HAIR_LINKS); + _hairNormals[vertexIndex] = glm::normalize(randVector()); + if (randFloat() < elevation / PI_OVER_TWO) { + _hairColors[vertexIndex] = HAIR_COLOR1 * ((float)(link + 1) / (float)HAIR_LINKS); + } else { + _hairColors[vertexIndex] = HAIR_COLOR2 * ((float)(link + 1) / (float)HAIR_LINKS); + } + + } + } + qDebug() << "Initialize Hair"; } bool Avatar::shouldRenderHead(const glm::vec3& cameraPosition, RenderMode renderMode) const { @@ -579,10 +822,9 @@ bool Avatar::findRayIntersection(const glm::vec3& origin, const glm::vec3& direc return false; } -bool Avatar::findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, - CollisionList& collisions, int skeletonSkipIndex) { - return _skeletonModel.findSphereCollisions(penetratorCenter, penetratorRadius, collisions, skeletonSkipIndex); - // Temporarily disabling collisions against the head because most of its collision proxies are bad. +bool Avatar::findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, CollisionList& collisions) { + return _skeletonModel.findSphereCollisions(penetratorCenter, penetratorRadius, collisions); + // TODO: Andrew to fix: Temporarily disabling collisions against the head //return getHead()->getFaceModel().findSphereCollisions(penetratorCenter, penetratorRadius, collisions); } @@ -591,18 +833,6 @@ bool Avatar::findPlaneCollisions(const glm::vec4& plane, CollisionList& collisio getHead()->getFaceModel().findPlaneCollisions(plane, collisions); } -void Avatar::updateShapePositions() { - _skeletonModel.updateShapePositions(); - Model& headModel = getHead()->getFaceModel(); - headModel.updateShapePositions(); - /* KEEP FOR DEBUG: use this in rather than code above to see shapes - * in their default positions where the bounding shape is computed. - _skeletonModel.resetShapePositions(); - Model& headModel = getHead()->getFaceModel(); - headModel.resetShapePositions(); - */ -} - bool Avatar::findCollisions(const QVector<const Shape*>& shapes, CollisionList& collisions) { // TODO: Andrew to fix: also collide against _skeleton //bool collided = _skeletonModel.findCollisions(shapes, collisions); @@ -613,69 +843,6 @@ bool Avatar::findCollisions(const QVector<const Shape*>& shapes, CollisionList& return collided; } -bool Avatar::findParticleCollisions(const glm::vec3& particleCenter, float particleRadius, CollisionList& collisions) { - if (_collisionGroups & COLLISION_GROUP_PARTICLES) { - return false; - } - bool collided = false; - // first do the hand collisions - const HandData* handData = getHandData(); - if (handData) { - for (int i = 0; i < NUM_HANDS; i++) { - const PalmData* palm = handData->getPalm(i); - if (palm && palm->hasPaddle()) { - // create a disk collision proxy where the hand is - int jointIndex = -1; - glm::vec3 handPosition; - if (i == 0) { - _skeletonModel.getLeftHandPosition(handPosition); - jointIndex = _skeletonModel.getLeftHandJointIndex(); - } - else { - _skeletonModel.getRightHandPosition(handPosition); - jointIndex = _skeletonModel.getRightHandJointIndex(); - } - - glm::vec3 fingerAxis = palm->getFingerDirection(); - glm::vec3 diskCenter = handPosition + HAND_PADDLE_OFFSET * fingerAxis; - glm::vec3 diskNormal = palm->getNormal(); - const float DISK_THICKNESS = 0.08f; - - // collide against the disk - glm::vec3 penetration; - if (findSphereDiskPenetration(particleCenter, particleRadius, - diskCenter, HAND_PADDLE_RADIUS, DISK_THICKNESS, diskNormal, - penetration)) { - CollisionInfo* collision = collisions.getNewCollision(); - if (collision) { - collision->_type = COLLISION_TYPE_PADDLE_HAND; - collision->_intData = jointIndex; - collision->_penetration = penetration; - collision->_addedVelocity = palm->getVelocity(); - collided = true; - } else { - // collisions are full, so we might as well bail now - return collided; - } - } - } - } - } - // then collide against the models - int preNumCollisions = collisions.size(); - if (_skeletonModel.findSphereCollisions(particleCenter, particleRadius, collisions)) { - // the Model doesn't have velocity info, so we have to set it for each new collision - int postNumCollisions = collisions.size(); - for (int i = preNumCollisions; i < postNumCollisions; ++i) { - CollisionInfo* collision = collisions.getCollision(i); - collision->_penetration /= (float)(TREE_SCALE); - collision->_addedVelocity = getVelocity(); - } - collided = true; - } - return collided; -} - glm::quat Avatar::getJointRotation(int index) const { if (QThread::currentThread() != thread()) { return AvatarData::getJointRotation(index); @@ -909,25 +1076,6 @@ float Avatar::getHeadHeight() const { return DEFAULT_HEAD_HEIGHT; } -bool Avatar::collisionWouldMoveAvatar(CollisionInfo& collision) const { - if (!collision._data || collision._type != COLLISION_TYPE_MODEL) { - return false; - } - Model* model = static_cast<Model*>(collision._data); - int jointIndex = collision._intData; - - if (model == &(_skeletonModel) && jointIndex != -1) { - // collision response of skeleton is temporarily disabled - return false; - //return _skeletonModel.collisionHitsMoveableJoint(collision); - } - if (model == &(getHead()->getFaceModel())) { - // ATM we always handle COLLISION_TYPE_MODEL against the face. - return true; - } - return false; -} - float Avatar::getBoundingRadius() const { // TODO: also use head model when computing the avatar's bounding radius return _skeletonModel.getBoundingRadius(); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index f928881068..f20db1019d 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -32,6 +32,10 @@ static const float RESCALING_TOLERANCE = .02f; extern const float CHAT_MESSAGE_SCALE; extern const float CHAT_MESSAGE_HEIGHT; +const int HAIR_STRANDS = 150; // Number of strands of hair +const int HAIR_LINKS = 10; // Number of links in a hair strand +const int HAIR_MAX_CONSTRAINTS = 2; // Hair verlet is connected to at most how many others + enum DriveKeys { FWD = 0, BACK, @@ -101,14 +105,12 @@ public: /// \return true if at least one shape collided with avatar bool findCollisions(const QVector<const Shape*>& shapes, CollisionList& collisions); - /// Checks for penetration between the described sphere and the avatar. + /// Checks for penetration between the a sphere and the avatar's models. /// \param penetratorCenter the center of the penetration test sphere /// \param penetratorRadius the radius of the penetration test sphere /// \param collisions[out] a list to which collisions get appended - /// \param skeletonSkipIndex if not -1, the index of a joint to skip (along with its descendents) in the skeleton model /// \return whether or not the sphere penetrated - bool findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, - CollisionList& collisions, int skeletonSkipIndex = -1); + bool findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, CollisionList& collisions); /// Checks for penetration between the described plane and the avatar. /// \param plane the penetration plane @@ -116,13 +118,6 @@ public: /// \return whether or not the plane penetrated bool findPlaneCollisions(const glm::vec4& plane, CollisionList& collisions); - /// Checks for collision between the a spherical particle and the avatar (including paddle hands) - /// \param collisionCenter the center of particle's bounding sphere - /// \param collisionRadius the radius of particle's bounding sphere - /// \param collisions[out] a list to which collisions get appended - /// \return whether or not the particle collided - bool findParticleCollisions(const glm::vec3& particleCenter, float particleRadius, CollisionList& collisions); - virtual bool isMyAvatar() { return false; } virtual glm::quat getJointRotation(int index) const; @@ -141,14 +136,10 @@ public: static void renderJointConnectingCone(glm::vec3 position1, glm::vec3 position2, float radius1, float radius2); - /// \return true if we expect the avatar would move as a result of the collision - bool collisionWouldMoveAvatar(CollisionInfo& collision) const; - virtual void applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration) { } /// \return bounding radius of avatar virtual float getBoundingRadius() const; - void updateShapePositions(); quint32 getCollisionGroups() const { return _collisionGroups; } virtual void setCollisionGroups(quint32 collisionGroups) { _collisionGroups = (collisionGroups & VALID_COLLISION_GROUPS); } @@ -158,6 +149,9 @@ public: Q_INVOKABLE glm::quat getJointCombinedRotation(int index) const; Q_INVOKABLE glm::quat getJointCombinedRotation(const QString& name) const; + glm::vec3 getAcceleration() const { return _acceleration; } + glm::vec3 getAngularVelocity() const { return _angularVelocity; } + public slots: void updateCollisionGroups(); @@ -169,6 +163,10 @@ protected: QVector<Model*> _attachmentModels; float _bodyYawDelta; glm::vec3 _velocity; + glm::vec3 _lastVelocity; + glm::vec3 _acceleration; + glm::vec3 _angularVelocity; + glm::quat _lastOrientation; float _leanScale; float _scale; glm::vec3 _worldUpDirection; @@ -185,6 +183,7 @@ protected: glm::vec3 getBodyFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } glm::quat computeRotationFromBodyToWorldUp(float proportion = 1.0f) const; void setScale(float scale); + void updateAcceleration(float deltaTime); float getSkeletonHeight() const; float getHeadHeight() const; @@ -200,6 +199,18 @@ protected: virtual void renderAttachments(RenderMode renderMode); virtual void updateJointMappings(); + + glm::vec3 _hairPosition[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairOriginalPosition[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairLastPosition[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairQuadDelta[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairNormals[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairColors[HAIR_STRANDS * HAIR_LINKS]; + int _hairIsMoveable[HAIR_STRANDS * HAIR_LINKS]; + int _hairConstraints[HAIR_STRANDS * HAIR_LINKS * 2]; // Hair can link to two others + void renderHair(); + void simulateHair(float deltaTime); + void initializeHair(); private: @@ -211,6 +222,7 @@ private: void renderBillboard(); float getBillboardSize() const; + }; #endif // hifi_Avatar_h diff --git a/interface/src/avatar/Hand.cpp b/interface/src/avatar/Hand.cpp index 3aff984893..94f734ba06 100644 --- a/interface/src/avatar/Hand.cpp +++ b/interface/src/avatar/Hand.cpp @@ -94,39 +94,6 @@ void Hand::collideAgainstAvatar(Avatar* avatar, bool isMyHand) { } } -void Hand::collideAgainstOurself() { - if (!Menu::getInstance()->isOptionChecked(MenuOption::HandsCollideWithSelf)) { - return; - } - - int leftPalmIndex, rightPalmIndex; - getLeftRightPalmIndices(leftPalmIndex, rightPalmIndex); - float scaledPalmRadius = PALM_COLLISION_RADIUS * _owningAvatar->getScale(); - - const SkeletonModel& skeletonModel = _owningAvatar->getSkeletonModel(); - for (int i = 0; i < int(getNumPalms()); i++) { - PalmData& palm = getPalms()[i]; - if (!palm.isActive()) { - continue; - } - // ignoring everything below the parent of the parent of the last free joint - int skipIndex = skeletonModel.getParentJointIndex(skeletonModel.getParentJointIndex( - skeletonModel.getLastFreeJointIndex((int(i) == leftPalmIndex) ? skeletonModel.getLeftHandJointIndex() : - (int(i) == rightPalmIndex) ? skeletonModel.getRightHandJointIndex() : -1))); - - handCollisions.clear(); - if (_owningAvatar->findSphereCollisions(palm.getPosition(), scaledPalmRadius, handCollisions, skipIndex)) { - glm::vec3 totalPenetration; - for (int j = 0; j < handCollisions.size(); ++j) { - CollisionInfo* collision = handCollisions.getCollision(j); - totalPenetration = addPenetrations(totalPenetration, collision->_penetration); - } - // resolve penetration - palm.addToPenetration(totalPenetration); - } - } -} - void Hand::resolvePenetrations() { for (size_t i = 0; i < getNumPalms(); ++i) { PalmData& palm = getPalms()[i]; diff --git a/interface/src/avatar/Hand.h b/interface/src/avatar/Hand.h index 5d171f2809..ed2fa3e1ab 100755 --- a/interface/src/avatar/Hand.h +++ b/interface/src/avatar/Hand.h @@ -54,7 +54,6 @@ public: void render(bool isMine, Model::RenderMode renderMode = Model::DEFAULT_RENDER_MODE); void collideAgainstAvatar(Avatar* avatar, bool isMyHand); - void collideAgainstOurself(); void resolvePenetrations(); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3482c380a0..1e50ea9d87 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -76,14 +76,21 @@ MyAvatar::MyAvatar() : _lastFloorContactPoint(0.0f), _lookAtTargetAvatar(), _shouldRender(true), - _billboardValid(false) + _billboardValid(false), + _physicsSimulation() { for (int i = 0; i < MAX_DRIVE_KEYS; i++) { _driveKeys[i] = 0.0f; } + _skeletonModel.setEnableShapes(true); + // The skeleton is both a PhysicsEntity and Ragdoll, so we add it to the simulation once for each type. + _physicsSimulation.addEntity(&_skeletonModel); + _physicsSimulation.addRagdoll(&_skeletonModel); } MyAvatar::~MyAvatar() { + _physicsSimulation.removeEntity(&_skeletonModel); + _physicsSimulation.removeRagdoll(&_skeletonModel); _lookAtTargetAvatar.clear(); } @@ -154,7 +161,6 @@ void MyAvatar::simulate(float deltaTime) { { PerformanceTimer perfTimer("MyAvatar::simulate/hand Collision,simulate"); // update avatar skeleton and simulate hand and head - getHand()->collideAgainstOurself(); getHand()->simulate(deltaTime, true); } @@ -188,6 +194,25 @@ void MyAvatar::simulate(float deltaTime) { head->setScale(_scale); head->simulate(deltaTime, true); } + + { + PerformanceTimer perfTimer("MyAvatar::simulate/hair Simulate"); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + simulateHair(deltaTime); + } + } + + { + PerformanceTimer perfTimer("MyAvatar::simulate/ragdoll"); + if (Menu::getInstance()->isOptionChecked(MenuOption::CollideAsRagdoll)) { + const int minError = 0.01f; + const float maxIterations = 10; + const quint64 maxUsec = 2000; + _physicsSimulation.stepForward(deltaTime, minError, maxIterations, maxUsec); + } else { + _skeletonModel.moveShapesTowardJoints(1.0f); + } + } // now that we're done stepping the avatar forward in time, compute new collisions if (_collisionGroups != 0) { @@ -199,7 +224,6 @@ void MyAvatar::simulate(float deltaTime) { radius = myCamera->getAspectRatio() * (myCamera->getNearClip() / cos(myCamera->getFieldOfView() / 2.0f)); radius *= COLLISION_RADIUS_SCALAR; } - updateShapePositions(); if (_collisionGroups & COLLISION_GROUP_ENVIRONMENT) { PerformanceTimer perfTimer("MyAvatar::simulate/updateCollisionWithEnvironment"); updateCollisionWithEnvironment(deltaTime, radius); @@ -210,10 +234,12 @@ void MyAvatar::simulate(float deltaTime) { } else { _trapDuration = 0.0f; } + /* TODO: Andrew to make this work if (_collisionGroups & COLLISION_GROUP_AVATARS) { PerformanceTimer perfTimer("MyAvatar::simulate/updateCollisionWithAvatars"); updateCollisionWithAvatars(deltaTime); } + */ } // consider updating our billboard @@ -378,6 +404,7 @@ void MyAvatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { if (!_shouldRender) { return; // exit early } + Avatar::render(cameraPosition, renderMode); // don't display IK constraints in shadow mode @@ -430,6 +457,25 @@ void MyAvatar::renderHeadMouse(int screenWidth, int screenHeight) const { } } +const glm::vec3 HAND_TO_PALM_OFFSET(0.0f, 0.12f, 0.08f); + +glm::vec3 MyAvatar::getLeftPalmPosition() { + glm::vec3 leftHandPosition; + getSkeletonModel().getLeftHandPosition(leftHandPosition); + glm::quat leftRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftRotation); + leftHandPosition += HAND_TO_PALM_OFFSET * glm::inverse(leftRotation); + return leftHandPosition; +} +glm::vec3 MyAvatar::getRightPalmPosition() { + glm::vec3 rightHandPosition; + getSkeletonModel().getRightHandPosition(rightHandPosition); + glm::quat rightRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightRotation); + rightHandPosition += HAND_TO_PALM_OFFSET * glm::inverse(rightRotation); + return rightHandPosition; +} + void MyAvatar::setLocalGravity(glm::vec3 gravity) { _motionBehaviors |= AVATAR_MOTION_OBEY_LOCAL_GRAVITY; // Environmental and Local gravities are incompatible. Since Local is being set here @@ -839,8 +885,13 @@ void MyAvatar::renderBody(RenderMode renderMode, float glowLevel) { renderAttachments(renderMode); // Render head so long as the camera isn't inside it - if (shouldRenderHead(Application::getInstance()->getCamera()->getPosition(), renderMode)) { + const Camera *camera = Application::getInstance()->getCamera(); + const glm::vec3 cameraPos = camera->getPosition() + (camera->getRotation() * glm::vec3(0.0f, 0.0f, 1.0f)) * camera->getDistance(); + if (shouldRenderHead(cameraPos, renderMode)) { getHead()->render(1.0f, modelRenderMode); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + renderHair(); + } } getHand()->render(true, modelRenderMode); } @@ -891,11 +942,26 @@ void MyAvatar::updateOrientation(float deltaTime) { float yaw, pitch, roll; OculusManager::getEulerAngles(yaw, pitch, roll); // ... so they need to be converted to degrees before we do math... - + yaw *= DEGREES_PER_RADIAN; + pitch *= DEGREES_PER_RADIAN; + roll *= DEGREES_PER_RADIAN; + + // Record the angular velocity Head* head = getHead(); - head->setBaseYaw(yaw * DEGREES_PER_RADIAN); - head->setBasePitch(pitch * DEGREES_PER_RADIAN); - head->setBaseRoll(roll * DEGREES_PER_RADIAN); + glm::vec3 angularVelocity(yaw - head->getBaseYaw(), pitch - head->getBasePitch(), roll - head->getBaseRoll()); + head->setAngularVelocity(angularVelocity); + + //Invert yaw and roll when in mirror mode + if (Application::getInstance()->getCamera()->getMode() == CAMERA_MODE_MIRROR) { + head->setBaseYaw(-yaw); + head->setBasePitch(pitch); + head->setBaseRoll(-roll); + } else { + head->setBaseYaw(yaw); + head->setBasePitch(pitch); + head->setBaseRoll(roll); + } + } // update the euler angles @@ -984,6 +1050,7 @@ void MyAvatar::updatePosition(float deltaTime) { } else { _position += _velocity * deltaTime; } + updateAcceleration(deltaTime); } // update moving flag based on speed @@ -1262,7 +1329,7 @@ void MyAvatar::updateCollisionWithVoxels(float deltaTime, float radius) { float capsuleHalfHeight = boundingShape.getHalfHeight(); const float MAX_STEP_HEIGHT = capsuleRadius + capsuleHalfHeight; const float MIN_STEP_HEIGHT = 0.0f; - glm::vec3 footBase = boundingShape.getPosition() - (capsuleRadius + capsuleHalfHeight) * _worldUpDirection; + glm::vec3 footBase = boundingShape.getTranslation() - (capsuleRadius + capsuleHalfHeight) * _worldUpDirection; float highestStep = 0.0f; float lowestStep = MAX_STEP_HEIGHT; glm::vec3 floorPoint; @@ -1279,7 +1346,7 @@ void MyAvatar::updateCollisionWithVoxels(float deltaTime, float radius) { if (horizontalDepth > capsuleRadius || fabsf(verticalDepth) > MAX_STEP_HEIGHT) { isTrapped = true; if (_trapDuration > MAX_TRAP_PERIOD) { - float distance = glm::dot(boundingShape.getPosition() - cubeCenter, _worldUpDirection); + float distance = glm::dot(boundingShape.getTranslation() - cubeCenter, _worldUpDirection); if (distance < 0.0f) { distance = fabsf(distance) + 0.5f * cubeSide; } @@ -1464,7 +1531,6 @@ void MyAvatar::updateCollisionWithAvatars(float deltaTime) { // don't collide with ourselves continue; } - avatar->updateShapePositions(); float distance = glm::length(_position - avatar->getPosition()); if (_distanceToNearestAvatar > distance) { _distanceToNearestAvatar = distance; @@ -1490,17 +1556,10 @@ void MyAvatar::updateCollisionWithAvatars(float deltaTime) { } } - // collide our hands against them - // TODO: make this work when we can figure out when the other avatar won't yeild - // (for example, we're colliding against their chest or leg) - //getHand()->collideAgainstAvatar(avatar, true); - // collide their hands against us avatar->getHand()->collideAgainstAvatar(this, false); } } - // TODO: uncomment this when we handle collisions that won't affect other avatar - //getHand()->resolvePenetrations(); } class SortedAvatar { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 2fbc488feb..0ee76c6b45 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -14,6 +14,8 @@ #include <QSettings> +#include <PhysicsSimulation.h> + #include "Avatar.h" enum AvatarHandState @@ -137,7 +139,10 @@ public slots: void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } void updateMotionBehaviorsFromMenu(); - + + glm::vec3 getLeftPalmPosition(); + glm::vec3 getRightPalmPosition(); + signals: void transformChanged(); @@ -173,6 +178,7 @@ private: float _oculusYawOffset; QList<AnimationHandlePointer> _animationHandles; + PhysicsSimulation _physicsSimulation; // private methods float computeDistanceToFloor(const glm::vec3& startPoint); diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 3b8e67df47..b91c112b6a 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -13,23 +13,23 @@ #define hifi_SkeletonModel_h #include "renderer/Model.h" -#include "renderer/RagDoll.h" + +#include <CapsuleShape.h> +#include <Ragdoll.h> class Avatar; /// A skeleton loaded from a model. -class SkeletonModel : public Model { +class SkeletonModel : public Model, public Ragdoll { Q_OBJECT public: - SkeletonModel(Avatar* owningAvatar); + SkeletonModel(Avatar* owningAvatar, QObject* parent = NULL); void setJointStates(QVector<JointState> states); void simulate(float deltaTime, bool fullUpdate = true); - void simulateRagDoll(float deltaTime); - void updateShapePositions(); /// \param jointIndex index of hand joint /// \param shapes[out] list in which is stored pointers to hand shapes @@ -94,9 +94,27 @@ public: /// \return whether or not both eye meshes were found bool getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const; - void renderRagDoll(); + // virtual overrride from Ragdoll + virtual void stepRagdollForward(float deltaTime); + + void moveShapesTowardJoints(float fraction); + + void computeBoundingShape(const FBXGeometry& geometry); + void renderBoundingCollisionShapes(float alpha); + float getBoundingShapeRadius() const { return _boundingShape.getRadius(); } + const CapsuleShape& getBoundingShape() const { return _boundingShape; } + + void resetShapePositions(); // DEBUG method + + void renderRagdoll(); protected: + // virtual overrrides from Ragdoll + void initRagdollPoints(); + void buildRagdollConstraints(); + + void buildShapes(); + /// \param jointIndex index of joint in model /// \param position position of joint in model-frame void applyHandPosition(int jointIndex, const glm::vec3& position); @@ -120,7 +138,9 @@ private: void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation); Avatar* _owningAvatar; - RagDoll _ragDoll; + + CapsuleShape _boundingShape; + glm::vec3 _boundingShapeLocalOffset; }; #endif // hifi_SkeletonModel_h diff --git a/interface/src/devices/OculusManager.cpp b/interface/src/devices/OculusManager.cpp index b2ee4e8c18..dbe444bf9b 100644 --- a/interface/src/devices/OculusManager.cpp +++ b/interface/src/devices/OculusManager.cpp @@ -3,6 +3,7 @@ // interface/src/devices // // Created by Stephen Birarda on 5/9/13. +// Refactored by Ben Arnold on 6/30/2014 // Copyright 2012 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -11,67 +12,211 @@ #include "InterfaceConfig.h" +#include "OculusManager.h" + #include <QOpenGLFramebufferObject> #include <glm/glm.hpp> +#include <UserActivityLogger.h> #include "Application.h" -#include "OculusManager.h" + +#ifdef HAVE_LIBOVR + +using namespace OVR; ProgramObject OculusManager::_program; int OculusManager::_textureLocation; -int OculusManager::_lensCenterLocation; -int OculusManager::_screenCenterLocation; -int OculusManager::_scaleLocation; -int OculusManager::_scaleInLocation; -int OculusManager::_hmdWarpParamLocation; +int OculusManager::_eyeToSourceUVScaleLocation; +int OculusManager::_eyeToSourceUVOffsetLocation; +int OculusManager::_eyeRotationStartLocation; +int OculusManager::_eyeRotationEndLocation; +int OculusManager::_positionAttributeLocation; +int OculusManager::_colorAttributeLocation; +int OculusManager::_texCoord0AttributeLocation; +int OculusManager::_texCoord1AttributeLocation; +int OculusManager::_texCoord2AttributeLocation; bool OculusManager::_isConnected = false; -#ifdef HAVE_LIBOVR -using namespace OVR; -using namespace OVR::Util::Render; +ovrHmd OculusManager::_ovrHmd; +ovrHmdDesc OculusManager::_ovrHmdDesc; +ovrFovPort OculusManager::_eyeFov[ovrEye_Count]; +ovrEyeRenderDesc OculusManager::_eyeRenderDesc[ovrEye_Count]; +ovrSizei OculusManager::_renderTargetSize; +ovrVector2f OculusManager::_UVScaleOffset[ovrEye_Count][2]; +GLuint OculusManager::_vertices[ovrEye_Count] = { 0, 0 }; +GLuint OculusManager::_indices[ovrEye_Count] = { 0, 0 }; +GLsizei OculusManager::_meshSize[ovrEye_Count] = { 0, 0 }; +ovrFrameTiming OculusManager::_hmdFrameTiming; +ovrRecti OculusManager::_eyeRenderViewport[ovrEye_Count]; +unsigned int OculusManager::_frameIndex = 0; +bool OculusManager::_frameTimingActive = false; +bool OculusManager::_programInitialized = false; +Camera* OculusManager::_camera = NULL; -Ptr<DeviceManager> OculusManager::_deviceManager; -Ptr<HMDDevice> OculusManager::_hmdDevice; -Ptr<SensorDevice> OculusManager::_sensorDevice; -SensorFusion* OculusManager::_sensorFusion; -StereoConfig OculusManager::_stereoConfig; #endif void OculusManager::connect() { #ifdef HAVE_LIBOVR - System::Init(); - _deviceManager = *DeviceManager::Create(); - _hmdDevice = *_deviceManager->EnumerateDevices<HMDDevice>().CreateDevice(); + ovr_Initialize(); - if (_hmdDevice) { + _ovrHmd = ovrHmd_Create(0); + if (_ovrHmd) { + if (!_isConnected) { + UserActivityLogger::getInstance().connectedDevice("hmd", "oculus"); + } _isConnected = true; - - _sensorDevice = *_hmdDevice->GetSensor(); - _sensorFusion = new SensorFusion; - _sensorFusion->AttachToSensor(_sensorDevice); - _sensorFusion->SetPredictionEnabled(true); + + ovrHmd_GetDesc(_ovrHmd, &_ovrHmdDesc); + + _eyeFov[0] = _ovrHmdDesc.DefaultEyeFov[0]; + _eyeFov[1] = _ovrHmdDesc.DefaultEyeFov[1]; + + //Get texture size + ovrSizei recommendedTex0Size = ovrHmd_GetFovTextureSize(_ovrHmd, ovrEye_Left, + _eyeFov[0], 1.0f); + ovrSizei recommendedTex1Size = ovrHmd_GetFovTextureSize(_ovrHmd, ovrEye_Right, + _eyeFov[1], 1.0f); + _renderTargetSize.w = recommendedTex0Size.w + recommendedTex1Size.w; + _renderTargetSize.h = recommendedTex0Size.h; + if (_renderTargetSize.h < recommendedTex1Size.h) { + _renderTargetSize.h = recommendedTex1Size.h; + } + + _eyeRenderDesc[0] = ovrHmd_GetRenderDesc(_ovrHmd, ovrEye_Left, _eyeFov[0]); + _eyeRenderDesc[1] = ovrHmd_GetRenderDesc(_ovrHmd, ovrEye_Right, _eyeFov[1]); + + ovrHmd_SetEnabledCaps(_ovrHmd, ovrHmdCap_LowPersistence | ovrHmdCap_LatencyTest); + + ovrHmd_StartSensor(_ovrHmd, ovrSensorCap_Orientation | ovrSensorCap_YawCorrection | + ovrSensorCap_Position, + ovrSensorCap_Orientation); + + if (!_camera) { + _camera = new Camera; + } + + if (!_programInitialized) { + // Shader program + _programInitialized = true; + _program.addShaderFromSourceFile(QGLShader::Vertex, Application::resourcesPath() + "shaders/oculus.vert"); + _program.addShaderFromSourceFile(QGLShader::Fragment, Application::resourcesPath() + "shaders/oculus.frag"); + _program.link(); + + // Uniforms + _textureLocation = _program.uniformLocation("texture"); + _eyeToSourceUVScaleLocation = _program.uniformLocation("EyeToSourceUVScale"); + _eyeToSourceUVOffsetLocation = _program.uniformLocation("EyeToSourceUVOffset"); + _eyeRotationStartLocation = _program.uniformLocation("EyeRotationStart"); + _eyeRotationEndLocation = _program.uniformLocation("EyeRotationEnd"); + + // Attributes + _positionAttributeLocation = _program.attributeLocation("position"); + _colorAttributeLocation = _program.attributeLocation("color"); + _texCoord0AttributeLocation = _program.attributeLocation("texCoord0"); + _texCoord1AttributeLocation = _program.attributeLocation("texCoord1"); + _texCoord2AttributeLocation = _program.attributeLocation("texCoord2"); + } + + //Generate the distortion VBOs + generateDistortionMesh(); - HMDInfo info; - _hmdDevice->GetDeviceInfo(&info); - _stereoConfig.SetHMDInfo(info); - - _program.addShaderFromSourceFile(QGLShader::Fragment, Application::resourcesPath() + "shaders/oculus.frag"); - _program.link(); - - _textureLocation = _program.uniformLocation("texture"); - _lensCenterLocation = _program.uniformLocation("lensCenter"); - _screenCenterLocation = _program.uniformLocation("screenCenter"); - _scaleLocation = _program.uniformLocation("scale"); - _scaleInLocation = _program.uniformLocation("scaleIn"); - _hmdWarpParamLocation = _program.uniformLocation("hmdWarpParam"); } else { - _deviceManager.Clear(); - System::Destroy(); + _isConnected = false; + ovrHmd_Destroy(_ovrHmd); + ovr_Shutdown(); } #endif } +//Disconnects and deallocates the OR +void OculusManager::disconnect() { +#ifdef HAVE_LIBOVR + if (_isConnected) { + _isConnected = false; + ovrHmd_Destroy(_ovrHmd); + ovr_Shutdown(); + + //Free the distortion mesh data + for (int i = 0; i < ovrEye_Count; i++) { + if (_vertices[i] != 0) { + glDeleteBuffers(1, &(_vertices[i])); + _vertices[i] = 0; + } + if (_indices[i] != 0) { + glDeleteBuffers(1, &(_indices[i])); + _indices[i] = 0; + } + } + } +#endif +} + +#ifdef HAVE_LIBOVR +void OculusManager::generateDistortionMesh() { + + //Check if we already have the distortion mesh + if (_vertices[0] != 0) { + printf("WARNING: Tried to generate Oculus distortion mesh twice without freeing the VBOs."); + return; + } + + //Viewport for the render target for each eye + _eyeRenderViewport[0].Pos = Vector2i(0, 0); + _eyeRenderViewport[0].Size = Sizei(_renderTargetSize.w / 2, _renderTargetSize.h); + _eyeRenderViewport[1].Pos = Vector2i((_renderTargetSize.w + 1) / 2, 0); + _eyeRenderViewport[1].Size = _eyeRenderViewport[0].Size; + + for (int eyeNum = 0; eyeNum < ovrEye_Count; eyeNum++) { + // Allocate and generate distortion mesh vertices + ovrDistortionMesh meshData; + ovrHmd_CreateDistortionMesh(_ovrHmd, _eyeRenderDesc[eyeNum].Eye, _eyeRenderDesc[eyeNum].Fov, _ovrHmdDesc.DistortionCaps, &meshData); + + ovrHmd_GetRenderScaleAndOffset(_eyeRenderDesc[eyeNum].Fov, _renderTargetSize, _eyeRenderViewport[eyeNum], + _UVScaleOffset[eyeNum]); + + // Parse the vertex data and create a render ready vertex buffer + DistortionVertex* pVBVerts = (DistortionVertex*)OVR_ALLOC(sizeof(DistortionVertex) * meshData.VertexCount); + _meshSize[eyeNum] = meshData.IndexCount; + + // Convert the oculus vertex data to the DistortionVertex format. + DistortionVertex* v = pVBVerts; + ovrDistortionVertex* ov = meshData.pVertexData; + for (unsigned int vertNum = 0; vertNum < meshData.VertexCount; vertNum++) { + v->pos.x = ov->Pos.x; + v->pos.y = ov->Pos.y; + v->texR.x = ov->TexR.x; + v->texR.y = ov->TexR.y; + v->texG.x = ov->TexG.x; + v->texG.y = ov->TexG.y; + v->texB.x = ov->TexB.x; + v->texB.y = ov->TexB.y; + v->color.r = v->color.g = v->color.b = (GLubyte)(ov->VignetteFactor * 255.99f); + v->color.a = (GLubyte)(ov->TimeWarpFactor * 255.99f); + v++; + ov++; + } + + //vertices + glGenBuffers(1, &(_vertices[eyeNum])); + glBindBuffer(GL_ARRAY_BUFFER, _vertices[eyeNum]); + glBufferData(GL_ARRAY_BUFFER, sizeof(DistortionVertex) * meshData.VertexCount, pVBVerts, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + //indices + glGenBuffers(1, &(_indices[eyeNum])); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indices[eyeNum]); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned short) * meshData.IndexCount, meshData.pIndexData, GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + //Now that we have the VBOs we can get rid of the mesh data + OVR_FREE(pVBVerts); + ovrHmd_DestroyDistortionMesh(&meshData); + } + +} +#endif + bool OculusManager::isConnected() { #ifdef HAVE_LIBOVR return _isConnected && Menu::getInstance()->isOptionChecked(MenuOption::EnableVRMode); @@ -80,137 +225,237 @@ bool OculusManager::isConnected() { #endif } +//Begins the frame timing for oculus prediction purposes +void OculusManager::beginFrameTiming() { +#ifdef HAVE_LIBOVR + + if (_frameTimingActive) { + printf("WARNING: Called OculusManager::beginFrameTiming() twice in a row, need to call OculusManager::endFrameTiming()."); + } + + _hmdFrameTiming = ovrHmd_BeginFrameTiming(_ovrHmd, _frameIndex); + _frameTimingActive = true; +#endif +} + +//Ends frame timing +void OculusManager::endFrameTiming() { +#ifdef HAVE_LIBOVR + ovrHmd_EndFrameTiming(_ovrHmd); + _frameIndex++; + _frameTimingActive = false; +#endif +} + +//Sets the camera FoV and aspect ratio void OculusManager::configureCamera(Camera& camera, int screenWidth, int screenHeight) { #ifdef HAVE_LIBOVR - _stereoConfig.SetFullViewport(Viewport(0, 0, screenWidth, screenHeight)); - camera.setAspectRatio(_stereoConfig.GetAspect()); - camera.setFieldOfView(_stereoConfig.GetYFOVDegrees()); + camera.setAspectRatio(_renderTargetSize.w / _renderTargetSize.h); + camera.setFieldOfView(atan(_eyeFov[0].UpTan) * DEGREES_PER_RADIAN * 2.0f); #endif } -void OculusManager::display(Camera& whichCamera) { +//Displays everything for the oculus, frame timing must be active +void OculusManager::display(const glm::quat &bodyOrientation, const glm::vec3 &position, Camera& whichCamera) { #ifdef HAVE_LIBOVR + //beginFrameTiming must be called before display + if (!_frameTimingActive) { + printf("WARNING: Called OculusManager::display() without calling OculusManager::beginFrameTiming() first."); + return; + } + ApplicationOverlay& applicationOverlay = Application::getInstance()->getApplicationOverlay(); + // We only need to render the overlays to a texture once, then we just render the texture as a quad // PrioVR will only work if renderOverlay is called, calibration is connected to Application::renderingOverlay() applicationOverlay.renderOverlay(true); const bool displayOverlays = Menu::getInstance()->isOptionChecked(MenuOption::DisplayOculusOverlays); - - Application::getInstance()->getGlowEffect()->prepare(); - // render the left eye view to the left side of the screen - const StereoEyeParams& leftEyeParams = _stereoConfig.GetEyeRenderParams(StereoEye_Left); + //Bind our framebuffer object. If we are rendering the glow effect, we let the glow effect shader take care of it + if (Menu::getInstance()->isOptionChecked(MenuOption::EnableGlowEffect)) { + Application::getInstance()->getGlowEffect()->prepare(); + } else { + Application::getInstance()->getTextureCache()->getPrimaryFramebufferObject()->bind(); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + + ovrPosef eyeRenderPose[ovrEye_Count]; + + _camera->setTightness(0.0f); // In first person, camera follows (untweaked) head exactly without delay + _camera->setDistance(0.0f); + _camera->setUpShift(0.0f); + glMatrixMode(GL_PROJECTION); glPushMatrix(); - glLoadIdentity(); - glTranslatef(_stereoConfig.GetProjectionCenterOffset(), 0, 0); - gluPerspective(whichCamera.getFieldOfView(), whichCamera.getAspectRatio(), - whichCamera.getNearClip(), whichCamera.getFarClip()); - - glViewport(leftEyeParams.VP.x, leftEyeParams.VP.y, leftEyeParams.VP.w, leftEyeParams.VP.h); + glMatrixMode(GL_MODELVIEW); glPushMatrix(); - glLoadIdentity(); - glTranslatef(_stereoConfig.GetIPD() * 0.5f, 0, 0); - - Application::getInstance()->displaySide(whichCamera); + + glm::quat orientation; + + //Render each eye into an fbo + for (int eyeIndex = 0; eyeIndex < ovrEye_Count; eyeIndex++) { - if (displayOverlays) { - applicationOverlay.displayOverlayTextureOculus(whichCamera); - } - - // and the right eye to the right side - const StereoEyeParams& rightEyeParams = _stereoConfig.GetEyeRenderParams(StereoEye_Right); - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glTranslatef(-_stereoConfig.GetProjectionCenterOffset(), 0, 0); - gluPerspective(whichCamera.getFieldOfView(), whichCamera.getAspectRatio(), - whichCamera.getNearClip(), whichCamera.getFarClip()); - - glViewport(rightEyeParams.VP.x, rightEyeParams.VP.y, rightEyeParams.VP.w, rightEyeParams.VP.h); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - glTranslatef(_stereoConfig.GetIPD() * -0.5f, 0, 0); - - Application::getInstance()->displaySide(whichCamera); + ovrEyeType eye = _ovrHmdDesc.EyeRenderOrder[eyeIndex]; - if (displayOverlays) { - applicationOverlay.displayOverlayTextureOculus(whichCamera); + //Set the camera rotation for this eye + eyeRenderPose[eye] = ovrHmd_GetEyePose(_ovrHmd, eye); + orientation.x = eyeRenderPose[eye].Orientation.x; + orientation.y = eyeRenderPose[eye].Orientation.y; + orientation.z = eyeRenderPose[eye].Orientation.z; + orientation.w = eyeRenderPose[eye].Orientation.w; + + _camera->setTargetRotation(bodyOrientation * orientation); + _camera->setTargetPosition(position); + _camera->update(1.0f / Application::getInstance()->getFps()); + + Matrix4f proj = ovrMatrix4f_Projection(_eyeRenderDesc[eye].Fov, whichCamera.getNearClip(), whichCamera.getFarClip(), true); + proj.Transpose(); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glLoadMatrixf((GLfloat *)proj.M); + + glViewport(_eyeRenderViewport[eye].Pos.x, _eyeRenderViewport[eye].Pos.y, + _eyeRenderViewport[eye].Size.w, _eyeRenderViewport[eye].Size.h); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glTranslatef(_eyeRenderDesc[eye].ViewAdjust.x, _eyeRenderDesc[eye].ViewAdjust.y, _eyeRenderDesc[eye].ViewAdjust.z); + + Application::getInstance()->displaySide(*_camera); + + if (displayOverlays) { + applicationOverlay.displayOverlayTextureOculus(*_camera); + } } - + + //Wait till time-warp to reduce latency + ovr_WaitTillTime(_hmdFrameTiming.TimewarpPointSeconds); + glPopMatrix(); - - // restore our normal viewport - const Viewport& fullViewport = _stereoConfig.GetFullViewport(); - glViewport(fullViewport.x, fullViewport.y, fullViewport.w, fullViewport.h); - QOpenGLFramebufferObject* fbo = Application::getInstance()->getGlowEffect()->render(true); - glBindTexture(GL_TEXTURE_2D, fbo->texture()); + //Full texture viewport for glow effect + glViewport(0, 0, _renderTargetSize.w, _renderTargetSize.h); + + //Bind the output texture from the glow shader. If glow effect is disabled, we just grab the texture + if (Menu::getInstance()->isOptionChecked(MenuOption::EnableGlowEffect)) { + QOpenGLFramebufferObject* fbo = Application::getInstance()->getGlowEffect()->render(true); + glBindTexture(GL_TEXTURE_2D, fbo->texture()); + } else { + Application::getInstance()->getTextureCache()->getPrimaryFramebufferObject()->release(); + glBindTexture(GL_TEXTURE_2D, Application::getInstance()->getTextureCache()->getPrimaryFramebufferObject()->texture()); + } + + // restore our normal viewport + glViewport(0, 0, Application::getInstance()->getGLWidget()->width(), Application::getInstance()->getGLWidget()->height()); glMatrixMode(GL_PROJECTION); + glPopMatrix(); + + //Renders the distorted mesh onto the screen + renderDistortionMesh(eyeRenderPose); + + glBindTexture(GL_TEXTURE_2D, 0); + +#endif +} + +#ifdef HAVE_LIBOVR +void OculusManager::renderDistortionMesh(ovrPosef eyeRenderPose[ovrEye_Count]) { + glLoadIdentity(); - gluOrtho2D(fullViewport.x, fullViewport.x + fullViewport.w, fullViewport.y, fullViewport.y + fullViewport.h); + gluOrtho2D(0, Application::getInstance()->getGLWidget()->width(), 0, Application::getInstance()->getGLWidget()->height()); + glDisable(GL_DEPTH_TEST); - - // for reference on setting these values, see SDK file Samples/OculusRoomTiny/RenderTiny_Device.cpp - - float scaleFactor = 1.0 / _stereoConfig.GetDistortionScale(); - float aspectRatio = _stereoConfig.GetAspect(); - + glDisable(GL_BLEND); _program.bind(); _program.setUniformValue(_textureLocation, 0); - const DistortionConfig& distortionConfig = _stereoConfig.GetDistortionConfig(); - _program.setUniformValue(_lensCenterLocation, (0.5 + distortionConfig.XCenterOffset * 0.5) * 0.5, 0.5); - _program.setUniformValue(_screenCenterLocation, 0.25, 0.5); - _program.setUniformValue(_scaleLocation, 0.25 * scaleFactor, 0.5 * scaleFactor * aspectRatio); - _program.setUniformValue(_scaleInLocation, 4, 2 / aspectRatio); - _program.setUniformValue(_hmdWarpParamLocation, distortionConfig.K[0], distortionConfig.K[1], - distortionConfig.K[2], distortionConfig.K[3]); - glColor3f(1, 0, 1); - glBegin(GL_QUADS); - glTexCoord2f(0, 0); - glVertex2f(0, 0); - glTexCoord2f(0.5, 0); - glVertex2f(leftEyeParams.VP.w, 0); - glTexCoord2f(0.5, 1); - glVertex2f(leftEyeParams.VP.w, leftEyeParams.VP.h); - glTexCoord2f(0, 1); - glVertex2f(0, leftEyeParams.VP.h); - glEnd(); - - _program.setUniformValue(_lensCenterLocation, 0.5 + (0.5 - distortionConfig.XCenterOffset * 0.5) * 0.5, 0.5); - _program.setUniformValue(_screenCenterLocation, 0.75, 0.5); - - glBegin(GL_QUADS); - glTexCoord2f(0.5, 0); - glVertex2f(leftEyeParams.VP.w, 0); - glTexCoord2f(1, 0); - glVertex2f(fullViewport.w, 0); - glTexCoord2f(1, 1); - glVertex2f(fullViewport.w, leftEyeParams.VP.h); - glTexCoord2f(0.5, 1); - glVertex2f(leftEyeParams.VP.w, leftEyeParams.VP.h); - glEnd(); - - glEnable(GL_BLEND); - glBindTexture(GL_TEXTURE_2D, 0); + _program.enableAttributeArray(_positionAttributeLocation); + _program.enableAttributeArray(_colorAttributeLocation); + _program.enableAttributeArray(_texCoord0AttributeLocation); + _program.enableAttributeArray(_texCoord1AttributeLocation); + _program.enableAttributeArray(_texCoord2AttributeLocation); + + //Render the distortion meshes for each eye + for (int eyeNum = 0; eyeNum < ovrEye_Count; eyeNum++) { + GLfloat uvScale[2] = { _UVScaleOffset[eyeNum][0].x, _UVScaleOffset[eyeNum][0].y }; + _program.setUniformValueArray(_eyeToSourceUVScaleLocation, uvScale, 1, 2); + GLfloat uvOffset[2] = { _UVScaleOffset[eyeNum][1].x, _UVScaleOffset[eyeNum][1].y }; + _program.setUniformValueArray(_eyeToSourceUVOffsetLocation, uvOffset, 1, 2); + + ovrMatrix4f timeWarpMatrices[2]; + Matrix4f transposeMatrices[2]; + //Grabs the timewarp matrices to be used in the shader + ovrHmd_GetEyeTimewarpMatrices(_ovrHmd, (ovrEyeType)eyeNum, eyeRenderPose[eyeNum], timeWarpMatrices); + transposeMatrices[0] = Matrix4f(timeWarpMatrices[0]); + transposeMatrices[1] = Matrix4f(timeWarpMatrices[1]); + + //Have to transpose the matrices before using them + transposeMatrices[0].Transpose(); + transposeMatrices[1].Transpose(); + + glUniformMatrix4fv(_eyeRotationStartLocation, 1, GL_FALSE, (GLfloat *)transposeMatrices[0].M); + glUniformMatrix4fv(_eyeRotationEndLocation, 1, GL_FALSE, (GLfloat *)transposeMatrices[1].M); + + glBindBuffer(GL_ARRAY_BUFFER, _vertices[eyeNum]); + + //Set vertex attribute pointers + glVertexAttribPointer(_positionAttributeLocation, 2, GL_FLOAT, GL_FALSE, sizeof(DistortionVertex), (void *)0); + glVertexAttribPointer(_texCoord0AttributeLocation, 2, GL_FLOAT, GL_FALSE, sizeof(DistortionVertex), (void *)8); + glVertexAttribPointer(_texCoord1AttributeLocation, 2, GL_FLOAT, GL_FALSE, sizeof(DistortionVertex), (void *)16); + glVertexAttribPointer(_texCoord2AttributeLocation, 2, GL_FLOAT, GL_FALSE, sizeof(DistortionVertex), (void *)24); + glVertexAttribPointer(_colorAttributeLocation, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(DistortionVertex), (void *)32); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indices[eyeNum]); + glDrawElements(GL_TRIANGLES, _meshSize[eyeNum], GL_UNSIGNED_SHORT, 0); + } + + _program.disableAttributeArray(_positionAttributeLocation); + _program.disableAttributeArray(_colorAttributeLocation); + _program.disableAttributeArray(_texCoord0AttributeLocation); + _program.disableAttributeArray(_texCoord1AttributeLocation); + _program.disableAttributeArray(_texCoord2AttributeLocation); + + glEnable(GL_BLEND); + glEnable(GL_DEPTH_TEST); _program.release(); - - glPopMatrix(); -#endif + glBindBuffer(GL_ARRAY_BUFFER, 0); } +#endif +//Tries to reconnect to the sensors void OculusManager::reset() { #ifdef HAVE_LIBOVR - _sensorFusion->Reset(); + disconnect(); + connect(); #endif } +//Gets the current predicted angles from the oculus sensors void OculusManager::getEulerAngles(float& yaw, float& pitch, float& roll) { #ifdef HAVE_LIBOVR - _sensorFusion->GetPredictedOrientation().GetEulerAngles<Axis_Y, Axis_X, Axis_Z, Rotate_CCW, Handed_R>(&yaw, &pitch, &roll); + ovrSensorState ss = ovrHmd_GetSensorState(_ovrHmd, _hmdFrameTiming.ScanoutMidpointSeconds); + + if (ss.StatusFlags & (ovrStatus_OrientationTracked | ovrStatus_PositionTracked)) { + ovrPosef pose = ss.Predicted.Pose; + Quatf orientation = Quatf(pose.Orientation); + orientation.GetEulerAngles<Axis_Y, Axis_X, Axis_Z, Rotate_CCW, Handed_R>(&yaw, &pitch, &roll); + } +#endif +} + +//Used to set the size of the glow framebuffers +QSize OculusManager::getRenderTargetSize() { +#ifdef HAVE_LIBOVR + QSize rv; + rv.setWidth(_renderTargetSize.w); + rv.setHeight(_renderTargetSize.h); + return rv; +#else + return QSize(100, 100); #endif } diff --git a/interface/src/devices/OculusManager.h b/interface/src/devices/OculusManager.h index 21b9d67f4d..7798875c2c 100644 --- a/interface/src/devices/OculusManager.h +++ b/interface/src/devices/OculusManager.h @@ -3,6 +3,7 @@ // interface/src/devices // // Created by Stephen Birarda on 5/9/13. +// Refactored by Ben Arnold on 6/30/2014 // Copyright 2012 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -12,10 +13,9 @@ #ifndef hifi_OculusManager_h #define hifi_OculusManager_h -#include <iostream> - #ifdef HAVE_LIBOVR #include <OVR.h> +#include "../src/Util/Util_Render_Stereo.h" #endif #include "renderer/ProgramObject.h" @@ -28,38 +28,69 @@ class Camera; class OculusManager { public: static void connect(); - + static void disconnect(); static bool isConnected(); - + static void beginFrameTiming(); + static void endFrameTiming(); static void configureCamera(Camera& camera, int screenWidth, int screenHeight); - - static void display(Camera& whichCamera); - + static void display(const glm::quat &bodyOrientation, const glm::vec3 &position, Camera& whichCamera); static void reset(); /// param \yaw[out] yaw in radians /// param \pitch[out] pitch in radians /// param \roll[out] roll in radians static void getEulerAngles(float& yaw, float& pitch, float& roll); - - static void updateYawOffset(); + static QSize getRenderTargetSize(); private: +#ifdef HAVE_LIBOVR + static void generateDistortionMesh(); + static void renderDistortionMesh(ovrPosef eyeRenderPose[ovrEye_Count]); + + struct DistortionVertex { + glm::vec2 pos; + glm::vec2 texR; + glm::vec2 texG; + glm::vec2 texB; + struct { + GLubyte r; + GLubyte g; + GLubyte b; + GLubyte a; + } color; + }; + static ProgramObject _program; + //Uniforms static int _textureLocation; - static int _lensCenterLocation; - static int _screenCenterLocation; - static int _scaleLocation; - static int _scaleInLocation; - static int _hmdWarpParamLocation; + static int _eyeToSourceUVScaleLocation; + static int _eyeToSourceUVOffsetLocation; + static int _eyeRotationStartLocation; + static int _eyeRotationEndLocation; + //Attributes + static int _positionAttributeLocation; + static int _colorAttributeLocation; + static int _texCoord0AttributeLocation; + static int _texCoord1AttributeLocation; + static int _texCoord2AttributeLocation; + static bool _isConnected; -#ifdef HAVE_LIBOVR - static OVR::Ptr<OVR::DeviceManager> _deviceManager; - static OVR::Ptr<OVR::HMDDevice> _hmdDevice; - static OVR::Ptr<OVR::SensorDevice> _sensorDevice; - static OVR::SensorFusion* _sensorFusion; - static OVR::Util::Render::StereoConfig _stereoConfig; + static ovrHmd _ovrHmd; + static ovrHmdDesc _ovrHmdDesc; + static ovrFovPort _eyeFov[ovrEye_Count]; + static ovrEyeRenderDesc _eyeRenderDesc[ovrEye_Count]; + static ovrSizei _renderTargetSize; + static ovrVector2f _UVScaleOffset[ovrEye_Count][2]; + static GLuint _vertices[ovrEye_Count]; + static GLuint _indices[ovrEye_Count]; + static GLsizei _meshSize[ovrEye_Count]; + static ovrFrameTiming _hmdFrameTiming; + static ovrRecti _eyeRenderViewport[ovrEye_Count]; + static unsigned int _frameIndex; + static bool _frameTimingActive; + static bool _programInitialized; + static Camera* _camera; #endif }; diff --git a/interface/src/devices/SixenseManager.cpp b/interface/src/devices/SixenseManager.cpp index 07536d0af8..1b7baf2ee1 100644 --- a/interface/src/devices/SixenseManager.cpp +++ b/interface/src/devices/SixenseManager.cpp @@ -13,6 +13,7 @@ #include "Application.h" #include "SixenseManager.h" +#include "UserActivityLogger.h" #ifdef HAVE_SIXENSE const int CALIBRATION_STATE_IDLE = 0; @@ -39,6 +40,7 @@ SixenseManager::SixenseManager() { sixenseInit(); #endif + _hydrasConnected = false; _triggerPressed[0] = false; _bumperPressed[0] = false; _oldX[0] = -1; @@ -70,7 +72,11 @@ void SixenseManager::setFilter(bool filter) { void SixenseManager::update(float deltaTime) { #ifdef HAVE_SIXENSE if (sixenseGetNumActiveControllers() == 0) { + _hydrasConnected = false; return; + } else if (!_hydrasConnected) { + _hydrasConnected = true; + UserActivityLogger::getInstance().connectedDevice("spatial_controller", "hydra"); } MyAvatar* avatar = Application::getInstance()->getAvatar(); Hand* hand = avatar->getHand(); diff --git a/interface/src/devices/SixenseManager.h b/interface/src/devices/SixenseManager.h index 8803c2c006..8ca27ef77c 100644 --- a/interface/src/devices/SixenseManager.h +++ b/interface/src/devices/SixenseManager.h @@ -71,6 +71,7 @@ private: float _lastDistance; #endif + bool _hydrasConnected; quint64 _lastMovement; glm::vec3 _amountMoved; diff --git a/interface/src/location/LocationManager.cpp b/interface/src/location/LocationManager.cpp index 1d783cc8e7..32172d6e38 100644 --- a/interface/src/location/LocationManager.cpp +++ b/interface/src/location/LocationManager.cpp @@ -15,7 +15,7 @@ #include "LocationManager.h" const QString GET_USER_ADDRESS = "/api/v1/users/%1/address"; -const QString GET_PLACE_ADDRESS = "/api/v1/places/%1/address"; +const QString GET_PLACE_ADDRESS = "/api/v1/places/%1"; const QString GET_ADDRESSES = "/api/v1/addresses/%1"; const QString POST_PLACE_CREATE = "/api/v1/places/"; diff --git a/interface/src/models/ModelTreeRenderer.cpp b/interface/src/models/ModelTreeRenderer.cpp index 9c4b08fd99..78107db699 100644 --- a/interface/src/models/ModelTreeRenderer.cpp +++ b/interface/src/models/ModelTreeRenderer.cpp @@ -135,7 +135,7 @@ void ModelTreeRenderer::renderElement(OctreeElement* element, RenderArgs* args) args->_elementsTouched++; // actually render it here... // we need to iterate the actual modelItems of the element - ModelTreeElement* modelTreeElement = (ModelTreeElement*)element; + ModelTreeElement* modelTreeElement = static_cast<ModelTreeElement*>(element); QList<ModelItem>& modelItems = modelTreeElement->getModels(); diff --git a/interface/src/renderer/GlowEffect.cpp b/interface/src/renderer/GlowEffect.cpp index 262a632df0..c163136956 100644 --- a/interface/src/renderer/GlowEffect.cpp +++ b/interface/src/renderer/GlowEffect.cpp @@ -180,7 +180,7 @@ QOpenGLFramebufferObject* GlowEffect::render(bool toTexture) { glBindTexture(GL_TEXTURE_2D, oldDiffusedFBO->texture()); _diffuseProgram->bind(); - QSize size = Application::getInstance()->getGLWidget()->size(); + QSize size = primaryFBO->size(); _diffuseProgram->setUniformValue(_diffusionScaleLocation, 1.0f / size.width(), 1.0f / size.height()); renderFullscreenQuad(); diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index aff023c2a0..3b5cda4fd2 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -16,15 +16,15 @@ #include <glm/gtx/transform.hpp> #include <glm/gtx/norm.hpp> +#include <CapsuleShape.h> #include <GeometryUtil.h> +#include <PhysicsEntity.h> +#include <ShapeCollider.h> +#include <SphereShape.h> #include "Application.h" #include "Model.h" -#include <SphereShape.h> -#include <CapsuleShape.h> -#include <ShapeCollider.h> - using namespace std; static int modelPointerTypeId = qRegisterMetaType<QPointer<Model> >(); @@ -40,10 +40,7 @@ Model::Model(QObject* parent) : _snapModelToCenter(false), _snappedToCenter(false), _rootIndex(-1), - _shapesAreDirty(true), - _boundingRadius(0.0f), - _boundingShape(), - _boundingShapeLocalOffset(0.0f), + //_enableCollisionShapes(false), _lodDistance(0.0f), _pupilDilation(0.0f), _url("http://invalid.com") { @@ -129,7 +126,10 @@ void Model::setScaleInternal(const glm::vec3& scale) { const float ONE_PERCENT = 0.01f; if (relativeDeltaScale > ONE_PERCENT || scaleLength < EPSILON) { _scale = scale; - rebuildShapes(); + if (_shapes.size() > 0) { + clearShapes(); + buildShapes(); + } } } @@ -174,6 +174,7 @@ QVector<JointState> Model::createJointStates(const FBXGeometry& geometry) { int parentIndex = joint.parentIndex; if (parentIndex == -1) { _rootIndex = i; + // NOTE: in practice geometry.offset has a non-unity scale (rather than a translation) glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; state.computeTransform(parentTransform); } else { @@ -551,7 +552,6 @@ bool Model::updateGeometry() { model->setURL(attachment.url); _attachments.append(model); } - rebuildShapes(); needFullUpdate = true; } return needFullUpdate; @@ -560,6 +560,18 @@ bool Model::updateGeometry() { // virtual void Model::setJointStates(QVector<JointState> states) { _jointStates = states; + + // compute an approximate bounding radius for broadphase collision queries + // against PhysicsSimulation boundaries + int numJoints = _jointStates.size(); + float radius = 0.0f; + for (int i = 0; i < numJoints; ++i) { + float distance = glm::length(_jointStates[i].getPosition()); + if (distance > radius) { + radius = distance; + } + } + _boundingRadius = radius; } bool Model::render(float alpha, RenderMode mode, bool receiveShadows) { @@ -774,304 +786,13 @@ AnimationHandlePointer Model::createAnimationHandle() { return handle; } -void Model::clearShapes() { - for (int i = 0; i < _jointShapes.size(); ++i) { - delete _jointShapes[i]; - } - _jointShapes.clear(); -} - -void Model::rebuildShapes() { - clearShapes(); - - if (!_geometry || _rootIndex == -1) { - return; - } - - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - if (geometry.joints.isEmpty()) { - return; - } - - // We create the shapes with proper dimensions, but we set their transforms later. - float uniformScale = extractUniformScale(_scale); - for (int i = 0; i < _jointStates.size(); i++) { - const FBXJoint& joint = geometry.joints[i]; - - float radius = uniformScale * joint.boneRadius; - float halfHeight = 0.5f * uniformScale * joint.distanceToParent; - Shape::Type type = joint.shapeType; - if (type == Shape::CAPSULE_SHAPE && halfHeight < EPSILON) { - // this capsule is effectively a sphere - type = Shape::SPHERE_SHAPE; - } - if (type == Shape::CAPSULE_SHAPE) { - CapsuleShape* capsule = new CapsuleShape(radius, halfHeight); - _jointShapes.push_back(capsule); - } else if (type == Shape::SPHERE_SHAPE) { - SphereShape* sphere = new SphereShape(radius, glm::vec3(0.0f)); - _jointShapes.push_back(sphere); - } else { - // this shape type is not handled and the joint shouldn't collide, - // however we must have a shape for each joint, - // so we make a bogus sphere with zero radius. - // TODO: implement collision groups for more control over what collides with what - SphereShape* sphere = new SphereShape(0.0f, glm::vec3(0.0f)); - _jointShapes.push_back(sphere); - } - } - - // This method moves the shapes to their default positions in Model frame - // which is where we compute the bounding shape's parameters. - computeBoundingShape(geometry); - - // finally sync shapes to joint positions - _shapesAreDirty = true; - updateShapePositions(); -} - -void Model::computeBoundingShape(const FBXGeometry& geometry) { - // compute default joint transforms and rotations - // (in local frame, ignoring Model translation and rotation) - int numJoints = geometry.joints.size(); - QVector<glm::mat4> transforms; - transforms.fill(glm::mat4(), numJoints); - QVector<glm::quat> finalRotations; - finalRotations.fill(glm::quat(), numJoints); - - QVector<bool> shapeIsSet; - shapeIsSet.fill(false, numJoints); - int numShapesSet = 0; - int lastNumShapesSet = -1; - while (numShapesSet < numJoints && numShapesSet != lastNumShapesSet) { - lastNumShapesSet = numShapesSet; - for (int i = 0; i < numJoints; i++) { - const FBXJoint& joint = geometry.joints.at(i); - int parentIndex = joint.parentIndex; - - if (parentIndex == -1) { - glm::mat4 baseTransform = glm::scale(_scale) * glm::translate(_offset); - glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; - glm::mat4 rootTransform = baseTransform * geometry.offset * glm::translate(joint.translation) - * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; - // remove the tranlsation part before we save the root transform - transforms[i] = glm::translate(- extractTranslation(rootTransform)) * rootTransform; - - finalRotations[i] = combinedRotation; - ++numShapesSet; - shapeIsSet[i] = true; - } else if (shapeIsSet[parentIndex]) { - glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; - transforms[i] = transforms[parentIndex] * glm::translate(joint.translation) - * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; - finalRotations[i] = finalRotations[parentIndex] * combinedRotation; - ++numShapesSet; - shapeIsSet[i] = true; - } - } - } - - // sync shapes to joints - _boundingRadius = 0.0f; - float uniformScale = extractUniformScale(_scale); - for (int i = 0; i < _jointShapes.size(); i++) { - const FBXJoint& joint = geometry.joints[i]; - glm::vec3 jointToShapeOffset = uniformScale * (finalRotations[i] * joint.shapePosition); - glm::vec3 localPosition = extractTranslation(transforms[i]) + jointToShapeOffset; - Shape* shape = _jointShapes[i]; - shape->setPosition(localPosition); - shape->setRotation(finalRotations[i] * joint.shapeRotation); - float distance = glm::length(localPosition) + shape->getBoundingRadius(); - if (distance > _boundingRadius) { - _boundingRadius = distance; - } - } - - // compute bounding box - Extents totalExtents; - totalExtents.reset(); - for (int i = 0; i < _jointShapes.size(); i++) { - Extents shapeExtents; - shapeExtents.reset(); - - Shape* shape = _jointShapes[i]; - glm::vec3 localPosition = shape->getPosition(); - int type = shape->getType(); - if (type == Shape::CAPSULE_SHAPE) { - // add the two furthest surface points of the capsule - CapsuleShape* capsule = static_cast<CapsuleShape*>(shape); - glm::vec3 axis; - capsule->computeNormalizedAxis(axis); - float radius = capsule->getRadius(); - float halfHeight = capsule->getHalfHeight(); - axis = halfHeight * axis + glm::vec3(radius); - - shapeExtents.addPoint(localPosition + axis); - shapeExtents.addPoint(localPosition - axis); - totalExtents.addExtents(shapeExtents); - } else if (type == Shape::SPHERE_SHAPE) { - float radius = shape->getBoundingRadius(); - glm::vec3 axis = glm::vec3(radius); - shapeExtents.addPoint(localPosition + axis); - shapeExtents.addPoint(localPosition - axis); - totalExtents.addExtents(shapeExtents); - } - } - - // compute bounding shape parameters - // NOTE: we assume that the longest side of totalExtents is the yAxis... - glm::vec3 diagonal = totalExtents.maximum - totalExtents.minimum; - // ... and assume the radius is half the RMS of the X and Z sides: - float capsuleRadius = 0.5f * sqrtf(0.5f * (diagonal.x * diagonal.x + diagonal.z * diagonal.z)); - _boundingShape.setRadius(capsuleRadius); - _boundingShape.setHalfHeight(0.5f * diagonal.y - capsuleRadius); - _boundingShapeLocalOffset = 0.5f * (totalExtents.maximum + totalExtents.minimum); -} - -void Model::resetShapePositions() { - // DEBUG method. - // Moves shapes to the joint default locations for debug visibility into - // how the bounding shape is computed. - - if (!_geometry || _rootIndex == -1) { - // geometry or joints have not yet been created - return; - } - - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - if (geometry.joints.isEmpty() || _jointShapes.size() != geometry.joints.size()) { - return; - } - - // The shapes are moved to their default positions in computeBoundingShape(). - computeBoundingShape(geometry); - - // Then we move them into world frame for rendering at the Model's location. - for (int i = 0; i < _jointShapes.size(); i++) { - Shape* shape = _jointShapes[i]; - shape->setPosition(_translation + _rotation * shape->getPosition()); - shape->setRotation(_rotation * shape->getRotation()); - } - _boundingShape.setPosition(_translation + _rotation * _boundingShapeLocalOffset); - _boundingShape.setRotation(_rotation); +// virtual override from PhysicsEntity +void Model::buildShapes() { + // TODO: figure out how to load/build collision shapes for general models } void Model::updateShapePositions() { - if (_shapesAreDirty && _jointShapes.size() == _jointStates.size()) { - glm::vec3 rootPosition(0.0f); - _boundingRadius = 0.0f; - float uniformScale = extractUniformScale(_scale); - for (int i = 0; i < _jointStates.size(); i++) { - const JointState& state = _jointStates[i]; - const FBXJoint& joint = state.getFBXJoint(); - // shape position and rotation need to be in world-frame - glm::quat stateRotation = state.getRotation(); - glm::vec3 shapeOffset = uniformScale * (stateRotation * joint.shapePosition); - glm::vec3 worldPosition = _translation + _rotation * (state.getPosition() + shapeOffset); - Shape* shape = _jointShapes[i]; - shape->setPosition(worldPosition); - shape->setRotation(_rotation * stateRotation * joint.shapeRotation); - float distance = glm::distance(worldPosition, _translation) + shape->getBoundingRadius(); - if (distance > _boundingRadius) { - _boundingRadius = distance; - } - if (joint.parentIndex == -1) { - rootPosition = worldPosition; - } - } - _shapesAreDirty = false; - _boundingShape.setPosition(rootPosition + _rotation * _boundingShapeLocalOffset); - _boundingShape.setRotation(_rotation); - } -} - -bool Model::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance) const { - const glm::vec3 relativeOrigin = origin - _translation; - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - float minDistance = FLT_MAX; - float radiusScale = extractUniformScale(_scale); - for (int i = 0; i < _jointStates.size(); i++) { - const FBXJoint& joint = geometry.joints[i]; - glm::vec3 end = _translation + _rotation * _jointStates[i].getPosition(); - float endRadius = joint.boneRadius * radiusScale; - glm::vec3 start = end; - float startRadius = joint.boneRadius * radiusScale; - if (joint.parentIndex != -1) { - start = _translation + _rotation * _jointStates[joint.parentIndex].getPosition(); - startRadius = geometry.joints[joint.parentIndex].boneRadius * radiusScale; - } - // for now, use average of start and end radii - float capsuleDistance; - if (findRayCapsuleIntersection(relativeOrigin, direction, start, end, - (startRadius + endRadius) / 2.0f, capsuleDistance)) { - minDistance = qMin(minDistance, capsuleDistance); - } - } - if (minDistance < FLT_MAX) { - distance = minDistance; - return true; - } - return false; -} - -bool Model::findCollisions(const QVector<const Shape*> shapes, CollisionList& collisions) { - bool collided = false; - for (int i = 0; i < shapes.size(); ++i) { - const Shape* theirShape = shapes[i]; - for (int j = 0; j < _jointShapes.size(); ++j) { - const Shape* ourShape = _jointShapes[j]; - if (ShapeCollider::collideShapes(theirShape, ourShape, collisions)) { - collided = true; - } - } - } - return collided; -} - -bool Model::findSphereCollisions(const glm::vec3& sphereCenter, float sphereRadius, - CollisionList& collisions, int skipIndex) { - bool collided = false; - SphereShape sphere(sphereRadius, sphereCenter); - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - for (int i = 0; i < _jointShapes.size(); i++) { - const FBXJoint& joint = geometry.joints[i]; - if (joint.parentIndex != -1) { - if (skipIndex != -1) { - int ancestorIndex = joint.parentIndex; - do { - if (ancestorIndex == skipIndex) { - goto outerContinue; - } - ancestorIndex = geometry.joints[ancestorIndex].parentIndex; - - } while (ancestorIndex != -1); - } - } - if (ShapeCollider::collideShapes(&sphere, _jointShapes[i], collisions)) { - CollisionInfo* collision = collisions.getLastCollision(); - collision->_type = COLLISION_TYPE_MODEL; - collision->_data = (void*)(this); - collision->_intData = i; - collided = true; - } - outerContinue: ; - } - return collided; -} - -bool Model::findPlaneCollisions(const glm::vec4& plane, CollisionList& collisions) { - bool collided = false; - PlaneShape planeShape(plane); - for (int i = 0; i < _jointShapes.size(); i++) { - if (ShapeCollider::collideShapes(&planeShape, _jointShapes[i], collisions)) { - CollisionInfo* collision = collisions.getLastCollision(); - collision->_type = COLLISION_TYPE_MODEL; - collision->_data = (void*)(this); - collision->_intData = i; - collided = true; - } - } - return collided; + // TODO: implement this when we know how to build shapes for regular Models } class Blender : public QRunnable { @@ -1197,7 +918,7 @@ void Model::simulateInternal(float deltaTime) { for (int i = 0; i < _jointStates.size(); i++) { updateJointState(i); } - _shapesAreDirty = true; + _shapesAreDirty = ! _shapes.isEmpty(); // update the attachment transforms and simulate them const FBXGeometry& geometry = _geometry->getFBXGeometry(); @@ -1332,7 +1053,7 @@ bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const gl for (int j = freeLineage.size() - 1; j >= 0; j--) { updateJointState(freeLineage.at(j)); } - _shapesAreDirty = true; + _shapesAreDirty = !_shapes.isEmpty(); return true; } @@ -1370,14 +1091,17 @@ const int BALL_SUBDIVISIONS = 10; void Model::renderJointCollisionShapes(float alpha) { glPushMatrix(); Application::getInstance()->loadTranslatedViewMatrix(_translation); - for (int i = 0; i < _jointShapes.size(); i++) { - glPushMatrix(); + for (int i = 0; i < _shapes.size(); i++) { + Shape* shape = _shapes[i]; + if (!shape) { + continue; + } - Shape* shape = _jointShapes[i]; - + glPushMatrix(); + // NOTE: the shapes are in the avatar local-frame if (shape->getType() == Shape::SPHERE_SHAPE) { // shapes are stored in world-frame, so we have to transform into model frame - glm::vec3 position = shape->getPosition() - _translation; + glm::vec3 position = _rotation * shape->getTranslation(); glTranslatef(position.x, position.y, position.z); const glm::quat& rotation = shape->getRotation(); glm::vec3 axis = glm::axis(rotation); @@ -1392,7 +1116,7 @@ void Model::renderJointCollisionShapes(float alpha) { // draw a blue sphere at the capsule endpoint glm::vec3 endPoint; capsule->getEndPoint(endPoint); - endPoint = endPoint - _translation; + endPoint = _rotation * endPoint; glTranslatef(endPoint.x, endPoint.y, endPoint.z); glColor4f(0.6f, 0.6f, 0.8f, alpha); glutSolidSphere(capsule->getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); @@ -1400,7 +1124,7 @@ void Model::renderJointCollisionShapes(float alpha) { // draw a yellow sphere at the capsule startpoint glm::vec3 startPoint; capsule->getStartPoint(startPoint); - startPoint = startPoint - _translation; + startPoint = _rotation * startPoint; glm::vec3 axis = endPoint - startPoint; glTranslatef(-axis.x, -axis.y, -axis.z); glColor4f(0.8f, 0.8f, 0.6f, alpha); @@ -1416,85 +1140,6 @@ void Model::renderJointCollisionShapes(float alpha) { glPopMatrix(); } -void Model::renderBoundingCollisionShapes(float alpha) { - glPushMatrix(); - - Application::getInstance()->loadTranslatedViewMatrix(_translation); - - // draw a blue sphere at the capsule endpoint - glm::vec3 endPoint; - _boundingShape.getEndPoint(endPoint); - endPoint = endPoint - _translation; - glTranslatef(endPoint.x, endPoint.y, endPoint.z); - glColor4f(0.6f, 0.6f, 0.8f, alpha); - glutSolidSphere(_boundingShape.getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); - - // draw a yellow sphere at the capsule startpoint - glm::vec3 startPoint; - _boundingShape.getStartPoint(startPoint); - startPoint = startPoint - _translation; - glm::vec3 axis = endPoint - startPoint; - glTranslatef(-axis.x, -axis.y, -axis.z); - glColor4f(0.8f, 0.8f, 0.6f, alpha); - glutSolidSphere(_boundingShape.getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); - - // draw a green cylinder between the two points - glm::vec3 origin(0.0f); - glColor4f(0.6f, 0.8f, 0.6f, alpha); - Avatar::renderJointConnectingCone( origin, axis, _boundingShape.getRadius(), _boundingShape.getRadius()); - - glPopMatrix(); -} - -bool Model::collisionHitsMoveableJoint(CollisionInfo& collision) const { - if (collision._type == COLLISION_TYPE_MODEL) { - // the joint is pokable by a collision if it exists and is free to move - const FBXJoint& joint = _geometry->getFBXGeometry().joints[collision._intData]; - if (joint.parentIndex == -1 || _jointStates.isEmpty()) { - return false; - } - // an empty freeLineage means the joint can't move - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - int jointIndex = collision._intData; - const QVector<int>& freeLineage = geometry.joints.at(jointIndex).freeLineage; - return !freeLineage.isEmpty(); - } - return false; -} - -void Model::applyCollision(CollisionInfo& collision) { - if (collision._type != COLLISION_TYPE_MODEL) { - return; - } - - glm::vec3 jointPosition(0.0f); - int jointIndex = collision._intData; - if (getJointPositionInWorldFrame(jointIndex, jointPosition)) { - const FBXJoint& joint = _geometry->getFBXGeometry().joints[jointIndex]; - if (joint.parentIndex != -1) { - // compute the approximate distance (travel) that the joint needs to move - glm::vec3 start; - getJointPositionInWorldFrame(joint.parentIndex, start); - glm::vec3 contactPoint = collision._contactPoint - start; - glm::vec3 penetrationEnd = contactPoint + collision._penetration; - glm::vec3 axis = glm::cross(contactPoint, penetrationEnd); - float travel = glm::length(axis); - const float MIN_TRAVEL = 1.0e-8f; - if (travel > MIN_TRAVEL) { - // compute the new position of the joint - float angle = asinf(travel / (glm::length(contactPoint) * glm::length(penetrationEnd))); - axis = glm::normalize(axis); - glm::vec3 end; - getJointPositionInWorldFrame(jointIndex, end); - // transform into model-frame - glm::vec3 newEnd = glm::inverse(_rotation) * (start + glm::angleAxis(angle, axis) * (end - start) - _translation); - // try to move it - setJointPosition(jointIndex, newEnd, glm::quat(), false, -1, true); - } - } - } -} - void Model::setBlendedVertices(const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& normals) { if (_blendedVertexBuffers.isEmpty()) { return; diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 11e6861775..2045a0c9b5 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -16,7 +16,7 @@ #include <QObject> #include <QUrl> -#include <CapsuleShape.h> +#include <PhysicsEntity.h> #include <AnimationCache.h> @@ -33,7 +33,7 @@ typedef QSharedPointer<AnimationHandle> AnimationHandlePointer; typedef QWeakPointer<AnimationHandle> WeakAnimationHandlePointer; /// A generic 3D model displaying geometry loaded from a URL. -class Model : public QObject { +class Model : public QObject, public PhysicsEntity { Q_OBJECT public: @@ -41,12 +41,6 @@ public: Model(QObject* parent = NULL); virtual ~Model(); - void setTranslation(const glm::vec3& translation) { _translation = translation; } - const glm::vec3& getTranslation() const { return _translation; } - - void setRotation(const glm::quat& rotation) { _rotation = rotation; } - const glm::quat& getRotation() const { return _rotation; } - /// enables/disables scale to fit behavior, the model will be automatically scaled to the specified largest dimension void setScaleToFit(bool scaleToFit, float largestDimension = 0.0f); bool getScaleToFit() const { return _scaleToFit; } /// is scale to fit enabled @@ -67,7 +61,7 @@ public: void setBlendshapeCoefficients(const QVector<float>& coefficients) { _blendshapeCoefficients = coefficients; } const QVector<float>& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } - + bool isActive() const { return _geometry && _geometry->isLoaded(); } bool isRenderable() const { return !_meshStates.isEmpty() || (isActive() && _geometry->getMeshes().isEmpty()); } @@ -134,69 +128,34 @@ public: QStringList getJointNames() const; AnimationHandlePointer createAnimationHandle(); - + const QList<AnimationHandlePointer>& getRunningAnimations() const { return _runningAnimations; } - - void clearShapes(); - void rebuildShapes(); - void resetShapePositions(); + + // virtual overrides from PhysicsEntity + virtual void buildShapes(); virtual void updateShapePositions(); + void renderJointCollisionShapes(float alpha); - void renderBoundingCollisionShapes(float alpha); - bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance) const; - - /// \param shapes list of pointers shapes to test against Model - /// \param collisions list to store collision results - /// \return true if at least one shape collided agains Model - bool findCollisions(const QVector<const Shape*> shapes, CollisionList& collisions); - - bool findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, - CollisionList& collisions, int skipIndex = -1); - - bool findPlaneCollisions(const glm::vec4& plane, CollisionList& collisions); - - /// \param collision details about the collisions - /// \return true if the collision is against a moveable joint - bool collisionHitsMoveableJoint(CollisionInfo& collision) const; - - /// \param collision details about the collision - /// Use the collision to affect the model - void applyCollision(CollisionInfo& collision); - - float getBoundingRadius() const { return _boundingRadius; } - float getBoundingShapeRadius() const { return _boundingShape.getRadius(); } - /// Sets blended vertices computed in a separate thread. void setBlendedVertices(const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& normals); - const CapsuleShape& getBoundingShape() const { return _boundingShape; } - protected: - QSharedPointer<NetworkGeometry> _geometry; - glm::vec3 _translation; - glm::quat _rotation; glm::vec3 _scale; glm::vec3 _offset; bool _scaleToFit; /// If you set scaleToFit, we will calculate scale based on MeshExtents float _scaleToFitLargestDimension; /// this is the dimension that scale to fit will use bool _scaledToFit; /// have we scaled to fit - + bool _snapModelToCenter; /// is the model's offset automatically adjusted to center around 0,0,0 in model space bool _snappedToCenter; /// are we currently snapped to center int _rootIndex; - bool _shapesAreDirty; QVector<JointState> _jointStates; - QVector<Shape*> _jointShapes; - - float _boundingRadius; - CapsuleShape _boundingShape; - glm::vec3 _boundingShapeLocalOffset; - + class MeshState { public: QVector<glm::mat4> clusterMatrices; @@ -240,8 +199,6 @@ protected: /// first free ancestor. float getLimbLength(int jointIndex) const; - void computeBoundingShape(const FBXGeometry& geometry); - private: friend class AnimationHandle; diff --git a/interface/src/renderer/RagDoll.cpp b/interface/src/renderer/RagDoll.cpp deleted file mode 100644 index 305724d6e4..0000000000 --- a/interface/src/renderer/RagDoll.cpp +++ /dev/null @@ -1,167 +0,0 @@ -// -// RagDoll.cpp -// interface/src/avatar -// -// Created by Andrew Meadows 2014.05.30 -// Copyright 2014 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 <glm/glm.hpp> -#include <glm/gtx/quaternion.hpp> -#include <glm/gtx/transform.hpp> - -#include <CollisionInfo.h> -#include <SharedUtil.h> -#include <CapsuleShape.h> -#include <SphereShape.h> - -#include "RagDoll.h" - -// ---------------------------------------------------------------------------- -// FixedConstraint -// ---------------------------------------------------------------------------- -FixedConstraint::FixedConstraint(glm::vec3* point, const glm::vec3& anchor) : _point(point), _anchor(anchor) { -} - -float FixedConstraint::enforce() { - assert(_point != NULL); - float distance = glm::distance(_anchor, *_point); - *_point = _anchor; - return distance; -} - -void FixedConstraint::setPoint(glm::vec3* point) { - _point = point; -} - -void FixedConstraint::setAnchor(const glm::vec3& anchor) { - _anchor = anchor; -} - -// ---------------------------------------------------------------------------- -// DistanceConstraint -// ---------------------------------------------------------------------------- -DistanceConstraint::DistanceConstraint(glm::vec3* startPoint, glm::vec3* endPoint) : _distance(-1.0f) { - _points[0] = startPoint; - _points[1] = endPoint; - _distance = glm::distance(*(_points[0]), *(_points[1])); -} - -DistanceConstraint::DistanceConstraint(const DistanceConstraint& other) { - _distance = other._distance; - _points[0] = other._points[0]; - _points[1] = other._points[1]; -} - -void DistanceConstraint::setDistance(float distance) { - _distance = fabsf(distance); -} - -float DistanceConstraint::enforce() { - float newDistance = glm::distance(*(_points[0]), *(_points[1])); - glm::vec3 direction(0.0f, 1.0f, 0.0f); - if (newDistance > EPSILON) { - direction = (*(_points[0]) - *(_points[1])) / newDistance; - } - glm::vec3 center = 0.5f * (*(_points[0]) + *(_points[1])); - *(_points[0]) = center + (0.5f * _distance) * direction; - *(_points[1]) = center - (0.5f * _distance) * direction; - return glm::abs(newDistance - _distance); -} - -void DistanceConstraint::updateProxyShape(Shape* shape, const glm::quat& rotation, const glm::vec3& translation) const { - if (!shape) { - return; - } - switch (shape->getType()) { - case Shape::SPHERE_SHAPE: { - // sphere collides at endPoint - SphereShape* sphere = static_cast<SphereShape*>(shape); - sphere->setPosition(translation + rotation * (*_points[1])); - } - break; - case Shape::CAPSULE_SHAPE: { - // capsule collides from startPoint to endPoint - CapsuleShape* capsule = static_cast<CapsuleShape*>(shape); - capsule->setEndPoints(translation + rotation * (*_points[0]), translation + rotation * (*_points[1])); - } - break; - default: - break; - } -} - -// ---------------------------------------------------------------------------- -// RagDoll -// ---------------------------------------------------------------------------- - -RagDoll::RagDoll() { -} - -RagDoll::~RagDoll() { - clear(); -} - -void RagDoll::init(const QVector<JointState>& states) { - clear(); - const int numStates = states.size(); - _points.reserve(numStates); - for (int i = 0; i < numStates; ++i) { - const JointState& state = states[i]; - _points.push_back(state.getPosition()); - int parentIndex = state.getFBXJoint().parentIndex; - assert(parentIndex < i); - if (parentIndex == -1) { - FixedConstraint* anchor = new FixedConstraint(&(_points[i]), glm::vec3(0.0f)); - _constraints.push_back(anchor); - } else { - DistanceConstraint* stick = new DistanceConstraint(&(_points[i]), &(_points[parentIndex])); - _constraints.push_back(stick); - } - } -} - -/// Delete all data. -void RagDoll::clear() { - int numConstraints = _constraints.size(); - for (int i = 0; i < numConstraints; ++i) { - delete _constraints[i]; - } - _constraints.clear(); - _points.clear(); -} - -float RagDoll::slaveToSkeleton(const QVector<JointState>& states, float fraction) { - const int numStates = states.size(); - assert(numStates == _points.size()); - fraction = glm::clamp(fraction, 0.0f, 1.0f); - float maxDistance = 0.0f; - for (int i = 0; i < numStates; ++i) { - glm::vec3 oldPoint = _points[i]; - _points[i] = (1.0f - fraction) * _points[i] + fraction * states[i].getPosition(); - maxDistance = glm::max(maxDistance, glm::distance(oldPoint, _points[i])); - } - return maxDistance; -} - -float RagDoll::enforceConstraints() { - float maxDistance = 0.0f; - const int numConstraints = _constraints.size(); - for (int i = 0; i < numConstraints; ++i) { - DistanceConstraint* c = static_cast<DistanceConstraint*>(_constraints[i]); - //maxDistance = glm::max(maxDistance, _constraints[i]->enforce()); - maxDistance = glm::max(maxDistance, c->enforce()); - } - return maxDistance; -} - -void RagDoll::updateShapes(const QVector<Shape*>& shapes, const glm::quat& rotation, const glm::vec3& translation) const { - int numShapes = shapes.size(); - int numConstraints = _constraints.size(); - for (int i = 0; i < numShapes && i < numConstraints; ++i) { - _constraints[i]->updateProxyShape(shapes[i], rotation, translation); - } -} diff --git a/interface/src/renderer/RagDoll.h b/interface/src/renderer/RagDoll.h deleted file mode 100644 index 60e242d19b..0000000000 --- a/interface/src/renderer/RagDoll.h +++ /dev/null @@ -1,95 +0,0 @@ -// -// RagDoll.h -// interface/src/avatar -// -// Created by Andrew Meadows 2014.05.30 -// Copyright 2014 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_RagDoll_h -#define hifi_RagDoll_h - -#include "renderer/Model.h" - -class Shape; - -class Constraint { -public: - Constraint() {} - virtual ~Constraint() {} - - /// Enforce contraint by moving relevant points. - /// \return max distance of point movement - virtual float enforce() = 0; - - /// \param shape pointer to shape that will be this Constraint's collision proxy - /// \param rotation rotation into shape's collision frame - /// \param translation translation into shape's collision frame - /// Moves the shape such that it will collide at this constraint's position - virtual void updateProxyShape(Shape* shape, const glm::quat& rotation, const glm::vec3& translation) const {} - -protected: - int _type; -}; - -class FixedConstraint : public Constraint { -public: - FixedConstraint(glm::vec3* point, const glm::vec3& anchor); - float enforce(); - void setPoint(glm::vec3* point); - void setAnchor(const glm::vec3& anchor); -private: - glm::vec3* _point; - glm::vec3 _anchor; -}; - -class DistanceConstraint : public Constraint { -public: - DistanceConstraint(glm::vec3* startPoint, glm::vec3* endPoint); - DistanceConstraint(const DistanceConstraint& other); - float enforce(); - void setDistance(float distance); - void updateProxyShape(Shape* shape, const glm::quat& rotation, const glm::vec3& translation) const; -private: - float _distance; - glm::vec3* _points[2]; -}; - -class RagDoll { -public: - - RagDoll(); - virtual ~RagDoll(); - - /// Create points and constraints based on topology of collection of joints - /// \param joints list of connected joint states - void init(const QVector<JointState>& states); - - /// Delete all data. - void clear(); - - /// \param states list of joint states - /// \param fraction range from 0.0 (no movement) to 1.0 (use joint locations) - /// \return max distance of point movement - float slaveToSkeleton(const QVector<JointState>& states, float fraction); - - /// Enforce contraints. - /// \return max distance of point movement - float enforceConstraints(); - - const QVector<glm::vec3>& getPoints() const { return _points; } - - /// \param shapes list of shapes to be updated with new positions - /// \param rotation rotation into shapes' collision frame - /// \param translation translation into shapes' collision frame - void updateShapes(const QVector<Shape*>& shapes, const glm::quat& rotation, const glm::vec3& translation) const; - -private: - QVector<Constraint*> _constraints; - QVector<glm::vec3> _points; -}; - -#endif // hifi_RagDoll_h diff --git a/interface/src/renderer/TextureCache.cpp b/interface/src/renderer/TextureCache.cpp index 0588ca70d2..55a67ce854 100644 --- a/interface/src/renderer/TextureCache.cpp +++ b/interface/src/renderer/TextureCache.cpp @@ -28,10 +28,12 @@ TextureCache::TextureCache() : _permutationNormalTextureID(0), _whiteTextureID(0), _blueTextureID(0), + _primaryDepthTextureID(0), _primaryFramebufferObject(NULL), _secondaryFramebufferObject(NULL), _tertiaryFramebufferObject(NULL), - _shadowFramebufferObject(NULL) + _shadowFramebufferObject(NULL), + _frameBufferSize(100, 100) { } @@ -46,9 +48,41 @@ TextureCache::~TextureCache() { glDeleteTextures(1, &_primaryDepthTextureID); } - delete _primaryFramebufferObject; - delete _secondaryFramebufferObject; - delete _tertiaryFramebufferObject; + if (_primaryFramebufferObject) { + delete _primaryFramebufferObject; + } + + if (_secondaryFramebufferObject) { + delete _secondaryFramebufferObject; + } + + if (_tertiaryFramebufferObject) { + delete _tertiaryFramebufferObject; + } +} + +void TextureCache::setFrameBufferSize(QSize frameBufferSize) { + //If the size changed, we need to delete our FBOs + if (_frameBufferSize != frameBufferSize) { + _frameBufferSize = frameBufferSize; + + if (_primaryFramebufferObject) { + delete _primaryFramebufferObject; + _primaryFramebufferObject = NULL; + glDeleteTextures(1, &_primaryDepthTextureID); + _primaryDepthTextureID = 0; + } + + if (_secondaryFramebufferObject) { + delete _secondaryFramebufferObject; + _secondaryFramebufferObject = NULL; + } + + if (_tertiaryFramebufferObject) { + delete _tertiaryFramebufferObject; + _tertiaryFramebufferObject = NULL; + } + } } GLuint TextureCache::getPermutationNormalTextureID() { @@ -131,13 +165,14 @@ QSharedPointer<NetworkTexture> TextureCache::getTexture(const QUrl& url, bool no } QOpenGLFramebufferObject* TextureCache::getPrimaryFramebufferObject() { + if (!_primaryFramebufferObject) { _primaryFramebufferObject = createFramebufferObject(); - + glGenTextures(1, &_primaryDepthTextureID); glBindTexture(GL_TEXTURE_2D, _primaryDepthTextureID); - QSize size = Application::getInstance()->getGLWidget()->size(); - glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, size.width(), size.height(), + + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, _frameBufferSize.width(), _frameBufferSize.height(), 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); @@ -230,7 +265,7 @@ QSharedPointer<Resource> TextureCache::createResource(const QUrl& url, } QOpenGLFramebufferObject* TextureCache::createFramebufferObject() { - QOpenGLFramebufferObject* fbo = new QOpenGLFramebufferObject(Application::getInstance()->getGLWidget()->size()); + QOpenGLFramebufferObject* fbo = new QOpenGLFramebufferObject(_frameBufferSize); Application::getInstance()->getGLWidget()->installEventFilter(this); glBindTexture(GL_TEXTURE_2D, fbo->texture()); diff --git a/interface/src/renderer/TextureCache.h b/interface/src/renderer/TextureCache.h index f4444b6dfc..248a451e3a 100644 --- a/interface/src/renderer/TextureCache.h +++ b/interface/src/renderer/TextureCache.h @@ -32,6 +32,9 @@ public: TextureCache(); virtual ~TextureCache(); + /// Sets the desired texture resolution for the framebuffer objects. + void setFrameBufferSize(QSize frameBufferSize); + /// Returns the ID of the permutation/normal texture used for Perlin noise shader programs. This texture /// has two lines: the first, a set of random numbers in [0, 255] to be used as permutation offsets, and /// the second, a set of random unit vectors to be used as noise gradients. @@ -94,6 +97,8 @@ private: QOpenGLFramebufferObject* _shadowFramebufferObject; GLuint _shadowDepthTextureID; + + QSize _frameBufferSize; }; /// A simple object wrapper for an OpenGL texture. diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 9be556cf62..77e8986297 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -39,9 +39,10 @@ inline float min(float a, float b) { ApplicationOverlay::ApplicationOverlay() : _framebufferObject(NULL), _textureFov(DEFAULT_OCULUS_UI_ANGULAR_SIZE * RADIANS_PER_DEGREE), - _crosshairTexture(0), _alpha(1.0f), - _active(true) { + _active(true), + _crosshairTexture(0) +{ memset(_reticleActive, 0, sizeof(_reticleActive)); memset(_magActive, 0, sizeof(_reticleActive)); @@ -375,7 +376,7 @@ void ApplicationOverlay::renderControllerPointers() { //then disable it. const int MAX_BUTTON_PRESS_TIME = 250 * MSECS_TO_USECS; - if (usecTimestampNow() - pressedTime[index] < MAX_BUTTON_PRESS_TIME) { + if (usecTimestampNow() < pressedTime[index] + MAX_BUTTON_PRESS_TIME) { _magActive[index] = !stateWhenPressed[index]; } } diff --git a/interface/src/ui/ChatWindow.cpp b/interface/src/ui/ChatWindow.cpp index fde77334f4..23bf2eafc4 100644 --- a/interface/src/ui/ChatWindow.cpp +++ b/interface/src/ui/ChatWindow.cpp @@ -30,6 +30,9 @@ const int NUM_MESSAGES_TO_TIME_STAMP = 20; +const float OPACITY_ACTIVE = 1.0; +const float OPACITY_INACTIVE = 0.8; + const QRegularExpression regexLinks("((?:(?:ftp)|(?:https?)|(?:hifi))://\\S+)"); const QRegularExpression regexHifiLinks("([#@]\\S+)"); const QString mentionSoundsPath("/mention-sounds/"); @@ -108,7 +111,7 @@ ChatWindow::~ChatWindow() { void ChatWindow::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Escape) { - hide(); + Application::getInstance()->getWindow()->activateWindow(); } else { FramelessDialog::keyPressEvent(event); } @@ -178,7 +181,7 @@ void ChatWindow::addTimeStamp() { QLabel* timeLabel = new QLabel(timeString); timeLabel->setStyleSheet("color: #333333;" "background-color: white;" - "font-size: 14pt;" + "font-size: 14px;" "padding: 4px;"); timeLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); timeLabel->setAlignment(Qt::AlignLeft); @@ -284,7 +287,7 @@ void ChatWindow::participantsChanged() { "padding-bottom: 2px;" "padding-left: 2px;" "border: 1px solid palette(shadow);" - "font-size: 14pt;" + "font-size: 14px;" "font-weight: bold"); userLabel->setProperty("user", participantName); userLabel->setCursor(Qt::PointingHandCursor); @@ -320,7 +323,7 @@ void ChatWindow::messageReceived(const QXmppMessage& message) { "padding-right: 20px;" "margin: 0px;" "color: #333333;" - "font-size: 14pt;" + "font-size: 14px;" "background-color: rgba(0, 0, 0, 0%);" "border: 0; }" "QMenu{ border: 2px outset gray; }"); @@ -383,3 +386,12 @@ void ChatWindow::scrollToBottom() { QScrollBar* verticalScrollBar = ui->messagesScrollArea->verticalScrollBar(); verticalScrollBar->setValue(verticalScrollBar->maximum()); } + +bool ChatWindow::event(QEvent* event) { + if (event->type() == QEvent::WindowActivate) { + setWindowOpacity(OPACITY_ACTIVE); + } else if (event->type() == QEvent::WindowDeactivate) { + setWindowOpacity(OPACITY_INACTIVE); + } + return FramelessDialog::event(event); +} diff --git a/interface/src/ui/ChatWindow.h b/interface/src/ui/ChatWindow.h index 1e0f533e9e..652dcb5b08 100644 --- a/interface/src/ui/ChatWindow.h +++ b/interface/src/ui/ChatWindow.h @@ -50,6 +50,7 @@ protected: virtual void keyPressEvent(QKeyEvent *event); virtual void showEvent(QShowEvent* event); + virtual bool event(QEvent* event); private: #ifdef HAVE_QXMPP diff --git a/interface/src/ui/MetavoxelEditor.cpp b/interface/src/ui/MetavoxelEditor.cpp index a42c84470b..1f0c2498c5 100644 --- a/interface/src/ui/MetavoxelEditor.cpp +++ b/interface/src/ui/MetavoxelEditor.cpp @@ -29,6 +29,7 @@ #include <QVBoxLayout> #include <AttributeRegistry.h> +#include <MetavoxelMessages.h> #include "Application.h" #include "MetavoxelEditor.h" @@ -771,7 +772,7 @@ int VoxelizationVisitor::visit(MetavoxelInfo& info) { } return DEFAULT_ORDER; } - QRgb closestColor; + QRgb closestColor = QRgb(); float closestDistance = FLT_MAX; for (unsigned int i = 0; i < sizeof(DIRECTION_ROTATIONS) / sizeof(DIRECTION_ROTATIONS[0]); i++) { glm::vec3 rotated = DIRECTION_ROTATIONS[i] * center; diff --git a/interface/src/ui/ModelsBrowser.cpp b/interface/src/ui/ModelsBrowser.cpp index 4296a096a0..203c54d97a 100644 --- a/interface/src/ui/ModelsBrowser.cpp +++ b/interface/src/ui/ModelsBrowser.cpp @@ -89,6 +89,12 @@ ModelsBrowser::ModelsBrowser(ModelType modelsType, QWidget* parent) : _view.setEditTriggers(QAbstractItemView::NoEditTriggers); _view.setRootIsDecorated(false); _view.setModel(_handler->getModel()); + _view.blockSignals(true); + + // Initialize the search bar + _searchBar = new QLineEdit; + _searchBar->setDisabled(true); + connect(_handler, SIGNAL(doneDownloading()), SLOT(enableSearchBar())); } void ModelsBrowser::applyFilter(const QString &filter) { @@ -130,6 +136,11 @@ void ModelsBrowser::resizeView() { } } +void ModelsBrowser::enableSearchBar() { + _view.blockSignals(false); + _searchBar->setEnabled(true); +} + void ModelsBrowser::browse() { QDialog dialog; dialog.setWindowTitle("Browse models"); @@ -138,12 +149,10 @@ void ModelsBrowser::browse() { QGridLayout* layout = new QGridLayout(&dialog); dialog.setLayout(layout); - QLineEdit* searchBar = new QLineEdit(&dialog); - layout->addWidget(searchBar, 0, 0); - + layout->addWidget(_searchBar, 0, 0); layout->addWidget(&_view, 1, 0); dialog.connect(&_view, SIGNAL(doubleClicked(const QModelIndex&)), SLOT(accept())); - connect(searchBar, SIGNAL(textChanged(const QString&)), SLOT(applyFilter(const QString&))); + connect(_searchBar, SIGNAL(textChanged(const QString&)), SLOT(applyFilter(const QString&))); QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); layout->addWidget(buttons, 2, 0); diff --git a/interface/src/ui/ModelsBrowser.h b/interface/src/ui/ModelsBrowser.h index ff273a45bc..3e832c9dbe 100644 --- a/interface/src/ui/ModelsBrowser.h +++ b/interface/src/ui/ModelsBrowser.h @@ -74,9 +74,11 @@ public slots: private slots: void applyFilter(const QString& filter); void resizeView(); + void enableSearchBar(); private: ModelHandler* _handler; + QLineEdit* _searchBar; QTreeView _view; }; diff --git a/interface/src/ui/OAuthWebViewHandler.cpp b/interface/src/ui/OAuthWebViewHandler.cpp index 5b4431bd0f..8ec415584d 100644 --- a/interface/src/ui/OAuthWebViewHandler.cpp +++ b/interface/src/ui/OAuthWebViewHandler.cpp @@ -109,6 +109,8 @@ void OAuthWebViewHandler::displayWebviewForAuthorizationURL(const QUrl& authoriz connect(_activeWebView->page()->networkAccessManager(), &QNetworkAccessManager::sslErrors, this, &OAuthWebViewHandler::handleSSLErrors); + connect(_activeWebView->page()->networkAccessManager(), &QNetworkAccessManager::finished, + this, &OAuthWebViewHandler::handleReplyFinished); connect(_activeWebView.data(), &QWebView::loadFinished, this, &OAuthWebViewHandler::handleLoadFinished); // connect to the destroyed signal so after the web view closes we can start a timer @@ -132,6 +134,14 @@ void OAuthWebViewHandler::handleLoadFinished(bool success) { NodeList::getInstance()->setSessionUUID(QUuid(authQuery.queryItemValue(AUTH_STATE_QUERY_KEY))); _activeWebView->close(); + _activeWebView = NULL; + } +} + +void OAuthWebViewHandler::handleReplyFinished(QNetworkReply* reply) { + if (_activeWebView && reply->error() != QNetworkReply::NoError) { + qDebug() << "Error loading" << reply->url() << "-" << reply->errorString(); + _activeWebView->close(); } } @@ -148,6 +158,7 @@ void OAuthWebViewHandler::handleURLChanged(const QUrl& url) { _activeWebView->show(); } else if (url.toString() == DEFAULT_NODE_AUTH_URL.toString() + "/login") { // this is a login request - we're going to close the webview and signal the AccountManager that we need a login + qDebug() << "data-server replied with login request. Signalling that login is required to proceed with OAuth."; _activeWebView->close(); AccountManager::getInstance().checkAndSignalForAccessToken(); } diff --git a/interface/src/ui/OAuthWebViewHandler.h b/interface/src/ui/OAuthWebViewHandler.h index 8f0c01c90d..1a95f17dfd 100644 --- a/interface/src/ui/OAuthWebViewHandler.h +++ b/interface/src/ui/OAuthWebViewHandler.h @@ -31,6 +31,7 @@ public slots: private slots: void handleSSLErrors(QNetworkReply* networkReply, const QList<QSslError>& errorList); void handleLoadFinished(bool success); + void handleReplyFinished(QNetworkReply* reply); void handleWebViewDestroyed(QObject* destroyedObject); void handleURLChanged(const QUrl& url); private: diff --git a/interface/src/ui/OctreeStatsDialog.cpp b/interface/src/ui/OctreeStatsDialog.cpp index 3296d8ccb2..afa799815f 100644 --- a/interface/src/ui/OctreeStatsDialog.cpp +++ b/interface/src/ui/OctreeStatsDialog.cpp @@ -365,13 +365,14 @@ void OctreeStatsDialog::showOctreeServersOfType(int& serverCount, NodeType_t ser QString incomingPacketsString = locale.toString((uint)stats.getIncomingPackets()); QString incomingBytesString = locale.toString((uint)stats.getIncomingBytes()); QString incomingWastedBytesString = locale.toString((uint)stats.getIncomingWastedBytes()); - QString incomingOutOfOrderString = locale.toString((uint)stats.getIncomingOutOfOrder()); - QString incomingLateString = locale.toString((uint)stats.getIncomingLate()); - QString incomingReallyLateString = locale.toString((uint)stats.getIncomingReallyLate()); - QString incomingEarlyString = locale.toString((uint)stats.getIncomingEarly()); - QString incomingLikelyLostString = locale.toString((uint)stats.getIncomingLikelyLost()); - QString incomingRecovered = locale.toString((uint)stats.getIncomingRecovered()); - QString incomingDuplicateString = locale.toString((uint)stats.getIncomingPossibleDuplicate()); + const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats(); + QString incomingOutOfOrderString = locale.toString((uint)seqStats.getNumOutOfOrder()); + QString incomingLateString = locale.toString((uint)seqStats.getNumLate()); + QString incomingUnreasonableString = locale.toString((uint)seqStats.getNumUnreasonable()); + QString incomingEarlyString = locale.toString((uint)seqStats.getNumEarly()); + QString incomingLikelyLostString = locale.toString((uint)seqStats.getNumLost()); + QString incomingRecovered = locale.toString((uint)seqStats.getNumRecovered()); + QString incomingDuplicateString = locale.toString((uint)seqStats.getNumDuplicate()); int clockSkewInMS = node->getClockSkewUsec() / (int)USECS_PER_MSEC; QString incomingFlightTimeString = locale.toString((int)stats.getIncomingFlightTimeAverage()); @@ -385,7 +386,7 @@ void OctreeStatsDialog::showOctreeServersOfType(int& serverCount, NodeType_t ser serverDetails << "<br/>" << " Out of Order: " << qPrintable(incomingOutOfOrderString) << "/ Early: " << qPrintable(incomingEarlyString) << "/ Late: " << qPrintable(incomingLateString) << - "/ Really Late: " << qPrintable(incomingReallyLateString) << + "/ Unreasonable: " << qPrintable(incomingUnreasonableString) << "/ Duplicate: " << qPrintable(incomingDuplicateString); serverDetails << "<br/>" << diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 5e6c6984eb..9c89826cb9 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -12,8 +12,9 @@ #include "Application.h" #include "Menu.h" -#include "PreferencesDialog.h" #include "ModelsBrowser.h" +#include "PreferencesDialog.h" +#include "UserActivityLogger.h" const int SCROLL_PANEL_BOTTOM_MARGIN = 30; const int OK_BUTTON_RIGHT_MARGIN = 30; @@ -29,6 +30,7 @@ PreferencesDialog::PreferencesDialog(QWidget* parent, Qt::WindowFlags flags) : F connect(ui.buttonBrowseHead, &QPushButton::clicked, this, &PreferencesDialog::openHeadModelBrowser); connect(ui.buttonBrowseBody, &QPushButton::clicked, this, &PreferencesDialog::openBodyModelBrowser); connect(ui.buttonBrowseLocation, &QPushButton::clicked, this, &PreferencesDialog::openSnapshotLocationBrowser); + connect(ui.buttonBrowseScriptsLocation, &QPushButton::clicked, this, &PreferencesDialog::openScriptsLocationBrowser); connect(ui.buttonReloadDefaultScripts, &QPushButton::clicked, Application::getInstance(), &Application::loadDefaultScripts); } @@ -72,13 +74,32 @@ void PreferencesDialog::openBodyModelBrowser() { void PreferencesDialog::openSnapshotLocationBrowser() { setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); + show(); + QString dir = QFileDialog::getExistingDirectory(this, tr("Snapshots Location"), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (!dir.isNull() && !dir.isEmpty()) { ui.snapshotLocationEdit->setText(dir); } + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + show(); +} + +void PreferencesDialog::openScriptsLocationBrowser() { + setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); + show(); + + QString dir = QFileDialog::getExistingDirectory(this, tr("Scripts Location"), + ui.scriptsLocationEdit->text(), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!dir.isNull() && !dir.isEmpty()) { + ui.scriptsLocationEdit->setText(dir); + } + + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + show(); } void PreferencesDialog::resizeEvent(QResizeEvent *resizeEvent) { @@ -118,6 +139,8 @@ void PreferencesDialog::loadPreferences() { ui.snapshotLocationEdit->setText(menuInstance->getSnapshotsLocation()); + ui.scriptsLocationEdit->setText(menuInstance->getScriptsLocation()); + ui.pupilDilationSlider->setValue(myAvatar->getHead()->getPupilDilation() * ui.pupilDilationSlider->maximum()); @@ -154,6 +177,7 @@ void PreferencesDialog::savePreferences() { QString displayNameStr(ui.displayNameEdit->text()); if (displayNameStr != _displayNameString) { myAvatar->setDisplayName(displayNameStr); + UserActivityLogger::getInstance().changedDisplayName(displayNameStr); shouldDispatchIdentityPacket = true; } @@ -161,6 +185,7 @@ void PreferencesDialog::savePreferences() { if (faceModelURL.toString() != _faceURLString) { // change the faceModelURL in the profile, it will also update this user's BlendFace myAvatar->setFaceModelURL(faceModelURL); + UserActivityLogger::getInstance().changedModel("head", faceModelURL.toString()); shouldDispatchIdentityPacket = true; } @@ -168,6 +193,7 @@ void PreferencesDialog::savePreferences() { if (skeletonModelURL.toString() != _skeletonURLString) { // change the skeletonModelURL in the profile, it will also update this user's Body myAvatar->setSkeletonModelURL(skeletonModelURL); + UserActivityLogger::getInstance().changedModel("skeleton", skeletonModelURL.toString()); shouldDispatchIdentityPacket = true; } @@ -180,6 +206,10 @@ void PreferencesDialog::savePreferences() { Menu::getInstance()->setSnapshotsLocation(ui.snapshotLocationEdit->text()); } + if (!ui.scriptsLocationEdit->text().isEmpty() && QDir(ui.scriptsLocationEdit->text()).exists()) { + Menu::getInstance()->setScriptsLocation(ui.scriptsLocationEdit->text()); + } + myAvatar->getHead()->setPupilDilation(ui.pupilDilationSlider->value() / (float)ui.pupilDilationSlider->maximum()); myAvatar->setLeanScale(ui.leanScaleSpin->value()); myAvatar->setClampedTargetScale(ui.avatarScaleSpin->value()); diff --git a/interface/src/ui/PreferencesDialog.h b/interface/src/ui/PreferencesDialog.h index c52986cc5c..0573304eda 100644 --- a/interface/src/ui/PreferencesDialog.h +++ b/interface/src/ui/PreferencesDialog.h @@ -42,6 +42,7 @@ private slots: void setHeadUrl(QString modelUrl); void setSkeletonUrl(QString modelUrl); void openSnapshotLocationBrowser(); + void openScriptsLocationBrowser(); }; diff --git a/interface/src/ui/RunningScriptsWidget.cpp b/interface/src/ui/RunningScriptsWidget.cpp index 9bef454829..8a7ebcbfd4 100644 --- a/interface/src/ui/RunningScriptsWidget.cpp +++ b/interface/src/ui/RunningScriptsWidget.cpp @@ -3,6 +3,7 @@ // interface/src/ui // // Created by Mohammed Nafees on 03/28/2014. +// Updated by Ryan Huffman on 05/13/2014. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -12,18 +13,27 @@ #include "ui_runningScriptsWidget.h" #include "RunningScriptsWidget.h" +#include <QAbstractProxyModel> #include <QFileInfo> #include <QKeyEvent> #include <QPainter> #include <QTableWidgetItem> #include "Application.h" +#include "Menu.h" +#include "ScriptsModel.h" + RunningScriptsWidget::RunningScriptsWidget(QWidget* parent) : FramelessDialog(parent, 0, POSITION_LEFT), - ui(new Ui::RunningScriptsWidget) { + ui(new Ui::RunningScriptsWidget), + _signalMapper(this), + _proxyModel(this), + _scriptsModel(this) { ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, false); + setAllowResize(false); ui->hideWidgetButton->setIcon(QIcon(Application::resourcesPath() + "images/close.svg")); @@ -31,17 +41,24 @@ RunningScriptsWidget::RunningScriptsWidget(QWidget* parent) : ui->stopAllButton->setIcon(QIcon(Application::resourcesPath() + "images/stop.svg")); ui->loadScriptButton->setIcon(QIcon(Application::resourcesPath() + "images/plus-white.svg")); - _runningScriptsTable = new ScriptsTableWidget(ui->runningScriptsTableWidget); - _runningScriptsTable->setColumnCount(2); - _runningScriptsTable->setColumnWidth(0, 245); - _runningScriptsTable->setColumnWidth(1, 22); - connect(_runningScriptsTable, &QTableWidget::cellClicked, this, &RunningScriptsWidget::stopScript); + ui->recentlyLoadedScriptsArea->hide(); + + ui->filterLineEdit->installEventFilter(this); + + connect(&_proxyModel, &QSortFilterProxyModel::modelReset, + this, &RunningScriptsWidget::selectFirstInList); + + _proxyModel.setSourceModel(&_scriptsModel); + _proxyModel.sort(0, Qt::AscendingOrder); + _proxyModel.setDynamicSortFilter(true); + ui->scriptListView->setModel(&_proxyModel); + + connect(ui->filterLineEdit, &QLineEdit::textChanged, this, &RunningScriptsWidget::updateFileFilter); + connect(ui->scriptListView, &QListView::doubleClicked, this, &RunningScriptsWidget::loadScriptFromList); _recentlyLoadedScriptsTable = new ScriptsTableWidget(ui->recentlyLoadedScriptsTableWidget); _recentlyLoadedScriptsTable->setColumnCount(1); _recentlyLoadedScriptsTable->setColumnWidth(0, 265); - connect(_recentlyLoadedScriptsTable, &QTableWidget::cellClicked, - this, &RunningScriptsWidget::loadScript); connect(ui->hideWidgetButton, &QPushButton::clicked, Application::getInstance(), &Application::toggleRunningScriptsWidget); @@ -51,59 +68,121 @@ RunningScriptsWidget::RunningScriptsWidget(QWidget* parent) : this, &RunningScriptsWidget::allScriptsStopped); connect(ui->loadScriptButton, &QPushButton::clicked, Application::getInstance(), &Application::loadDialog); + connect(&_signalMapper, SIGNAL(mapped(QString)), Application::getInstance(), SLOT(stopScript(const QString&))); } RunningScriptsWidget::~RunningScriptsWidget() { delete ui; } +void RunningScriptsWidget::updateFileFilter(const QString& filter) { + QRegExp regex("^.*" + QRegExp::escape(filter) + ".*$", Qt::CaseInsensitive); + _proxyModel.setFilterRegExp(regex); + selectFirstInList(); +} + +void RunningScriptsWidget::loadScriptFromList(const QModelIndex& index) { + QVariant scriptFile = _proxyModel.data(index, ScriptsModel::ScriptPath); + Application::getInstance()->loadScript(scriptFile.toString(), false, false); +} + +void RunningScriptsWidget::loadSelectedScript() { + QModelIndex selectedIndex = ui->scriptListView->currentIndex(); + if (selectedIndex.isValid()) { + loadScriptFromList(selectedIndex); + } +} + void RunningScriptsWidget::setBoundary(const QRect& rect) { _boundary = rect; } void RunningScriptsWidget::setRunningScripts(const QStringList& list) { - _runningScriptsTable->setRowCount(list.size()); + setUpdatesEnabled(false); + QLayoutItem* widget; + while ((widget = ui->scrollAreaWidgetContents->layout()->takeAt(0)) != NULL) { + delete widget->widget(); + delete widget; + } + const int CLOSE_ICON_HEIGHT = 12; + for (int i = 0; i < list.size(); i++) { + QWidget* row = new QWidget(ui->scrollAreaWidgetContents); + row->setLayout(new QHBoxLayout(row)); + + QUrl url = QUrl(list.at(i)); + QLabel* name = new QLabel(url.fileName(), row); + QPushButton* closeButton = new QPushButton(row); + closeButton->setFlat(true); + closeButton->setIcon( + QIcon(QPixmap(Application::resourcesPath() + "images/kill-script.svg").scaledToHeight(CLOSE_ICON_HEIGHT))); + closeButton->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred)); + closeButton->setStyleSheet("border: 0;"); + closeButton->setCursor(Qt::PointingHandCursor); + + connect(closeButton, SIGNAL(clicked()), &_signalMapper, SLOT(map())); + _signalMapper.setMapping(closeButton, url.toString()); + + row->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); + + row->layout()->setContentsMargins(4, 4, 4, 4); + row->layout()->setSpacing(0); + + row->layout()->addWidget(name); + row->layout()->addWidget(closeButton); + + row->setToolTip(url.toString()); + + QFrame* line = new QFrame(row); + line->setFrameShape(QFrame::HLine); + line->setStyleSheet("color: #E1E1E1; margin-left: 6px; margin-right: 6px;"); + + ui->scrollAreaWidgetContents->layout()->addWidget(row); + ui->scrollAreaWidgetContents->layout()->addWidget(line); + } + ui->noRunningScriptsLabel->setVisible(list.isEmpty()); - ui->currentlyRunningLabel->setVisible(!list.isEmpty()); - ui->runningScriptsTableWidget->setVisible(!list.isEmpty()); ui->reloadAllButton->setVisible(!list.isEmpty()); ui->stopAllButton->setVisible(!list.isEmpty()); - const int CLOSE_ICON_HEIGHT = 12; + ui->scrollAreaWidgetContents->updateGeometry(); + setUpdatesEnabled(true); + Application::processEvents(); + repaint(); +} - for (int i = 0; i < list.size(); ++i) { - QTableWidgetItem *scriptName = new QTableWidgetItem; - scriptName->setText(QFileInfo(list.at(i)).fileName()); - scriptName->setToolTip(list.at(i)); - scriptName->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); - QTableWidgetItem *closeIcon = new QTableWidgetItem; - closeIcon->setIcon(QIcon(QPixmap(Application::resourcesPath() + "images/kill-script.svg").scaledToHeight(CLOSE_ICON_HEIGHT))); - - _runningScriptsTable->setItem(i, 0, scriptName); - _runningScriptsTable->setItem(i, 1, closeIcon); +void RunningScriptsWidget::showEvent(QShowEvent* event) { + if (!event->spontaneous()) { + ui->filterLineEdit->setFocus(); } - const int RUNNING_SCRIPTS_TABLE_LEFT_MARGIN = 12; - const int RECENTLY_LOADED_TOP_MARGIN = 61; - const int RECENTLY_LOADED_LABEL_TOP_MARGIN = 19; + FramelessDialog::showEvent(event); +} - int y = ui->runningScriptsTableWidget->y() + RUNNING_SCRIPTS_TABLE_LEFT_MARGIN; - for (int i = 0; i < _runningScriptsTable->rowCount(); ++i) { - y += _runningScriptsTable->rowHeight(i); +void RunningScriptsWidget::selectFirstInList() { + if (_proxyModel.rowCount() > 0) { + ui->scriptListView->setCurrentIndex(_proxyModel.index(0, 0)); + } +} + +bool RunningScriptsWidget::eventFilter(QObject* sender, QEvent* event) { + if (sender == ui->filterLineEdit) { + if (event->type() != QEvent::KeyPress) { + return false; + } + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { + QModelIndex selectedIndex = ui->scriptListView->currentIndex(); + if (selectedIndex.isValid()) { + loadScriptFromList(selectedIndex); + } + event->accept(); + return true; + } + return false; } - ui->runningScriptsTableWidget->resize(ui->runningScriptsTableWidget->width(), y - RUNNING_SCRIPTS_TABLE_LEFT_MARGIN); - _runningScriptsTable->resize(_runningScriptsTable->width(), y - RUNNING_SCRIPTS_TABLE_LEFT_MARGIN); - ui->reloadAllButton->move(ui->reloadAllButton->x(), y); - ui->stopAllButton->move(ui->stopAllButton->x(), y); - ui->recentlyLoadedLabel->move(ui->recentlyLoadedLabel->x(), - ui->stopAllButton->y() + ui->stopAllButton->height() + RECENTLY_LOADED_TOP_MARGIN); - ui->recentlyLoadedScriptsTableWidget->move(ui->recentlyLoadedScriptsTableWidget->x(), - ui->recentlyLoadedLabel->y() + RECENTLY_LOADED_LABEL_TOP_MARGIN); - - - createRecentlyLoadedScriptsTable(); + return FramelessDialog::eventFilter(sender, event); } void RunningScriptsWidget::keyPressEvent(QKeyEvent *keyEvent) { @@ -114,79 +193,10 @@ void RunningScriptsWidget::keyPressEvent(QKeyEvent *keyEvent) { } } -void RunningScriptsWidget::paintEvent(QPaintEvent* event) { - QPainter painter(this); - painter.setPen(QColor::fromRgb(225, 225, 225)); // #e1e1e1 - - if (ui->currentlyRunningLabel->isVisible()) { - // line below the 'Currently Running' label - painter.drawLine(36, ui->currentlyRunningLabel->y() + ui->currentlyRunningLabel->height(), - 300, ui->currentlyRunningLabel->y() + ui->currentlyRunningLabel->height()); - } - - if (ui->recentlyLoadedLabel->isVisible()) { - // line below the 'Recently loaded' label - painter.drawLine(36, ui->recentlyLoadedLabel->y() + ui->recentlyLoadedLabel->height(), - 300, ui->recentlyLoadedLabel->y() + ui->recentlyLoadedLabel->height()); - } - - painter.end(); -} - void RunningScriptsWidget::scriptStopped(const QString& scriptName) { - _recentlyLoadedScripts.prepend(scriptName); -} - -void RunningScriptsWidget::stopScript(int row, int column) { - if (column == 1) { // make sure the user has clicked on the close icon - _lastStoppedScript = _runningScriptsTable->item(row, 0)->toolTip(); - emit stopScriptName(_runningScriptsTable->item(row, 0)->toolTip()); - } -} - -void RunningScriptsWidget::loadScript(int row, int column) { - Application::getInstance()->loadScript(_recentlyLoadedScriptsTable->item(row, column)->toolTip()); + // _recentlyLoadedScripts.prepend(scriptName); } void RunningScriptsWidget::allScriptsStopped() { Application::getInstance()->stopAllScripts(); } - -void RunningScriptsWidget::createRecentlyLoadedScriptsTable() { - if (!_recentlyLoadedScripts.contains(_lastStoppedScript) && !_lastStoppedScript.isEmpty()) { - _recentlyLoadedScripts.prepend(_lastStoppedScript); - _lastStoppedScript = ""; - } - - for (int i = 0; i < _recentlyLoadedScripts.size(); ++i) { - if (Application::getInstance()->getRunningScripts().contains(_recentlyLoadedScripts.at(i))) { - _recentlyLoadedScripts.removeOne(_recentlyLoadedScripts.at(i)); - } - } - - ui->recentlyLoadedLabel->setVisible(!_recentlyLoadedScripts.isEmpty()); - ui->recentlyLoadedScriptsTableWidget->setVisible(!_recentlyLoadedScripts.isEmpty()); - ui->recentlyLoadedInstruction->setVisible(!_recentlyLoadedScripts.isEmpty()); - - int limit = _recentlyLoadedScripts.size() > 9 ? 9 : _recentlyLoadedScripts.size(); - _recentlyLoadedScriptsTable->setRowCount(limit); - for (int i = 0; i < limit; i++) { - QTableWidgetItem *scriptName = new QTableWidgetItem; - scriptName->setText(QString::number(i + 1) + ". " + QFileInfo(_recentlyLoadedScripts.at(i)).fileName()); - scriptName->setToolTip(_recentlyLoadedScripts.at(i)); - scriptName->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); - - _recentlyLoadedScriptsTable->setItem(i, 0, scriptName); - } - - int y = ui->recentlyLoadedScriptsTableWidget->y() + 15; - for (int i = 0; i < _recentlyLoadedScriptsTable->rowCount(); ++i) { - y += _recentlyLoadedScriptsTable->rowHeight(i); - } - - ui->recentlyLoadedInstruction->setGeometry(36, y, - ui->recentlyLoadedInstruction->width(), - ui->recentlyLoadedInstruction->height()); - - repaint(); -} diff --git a/interface/src/ui/RunningScriptsWidget.h b/interface/src/ui/RunningScriptsWidget.h index 14a1f4a58e..6810aca487 100644 --- a/interface/src/ui/RunningScriptsWidget.h +++ b/interface/src/ui/RunningScriptsWidget.h @@ -3,6 +3,7 @@ // interface/src/ui // // Created by Mohammed Nafees on 03/28/2014. +// Updated by Ryan Huffman on 05/13/2014. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -12,6 +13,11 @@ #ifndef hifi_RunningScriptsWidget_h #define hifi_RunningScriptsWidget_h +#include <QFileSystemModel> +#include <QSignalMapper> +#include <QSortFilterProxyModel> + +#include "ScriptsModel.h" #include "FramelessDialog.h" #include "ScriptsTableWidget.h" @@ -32,27 +38,31 @@ signals: void stopScriptName(const QString& name); protected: + virtual bool eventFilter(QObject* sender, QEvent* event); + virtual void keyPressEvent(QKeyEvent* event); - virtual void paintEvent(QPaintEvent* event); + virtual void showEvent(QShowEvent* event); public slots: void scriptStopped(const QString& scriptName); void setBoundary(const QRect& rect); private slots: - void stopScript(int row, int column); - void loadScript(int row, int column); void allScriptsStopped(); + void updateFileFilter(const QString& filter); + void loadScriptFromList(const QModelIndex& index); + void loadSelectedScript(); + void selectFirstInList(); private: Ui::RunningScriptsWidget* ui; - ScriptsTableWidget* _runningScriptsTable; + QSignalMapper _signalMapper; + QSortFilterProxyModel _proxyModel; + ScriptsModel _scriptsModel; ScriptsTableWidget* _recentlyLoadedScriptsTable; QStringList _recentlyLoadedScripts; QString _lastStoppedScript; QRect _boundary; - - void createRecentlyLoadedScriptsTable(); }; #endif // hifi_RunningScriptsWidget_h diff --git a/interface/src/ui/ScriptEditorWidget.cpp b/interface/src/ui/ScriptEditorWidget.cpp index be5577e0e8..513bbd899a 100644 --- a/interface/src/ui/ScriptEditorWidget.cpp +++ b/interface/src/ui/ScriptEditorWidget.cpp @@ -28,8 +28,12 @@ ScriptEditorWidget::ScriptEditorWidget() : _scriptEditorWidgetUI(new Ui::ScriptEditorWidget), - _scriptEngine(NULL) + _scriptEngine(NULL), + _isRestarting(false), + _isReloading(false) { + setAttribute(Qt::WA_DeleteOnClose); + _scriptEditorWidgetUI->setupUi(this); connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::modificationChanged, this, @@ -51,15 +55,19 @@ ScriptEditorWidget::~ScriptEditorWidget() { } void ScriptEditorWidget::onScriptModified() { - if(_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isRunning()) { + if(_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isModified() && isRunning() && !_isReloading) { + _isRestarting = true; setRunning(false); - setRunning(true); + // Script is restarted once current script instance finishes. } } -void ScriptEditorWidget::onScriptEnding() { - // signals will automatically be disonnected when the _scriptEngine is deleted later +void ScriptEditorWidget::onScriptFinished(const QString& scriptPath) { _scriptEngine = NULL; + if (_isRestarting) { + _isRestarting = false; + setRunning(true); + } } bool ScriptEditorWidget::isModified() { @@ -71,27 +79,28 @@ bool ScriptEditorWidget::isRunning() { } bool ScriptEditorWidget::setRunning(bool run) { - if (run && !save()) { + if (run && isModified() && !save()) { return false; } - // Clean-up old connections. if (_scriptEngine != NULL) { disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &ScriptEditorWidget::onScriptError); disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &ScriptEditorWidget::onScriptPrint); - disconnect(_scriptEngine, &ScriptEngine::scriptEnding, this, &ScriptEditorWidget::onScriptEnding); + disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); } if (run) { - _scriptEngine = Application::getInstance()->loadScript(_currentScript, true); + const QString& scriptURLString = QUrl(_currentScript).toString(); + _scriptEngine = Application::getInstance()->loadScript(scriptURLString, true); connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - - // Make new connections. connect(_scriptEngine, &ScriptEngine::errorMessage, this, &ScriptEditorWidget::onScriptError); connect(_scriptEngine, &ScriptEngine::printedMessage, this, &ScriptEditorWidget::onScriptPrint); - connect(_scriptEngine, &ScriptEngine::scriptEnding, this, &ScriptEditorWidget::onScriptEnding); + connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); } else { + connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); Application::getInstance()->stopScript(_currentScript); _scriptEngine = NULL; } @@ -108,13 +117,14 @@ bool ScriptEditorWidget::saveFile(const QString &scriptPath) { QTextStream out(&file); out << _scriptEditorWidgetUI->scriptEdit->toPlainText(); + file.close(); setScriptFile(scriptPath); return true; } void ScriptEditorWidget::loadFile(const QString& scriptPath) { - QUrl url(scriptPath); + QUrl url(scriptPath); // if the scheme length is one or lower, maybe they typed in a file, let's try const int WINDOWS_DRIVE_LETTER_SIZE = 1; @@ -127,13 +137,15 @@ void ScriptEditorWidget::loadFile(const QString& scriptPath) { } QTextStream in(&file); _scriptEditorWidgetUI->scriptEdit->setPlainText(in.readAll()); + file.close(); setScriptFile(scriptPath); if (_scriptEngine != NULL) { disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &ScriptEditorWidget::onScriptError); disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &ScriptEditorWidget::onScriptPrint); - disconnect(_scriptEngine, &ScriptEngine::scriptEnding, this, &ScriptEditorWidget::onScriptEnding); + disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); } } else { QNetworkAccessManager* networkManager = new QNetworkAccessManager(this); @@ -148,12 +160,14 @@ void ScriptEditorWidget::loadFile(const QString& scriptPath) { } } - _scriptEngine = Application::getInstance()->getScriptEngine(_currentScript); + const QString& scriptURLString = QUrl(_currentScript).toString(); + _scriptEngine = Application::getInstance()->getScriptEngine(scriptURLString); if (_scriptEngine != NULL) { connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); connect(_scriptEngine, &ScriptEngine::errorMessage, this, &ScriptEditorWidget::onScriptError); connect(_scriptEngine, &ScriptEngine::printedMessage, this, &ScriptEditorWidget::onScriptPrint); - connect(_scriptEngine, &ScriptEngine::scriptEnding, this, &ScriptEditorWidget::onScriptEnding); + connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); } } @@ -175,6 +189,7 @@ bool ScriptEditorWidget::saveAs() { void ScriptEditorWidget::setScriptFile(const QString& scriptPath) { _currentScript = scriptPath; + _currentScriptModified = QFileInfo(_currentScript).lastModified(); _scriptEditorWidgetUI->scriptEdit->document()->setModified(false); setWindowModified(false); @@ -198,3 +213,29 @@ void ScriptEditorWidget::onScriptError(const QString& message) { void ScriptEditorWidget::onScriptPrint(const QString& message) { _scriptEditorWidgetUI->debugText->appendPlainText("> " + message); } + +void ScriptEditorWidget::onWindowActivated() { + if (!_isReloading) { + _isReloading = true; + + if (QFileInfo(_currentScript).lastModified() > _currentScriptModified) { + if (static_cast<ScriptEditorWindow*>(this->parent()->parent()->parent())->autoReloadScripts() + || QMessageBox::warning(this, _currentScript, + tr("This file has been modified outside of the Interface editor.") + "\n\n" + + (isModified() + ? tr("Do you want to reload it and lose the changes you've made in the Interface editor?") + : tr("Do you want to reload it?")), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + loadFile(_currentScript); + if (_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isRunning()) { + _isRestarting = true; + setRunning(false); + // Script is restarted once current script instance finishes. + } + + } + } + + _isReloading = false; + } +} diff --git a/interface/src/ui/ScriptEditorWidget.h b/interface/src/ui/ScriptEditorWidget.h index 3e95ea322b..8dd847ee6d 100644 --- a/interface/src/ui/ScriptEditorWidget.h +++ b/interface/src/ui/ScriptEditorWidget.h @@ -13,6 +13,7 @@ #define hifi_ScriptEditorWidget_h #include <QDockWidget> + #include "ScriptEngine.h" namespace Ui { @@ -42,16 +43,22 @@ signals: void scriptnameChanged(); void scriptModified(); +public slots: + void onWindowActivated(); + private slots: void onScriptError(const QString& message); void onScriptPrint(const QString& message); void onScriptModified(); - void onScriptEnding(); + void onScriptFinished(const QString& scriptName); private: Ui::ScriptEditorWidget* _scriptEditorWidgetUI; ScriptEngine* _scriptEngine; QString _currentScript; + QDateTime _currentScriptModified; + bool _isRestarting; + bool _isReloading; }; #endif // hifi_ScriptEditorWidget_h diff --git a/interface/src/ui/ScriptEditorWindow.cpp b/interface/src/ui/ScriptEditorWindow.cpp index 3f63f0741b..895d725699 100644 --- a/interface/src/ui/ScriptEditorWindow.cpp +++ b/interface/src/ui/ScriptEditorWindow.cpp @@ -36,6 +36,8 @@ ScriptEditorWindow::ScriptEditorWindow() : _loadMenu(new QMenu), _saveMenu(new QMenu) { + setAttribute(Qt::WA_DeleteOnClose); + _ScriptEditorWindowUI->setupUi(this); this->setWindowFlags(Qt::Tool); show(); @@ -140,6 +142,7 @@ ScriptEditorWidget* ScriptEditorWindow::addScriptEditorWidget(QString title) { connect(newScriptEditorWidget, &ScriptEditorWidget::scriptnameChanged, this, &ScriptEditorWindow::updateScriptNameOrStatus); connect(newScriptEditorWidget, &ScriptEditorWidget::scriptModified, this, &ScriptEditorWindow::updateScriptNameOrStatus); connect(newScriptEditorWidget, &ScriptEditorWidget::runningStateChanged, this, &ScriptEditorWindow::updateButtons); + connect(this, &ScriptEditorWindow::windowActivated, newScriptEditorWidget, &ScriptEditorWidget::onWindowActivated); _ScriptEditorWindowUI->tabWidget->addTab(newScriptEditorWidget, title); _ScriptEditorWindowUI->tabWidget->setCurrentWidget(newScriptEditorWidget); newScriptEditorWidget->setUpdatesEnabled(true); @@ -216,3 +219,15 @@ void ScriptEditorWindow::terminateCurrentTab() { this->raise(); } } + +bool ScriptEditorWindow::autoReloadScripts() { + return _ScriptEditorWindowUI->autoReloadCheckBox->isChecked(); +} + +bool ScriptEditorWindow::event(QEvent* event) { + if (event->type() == QEvent::WindowActivate) { + emit windowActivated(); + } + return QWidget::event(event); +} + diff --git a/interface/src/ui/ScriptEditorWindow.h b/interface/src/ui/ScriptEditorWindow.h index 360e902cc2..1915014b69 100644 --- a/interface/src/ui/ScriptEditorWindow.h +++ b/interface/src/ui/ScriptEditorWindow.h @@ -26,9 +26,14 @@ public: ~ScriptEditorWindow(); void terminateCurrentTab(); + bool autoReloadScripts(); + +signals: + void windowActivated(); protected: void closeEvent(QCloseEvent* event); + virtual bool event(QEvent* event); private: Ui::ScriptEditorWindow* _ScriptEditorWindowUI; diff --git a/interface/src/ui/ScriptsTableWidget.cpp b/interface/src/ui/ScriptsTableWidget.cpp index 95acca052c..7b4f9e6b1f 100644 --- a/interface/src/ui/ScriptsTableWidget.cpp +++ b/interface/src/ui/ScriptsTableWidget.cpp @@ -23,7 +23,7 @@ ScriptsTableWidget::ScriptsTableWidget(QWidget* parent) : setShowGrid(false); setSelectionMode(QAbstractItemView::NoSelection); setEditTriggers(QAbstractItemView::NoEditTriggers); - setStyleSheet("QTableWidget { background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }"); + setStyleSheet("QTableWidget { border: none; background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }"); setToolTipDuration(200); setWordWrap(true); setGeometry(0, 0, parent->width(), parent->height()); diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index fa62ecdb9b..379dd35df7 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -286,11 +286,16 @@ void Stats::display( pingVoxel = totalPingVoxel/voxelServerCount; } - lines = _expanded ? 4 : 3; + + Audio* audio = Application::getInstance()->getAudio(); + const AudioStreamStats& audioMixerAvatarStreamStats = audio->getAudioMixerAvatarStreamStats(); + const QHash<QUuid, AudioStreamStats>& audioMixerInjectedStreamStatsMap = audio->getAudioMixerInjectedStreamStatsMap(); + + lines = _expanded ? 10 + audioMixerInjectedStreamStatsMap.size(): 3; drawBackground(backgroundColor, horizontalOffset, 0, _pingStatsWidth, lines * STATS_PELS_PER_LINE + 10); horizontalOffset += 5; - Audio* audio = Application::getInstance()->getAudio(); + char audioJitter[30]; sprintf(audioJitter, @@ -299,10 +304,9 @@ void Stats::display( (float) audio->getNetworkSampleRate() * 1000.f); drawText(30, glWidget->height() - 22, scale, rotation, font, audioJitter, color); - + char audioPing[30]; sprintf(audioPing, "Audio ping: %d", pingAudio); - char avatarPing[30]; sprintf(avatarPing, "Avatar ping: %d", pingAvatar); @@ -322,12 +326,54 @@ void Stats::display( verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, voxelMaxPing, color); + + char audioMixerStatsLabelString[] = "AudioMixer stats:"; + char streamStatsFormatLabelString[] = "early/late/lost, jframes"; + + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, audioMixerStatsLabelString, color); + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, streamStatsFormatLabelString, color); + + + char downstreamLabelString[] = " Downstream:"; + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, downstreamLabelString, color); + + const SequenceNumberStats& downstreamAudioSequenceNumberStats = audio->getIncomingMixedAudioSequenceNumberStats(); + char downstreamAudioStatsString[30]; + sprintf(downstreamAudioStatsString, " mix: %d/%d/%d, %d", downstreamAudioSequenceNumberStats.getNumEarly(), + downstreamAudioSequenceNumberStats.getNumLate(), downstreamAudioSequenceNumberStats.getNumLost(), + audio->getJitterBufferSamples() / NETWORK_BUFFER_LENGTH_SAMPLES_STEREO); + + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, downstreamAudioStatsString, color); + + char upstreamLabelString[] = " Upstream:"; + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, upstreamLabelString, color); + + char upstreamAudioStatsString[30]; + sprintf(upstreamAudioStatsString, " mic: %d/%d/%d, %d", audioMixerAvatarStreamStats._packetsEarly, + audioMixerAvatarStreamStats._packetsLate, audioMixerAvatarStreamStats._packetsLost, + audioMixerAvatarStreamStats._jitterBufferFrames); + + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, upstreamAudioStatsString, color); + + foreach(AudioStreamStats injectedStreamStats, audioMixerInjectedStreamStatsMap) { + sprintf(upstreamAudioStatsString, " inj: %d/%d/%d, %d", injectedStreamStats._packetsEarly, + injectedStreamStats._packetsLate, injectedStreamStats._packetsLost, injectedStreamStats._jitterBufferFrames); + + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, upstreamAudioStatsString, color); + } } verticalOffset = 0; horizontalOffset = _lastHorizontalOffset + _generalStatsWidth + _pingStatsWidth + 2; } - + MyAvatar* myAvatar = Application::getInstance()->getAvatar(); glm::vec3 avatarPos = myAvatar->getPosition(); diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp new file mode 100644 index 0000000000..40de565155 --- /dev/null +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -0,0 +1,136 @@ +// +// BillboardOverlay.cpp +// +// +// Created by Clement on 7/1/14. +// Copyright 2014 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 "../../Application.h" + +#include "BillboardOverlay.h" + +BillboardOverlay::BillboardOverlay() +: _manager(NULL), + _scale(1.0f), + _isFacingAvatar(true) { +} + +void BillboardOverlay::render() { + if (_billboard.isEmpty()) { + return; + } + if (!_billboardTexture) { + QImage image = QImage::fromData(_billboard); + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + _size = image.size(); + _billboardTexture.reset(new Texture()); + glBindTexture(GL_TEXTURE_2D, _billboardTexture->getID()); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _size.width(), _size.height(), 0, + GL_BGRA, GL_UNSIGNED_BYTE, image.constBits()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + } else { + glBindTexture(GL_TEXTURE_2D, _billboardTexture->getID()); + } + + glEnable(GL_ALPHA_TEST); + glAlphaFunc(GL_GREATER, 0.5f); + + glEnable(GL_TEXTURE_2D); + glDisable(GL_LIGHTING); + + glPushMatrix(); { + glTranslatef(_position.x, _position.y, _position.z); + if (_isFacingAvatar) { + // rotate about vertical to face the camera + glm::quat rotation = Application::getInstance()->getCamera()->getRotation(); + rotation *= glm::angleAxis(glm::pi<float>(), glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); + } else { + glm::vec3 axis = glm::axis(_rotation); + glRotatef(glm::degrees(glm::angle(_rotation)), axis.x, axis.y, axis.z); + } + glScalef(_scale, _scale, _scale); + + float maxSize = glm::max(_size.width(), _size.height()); + float x = _size.width() / (2.0f * maxSize); + float y = -_size.height() / (2.0f * maxSize); + + glColor3f(1.0f, 1.0f, 1.0f); + glBegin(GL_QUADS); { + glTexCoord2f(0.0f, 0.0f); + glVertex2f(-x, -y); + glTexCoord2f(1.0f, 0.0f); + glVertex2f(x, -y); + glTexCoord2f(1.0f, 1.0f); + glVertex2f(x, y); + glTexCoord2f(0.0f, 1.0f); + glVertex2f(-x, y); + } glEnd(); + + } glPopMatrix(); + + glDisable(GL_TEXTURE_2D); + glEnable(GL_LIGHTING); + glDisable(GL_ALPHA_TEST); + + glBindTexture(GL_TEXTURE_2D, 0); +} + +void BillboardOverlay::setProperties(const QScriptValue &properties) { + Base3DOverlay::setProperties(properties); + + QScriptValue urlValue = properties.property("url"); + if (urlValue.isValid()) { + _url = urlValue.toVariant().toString(); + + setBillboardURL(_url); + } + + QScriptValue scaleValue = properties.property("scale"); + if (scaleValue.isValid()) { + _scale = scaleValue.toVariant().toFloat(); + } + + QScriptValue rotationValue = properties.property("rotation"); + if (rotationValue.isValid()) { + QScriptValue x = rotationValue.property("x"); + QScriptValue y = rotationValue.property("y"); + QScriptValue z = rotationValue.property("z"); + QScriptValue w = rotationValue.property("w"); + if (x.isValid() && y.isValid() && z.isValid() && w.isValid()) { + _rotation.x = x.toVariant().toFloat(); + _rotation.y = y.toVariant().toFloat(); + _rotation.z = z.toVariant().toFloat(); + _rotation.w = w.toVariant().toFloat(); + } + } + + QScriptValue isFacingAvatarValue = properties.property("isFacingAvatar"); + if (isFacingAvatarValue.isValid()) { + _isFacingAvatar = isFacingAvatarValue.toVariant().toBool(); + } +} + +// TODO: handle setting image multiple times, how do we manage releasing the bound texture? +void BillboardOverlay::setBillboardURL(const QUrl url) { + // TODO: are we creating too many QNetworkAccessManager() when multiple calls to setImageURL are made? + _manager->deleteLater(); + _manager = new QNetworkAccessManager(); + connect(_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*))); + _manager->get(QNetworkRequest(url)); +} + +void BillboardOverlay::replyFinished(QNetworkReply* reply) { + // replace our byte array with the downloaded data + _billboard = reply->readAll(); + _manager->deleteLater(); + _manager = NULL; +} diff --git a/interface/src/ui/overlays/BillboardOverlay.h b/interface/src/ui/overlays/BillboardOverlay.h new file mode 100644 index 0000000000..473e8a066f --- /dev/null +++ b/interface/src/ui/overlays/BillboardOverlay.h @@ -0,0 +1,46 @@ +// +// BillboardOverlay.h +// +// +// Created by Clement on 7/1/14. +// Copyright 2014 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_BillboardOverlay_h +#define hifi_BillboardOverlay_h + +#include <QScopedPointer> +#include <QUrl> + +#include "Base3DOverlay.h" +#include "../../renderer/TextureCache.h" + +class BillboardOverlay : public Base3DOverlay { + Q_OBJECT +public: + BillboardOverlay(); + + virtual void render(); + virtual void setProperties(const QScriptValue& properties); + +private slots: + void replyFinished(QNetworkReply* reply); + +private: + void setBillboardURL(const QUrl url); + + QNetworkAccessManager* _manager; + QUrl _url; + QByteArray _billboard; + QSize _size; + QScopedPointer<Texture> _billboardTexture; + + glm::quat _rotation; + float _scale; + bool _isFacingAvatar; +}; + +#endif // hifi_BillboardOverlay_h \ No newline at end of file diff --git a/interface/src/ui/overlays/ImageOverlay.cpp b/interface/src/ui/overlays/ImageOverlay.cpp index aa4766488a..79b1b23de5 100644 --- a/interface/src/ui/overlays/ImageOverlay.cpp +++ b/interface/src/ui/overlays/ImageOverlay.cpp @@ -19,7 +19,7 @@ #include "ImageOverlay.h" ImageOverlay::ImageOverlay() : - _manager(0), + _manager(NULL), _textureID(0), _renderImage(false), _textureBound(false), @@ -37,6 +37,7 @@ ImageOverlay::~ImageOverlay() { // TODO: handle setting image multiple times, how do we manage releasing the bound texture? void ImageOverlay::setImageURL(const QUrl& url) { // TODO: are we creating too many QNetworkAccessManager() when multiple calls to setImageURL are made? + _manager->deleteLater(); _manager = new QNetworkAccessManager(); connect(_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*))); _manager->get(QNetworkRequest(url)); @@ -49,6 +50,7 @@ void ImageOverlay::replyFinished(QNetworkReply* reply) { _textureImage.loadFromData(rawData); _renderImage = true; _manager->deleteLater(); + _manager = NULL; } void ImageOverlay::render() { diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp new file mode 100644 index 0000000000..bc0cc720c2 --- /dev/null +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -0,0 +1,115 @@ +// +// ModelOverlay.cpp +// +// +// Created by Clement on 6/30/14. +// Copyright 2014 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 "../../Menu.h" + +#include "ModelOverlay.h" + +ModelOverlay::ModelOverlay() + : _model(), + _scale(1.0f), + _updateModel(false) { + _model.init(); +} + +void ModelOverlay::update(float deltatime) { + if (_updateModel) { + _updateModel = false; + + _model.setScaleToFit(true, _scale); + _model.setSnapModelToCenter(true); + _model.setRotation(_rotation); + _model.setTranslation(_position); + _model.setURL(_url); + _model.simulate(deltatime, true); + } else { + _model.simulate(deltatime); + } +} + +void ModelOverlay::render() { + if (_model.isActive()) { + + if (_model.isRenderable()) { + _model.render(_alpha); + } + bool displayModelBounds = Menu::getInstance()->isOptionChecked(MenuOption::DisplayModelBounds); + if (displayModelBounds) { + glm::vec3 unRotatedMinimum = _model.getUnscaledMeshExtents().minimum; + glm::vec3 unRotatedMaximum = _model.getUnscaledMeshExtents().maximum; + glm::vec3 unRotatedExtents = unRotatedMaximum - unRotatedMinimum; + + float width = unRotatedExtents.x; + float height = unRotatedExtents.y; + float depth = unRotatedExtents.z; + + Extents rotatedExtents = _model.getUnscaledMeshExtents(); + calculateRotatedExtents(rotatedExtents, _rotation); + + glm::vec3 rotatedSize = rotatedExtents.maximum - rotatedExtents.minimum; + + const glm::vec3& modelScale = _model.getScale(); + + glPushMatrix(); { + glTranslatef(_position.x, _position.y, _position.z); + + // draw the rotated bounding cube + glColor4f(0.0f, 0.0f, 1.0f, 1.0f); + glPushMatrix(); { + glScalef(rotatedSize.x * modelScale.x, rotatedSize.y * modelScale.y, rotatedSize.z * modelScale.z); + glutWireCube(1.0); + } glPopMatrix(); + + // draw the model relative bounding box + glm::vec3 axis = glm::axis(_rotation); + glRotatef(glm::degrees(glm::angle(_rotation)), axis.x, axis.y, axis.z); + glScalef(width * modelScale.x, height * modelScale.y, depth * modelScale.z); + glColor3f(0.0f, 1.0f, 0.0f); + glutWireCube(1.0); + + } glPopMatrix(); + } + } +} + +void ModelOverlay::setProperties(const QScriptValue &properties) { + Base3DOverlay::setProperties(properties); + + QScriptValue urlValue = properties.property("url"); + if (urlValue.isValid()) { + _url = urlValue.toVariant().toString(); + _updateModel = true; + } + + QScriptValue scaleValue = properties.property("scale"); + if (scaleValue.isValid()) { + _scale = scaleValue.toVariant().toFloat(); + _updateModel = true; + } + + QScriptValue rotationValue = properties.property("rotation"); + if (rotationValue.isValid()) { + QScriptValue x = rotationValue.property("x"); + QScriptValue y = rotationValue.property("y"); + QScriptValue z = rotationValue.property("z"); + QScriptValue w = rotationValue.property("w"); + if (x.isValid() && y.isValid() && z.isValid() && w.isValid()) { + _rotation.x = x.toVariant().toFloat(); + _rotation.y = y.toVariant().toFloat(); + _rotation.z = z.toVariant().toFloat(); + _rotation.w = w.toVariant().toFloat(); + } + _updateModel = true; + } + + if (properties.property("position").isValid()) { + _updateModel = true; + } +} \ No newline at end of file diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h new file mode 100644 index 0000000000..e0f979676f --- /dev/null +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -0,0 +1,38 @@ +// +// ModelOverlay.h +// +// +// Created by Clement on 6/30/14. +// Copyright 2014 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_ModelOverlay_h +#define hifi_ModelOverlay_h + +#include "Base3DOverlay.h" + +#include "../../renderer/Model.h" + +class ModelOverlay : public Base3DOverlay { + Q_OBJECT +public: + ModelOverlay(); + + virtual void update(float deltatime); + virtual void render(); + virtual void setProperties(const QScriptValue& properties); +private: + + Model _model; + + QUrl _url; + glm::quat _rotation; + float _scale; + + bool _updateModel; +}; + +#endif // hifi_ModelOverlay_h \ No newline at end of file diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 95f4f2b2fe..dd483da27a 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -10,13 +10,15 @@ #include <Application.h> +#include "BillboardOverlay.h" #include "Cube3DOverlay.h" #include "ImageOverlay.h" #include "Line3DOverlay.h" +#include "LocalVoxelsOverlay.h" +#include "ModelOverlay.h" #include "Overlays.h" #include "Sphere3DOverlay.h" #include "TextOverlay.h" -#include "LocalVoxelsOverlay.h" Overlays::Overlays() : _nextOverlayID(1) { } @@ -156,6 +158,18 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope thisOverlay->setProperties(properties); created = true; is3D = true; + } else if (type == "model") { + thisOverlay = new ModelOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + is3D = true; + } else if (type == "billboard") { + thisOverlay = new BillboardOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + is3D = true; } if (created) { diff --git a/interface/ui/preferencesDialog.ui b/interface/ui/preferencesDialog.ui index f00d7c4788..95678bf6f8 100644 --- a/interface/ui/preferencesDialog.ui +++ b/interface/ui/preferencesDialog.ui @@ -154,9 +154,9 @@ color: #0e7077</string> <property name="geometry"> <rect> <x>0</x> - <y>-1002</y> - <width>477</width> - <height>1386</height> + <y>0</y> + <width>600</width> + <height>1091</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout_2"> @@ -645,6 +645,112 @@ color: #0e7077</string> </property> </widget> </item> + <item> + <widget class="QLabel" name="headLabel_4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>30</height> + </size> + </property> + <property name="font"> + <font> + <family>Arial</family> + <pointsize>16</pointsize> + </font> + </property> + <property name="styleSheet"> + <string notr="true">color: #0e7077</string> + </property> + <property name="text"> + <string>Load scripts from this directory:</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="buddy"> + <cstring>snapshotLocationEdit</cstring> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_11"> + <item> + <widget class="QLineEdit" name="scriptsLocationEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <family>Arial</family> + </font> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_11"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="buttonBrowseScriptsLocation"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="text"> + <string/> + </property> + <property name="iconSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + </widget> + </item> + </layout> + </item> <item> <widget class="QWidget" name="widget" native="true"> <layout class="QHBoxLayout" name="horizontalLayout_12"> @@ -660,7 +766,7 @@ color: #0e7077</string> <string notr="true">background: #0e7077; color: #fff; border-radius: 4px; -font: bold 14pt; +font: bold 14px; padding: 10px;margin-top:10px</string> </property> <property name="text"> @@ -796,7 +902,7 @@ padding: 10px;margin-top:10px</string> </widget> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_11"> + <layout class="QHBoxLayout" name="horizontalLayout_111"> <property name="spacing"> <number>0</number> </property> @@ -831,7 +937,7 @@ padding: 10px;margin-top:10px</string> </widget> </item> <item> - <spacer name="horizontalSpacer_11"> + <spacer name="horizontalSpacer_111"> <property name="font"> <font> <family>Arial</family> diff --git a/interface/ui/runningScriptsWidget.ui b/interface/ui/runningScriptsWidget.ui index 6cb23f4c89..71efe6970a 100644 --- a/interface/ui/runningScriptsWidget.ui +++ b/interface/ui/runningScriptsWidget.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>323</width> - <height>894</height> + <width>324</width> + <height>971</height> </rect> </property> <property name="windowTitle"> @@ -21,240 +21,623 @@ QWidget { background: #f7f7f7; }</string> </property> - <widget class="QLabel" name="widgetTitle"> - <property name="geometry"> - <rect> - <x>37</x> - <y>29</y> - <width>251</width> - <height>20</height> - </rect> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="leftMargin"> + <number>20</number> </property> - <property name="styleSheet"> - <string notr="true">color: #0e7077; -font-size: 20pt; + <property name="topMargin"> + <number>20</number> + </property> + <property name="rightMargin"> + <number>20</number> + </property> + <property name="bottomMargin"> + <number>20</number> + </property> + <item> + <widget class="QWidget" name="header" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="widgetTitle"> + <property name="styleSheet"> + <string notr="true">color: #0e7077; +font-size: 20px; background: transparent;</string> - </property> - <property name="text"> - <string><html><head/><body><p><span style=" font-size:18pt;">Running Scripts</span></p></body></html></string> - </property> - <property name="margin"> - <number>0</number> - </property> - <property name="indent"> - <number>-1</number> - </property> - </widget> - <widget class="QLabel" name="currentlyRunningLabel"> - <property name="geometry"> - <rect> - <x>36</x> - <y>110</y> - <width>270</width> - <height>20</height> - </rect> - </property> - <property name="styleSheet"> - <string notr="true">color: #0e7077; -font: bold 14pt; + </property> + <property name="text"> + <string><html><head/><body><p><span style=" font-size:18px;">Running Scripts</span></p></body></html></string> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="indent"> + <number>-1</number> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="hideWidgetButton"> + <property name="cursor"> + <cursorShape>PointingHandCursor</cursorShape> + </property> + <property name="styleSheet"> + <string notr="true">border: 0</string> + </property> + <property name="text"> + <string/> + </property> + <property name="iconSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="runningScriptsArea" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>141</height> + </size> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="currentlyRunningLabel"> + <property name="font"> + <font> + <family>Helvetica,Arial,sans-serif</family> + <pointsize>16</pointsize> + <weight>75</weight> + <italic>false</italic> + <bold>true</bold> + </font> + </property> + <property name="styleSheet"> + <string notr="true">color: #0e7077; +font: bold 16px; background: transparent;</string> - </property> - <property name="text"> - <string><html><head/><body><p><span style=" font-weight:600;">Currently running</span></p></body></html></string> - </property> - </widget> - <widget class="QPushButton" name="reloadAllButton"> - <property name="geometry"> - <rect> - <x>36</x> - <y>270</y> - <width>111</width> - <height>35</height> - </rect> - </property> - <property name="cursor"> - <cursorShape>PointingHandCursor</cursorShape> - </property> - <property name="autoFillBackground"> - <bool>false</bool> - </property> - <property name="styleSheet"> - <string notr="true">background: #0e7077; + </property> + <property name="text"> + <string><html><head/><body><p><span style=" font-weight:600;">Currently running</span></p></body></html></string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Minimum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>8</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QWidget" name="reloadStopButtonArea" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="spacing"> + <number>24</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="reloadAllButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>111</width> + <height>35</height> + </size> + </property> + <property name="cursor"> + <cursorShape>PointingHandCursor</cursorShape> + </property> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="styleSheet"> + <string notr="true">background: #0e7077; color: #fff; border-radius: 4px; -font: bold 14pt; +font: bold 14px; padding-top: 3px;</string> - </property> - <property name="text"> - <string>Reload all</string> - </property> - </widget> - <widget class="QPushButton" name="stopAllButton"> - <property name="geometry"> - <rect> - <x>160</x> - <y>270</y> - <width>93</width> - <height>35</height> - </rect> - </property> - <property name="cursor"> - <cursorShape>PointingHandCursor</cursorShape> - </property> - <property name="styleSheet"> - <string notr="true">background: #0e7077; + </property> + <property name="text"> + <string>Reload all</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="stopAllButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>111</width> + <height>35</height> + </size> + </property> + <property name="cursor"> + <cursorShape>PointingHandCursor</cursorShape> + </property> + <property name="styleSheet"> + <string notr="true">background: #0e7077; color: #fff; border-radius: 4px; -font: bold 14pt; +font: bold 14px; padding-top: 3px;</string> - </property> - <property name="text"> - <string>Stop all</string> - </property> - </widget> - <widget class="QLabel" name="recentlyLoadedLabel"> - <property name="geometry"> - <rect> - <x>36</x> - <y>320</y> - <width>265</width> - <height>20</height> - </rect> - </property> - <property name="styleSheet"> - <string notr="true">color: #0e7077; -font: bold 14pt;</string> - </property> - <property name="text"> - <string><html><head/><body><p><span style=" font-weight:600;">Recently loaded</span></p></body></html></string> - </property> - </widget> - <widget class="QLabel" name="recentlyLoadedInstruction"> - <property name="geometry"> - <rect> - <x>36</x> - <y>630</y> - <width>211</width> - <height>41</height> - </rect> - </property> - <property name="styleSheet"> - <string notr="true">color: #95a5a6; -font-size: 14pt;</string> - </property> - <property name="text"> - <string>(click a script to load and run it)</string> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - </widget> - <widget class="QPushButton" name="hideWidgetButton"> - <property name="geometry"> - <rect> - <x>285</x> - <y>29</y> - <width>16</width> - <height>16</height> - </rect> - </property> - <property name="cursor"> - <cursorShape>PointingHandCursor</cursorShape> - </property> - <property name="text"> - <string/> - </property> - <property name="iconSize"> - <size> - <width>16</width> - <height>16</height> - </size> - </property> - <property name="flat"> - <bool>true</bool> - </property> - </widget> - <widget class="QLabel" name="noRunningScriptsLabel"> - <property name="geometry"> - <rect> - <x>36</x> - <y>110</y> - <width>271</width> - <height>51</height> - </rect> - </property> - <property name="styleSheet"> - <string notr="true">font: 14pt;</string> - </property> - <property name="text"> - <string>There are no scripts currently running.</string> - </property> - <property name="alignment"> - <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> - </property> - </widget> - <widget class="QWidget" name="recentlyLoadedScriptsTableWidget" native="true"> - <property name="geometry"> - <rect> - <x>30</x> - <y>340</y> - <width>272</width> - <height>280</height> - </rect> - </property> - <property name="styleSheet"> - <string notr="true">background: transparent; -font-size: 14pt;</string> - </property> - </widget> - <widget class="QWidget" name="runningScriptsTableWidget" native="true"> - <property name="geometry"> - <rect> - <x>30</x> - <y>128</y> - <width>272</width> - <height>161</height> - </rect> - </property> - <property name="styleSheet"> - <string notr="true">background: transparent; -font-size: 14pt;</string> - </property> - </widget> - <widget class="QPushButton" name="loadScriptButton"> - <property name="geometry"> - <rect> - <x>36</x> - <y>70</y> - <width>111</width> - <height>35</height> - </rect> - </property> - <property name="cursor"> - <cursorShape>PointingHandCursor</cursorShape> - </property> - <property name="styleSheet"> - <string notr="true">background: #0e7077; + </property> + <property name="text"> + <string>Stop all</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>8</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="noRunningScriptsLabel"> + <property name="styleSheet"> + <string notr="true">font: 14px;</string> + </property> + <property name="text"> + <string>There are no scripts currently running.</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>10</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QScrollArea" name="runningScriptsList"> + <property name="font"> + <font> + <family>Helvetica,Arial,sans-serif</family> + <pointsize>14</pointsize> + </font> + </property> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="styleSheet"> + <string notr="true">margin: 0;</string> + </property> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="lineWidth"> + <number>0</number> + </property> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOn</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>269</width> + <height>16</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="styleSheet"> + <string notr="true">font-size: 14px;</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="recentlyLoadedScriptsArea" native="true"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>100</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>16777215</height> + </size> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="recentlyLoadedLabel"> + <property name="styleSheet"> + <string notr="true">color: #0e7077; +font: bold 16px;</string> + </property> + <property name="text"> + <string><html><head/><body><p><span style=" font-weight:600;">Recently loaded</span></p></body></html></string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="noRecentlyLoadedLabel"> + <property name="styleSheet"> + <string notr="true">font: 14px;</string> + </property> + <property name="text"> + <string>There are no recently loaded scripts.</string> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="recentlyLoadedScriptsTableWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>284</width> + <height>0</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true">background: transparent; +font-size: 14px;</string> + </property> + <zorder>runningScriptsList</zorder> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="quickLoadArea" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>300</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="widget" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>15</number> + </property> + <item> + <widget class="QLabel" name="label"> + <property name="styleSheet"> + <string notr="true">color: #0e7077; +font: bold 16px;</string> + </property> + <property name="text"> + <string>Scripts</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="loadScriptButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>111</width> + <height>35</height> + </size> + </property> + <property name="cursor"> + <cursorShape>PointingHandCursor</cursorShape> + </property> + <property name="styleSheet"> + <string notr="true">background: #0e7077; color: #fff; border-radius: 4px; -font: bold 14pt; +font: bold 14px; padding-top: 3px;</string> - </property> - <property name="text"> - <string>Load script</string> - </property> - </widget> - <zorder>widgetTitle</zorder> - <zorder>currentlyRunningLabel</zorder> - <zorder>recentlyLoadedLabel</zorder> - <zorder>recentlyLoadedInstruction</zorder> - <zorder>hideWidgetButton</zorder> - <zorder>recentlyLoadedScriptsTableWidget</zorder> - <zorder>runningScriptsTableWidget</zorder> - <zorder>noRunningScriptsLabel</zorder> - <zorder>reloadAllButton</zorder> - <zorder>stopAllButton</zorder> - <zorder>loadScriptButton</zorder> + </property> + <property name="text"> + <string>Load script</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QLineEdit" name="filterLineEdit"> + <property name="styleSheet"> + <string notr="true">border: 1px solid rgb(128, 128, 128); +border-radius: 2px; +padding: 4px; +background-color: white;</string> + </property> + <property name="text"> + <string/> + </property> + <property name="placeholderText"> + <string>filter</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>6</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QListView" name="scriptListView"> + <property name="styleSheet"> + <string notr="true">QListView { + border: 1px solid rgb(128, 128, 128); + border-radius: 2px; +} +QListView::item { + padding-top: 2px; + padding-bottom: 2px; +}</string> + </property> + <property name="frameShape"> + <enum>QFrame::Box</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAsNeeded</enum> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> </widget> <resources/> <connections/> diff --git a/interface/ui/scriptEditorWindow.ui b/interface/ui/scriptEditorWindow.ui index 9103fc1f57..0379f51e97 100644 --- a/interface/ui/scriptEditorWindow.ui +++ b/interface/ui/scriptEditorWindow.ui @@ -33,7 +33,7 @@ <number>0</number> </property> <item> - <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0"> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0,0"> <property name="spacing"> <number>3</number> </property> @@ -185,6 +185,16 @@ </property> </spacer> </item> + <item> + <widget class="QCheckBox" name="autoReloadCheckBox"> + <property name="styleSheet"> + <string notr="true">font: 13px "Helvetica","Arial","sans-serif";</string> + </property> + <property name="text"> + <string>Automatically reload externally changed files</string> + </property> + </widget> + </item> </layout> </item> <item> diff --git a/interface/ui/shareSnapshot.ui b/interface/ui/shareSnapshot.ui index df7fc4939f..19e0772f13 100644 --- a/interface/ui/shareSnapshot.ui +++ b/interface/ui/shareSnapshot.ui @@ -277,7 +277,7 @@ padding-left:20px;</string> <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Helvetica'; font-size:14pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'Helvetica'; font-size:14px; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html></string> </property> </widget> diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 129dc47bd0..e5c1230832 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -61,6 +61,11 @@ void AudioInjector::injectAudio() { QByteArray injectAudioPacket = byteArrayWithPopulatedHeader(PacketTypeInjectAudio); QDataStream packetStream(&injectAudioPacket, QIODevice::Append); + // pack some placeholder sequence number for now + int numPreSequenceNumberBytes = injectAudioPacket.size(); + packetStream << (quint16)0; + + // pack stream identifier (a generated UUID) packetStream << QUuid::createUuid(); // pack the flag for loopback @@ -91,6 +96,7 @@ void AudioInjector::injectAudio() { bool shouldLoop = _options.getLoop(); // loop to send off our audio in NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL byte chunks + quint16 outgoingInjectedAudioSequenceNumber = 0; while (currentSendPosition < soundByteArray.size() && !_shouldStop) { int bytesToCopy = std::min(NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL, @@ -98,6 +104,9 @@ void AudioInjector::injectAudio() { // resize the QByteArray to the right size injectAudioPacket.resize(numPreAudioDataBytes + bytesToCopy); + + // pack the sequence number + memcpy(injectAudioPacket.data() + numPreSequenceNumberBytes, &outgoingInjectedAudioSequenceNumber, sizeof(quint16)); // copy the next NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL bytes to the packet memcpy(injectAudioPacket.data() + numPreAudioDataBytes, soundByteArray.data() + currentSendPosition, bytesToCopy); @@ -107,6 +116,7 @@ void AudioInjector::injectAudio() { // send off this audio packet nodeList->writeDatagram(injectAudioPacket, audioMixer); + outgoingInjectedAudioSequenceNumber++; currentSendPosition += bytesToCopy; diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index 2101fcb9cd..6ae3b19541 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -16,12 +16,14 @@ #include <QtCore/QDebug> #include "PacketHeaders.h" - #include "AudioRingBuffer.h" + AudioRingBuffer::AudioRingBuffer(int numFrameSamples, bool randomAccessMode) : NodeData(), + _overflowCount(0), _sampleCapacity(numFrameSamples * RING_BUFFER_LENGTH_FRAMES), + _isFull(false), _numFrameSamples(numFrameSamples), _isStarved(true), _hasStarted(false), @@ -63,8 +65,9 @@ void AudioRingBuffer::resizeForFrameSize(qint64 numFrameSamples) { } int AudioRingBuffer::parseData(const QByteArray& packet) { - int numBytesPacketHeader = numBytesForPacketHeader(packet); - return writeData(packet.data() + numBytesPacketHeader, packet.size() - numBytesPacketHeader); + // skip packet header and sequence number + int numBytesBeforeAudioData = numBytesForPacketHeader(packet) + sizeof(quint16); + return writeData(packet.data() + numBytesBeforeAudioData, packet.size() - numBytesBeforeAudioData); } qint64 AudioRingBuffer::readSamples(int16_t* destination, qint64 maxSamples) { @@ -108,6 +111,9 @@ qint64 AudioRingBuffer::readData(char *data, qint64 maxSize) { // push the position of _nextOutput by the number of samples read _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, numReadSamples); + if (numReadSamples > 0) { + _isFull = false; + } return numReadSamples * sizeof(int16_t); } @@ -119,22 +125,17 @@ qint64 AudioRingBuffer::writeSamples(const int16_t* source, qint64 maxSamples) { qint64 AudioRingBuffer::writeData(const char* data, qint64 maxSize) { // make sure we have enough bytes left for this to be the right amount of audio // otherwise we should not copy that data, and leave the buffer pointers where they are - int samplesToCopy = std::min((quint64)(maxSize / sizeof(int16_t)), (quint64)_sampleCapacity); - - std::less<int16_t*> less; - std::less_equal<int16_t*> lessEqual; - - if (_hasStarted - && (less(_endOfLastWrite, _nextOutput) - && lessEqual(_nextOutput, shiftedPositionAccomodatingWrap(_endOfLastWrite, samplesToCopy)))) { - // this read will cross the next output, so call us starved and reset the buffer - qDebug() << "Filled the ring buffer. Resetting."; - _endOfLastWrite = _buffer; - _nextOutput = _buffer; - _isStarved = true; + + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + if (samplesToCopy > samplesRoomFor) { + // there's not enough room for this write. erase old data to make room for this new data + int samplesToDelete = samplesToCopy - samplesRoomFor; + _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, samplesToDelete); + _overflowCount++; + qDebug() << "Overflowed ring buffer! Overwriting old data"; } - + if (_endOfLastWrite + samplesToCopy <= _buffer + _sampleCapacity) { memcpy(_endOfLastWrite, data, samplesToCopy * sizeof(int16_t)); } else { @@ -144,6 +145,9 @@ qint64 AudioRingBuffer::writeData(const char* data, qint64 maxSize) { } _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, samplesToCopy); + if (samplesToCopy > 0 && _endOfLastWrite == _nextOutput) { + _isFull = true; + } return samplesToCopy * sizeof(int16_t); } @@ -157,36 +161,51 @@ const int16_t& AudioRingBuffer::operator[] (const int index) const { } void AudioRingBuffer::shiftReadPosition(unsigned int numSamples) { - _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, numSamples); + if (numSamples > 0) { + _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, numSamples); + _isFull = false; + } } unsigned int AudioRingBuffer::samplesAvailable() const { if (!_endOfLastWrite) { return 0; - } else { - int sampleDifference = _endOfLastWrite - _nextOutput; - - if (sampleDifference < 0) { - sampleDifference += _sampleCapacity; - } - - return sampleDifference; } + if (_isFull) { + return _sampleCapacity; + } + + int sampleDifference = _endOfLastWrite - _nextOutput; + if (sampleDifference < 0) { + sampleDifference += _sampleCapacity; + } + return sampleDifference; } -void AudioRingBuffer::addSilentFrame(int numSilentSamples) { +int AudioRingBuffer::addSilentFrame(int numSilentSamples) { + + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + if (numSilentSamples > samplesRoomFor) { + // there's not enough room for this write. write as many silent samples as we have room for + numSilentSamples = samplesRoomFor; + qDebug() << "Dropping some silent samples to prevent ring buffer overflow"; + } + // memset zeroes into the buffer, accomodate a wrap around the end // push the _endOfLastWrite to the correct spot if (_endOfLastWrite + numSilentSamples <= _buffer + _sampleCapacity) { memset(_endOfLastWrite, 0, numSilentSamples * sizeof(int16_t)); - _endOfLastWrite += numSilentSamples; } else { int numSamplesToEnd = (_buffer + _sampleCapacity) - _endOfLastWrite; memset(_endOfLastWrite, 0, numSamplesToEnd * sizeof(int16_t)); memset(_buffer, 0, (numSilentSamples - numSamplesToEnd) * sizeof(int16_t)); - - _endOfLastWrite = _buffer + (numSilentSamples - numSamplesToEnd); } + _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numSilentSamples); + if (numSilentSamples > 0 && _nextOutput == _endOfLastWrite) { + _isFull = true; + } + + return numSilentSamples * sizeof(int16_t); } bool AudioRingBuffer::isNotStarvedOrHasMinimumSamples(unsigned int numRequiredSamples) const { diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 04cc67c8ac..8d19f9c0bb 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -71,17 +71,21 @@ public: bool isStarved() const { return _isStarved; } void setIsStarved(bool isStarved) { _isStarved = isStarved; } + int getOverflowCount() const { return _overflowCount; } /// how many times has the ring buffer has overwritten old data bool hasStarted() const { return _hasStarted; } - void addSilentFrame(int numSilentSamples); + int addSilentFrame(int numSilentSamples); protected: // disallow copying of AudioRingBuffer objects AudioRingBuffer(const AudioRingBuffer&); AudioRingBuffer& operator= (const AudioRingBuffer&); int16_t* shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const; + + int _overflowCount; /// how many times has the ring buffer has overwritten old data int _sampleCapacity; + bool _isFull; int _numFrameSamples; int16_t* _nextOutput; int16_t* _endOfLastWrite; diff --git a/libraries/audio/src/AudioStreamStats.h b/libraries/audio/src/AudioStreamStats.h new file mode 100644 index 0000000000..004d697fcf --- /dev/null +++ b/libraries/audio/src/AudioStreamStats.h @@ -0,0 +1,46 @@ +// +// AudioStreamStats.h +// libraries/audio/src +// +// Created by Yixin Wang on 6/25/2014 +// Copyright 2014 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_AudioStreamStats_h +#define hifi_AudioStreamStats_h + +#include "PositionalAudioRingBuffer.h" + +class AudioStreamStats { +public: + AudioStreamStats() + : _streamType(PositionalAudioRingBuffer::Microphone), + _streamIdentifier(), + _jitterBufferFrames(0), + _packetsReceived(0), + _packetsUnreasonable(0), + _packetsEarly(0), + _packetsLate(0), + _packetsLost(0), + _packetsRecovered(0), + _packetsDuplicate(0) + {} + + PositionalAudioRingBuffer::Type _streamType; + QUuid _streamIdentifier; + + quint16 _jitterBufferFrames; + + quint32 _packetsReceived; + quint32 _packetsUnreasonable; + quint32 _packetsEarly; + quint32 _packetsLate; + quint32 _packetsLost; + quint32 _packetsRecovered; + quint32 _packetsDuplicate; +}; + +#endif // hifi_AudioStreamStats_h diff --git a/libraries/audio/src/InjectedAudioRingBuffer.cpp b/libraries/audio/src/InjectedAudioRingBuffer.cpp index 2658b4c336..0d7cea356b 100644 --- a/libraries/audio/src/InjectedAudioRingBuffer.cpp +++ b/libraries/audio/src/InjectedAudioRingBuffer.cpp @@ -19,8 +19,8 @@ #include "InjectedAudioRingBuffer.h" -InjectedAudioRingBuffer::InjectedAudioRingBuffer(const QUuid& streamIdentifier) : - PositionalAudioRingBuffer(PositionalAudioRingBuffer::Injector), +InjectedAudioRingBuffer::InjectedAudioRingBuffer(const QUuid& streamIdentifier, bool dynamicJitterBuffer) : + PositionalAudioRingBuffer(PositionalAudioRingBuffer::Injector, /* isStereo=*/ false , dynamicJitterBuffer), _streamIdentifier(streamIdentifier), _radius(0.0f), _attenuationRatio(0) @@ -31,10 +31,16 @@ InjectedAudioRingBuffer::InjectedAudioRingBuffer(const QUuid& streamIdentifier) const uchar MAX_INJECTOR_VOLUME = 255; int InjectedAudioRingBuffer::parseData(const QByteArray& packet) { + _interframeTimeGapStats.frameReceived(); + updateDesiredJitterBufferFrames(); + // setup a data stream to read from this packet QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); + // push past the sequence number + packetStream.skipRawData(sizeof(quint16)); + // push past the stream identifier packetStream.skipRawData(NUM_BYTES_RFC4122_UUID); diff --git a/libraries/audio/src/InjectedAudioRingBuffer.h b/libraries/audio/src/InjectedAudioRingBuffer.h index fd766e2848..4e3fea672b 100644 --- a/libraries/audio/src/InjectedAudioRingBuffer.h +++ b/libraries/audio/src/InjectedAudioRingBuffer.h @@ -18,7 +18,7 @@ class InjectedAudioRingBuffer : public PositionalAudioRingBuffer { public: - InjectedAudioRingBuffer(const QUuid& streamIdentifier = QUuid()); + InjectedAudioRingBuffer(const QUuid& streamIdentifier = QUuid(), bool dynamicJitterBuffer = false); int parseData(const QByteArray& packet); diff --git a/libraries/audio/src/PositionalAudioRingBuffer.cpp b/libraries/audio/src/PositionalAudioRingBuffer.cpp index 1cc4147175..546ed97fe2 100644 --- a/libraries/audio/src/PositionalAudioRingBuffer.cpp +++ b/libraries/audio/src/PositionalAudioRingBuffer.cpp @@ -19,8 +19,75 @@ #include <UUID.h> #include "PositionalAudioRingBuffer.h" +#include "SharedUtil.h" -PositionalAudioRingBuffer::PositionalAudioRingBuffer(PositionalAudioRingBuffer::Type type, bool isStereo) : +InterframeTimeGapStats::InterframeTimeGapStats() + : _lastFrameReceivedTime(0), + _numSamplesInCurrentInterval(0), + _currentIntervalMaxGap(0), + _newestIntervalMaxGapAt(0), + _windowMaxGap(0), + _newWindowMaxGapAvailable(false) +{ + memset(_intervalMaxGaps, 0, TIME_GAP_NUM_INTERVALS_IN_WINDOW * sizeof(quint64)); +} + +void InterframeTimeGapStats::frameReceived() { + quint64 now = usecTimestampNow(); + + // make sure this isn't the first time frameReceived() is called so can actually calculate a gap. + if (_lastFrameReceivedTime != 0) { + quint64 gap = now - _lastFrameReceivedTime; + + // update the current interval max + if (gap > _currentIntervalMaxGap) { + _currentIntervalMaxGap = gap; + + // keep the window max gap at least as large as the current interval max + // this allows the window max gap to respond immediately to a sudden spike in gap times + // also, this prevents the window max gap from staying at 0 until the first interval of samples filled up + if (_currentIntervalMaxGap > _windowMaxGap) { + _windowMaxGap = _currentIntervalMaxGap; + _newWindowMaxGapAvailable = true; + } + } + _numSamplesInCurrentInterval++; + + // if the current interval of samples is now full, record it in our interval maxes + if (_numSamplesInCurrentInterval == TIME_GAP_NUM_SAMPLES_IN_INTERVAL) { + + // find location to insert this interval's max (increment index cyclically) + _newestIntervalMaxGapAt = _newestIntervalMaxGapAt == TIME_GAP_NUM_INTERVALS_IN_WINDOW - 1 ? 0 : _newestIntervalMaxGapAt + 1; + + // record the current interval's max gap as the newest + _intervalMaxGaps[_newestIntervalMaxGapAt] = _currentIntervalMaxGap; + + // update the window max gap, which is the max out of all the past intervals' max gaps + _windowMaxGap = 0; + for (int i = 0; i < TIME_GAP_NUM_INTERVALS_IN_WINDOW; i++) { + if (_intervalMaxGaps[i] > _windowMaxGap) { + _windowMaxGap = _intervalMaxGaps[i]; + } + } + _newWindowMaxGapAvailable = true; + + // reset the current interval + _numSamplesInCurrentInterval = 0; + _currentIntervalMaxGap = 0; + } + } + _lastFrameReceivedTime = now; +} + +quint64 InterframeTimeGapStats::getWindowMaxGap() { + _newWindowMaxGapAvailable = false; + return _windowMaxGap; +} + + +PositionalAudioRingBuffer::PositionalAudioRingBuffer(PositionalAudioRingBuffer::Type type, + bool isStereo, bool dynamicJitterBuffers) : + AudioRingBuffer(isStereo ? NETWORK_BUFFER_LENGTH_SAMPLES_STEREO : NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL), _type(type), _position(0.0f, 0.0f, 0.0f), @@ -29,15 +96,20 @@ PositionalAudioRingBuffer::PositionalAudioRingBuffer(PositionalAudioRingBuffer:: _shouldLoopbackForNode(false), _shouldOutputStarveDebug(true), _isStereo(isStereo), - _listenerUnattenuatedZone(NULL) + _listenerUnattenuatedZone(NULL), + _desiredJitterBufferFrames(1), + _currentJitterBufferFrames(0), + _dynamicJitterBuffers(dynamicJitterBuffers) { - } int PositionalAudioRingBuffer::parseData(const QByteArray& packet) { // skip the packet header (includes the source UUID) int readBytes = numBytesForPacketHeader(packet); + + // skip the sequence number + readBytes += sizeof(quint16); // hop over the channel flag that has already been read in AudioMixerClientData readBytes += sizeof(quint8); @@ -53,14 +125,35 @@ int PositionalAudioRingBuffer::parseData(const QByteArray& packet) { readBytes += sizeof(int16_t); + // NOTE: fixes a bug in old clients that would send garbage for their number of silentSamples + numSilentSamples = getSamplesPerFrame(); + if (numSilentSamples > 0) { - addSilentFrame(numSilentSamples); + if (_currentJitterBufferFrames > _desiredJitterBufferFrames) { + // our current jitter buffer size exceeds its desired value, so ignore some silent + // frames to get that size as close to desired as possible + int samplesPerFrame = getSamplesPerFrame(); + int numSilentFrames = numSilentSamples / samplesPerFrame; + int numFramesToDropDesired = _currentJitterBufferFrames - _desiredJitterBufferFrames; + + if (numSilentFrames > numFramesToDropDesired) { + // we have more than enough frames to drop to get the jitter buffer to its desired length + int numSilentFramesToAdd = numSilentFrames - numFramesToDropDesired; + addSilentFrame(numSilentFramesToAdd * samplesPerFrame); + _currentJitterBufferFrames = _desiredJitterBufferFrames; + + } else { + // we need to drop all frames to get the jitter buffer close as possible to its desired length + _currentJitterBufferFrames -= numSilentFrames; + } + } else { + addSilentFrame(numSilentSamples); + } } } else { // there is audio data to read readBytes += writeData(packet.data() + readBytes, packet.size() - readBytes); } - return readBytes; } @@ -106,29 +199,72 @@ void PositionalAudioRingBuffer::updateNextOutputTrailingLoudness() { } } -bool PositionalAudioRingBuffer::shouldBeAddedToMix(int numJitterBufferSamples) { - if (!isNotStarvedOrHasMinimumSamples(NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL + numJitterBufferSamples)) { +bool PositionalAudioRingBuffer::shouldBeAddedToMix() { + int samplesPerFrame = getSamplesPerFrame(); + int desiredJitterBufferSamples = _desiredJitterBufferFrames * samplesPerFrame; + + if (!isNotStarvedOrHasMinimumSamples(samplesPerFrame + desiredJitterBufferSamples)) { + // if the buffer was starved, allow it to accrue at least the desired number of + // jitter buffer frames before we start taking frames from it for mixing + if (_shouldOutputStarveDebug) { _shouldOutputStarveDebug = false; } - - return false; - } else if (samplesAvailable() < NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL) { + + return false; + } else if (samplesAvailable() < (unsigned int)samplesPerFrame) { + // if the buffer doesn't have a full frame of samples to take for mixing, it is starved _isStarved = true; + // set to 0 to indicate the jitter buffer is starved + _currentJitterBufferFrames = 0; + // reset our _shouldOutputStarveDebug to true so the next is printed _shouldOutputStarveDebug = true; - + return false; - } else { - // good buffer, add this to the mix + } + + // good buffer, add this to the mix + if (_isStarved) { + // if this buffer has just finished replenishing after being starved, the number of frames in it now + // minus one (since a frame will be read immediately after this) is the length of the jitter buffer + _currentJitterBufferFrames = samplesAvailable() / samplesPerFrame - 1; _isStarved = false; - - // since we've read data from ring buffer at least once - we've started - _hasStarted = true; - - return true; } - return false; + // since we've read data from ring buffer at least once - we've started + _hasStarted = true; + + return true; +} + +int PositionalAudioRingBuffer::getCalculatedDesiredJitterBufferFrames() const { + int calculatedDesiredJitterBufferFrames = 1; + const float USECS_PER_FRAME = NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL * USECS_PER_SECOND / (float)SAMPLE_RATE; + + calculatedDesiredJitterBufferFrames = ceilf((float)_interframeTimeGapStats.peekWindowMaxGap() / USECS_PER_FRAME); + if (calculatedDesiredJitterBufferFrames < 1) { + calculatedDesiredJitterBufferFrames = 1; + } + return calculatedDesiredJitterBufferFrames; +} + +void PositionalAudioRingBuffer::updateDesiredJitterBufferFrames() { + if (_interframeTimeGapStats.hasNewWindowMaxGapAvailable()) { + if (!_dynamicJitterBuffers) { + _desiredJitterBufferFrames = 1; // HACK to see if this fixes the audio silence + } else { + const float USECS_PER_FRAME = NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL * USECS_PER_SECOND / (float)SAMPLE_RATE; + + _desiredJitterBufferFrames = ceilf((float)_interframeTimeGapStats.getWindowMaxGap() / USECS_PER_FRAME); + if (_desiredJitterBufferFrames < 1) { + _desiredJitterBufferFrames = 1; + } + const int maxDesired = RING_BUFFER_LENGTH_FRAMES - 1; + if (_desiredJitterBufferFrames > maxDesired) { + _desiredJitterBufferFrames = maxDesired; + } + } + } } diff --git a/libraries/audio/src/PositionalAudioRingBuffer.h b/libraries/audio/src/PositionalAudioRingBuffer.h index 00362c245a..b204dc766b 100644 --- a/libraries/audio/src/PositionalAudioRingBuffer.h +++ b/libraries/audio/src/PositionalAudioRingBuffer.h @@ -18,6 +18,31 @@ #include "AudioRingBuffer.h" +// this means that every 500 samples, the max for the past 10*500 samples will be calculated +const int TIME_GAP_NUM_SAMPLES_IN_INTERVAL = 500; +const int TIME_GAP_NUM_INTERVALS_IN_WINDOW = 10; + +// class used to track time between incoming frames for the purpose of varying the jitter buffer length +class InterframeTimeGapStats { +public: + InterframeTimeGapStats(); + + void frameReceived(); + bool hasNewWindowMaxGapAvailable() const { return _newWindowMaxGapAvailable; } + quint64 peekWindowMaxGap() const { return _windowMaxGap; } + quint64 getWindowMaxGap(); + +private: + quint64 _lastFrameReceivedTime; + + int _numSamplesInCurrentInterval; + quint64 _currentIntervalMaxGap; + quint64 _intervalMaxGaps[TIME_GAP_NUM_INTERVALS_IN_WINDOW]; + int _newestIntervalMaxGapAt; + quint64 _windowMaxGap; + bool _newWindowMaxGapAvailable; +}; + class PositionalAudioRingBuffer : public AudioRingBuffer { public: enum Type { @@ -25,7 +50,7 @@ public: Injector }; - PositionalAudioRingBuffer(PositionalAudioRingBuffer::Type type, bool isStereo = false); + PositionalAudioRingBuffer(PositionalAudioRingBuffer::Type type, bool isStereo = false, bool dynamicJitterBuffers = false); int parseData(const QByteArray& packet); int parsePositionalData(const QByteArray& positionalByteArray); @@ -34,7 +59,7 @@ public: void updateNextOutputTrailingLoudness(); float getNextOutputTrailingLoudness() const { return _nextOutputTrailingLoudness; } - bool shouldBeAddedToMix(int numJitterBufferSamples); + bool shouldBeAddedToMix(); bool willBeAddedToMix() const { return _willBeAddedToMix; } void setWillBeAddedToMix(bool willBeAddedToMix) { _willBeAddedToMix = willBeAddedToMix; } @@ -50,10 +75,18 @@ public: AABox* getListenerUnattenuatedZone() const { return _listenerUnattenuatedZone; } void setListenerUnattenuatedZone(AABox* listenerUnattenuatedZone) { _listenerUnattenuatedZone = listenerUnattenuatedZone; } + int getSamplesPerFrame() const { return _isStereo ? NETWORK_BUFFER_LENGTH_SAMPLES_STEREO : NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL; } + + int getCalculatedDesiredJitterBufferFrames() const; /// returns what we would calculate our desired as if asked + int getDesiredJitterBufferFrames() const { return _desiredJitterBufferFrames; } + int getCurrentJitterBufferFrames() const { return _currentJitterBufferFrames; } + protected: // disallow copying of PositionalAudioRingBuffer objects PositionalAudioRingBuffer(const PositionalAudioRingBuffer&); PositionalAudioRingBuffer& operator= (const PositionalAudioRingBuffer&); + + void updateDesiredJitterBufferFrames(); PositionalAudioRingBuffer::Type _type; glm::vec3 _position; @@ -65,6 +98,11 @@ protected: float _nextOutputTrailingLoudness; AABox* _listenerUnattenuatedZone; + + InterframeTimeGapStats _interframeTimeGapStats; + int _desiredJitterBufferFrames; + int _currentJitterBufferFrames; + bool _dynamicJitterBuffers; }; #endif // hifi_PositionalAudioRingBuffer_h diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 8f658678b5..4c7136fd0a 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -223,7 +223,7 @@ public: virtual const glm::vec3& getVelocity() const { return vec3Zero; } - virtual bool findParticleCollisions(const glm::vec3& particleCenter, float particleRadius, CollisionList& collisions) { + virtual bool findSphereCollisions(const glm::vec3& particleCenter, float particleRadius, CollisionList& collisions) { return false; } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 9aeb81a2a3..56fb566d6a 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1717,7 +1717,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) glm::vec3 boneEnd = extractTranslation(transformJointToMesh); glm::vec3 boneBegin = boneEnd; glm::vec3 boneDirection; - float boneLength; + float boneLength = 0.0f; if (joint.parentIndex != -1) { boneBegin = extractTranslation(inverseModelTransform * geometry.joints[joint.parentIndex].bindTransform); boneDirection = boneEnd - boneBegin; @@ -1779,7 +1779,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) glm::vec3 boneBegin = boneEnd; glm::vec3 boneDirection; - float boneLength; + float boneLength = 0.0f; if (joint.parentIndex != -1) { boneBegin = extractTranslation(inverseModelTransform * geometry.joints[joint.parentIndex].bindTransform); boneDirection = boneEnd - boneBegin; @@ -1897,7 +1897,20 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } geometry.attachments.append(attachment); } - + + // Add sitting points + QVariantHash sittingPoints = mapping.value("sit").toHash(); + for (QVariantHash::const_iterator it = sittingPoints.constBegin(); it != sittingPoints.constEnd(); it++) { + SittingPoint sittingPoint; + sittingPoint.name = it.key(); + + QVariantList properties = it->toList(); + sittingPoint.position = parseVec3(properties.at(0).toString()); + sittingPoint.rotation = glm::quat(glm::radians(parseVec3(properties.at(1).toString()))); + + geometry.sittingPoints.append(sittingPoint); + } + return geometry; } diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 4c93f3dc5e..c336252574 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -182,6 +182,14 @@ public: glm::vec3 scale; }; +/// A point where an avatar can sit +class SittingPoint { +public: + QString name; + glm::vec3 position; // relative postion + glm::quat rotation; // relative orientation +}; + /// A set of meshes extracted from an FBX document. class FBXGeometry { public: @@ -209,6 +217,8 @@ public: glm::vec3 palmDirection; + QVector<SittingPoint> sittingPoints; + glm::vec3 neckPivot; Extents bindExtents; diff --git a/libraries/metavoxels/src/AttributeRegistry.cpp b/libraries/metavoxels/src/AttributeRegistry.cpp index e7a7f41850..33ce298859 100644 --- a/libraries/metavoxels/src/AttributeRegistry.cpp +++ b/libraries/metavoxels/src/AttributeRegistry.cpp @@ -211,6 +211,11 @@ void Attribute::writeMetavoxelSubdivision(const MetavoxelNode& root, MetavoxelSt root.writeSubdivision(state); } +bool Attribute::metavoxelRootsEqual(const MetavoxelNode& firstRoot, const MetavoxelNode& secondRoot, + const glm::vec3& minimum, float size, const MetavoxelLOD& lod) { + return firstRoot.deepEquals(this, secondRoot, minimum, size, lod); +} + FloatAttribute::FloatAttribute(const QString& name, float defaultValue) : SimpleInlineAttribute<float>(name, defaultValue) { } @@ -449,6 +454,12 @@ void SharedObjectAttribute::write(Bitstream& out, void* value, bool isLeaf) cons } } +bool SharedObjectAttribute::deepEqual(void* first, void* second) const { + SharedObjectPointer firstObject = decodeInline<SharedObjectPointer>(first); + SharedObjectPointer secondObject = decodeInline<SharedObjectPointer>(second); + return firstObject ? firstObject->equals(secondObject) : !secondObject; +} + bool SharedObjectAttribute::merge(void*& parent, void* children[], bool postRead) const { SharedObjectPointer firstChild = decodeInline<SharedObjectPointer>(children[0]); for (int i = 1; i < MERGE_COUNT; i++) { @@ -489,6 +500,35 @@ MetavoxelNode* SharedObjectSetAttribute::createMetavoxelNode( return new MetavoxelNode(value, original); } +static bool setsEqual(const SharedObjectSet& firstSet, const SharedObjectSet& secondSet) { + if (firstSet.size() != secondSet.size()) { + return false; + } + // some hackiness here: we assume that the local ids of the first set correspond to the remote ids of the second, + // so that this will work with the tests + foreach (const SharedObjectPointer& firstObject, firstSet) { + int id = firstObject->getID(); + bool found = false; + foreach (const SharedObjectPointer& secondObject, secondSet) { + if (secondObject->getRemoteID() == id) { + if (!firstObject->equals(secondObject)) { + return false; + } + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; +} + +bool SharedObjectSetAttribute::deepEqual(void* first, void* second) const { + return setsEqual(decodeInline<SharedObjectSet>(first), decodeInline<SharedObjectSet>(second)); +} + bool SharedObjectSetAttribute::merge(void*& parent, void* children[], bool postRead) const { for (int i = 0; i < MERGE_COUNT; i++) { if (!decodeInline<SharedObjectSet>(children[i]).isEmpty()) { @@ -563,3 +603,12 @@ void SpannerSetAttribute::writeMetavoxelSubdivision(const MetavoxelNode& root, M state.stream << SharedObjectPointer(); } +bool SpannerSetAttribute::metavoxelRootsEqual(const MetavoxelNode& firstRoot, const MetavoxelNode& secondRoot, + const glm::vec3& minimum, float size, const MetavoxelLOD& lod) { + + SharedObjectSet firstSet; + firstRoot.getSpanners(this, minimum, size, lod, firstSet); + SharedObjectSet secondSet; + secondRoot.getSpanners(this, minimum, size, lod, secondSet); + return setsEqual(firstSet, secondSet); +} diff --git a/libraries/metavoxels/src/AttributeRegistry.h b/libraries/metavoxels/src/AttributeRegistry.h index 00d974b8b6..7dc2e110b8 100644 --- a/libraries/metavoxels/src/AttributeRegistry.h +++ b/libraries/metavoxels/src/AttributeRegistry.h @@ -27,6 +27,7 @@ class QScriptValue; class Attribute; class MetavoxelData; +class MetavoxelLOD; class MetavoxelNode; class MetavoxelStreamState; @@ -213,6 +214,11 @@ public: virtual bool equal(void* first, void* second) const = 0; + virtual bool deepEqual(void* first, void* second) const { return equal(first, second); } + + virtual bool metavoxelRootsEqual(const MetavoxelNode& firstRoot, const MetavoxelNode& secondRoot, + const glm::vec3& minimum, float size, const MetavoxelLOD& lod); + /// Merges the value of a parent and its children. /// \param postRead whether or not the merge is happening after a read /// \return whether or not the children and parent values are all equal @@ -406,6 +412,8 @@ public: virtual void read(Bitstream& in, void*& value, bool isLeaf) const; virtual void write(Bitstream& out, void* value, bool isLeaf) const; + virtual bool deepEqual(void* first, void* second) const; + virtual bool merge(void*& parent, void* children[], bool postRead = false) const; virtual void* createFromVariant(const QVariant& value) const; @@ -434,6 +442,8 @@ public: virtual MetavoxelNode* createMetavoxelNode(const AttributeValue& value, const MetavoxelNode* original) const; + virtual bool deepEqual(void* first, void* second) const; + virtual bool merge(void*& parent, void* children[], bool postRead = false) const; virtual AttributeValue inherit(const AttributeValue& parentValue) const; @@ -462,6 +472,9 @@ public: virtual void readMetavoxelSubdivision(MetavoxelData& data, MetavoxelStreamState& state); virtual void writeMetavoxelSubdivision(const MetavoxelNode& root, MetavoxelStreamState& state); + + virtual bool metavoxelRootsEqual(const MetavoxelNode& firstRoot, const MetavoxelNode& secondRoot, + const glm::vec3& minimum, float size, const MetavoxelLOD& lod); }; #endif // hifi_AttributeRegistry_h diff --git a/libraries/metavoxels/src/Bitstream.cpp b/libraries/metavoxels/src/Bitstream.cpp index 44342abe33..d18903f923 100644 --- a/libraries/metavoxels/src/Bitstream.cpp +++ b/libraries/metavoxels/src/Bitstream.cpp @@ -110,6 +110,10 @@ const TypeStreamer* Bitstream::getTypeStreamer(int type) { return getTypeStreamers().value(type); } +const ObjectStreamer* Bitstream::getObjectStreamer(const QMetaObject* metaObject) { + return getObjectStreamers().value(metaObject); +} + const QMetaObject* Bitstream::getMetaObject(const QByteArray& className) { return getMetaObjects().value(className); } @@ -1410,8 +1414,10 @@ Bitstream& Bitstream::operator<(const SharedObjectPointer& object) { *this << object->getOriginID(); QPointer<SharedObject> reference = _sharedObjectReferences.value(object->getOriginID()); if (reference) { + *this << true; writeRawDelta((const QObject*)object.data(), (const QObject*)reference.data()); } else { + *this << false; *this << (QObject*)object.data(); } return *this; @@ -1426,19 +1432,27 @@ Bitstream& Bitstream::operator>(SharedObjectPointer& object) { } int originID; *this >> originID; + bool delta; + *this >> delta; QPointer<SharedObject> reference = _sharedObjectReferences.value(originID); QPointer<SharedObject>& pointer = _weakSharedObjectHash[id]; if (pointer) { ObjectStreamerPointer objectStreamer; _objectStreamerStreamer >> objectStreamer; - if (reference) { + if (delta) { + if (!reference) { + qWarning() << "Delta without reference" << id << originID; + } objectStreamer->readRawDelta(*this, reference.data(), pointer.data()); } else { objectStreamer->read(*this, pointer.data()); } } else { QObject* rawObject; - if (reference) { + if (delta) { + if (!reference) { + qWarning() << "Delta without reference" << id << originID; + } readRawDelta(rawObject, (const QObject*)reference.data()); } else { *this >> rawObject; @@ -2316,6 +2330,15 @@ QObject* MappedObjectStreamer::putJSONData(JSONReader& reader, const QJsonObject return object; } +bool MappedObjectStreamer::equal(const QObject* first, const QObject* second) const { + foreach (const StreamerPropertyPair& property, _properties) { + if (!property.first->equal(property.second.read(first), property.second.read(second))) { + return false; + } + } + return true; +} + void MappedObjectStreamer::write(Bitstream& out, const QObject* object) const { foreach (const StreamerPropertyPair& property, _properties) { property.first->write(out, property.second.read(object)); @@ -2433,6 +2456,17 @@ QObject* GenericObjectStreamer::putJSONData(JSONReader& reader, const QJsonObjec return object; } +bool GenericObjectStreamer::equal(const QObject* first, const QObject* second) const { + const QVariantList& firstValues = static_cast<const GenericSharedObject*>(first)->getValues(); + const QVariantList& secondValues = static_cast<const GenericSharedObject*>(second)->getValues(); + for (int i = 0; i < _properties.size(); i++) { + if (!_properties.at(i).first->equal(firstValues.at(i), secondValues.at(i))) { + return false; + } + } + return true; +} + void GenericObjectStreamer::write(Bitstream& out, const QObject* object) const { const QVariantList& values = static_cast<const GenericSharedObject*>(object)->getValues(); for (int i = 0; i < _properties.size(); i++) { diff --git a/libraries/metavoxels/src/Bitstream.h b/libraries/metavoxels/src/Bitstream.h index 0d9e516640..e32f93dbe2 100644 --- a/libraries/metavoxels/src/Bitstream.h +++ b/libraries/metavoxels/src/Bitstream.h @@ -278,6 +278,9 @@ public: /// Returns the streamer registered for the supplied type, if any. static const TypeStreamer* getTypeStreamer(int type); + /// Returns the streamer registered for the supplied object, if any. + static const ObjectStreamer* getObjectStreamer(const QMetaObject* metaObject); + /// Returns the meta-object registered under the supplied class name, if any. static const QMetaObject* getMetaObject(const QByteArray& className); @@ -1022,6 +1025,7 @@ public: virtual QJsonObject getJSONData(JSONWriter& writer, const QObject* object) const = 0; virtual QObject* putJSONData(JSONReader& reader, const QJsonObject& jsonObject) const = 0; + virtual bool equal(const QObject* first, const QObject* second) const = 0; virtual void write(Bitstream& out, const QObject* object) const = 0; virtual void writeRawDelta(Bitstream& out, const QObject* object, const QObject* reference) const = 0; virtual QObject* read(Bitstream& in, QObject* object = NULL) const = 0; @@ -1047,6 +1051,7 @@ public: virtual QJsonObject getJSONMetadata(JSONWriter& writer) const; virtual QJsonObject getJSONData(JSONWriter& writer, const QObject* object) const; virtual QObject* putJSONData(JSONReader& reader, const QJsonObject& jsonObject) const; + virtual bool equal(const QObject* first, const QObject* second) const; virtual void write(Bitstream& out, const QObject* object) const; virtual void writeRawDelta(Bitstream& out, const QObject* object, const QObject* reference) const; virtual QObject* read(Bitstream& in, QObject* object = NULL) const; @@ -1070,6 +1075,7 @@ public: virtual QJsonObject getJSONMetadata(JSONWriter& writer) const; virtual QJsonObject getJSONData(JSONWriter& writer, const QObject* object) const; virtual QObject* putJSONData(JSONReader& reader, const QJsonObject& jsonObject) const; + virtual bool equal(const QObject* first, const QObject* second) const; virtual void write(Bitstream& out, const QObject* object) const; virtual void writeRawDelta(Bitstream& out, const QObject* object, const QObject* reference) const; virtual QObject* read(Bitstream& in, QObject* object = NULL) const; @@ -1104,7 +1110,7 @@ private: Q_DECLARE_METATYPE(const QMetaObject*) /// Macro for registering streamable meta-objects. Typically, one would use this macro at the top level of the source file -/// associated with the class. +/// associated with the class. The class should have a no-argument constructor flagged with Q_INVOKABLE. #define REGISTER_META_OBJECT(x) static int x##Registration = Bitstream::registerMetaObject(#x, &x::staticMetaObject); /// Contains a value along with a pointer to its streamer. This is stored in QVariants when using fallback generics and @@ -1563,8 +1569,8 @@ public: Bitstream::registerTypeStreamer(qMetaTypeId<X>(), new CollectionTypeStreamer<X>()); /// Declares the metatype and the streaming operators. Typically, one would use this immediately after the definition of a -/// type flagged as STREAMABLE in its header file. The last lines ensure that the generated file will be included in the link -/// phase. +/// type flagged as STREAMABLE in its header file. The type should have a no-argument constructor. The last lines of this +/// macro ensure that the generated file will be included in the link phase. #ifdef _WIN32 #define DECLARE_STREAMABLE_METATYPE(X) Q_DECLARE_METATYPE(X) \ Bitstream& operator<<(Bitstream& out, const X& obj); \ diff --git a/libraries/metavoxels/src/DatagramSequencer.cpp b/libraries/metavoxels/src/DatagramSequencer.cpp index f1f60e4d87..eb02497321 100644 --- a/libraries/metavoxels/src/DatagramSequencer.cpp +++ b/libraries/metavoxels/src/DatagramSequencer.cpp @@ -23,6 +23,9 @@ const int MAX_DATAGRAM_SIZE = MAX_PACKET_SIZE; const int DEFAULT_MAX_PACKET_SIZE = 3000; +// the default slow-start threshold, which will be lowered quickly when we first encounter packet loss +const float DEFAULT_SLOW_START_THRESHOLD = 1000.0f; + DatagramSequencer::DatagramSequencer(const QByteArray& datagramHeader, QObject* parent) : QObject(parent), _outgoingPacketStream(&_outgoingPacketData, QIODevice::WriteOnly), @@ -37,7 +40,12 @@ DatagramSequencer::DatagramSequencer(const QByteArray& datagramHeader, QObject* _incomingPacketStream(&_incomingPacketData, QIODevice::ReadOnly), _inputStream(_incomingPacketStream), _receivedHighPriorityMessages(0), - _maxPacketSize(DEFAULT_MAX_PACKET_SIZE) { + _maxPacketSize(DEFAULT_MAX_PACKET_SIZE), + _packetsPerGroup(1.0f), + _packetsToWrite(0.0f), + _slowStartThreshold(DEFAULT_SLOW_START_THRESHOLD), + _packetRateIncreasePacketNumber(0), + _packetRateDecreasePacketNumber(0) { _outgoingPacketStream.setByteOrder(QDataStream::LittleEndian); _incomingDatagramStream.setByteOrder(QDataStream::LittleEndian); @@ -71,6 +79,33 @@ ReliableChannel* DatagramSequencer::getReliableInputChannel(int index) { return channel; } +int DatagramSequencer::startPacketGroup(int desiredPackets) { + // figure out how much data we have enqueued and increase the number of packets desired + int totalAvailable = 0; + foreach (ReliableChannel* channel, _reliableOutputChannels) { + totalAvailable += channel->getBytesAvailable(); + } + desiredPackets += (totalAvailable / _maxPacketSize); + + // increment our packet counter and subtract/return the integer portion + _packetsToWrite += _packetsPerGroup; + int wholePackets = (int)_packetsToWrite; + _packetsToWrite -= wholePackets; + wholePackets = qMin(wholePackets, desiredPackets); + + // if we don't want to send any more, push out the rate increase number past the group + if (desiredPackets <= _packetsPerGroup) { + _packetRateIncreasePacketNumber = _outgoingPacketNumber + wholePackets + 1; + } + + // likewise, if we're only sending one packet, don't let its loss cause rate decrease + if (wholePackets == 1) { + _packetRateDecreasePacketNumber = _outgoingPacketNumber + 2; + } + + return wholePackets; +} + Bitstream& DatagramSequencer::startPacket() { // start with the list of acknowledgements _outgoingPacketStream << (quint32)_receiveRecords.size(); @@ -105,6 +140,12 @@ void DatagramSequencer::endPacket() { _outgoingPacketStream.device()->seek(0); } +void DatagramSequencer::cancelPacket() { + _outputStream.reset(); + _outputStream.getAndResetWriteMappings(); + _outgoingPacketStream.device()->seek(0); +} + /// Simple RAII-style object to keep a device open when in scope. class QIODeviceOpener { public: @@ -172,7 +213,10 @@ void DatagramSequencer::receivedDatagram(const QByteArray& datagram) { if (index < 0 || index >= _sendRecords.size()) { continue; } - QList<SendRecord>::iterator it = _sendRecords.begin() + index; + QList<SendRecord>::iterator it = _sendRecords.begin(); + for (int i = 0; i < index; i++) { + sendRecordLost(*it++); + } sendRecordAcknowledged(*it); emit sendAcknowledged(index); _sendRecords.erase(_sendRecords.begin(), it + 1); @@ -253,6 +297,28 @@ void DatagramSequencer::sendRecordAcknowledged(const SendRecord& record) { foreach (const ChannelSpan& span, record.spans) { getReliableOutputChannel(span.channel)->spanAcknowledged(span); } + + // increase the packet rate with every ack until we pass the slow start threshold; then, every round trip + if (record.packetNumber >= _packetRateIncreasePacketNumber) { + if (_packetsPerGroup >= _slowStartThreshold) { + _packetRateIncreasePacketNumber = _outgoingPacketNumber + 1; + } + _packetsPerGroup += 1.0f; + } +} + +void DatagramSequencer::sendRecordLost(const SendRecord& record) { + // notify the channels of their lost spans + foreach (const ChannelSpan& span, record.spans) { + getReliableOutputChannel(span.channel)->spanLost(record.packetNumber, _outgoingPacketNumber + 1); + } + + // halve the rate and remember as threshold + if (record.packetNumber >= _packetRateDecreasePacketNumber) { + _packetsPerGroup = qMax(_packetsPerGroup * 0.5f, 1.0f); + _slowStartThreshold = _packetsPerGroup; + _packetRateDecreasePacketNumber = _outgoingPacketNumber + 1; + } } void DatagramSequencer::appendReliableData(int bytes, QVector<ChannelSpan>& spans) { @@ -520,7 +586,9 @@ int SpanList::set(int offset, int length) { // look for an intersection within the list int position = 0; - for (QList<Span>::iterator it = _spans.begin(); it != _spans.end(); it++) { + for (int i = 0; i < _spans.size(); i++) { + QList<Span>::iterator it = _spans.begin() + i; + // if we intersect the unset portion, contract it position += it->unset; if (offset <= position) { @@ -530,16 +598,20 @@ int SpanList::set(int offset, int length) { // if we continue into the set portion, expand it and consume following spans int extra = offset + length - position; if (extra >= 0) { - int amount = setSpans(it + 1, extra); - it->set += amount; - _totalSet += amount; - + extra -= it->set; + it->set += remove; + _totalSet += remove; + if (extra > 0) { + int amount = setSpans(it + 1, extra); + _spans[i].set += amount; + _totalSet += amount; + } // otherwise, insert a new span } else { - Span span = { it->unset, length + extra }; - _spans.insert(it, span); + Span span = { it->unset, length }; it->unset = -extra; - _totalSet += span.set; + _spans.insert(it, span); + _totalSet += length; } return 0; } @@ -548,9 +620,11 @@ int SpanList::set(int offset, int length) { position += it->set; if (offset <= position) { int extra = offset + length - position; - int amount = setSpans(it + 1, extra); - it->set += amount; - _totalSet += amount; + if (extra > 0) { + int amount = setSpans(it + 1, extra); + _spans[i].set += amount; + _totalSet += amount; + } return 0; } } @@ -619,6 +693,7 @@ ReliableChannel::ReliableChannel(DatagramSequencer* sequencer, int index, bool o _priority(1.0f), _offset(0), _writePosition(0), + _writePositionResetPacketNumber(0), _messagesEnabled(true) { _buffer.open(output ? QIODevice::WriteOnly : QIODevice::ReadOnly); @@ -629,67 +704,76 @@ ReliableChannel::ReliableChannel(DatagramSequencer* sequencer, int index, bool o } void ReliableChannel::writeData(QDataStream& out, int bytes, QVector<DatagramSequencer::ChannelSpan>& spans) { - // find out how many spans we want to write - int spanCount = 0; - int remainingBytes = bytes; - bool first = true; - while (remainingBytes > 0) { - int position = 0; - foreach (const SpanList::Span& span, _acknowledged.getSpans()) { - if (remainingBytes <= 0) { - break; - } - spanCount++; - remainingBytes -= getBytesToWrite(first, qMin(remainingBytes, span.unset)); - position += (span.unset + span.set); - } - int leftover = _buffer.pos() - position; - if (remainingBytes > 0 && leftover > 0) { - spanCount++; - remainingBytes -= getBytesToWrite(first, qMin(remainingBytes, leftover)); - } + if (bytes == 0) { + out << (quint32)0; + return; } - - // write the count and the spans - out << (quint32)spanCount; - remainingBytes = bytes; - first = true; - while (remainingBytes > 0) { + _writePosition %= _buffer.pos(); + while (bytes > 0) { int position = 0; - foreach (const SpanList::Span& span, _acknowledged.getSpans()) { - if (remainingBytes <= 0) { - break; + for (int i = 0; i < _acknowledged.getSpans().size(); i++) { + const SpanList::Span& span = _acknowledged.getSpans().at(i); + position += span.unset; + if (_writePosition < position) { + int start = qMax(position - span.unset, _writePosition); + int length = qMin(bytes, position - start); + writeSpan(out, start, length, spans); + writeFullSpans(out, bytes - length, i + 1, position + span.set, spans); + out << (quint32)0; + return; } - remainingBytes -= writeSpan(out, first, position, qMin(remainingBytes, span.unset), spans); - position += (span.unset + span.set); + position += span.set; } int leftover = _buffer.pos() - position; - if (remainingBytes > 0 && leftover > 0) { - remainingBytes -= writeSpan(out, first, position, qMin(remainingBytes, leftover), spans); + position = _buffer.pos(); + + if (_writePosition < position && leftover > 0) { + int start = qMax(position - leftover, _writePosition); + int length = qMin(bytes, position - start); + writeSpan(out, start, length, spans); + writeFullSpans(out, bytes - length, 0, 0, spans); + out << (quint32)0; + return; + } + _writePosition = 0; + } +} + +void ReliableChannel::writeFullSpans(QDataStream& out, int bytes, int startingIndex, int position, + QVector<DatagramSequencer::ChannelSpan>& spans) { + int expandedSize = _acknowledged.getSpans().size() + 1; + for (int i = 0; i < expandedSize; i++) { + if (bytes == 0) { + return; + } + int index = (startingIndex + i) % expandedSize; + if (index == _acknowledged.getSpans().size()) { + int leftover = _buffer.pos() - position; + if (leftover > 0) { + int length = qMin(leftover, bytes); + writeSpan(out, position, length, spans); + bytes -= length; + } + position = 0; + + } else { + const SpanList::Span& span = _acknowledged.getSpans().at(index); + int length = qMin(span.unset, bytes); + writeSpan(out, position, length, spans); + bytes -= length; + position += (span.unset + span.set); } } } -int ReliableChannel::getBytesToWrite(bool& first, int length) const { - if (first) { - first = false; - return length - (_writePosition % length); - } - return length; -} - -int ReliableChannel::writeSpan(QDataStream& out, bool& first, int position, int length, QVector<DatagramSequencer::ChannelSpan>& spans) { - if (first) { - first = false; - position = _writePosition % length; - length -= position; - _writePosition += length; - } +int ReliableChannel::writeSpan(QDataStream& out, int position, int length, QVector<DatagramSequencer::ChannelSpan>& spans) { DatagramSequencer::ChannelSpan span = { _index, _offset + position, length }; spans.append(span); - out << (quint32)span.offset; out << (quint32)length; + out << (quint32)span.offset; _buffer.writeToStream(position, length, out); + _writePosition = position + length; + return length; } @@ -700,17 +784,28 @@ void ReliableChannel::spanAcknowledged(const DatagramSequencer::ChannelSpan& spa _buffer.seek(_buffer.size()); _offset += advancement; - _writePosition = qMax(_writePosition - advancement, 0); - } + _writePosition = qMax(_writePosition - advancement, 0); + } +} + +void ReliableChannel::spanLost(int packetNumber, int nextOutgoingPacketNumber) { + // reset the write position up to once each round trip time + if (packetNumber >= _writePositionResetPacketNumber) { + _writePosition = 0; + _writePositionResetPacketNumber = nextOutgoingPacketNumber; + } } void ReliableChannel::readData(QDataStream& in) { - quint32 segments; - in >> segments; bool readSome = false; - for (quint32 i = 0; i < segments; i++) { - quint32 offset, size; - in >> offset >> size; + forever { + quint32 size; + in >> size; + if (size == 0) { + break; + } + quint32 offset; + in >> offset; int position = offset - _offset; int end = position + size; diff --git a/libraries/metavoxels/src/DatagramSequencer.h b/libraries/metavoxels/src/DatagramSequencer.h index 5ac88556f0..aa8b6907ff 100644 --- a/libraries/metavoxels/src/DatagramSequencer.h +++ b/libraries/metavoxels/src/DatagramSequencer.h @@ -99,6 +99,11 @@ public: /// Returns the intput channel at the specified index, creating it if necessary. ReliableChannel* getReliableInputChannel(int index = 0); + /// Starts a packet group. + /// \param desiredPackets the number of packets we'd like to write in the group + /// \return the number of packets to write in the group + int startPacketGroup(int desiredPackets = 1); + /// Starts a new packet for transmission. /// \return a reference to the Bitstream to use for writing to the packet Bitstream& startPacket(); @@ -106,6 +111,9 @@ public: /// Sends the packet currently being written. void endPacket(); + /// Cancels the packet currently being written. + void cancelPacket(); + /// Processes a datagram received from the other party, emitting readyToRead when the entire packet /// has been successfully assembled. Q_INVOKABLE void receivedDatagram(const QByteArray& datagram); @@ -165,6 +173,9 @@ private: /// Notes that the described send was acknowledged by the other party. void sendRecordAcknowledged(const SendRecord& record); + /// Notes that the described send was lost in transit. + void sendRecordLost(const SendRecord& record); + /// Appends some reliable data to the outgoing packet. void appendReliableData(int bytes, QVector<ChannelSpan>& spans); @@ -200,6 +211,12 @@ private: int _maxPacketSize; + float _packetsPerGroup; + float _packetsToWrite; + float _slowStartThreshold; + int _packetRateIncreasePacketNumber; + int _packetRateDecreasePacketNumber; + QHash<int, ReliableChannel*> _reliableOutputChannels; QHash<int, ReliableChannel*> _reliableInputChannels; }; @@ -343,10 +360,12 @@ private: ReliableChannel(DatagramSequencer* sequencer, int index, bool output); void writeData(QDataStream& out, int bytes, QVector<DatagramSequencer::ChannelSpan>& spans); - int getBytesToWrite(bool& first, int length) const; - int writeSpan(QDataStream& out, bool& first, int position, int length, QVector<DatagramSequencer::ChannelSpan>& spans); + void writeFullSpans(QDataStream& out, int bytes, int startingIndex, int position, + QVector<DatagramSequencer::ChannelSpan>& spans); + int writeSpan(QDataStream& out, int position, int length, QVector<DatagramSequencer::ChannelSpan>& spans); void spanAcknowledged(const DatagramSequencer::ChannelSpan& span); + void spanLost(int packetNumber, int nextOutgoingPacketNumber); void readData(QDataStream& in); @@ -359,6 +378,7 @@ private: int _offset; int _writePosition; + int _writePositionResetPacketNumber; SpanList _acknowledged; bool _messagesEnabled; }; diff --git a/libraries/metavoxels/src/Endpoint.cpp b/libraries/metavoxels/src/Endpoint.cpp new file mode 100644 index 0000000000..c656054504 --- /dev/null +++ b/libraries/metavoxels/src/Endpoint.cpp @@ -0,0 +1,109 @@ +// +// Endpoint.cpp +// libraries/metavoxels/src +// +// Created by Andrzej Kapolka on 6/26/14. +// Copyright 2014 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 <PacketHeaders.h> + +#include "Endpoint.h" + +Endpoint::Endpoint(const SharedNodePointer& node, PacketRecord* baselineSendRecord, PacketRecord* baselineReceiveRecord) : + _node(node), + _sequencer(byteArrayWithPopulatedHeader(PacketTypeMetavoxelData)) { + + connect(&_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendDatagram(const QByteArray&))); + connect(&_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readMessage(Bitstream&))); + connect(&_sequencer, SIGNAL(sendAcknowledged(int)), SLOT(clearSendRecordsBefore(int))); + connect(&_sequencer, SIGNAL(receiveAcknowledged(int)), SLOT(clearReceiveRecordsBefore(int))); + + // insert the baseline send and receive records + _sendRecords.append(baselineSendRecord); + _receiveRecords.append(baselineReceiveRecord); +} + +Endpoint::~Endpoint() { + foreach (PacketRecord* record, _sendRecords) { + delete record; + } + foreach (PacketRecord* record, _receiveRecords) { + delete record; + } +} + +void Endpoint::update() { + Bitstream& out = _sequencer.startPacket(); + writeUpdateMessage(out); + _sequencer.endPacket(); + + // record the send + _sendRecords.append(maybeCreateSendRecord()); +} + +int Endpoint::parseData(const QByteArray& packet) { + // process through sequencer + _sequencer.receivedDatagram(packet); + return packet.size(); +} + +void Endpoint::sendDatagram(const QByteArray& data) { + NodeList::getInstance()->writeDatagram(data, _node); +} + +void Endpoint::readMessage(Bitstream& in) { + QVariant message; + in >> message; + handleMessage(message, in); + + // record the receipt + _receiveRecords.append(maybeCreateReceiveRecord()); +} + +void Endpoint::clearSendRecordsBefore(int index) { + QList<PacketRecord*>::iterator end = _sendRecords.begin() + index + 1; + for (QList<PacketRecord*>::const_iterator it = _sendRecords.begin(); it != end; it++) { + delete *it; + } + _sendRecords.erase(_sendRecords.begin(), end); +} + +void Endpoint::clearReceiveRecordsBefore(int index) { + QList<PacketRecord*>::iterator end = _receiveRecords.begin() + index + 1; + for (QList<PacketRecord*>::const_iterator it = _receiveRecords.begin(); it != end; it++) { + delete *it; + } + _receiveRecords.erase(_receiveRecords.begin(), end); +} + +void Endpoint::writeUpdateMessage(Bitstream& out) { + out << QVariant(); +} + +void Endpoint::handleMessage(const QVariant& message, Bitstream& in) { + if (message.userType() == QMetaType::QVariantList) { + foreach (const QVariant& element, message.toList()) { + handleMessage(element, in); + } + } +} + +PacketRecord* Endpoint::maybeCreateSendRecord() const { + return NULL; +} + +PacketRecord* Endpoint::maybeCreateReceiveRecord() const { + return NULL; +} + +PacketRecord::PacketRecord(const MetavoxelLOD& lod, const MetavoxelData& data) : + _lod(lod), + _data(data) { +} + +PacketRecord::~PacketRecord() { +} diff --git a/libraries/metavoxels/src/Endpoint.h b/libraries/metavoxels/src/Endpoint.h new file mode 100644 index 0000000000..d253a69ded --- /dev/null +++ b/libraries/metavoxels/src/Endpoint.h @@ -0,0 +1,78 @@ +// +// Endpoint.h +// libraries/metavoxels/src +// +// Created by Andrzej Kapolka on 6/26/14. +// Copyright 2014 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_Endpoint_h +#define hifi_Endpoint_h + +#include <NodeList.h> + +#include "DatagramSequencer.h" +#include "MetavoxelData.h" + +class PacketRecord; + +/// Base class for communication endpoints: clients and server sessions. +class Endpoint : public NodeData { + Q_OBJECT + +public: + + Endpoint(const SharedNodePointer& node, PacketRecord* baselineSendRecord = NULL, + PacketRecord* baselineReceiveRecord = NULL); + virtual ~Endpoint(); + + virtual void update(); + + virtual int parseData(const QByteArray& packet); + +protected slots: + + virtual void sendDatagram(const QByteArray& data); + virtual void readMessage(Bitstream& in); + + void clearSendRecordsBefore(int index); + void clearReceiveRecordsBefore(int index); + +protected: + + virtual void writeUpdateMessage(Bitstream& out); + virtual void handleMessage(const QVariant& message, Bitstream& in); + + virtual PacketRecord* maybeCreateSendRecord() const; + virtual PacketRecord* maybeCreateReceiveRecord() const; + + PacketRecord* getLastAcknowledgedSendRecord() const { return _sendRecords.first(); } + PacketRecord* getLastAcknowledgedReceiveRecord() const { return _receiveRecords.first(); } + + SharedNodePointer _node; + DatagramSequencer _sequencer; + + QList<PacketRecord*> _sendRecords; + QList<PacketRecord*> _receiveRecords; +}; + +/// Base class for packet records. +class PacketRecord { +public: + + PacketRecord(const MetavoxelLOD& lod = MetavoxelLOD(), const MetavoxelData& data = MetavoxelData()); + virtual ~PacketRecord(); + + const MetavoxelLOD& getLOD() const { return _lod; } + const MetavoxelData& getData() const { return _data; } + +private: + + MetavoxelLOD _lod; + MetavoxelData _data; +}; + +#endif // hifi_Endpoint_h diff --git a/libraries/metavoxels/src/MetavoxelClientManager.cpp b/libraries/metavoxels/src/MetavoxelClientManager.cpp new file mode 100644 index 0000000000..008a477187 --- /dev/null +++ b/libraries/metavoxels/src/MetavoxelClientManager.cpp @@ -0,0 +1,142 @@ +// +// MetavoxelClientManager.cpp +// libraries/metavoxels/src +// +// Created by Andrzej Kapolka on 6/26/14. +// Copyright 2014 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 "MetavoxelClientManager.h" +#include "MetavoxelMessages.h" + +void MetavoxelClientManager::init() { + connect(NodeList::getInstance(), SIGNAL(nodeAdded(SharedNodePointer)), SLOT(maybeAttachClient(const SharedNodePointer&))); +} + +void MetavoxelClientManager::update() { + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getType() == NodeType::MetavoxelServer) { + QMutexLocker locker(&node->getMutex()); + MetavoxelClient* client = static_cast<MetavoxelClient*>(node->getLinkedData()); + if (client) { + updateClient(client); + } + } + } +} + +SharedObjectPointer MetavoxelClientManager::findFirstRaySpannerIntersection(const glm::vec3& origin, + const glm::vec3& direction, const AttributePointer& attribute, float& distance) { + SharedObjectPointer closestSpanner; + float closestDistance = FLT_MAX; + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getType() == NodeType::MetavoxelServer) { + QMutexLocker locker(&node->getMutex()); + MetavoxelClient* client = static_cast<MetavoxelClient*>(node->getLinkedData()); + if (client) { + float clientDistance; + SharedObjectPointer clientSpanner = client->getData().findFirstRaySpannerIntersection( + origin, direction, attribute, clientDistance); + if (clientSpanner && clientDistance < closestDistance) { + closestSpanner = clientSpanner; + closestDistance = clientDistance; + } + } + } + } + if (closestSpanner) { + distance = closestDistance; + } + return closestSpanner; +} + +void MetavoxelClientManager::applyEdit(const MetavoxelEditMessage& edit, bool reliable) { + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getType() == NodeType::MetavoxelServer) { + QMutexLocker locker(&node->getMutex()); + MetavoxelClient* client = static_cast<MetavoxelClient*>(node->getLinkedData()); + if (client) { + client->applyEdit(edit, reliable); + } + } + } +} + +MetavoxelLOD MetavoxelClientManager::getLOD() const { + return MetavoxelLOD(); +} + +void MetavoxelClientManager::maybeAttachClient(const SharedNodePointer& node) { + if (node->getType() == NodeType::MetavoxelServer) { + QMutexLocker locker(&node->getMutex()); + node->setLinkedData(createClient(node)); + } +} + +MetavoxelClient* MetavoxelClientManager::createClient(const SharedNodePointer& node) { + return new MetavoxelClient(node, this); +} + +void MetavoxelClientManager::updateClient(MetavoxelClient* client) { + client->update(); +} + +MetavoxelClient::MetavoxelClient(const SharedNodePointer& node, MetavoxelClientManager* manager) : + Endpoint(node, new PacketRecord(), new PacketRecord()), + _manager(manager) { +} + +void MetavoxelClient::guide(MetavoxelVisitor& visitor) { + visitor.setLOD(_manager->getLOD()); + _data.guide(visitor); +} + +void MetavoxelClient::applyEdit(const MetavoxelEditMessage& edit, bool reliable) { + if (reliable) { + _sequencer.getReliableOutputChannel()->sendMessage(QVariant::fromValue(edit)); + + } else { + // apply immediately to local tree + edit.apply(_data, _sequencer.getWeakSharedObjectHash()); + + // start sending it out + _sequencer.sendHighPriorityMessage(QVariant::fromValue(edit)); + } +} + +void MetavoxelClient::writeUpdateMessage(Bitstream& out) { + ClientStateMessage state = { _manager->getLOD() }; + out << QVariant::fromValue(state); +} + +void MetavoxelClient::readMessage(Bitstream& in) { + Endpoint::readMessage(in); + + // reapply local edits + foreach (const DatagramSequencer::HighPriorityMessage& message, _sequencer.getHighPriorityMessages()) { + if (message.data.userType() == MetavoxelEditMessage::Type) { + message.data.value<MetavoxelEditMessage>().apply(_data, _sequencer.getWeakSharedObjectHash()); + } + } +} + +void MetavoxelClient::handleMessage(const QVariant& message, Bitstream& in) { + if (message.userType() == MetavoxelDeltaMessage::Type) { + PacketRecord* receiveRecord = getLastAcknowledgedReceiveRecord(); + _data.readDelta(receiveRecord->getData(), receiveRecord->getLOD(), in, getLastAcknowledgedSendRecord()->getLOD()); + + } else { + Endpoint::handleMessage(message, in); + } +} + +PacketRecord* MetavoxelClient::maybeCreateSendRecord() const { + return new PacketRecord(_manager->getLOD()); +} + +PacketRecord* MetavoxelClient::maybeCreateReceiveRecord() const { + return new PacketRecord(getLastAcknowledgedSendRecord()->getLOD(), _data); +} diff --git a/libraries/metavoxels/src/MetavoxelClientManager.h b/libraries/metavoxels/src/MetavoxelClientManager.h new file mode 100644 index 0000000000..dd11e871ec --- /dev/null +++ b/libraries/metavoxels/src/MetavoxelClientManager.h @@ -0,0 +1,75 @@ +// +// MetavoxelClientManager.h +// libraries/metavoxels/src +// +// Created by Andrzej Kapolka on 6/26/14. +// Copyright 2014 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_MetavoxelClientManager_h +#define hifi_MetavoxelClientManager_h + +#include "Endpoint.h" + +class MetavoxelClient; +class MetavoxelEditMessage; + +/// Manages the set of connected metavoxel clients. +class MetavoxelClientManager : public QObject { + Q_OBJECT + +public: + + virtual void init(); + void update(); + + SharedObjectPointer findFirstRaySpannerIntersection(const glm::vec3& origin, const glm::vec3& direction, + const AttributePointer& attribute, float& distance); + + Q_INVOKABLE void applyEdit(const MetavoxelEditMessage& edit, bool reliable = false); + + virtual MetavoxelLOD getLOD() const; + +private slots: + + void maybeAttachClient(const SharedNodePointer& node); + +protected: + + virtual MetavoxelClient* createClient(const SharedNodePointer& node); + virtual void updateClient(MetavoxelClient* client); +}; + +/// Base class for metavoxel clients. +class MetavoxelClient : public Endpoint { + Q_OBJECT + +public: + + MetavoxelClient(const SharedNodePointer& node, MetavoxelClientManager* manager); + + MetavoxelData& getData() { return _data; } + + void guide(MetavoxelVisitor& visitor); + + void applyEdit(const MetavoxelEditMessage& edit, bool reliable = false); + +protected: + + virtual void writeUpdateMessage(Bitstream& out); + virtual void readMessage(Bitstream& in); + virtual void handleMessage(const QVariant& message, Bitstream& in); + + virtual PacketRecord* maybeCreateSendRecord() const; + virtual PacketRecord* maybeCreateReceiveRecord() const; + +private: + + MetavoxelClientManager* _manager; + MetavoxelData _data; +}; + +#endif // hifi_MetavoxelClientManager_h diff --git a/libraries/metavoxels/src/MetavoxelData.cpp b/libraries/metavoxels/src/MetavoxelData.cpp index 0d52fc5ed6..2d61ede796 100644 --- a/libraries/metavoxels/src/MetavoxelData.cpp +++ b/libraries/metavoxels/src/MetavoxelData.cpp @@ -610,6 +610,23 @@ MetavoxelNode* MetavoxelData::createRoot(const AttributePointer& attribute) { return root = new MetavoxelNode(attribute); } +bool MetavoxelData::deepEquals(const MetavoxelData& other, const MetavoxelLOD& lod) const { + if (_size != other._size) { + return false; + } + if (_roots.size() != other._roots.size()) { + return false; + } + glm::vec3 minimum = getMinimum(); + for (QHash<AttributePointer, MetavoxelNode*>::const_iterator it = _roots.constBegin(); it != _roots.constEnd(); it++) { + MetavoxelNode* otherNode = other._roots.value(it.key()); + if (!(otherNode && it.key()->metavoxelRootsEqual(*it.value(), *otherNode, minimum, _size, lod))) { + return false; + } + } + return true; +} + bool MetavoxelData::operator==(const MetavoxelData& other) const { return _size == other._size && _roots == other._roots; } @@ -1006,6 +1023,44 @@ void MetavoxelNode::clearChildren(const AttributePointer& attribute) { } } +bool MetavoxelNode::deepEquals(const AttributePointer& attribute, const MetavoxelNode& other, + const glm::vec3& minimum, float size, const MetavoxelLOD& lod) const { + if (!attribute->deepEqual(_attributeValue, other._attributeValue)) { + return false; + } + if (!lod.shouldSubdivide(minimum, size, attribute->getLODThresholdMultiplier())) { + return true; + } + bool leaf = isLeaf(), otherLeaf = other.isLeaf(); + if (leaf && otherLeaf) { + return true; + } + if (leaf || otherLeaf) { + return false; + } + float nextSize = size * 0.5f; + for (int i = 0; i < CHILD_COUNT; i++) { + glm::vec3 nextMinimum = getNextMinimum(minimum, nextSize, i); + if (!_children[i]->deepEquals(attribute, *(other._children[i]), nextMinimum, nextSize, lod)) { + return false; + } + } + return true; +} + +void MetavoxelNode::getSpanners(const AttributePointer& attribute, const glm::vec3& minimum, + float size, const MetavoxelLOD& lod, SharedObjectSet& results) const { + results.unite(decodeInline<SharedObjectSet>(_attributeValue)); + if (isLeaf() || !lod.shouldSubdivide(minimum, size, attribute->getLODThresholdMultiplier())) { + return; + } + float nextSize = size * 0.5f; + for (int i = 0; i < CHILD_COUNT; i++) { + glm::vec3 nextMinimum = getNextMinimum(minimum, nextSize, i); + _children[i]->getSpanners(attribute, nextMinimum, nextSize, lod, results); + } +} + int MetavoxelVisitor::encodeOrder(int first, int second, int third, int fourth, int fifth, int sixth, int seventh, int eighth) { return first | (second << 3) | (third << 6) | (fourth << 9) | @@ -1034,6 +1089,25 @@ int MetavoxelVisitor::encodeOrder(const glm::vec3& direction) { indexDistances.at(6).index, indexDistances.at(7).index); } +const int ORDER_ELEMENT_BITS = 3; +const int ORDER_ELEMENT_MASK = (1 << ORDER_ELEMENT_BITS) - 1; + +int MetavoxelVisitor::encodeRandomOrder() { + // see http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_.22inside-out.22_algorithm + int order = 0; + int randomValues = rand(); + for (int i = 0, iShift = 0; i < MetavoxelNode::CHILD_COUNT; i++, iShift += ORDER_ELEMENT_BITS) { + int j = (randomValues >> iShift) % (i + 1); + int jShift = j * ORDER_ELEMENT_BITS; + if (j != i) { + int jValue = (order >> jShift) & ORDER_ELEMENT_MASK; + order |= (jValue << iShift); + } + order = (order & ~(ORDER_ELEMENT_MASK << jShift)) | (i << jShift); + } + return order; +} + const int MetavoxelVisitor::DEFAULT_ORDER = encodeOrder(0, 1, 2, 3, 4, 5, 6, 7); const int MetavoxelVisitor::STOP_RECURSION = 0; const int MetavoxelVisitor::SHORT_CIRCUIT = -1; @@ -1227,8 +1301,6 @@ bool DefaultMetavoxelGuide::guide(MetavoxelVisitation& visitation) { QVector<OwnedAttributeValue>(visitation.outputNodes.size()) } }; for (int i = 0; i < MetavoxelNode::CHILD_COUNT; i++) { // the encoded order tells us the child indices for each iteration - const int ORDER_ELEMENT_BITS = 3; - const int ORDER_ELEMENT_MASK = (1 << ORDER_ELEMENT_BITS) - 1; int index = encodedOrder & ORDER_ELEMENT_MASK; encodedOrder >>= ORDER_ELEMENT_BITS; for (int j = 0; j < visitation.inputNodes.size(); j++) { @@ -1269,7 +1341,7 @@ bool DefaultMetavoxelGuide::guide(MetavoxelVisitation& visitation) { } } MetavoxelNode* node = visitation.outputNodes.at(j); - MetavoxelNode* child = node->getChild(i); + MetavoxelNode* child = node->getChild(index); if (child) { child->decrementReferenceCount(value.getAttribute()); } else { diff --git a/libraries/metavoxels/src/MetavoxelData.h b/libraries/metavoxels/src/MetavoxelData.h index 2e6f6c4437..6a7ba33eb5 100644 --- a/libraries/metavoxels/src/MetavoxelData.h +++ b/libraries/metavoxels/src/MetavoxelData.h @@ -34,7 +34,8 @@ class NetworkValue; class Spanner; class SpannerRenderer; -/// Determines whether to subdivide each node when traversing. +/// Determines whether to subdivide each node when traversing. Contains the position (presumed to be of the viewer) and a +/// threshold value, where lower thresholds cause smaller/more distant voxels to be subdivided. class MetavoxelLOD { STREAMABLE @@ -46,6 +47,7 @@ public: bool isValid() const { return threshold > 0.0f; } + /// Checks whether, according to this LOD, we should subdivide the described voxel. bool shouldSubdivide(const glm::vec3& minimum, float size, float multiplier = 1.0f) const; /// Checks whether the node or any of the nodes underneath it have had subdivision enabled as compared to the reference. @@ -54,7 +56,8 @@ public: DECLARE_STREAMABLE_METATYPE(MetavoxelLOD) -/// The base metavoxel representation shared between server and client. +/// The base metavoxel representation shared between server and client. Contains a size (for all dimensions) and a set of +/// octrees for different attributes. class MetavoxelData { public: @@ -64,30 +67,38 @@ public: MetavoxelData& operator=(const MetavoxelData& other); + /// Sets the size in all dimensions. void setSize(float size) { _size = size; } float getSize() const { return _size; } + /// Returns the minimum extent of the octrees (which are centered about the origin). glm::vec3 getMinimum() const { return glm::vec3(_size, _size, _size) * -0.5f; } + /// Returns the bounds of the octrees. Box getBounds() const; /// Applies the specified visitor to the contained voxels. void guide(MetavoxelVisitor& visitor); + /// Inserts a spanner into the specified attribute layer. void insert(const AttributePointer& attribute, const SharedObjectPointer& object); void insert(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object); + /// Removes a spanner from the specified attribute layer. void remove(const AttributePointer& attribute, const SharedObjectPointer& object); void remove(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object); + /// Toggles the existence of a spanner in the specified attribute layer (removes if present, adds if not). void toggle(const AttributePointer& attribute, const SharedObjectPointer& object); void toggle(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object); + /// Replaces a spanner in the specified attribute layer. void replace(const AttributePointer& attribute, const SharedObjectPointer& oldObject, const SharedObjectPointer& newObject); void replace(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& oldObject, const SharedObjectPointer& newObject); - + + /// Clears all data in the specified attribute layer. void clear(const AttributePointer& attribute); /// Convenience function that finds the first spanner intersecting the provided ray. @@ -97,7 +108,7 @@ public: /// Sets part of the data. void set(const glm::vec3& minimum, const MetavoxelData& data, bool blend = false); - /// Expands the tree, increasing its capacity in all dimensions. + /// Expands the tree, doubling its size in all dimensions (that is, increasing its volume eightfold). void expand(); void read(Bitstream& in, const MetavoxelLOD& lod = MetavoxelLOD()); @@ -110,6 +121,10 @@ public: MetavoxelNode* getRoot(const AttributePointer& attribute) const { return _roots.value(attribute); } MetavoxelNode* createRoot(const AttributePointer& attribute); + /// Performs a deep comparison between this data and the specified other (as opposed to the == operator, which does a + /// shallow comparison). + bool deepEquals(const MetavoxelData& other, const MetavoxelLOD& lod = MetavoxelLOD()) const; + bool operator==(const MetavoxelData& other) const; bool operator!=(const MetavoxelData& other) const; @@ -198,6 +213,14 @@ public: void clearChildren(const AttributePointer& attribute); + /// Performs a deep comparison between this and the specified other node. + bool deepEquals(const AttributePointer& attribute, const MetavoxelNode& other, + const glm::vec3& minimum, float size, const MetavoxelLOD& lod) const; + + /// Retrieves all spanners satisfying the LOD constraint, placing them in the provided set. + void getSpanners(const AttributePointer& attribute, const glm::vec3& minimum, + float size, const MetavoxelLOD& lod, SharedObjectSet& results) const; + private: Q_DISABLE_COPY(MetavoxelNode) @@ -234,6 +257,9 @@ public: /// Encodes a visitation order sequence that visits each child as sorted along the specified direction. static int encodeOrder(const glm::vec3& direction); + /// Returns a random visitation order sequence. + static int encodeRandomOrder(); + /// The default visitation order. static const int DEFAULT_ORDER; diff --git a/libraries/metavoxels/src/SharedObject.cpp b/libraries/metavoxels/src/SharedObject.cpp index 05af5f1bf8..d0a1842d31 100644 --- a/libraries/metavoxels/src/SharedObject.cpp +++ b/libraries/metavoxels/src/SharedObject.cpp @@ -84,11 +84,19 @@ bool SharedObject::equals(const SharedObject* other, bool sharedAncestry) const if (metaObject != other->metaObject() && !sharedAncestry) { return false; } - for (int i = 0; i < metaObject->propertyCount(); i++) { - QMetaProperty property = metaObject->property(i); - if (property.isStored() && property.read(this) != property.read(other)) { + // use the streamer, if we have one + const ObjectStreamer* streamer = Bitstream::getObjectStreamer(metaObject); + if (streamer) { + if (!streamer->equal(this, other)) { return false; } + } else { + for (int i = 0; i < metaObject->propertyCount(); i++) { + QMetaProperty property = metaObject->property(i); + if (property.isStored() && property.read(this) != property.read(other)) { + return false; + } + } } QList<QByteArray> dynamicPropertyNames = this->dynamicPropertyNames(); if (dynamicPropertyNames.size() != other->dynamicPropertyNames().size()) { diff --git a/libraries/models/src/ModelItem.cpp b/libraries/models/src/ModelItem.cpp index b6f4fe6c1d..8c061102a0 100644 --- a/libraries/models/src/ModelItem.cpp +++ b/libraries/models/src/ModelItem.cpp @@ -886,7 +886,19 @@ QScriptValue ModelItemProperties::copyToScriptValue(QScriptEngine* engine) const properties.setProperty("shouldDie", _shouldDie); properties.setProperty("modelURL", _modelURL); - + + + QScriptValue sittingPoints = engine->newObject(); + for (int i = 0; i < _sittingPoints.size(); ++i) { + QScriptValue sittingPoint = engine->newObject(); + sittingPoint.setProperty("name", _sittingPoints[i].name); + sittingPoint.setProperty("position", vec3toScriptValue(engine, _sittingPoints[i].position)); + sittingPoint.setProperty("rotation", quatToScriptValue(engine, _sittingPoints[i].rotation)); + sittingPoints.setProperty(i, sittingPoint); + } + sittingPoints.setProperty("length", _sittingPoints.size()); + properties.setProperty("sittingPoints", sittingPoints); + QScriptValue modelRotation = quatToScriptValue(engine, _modelRotation); properties.setProperty("modelRotation", modelRotation); @@ -971,7 +983,7 @@ void ModelItemProperties::copyFromScriptValue(const QScriptValue &object) { _modelURLChanged = true; } } - + QScriptValue modelRotation = object.property("modelRotation"); if (modelRotation.isValid()) { QScriptValue x = modelRotation.property("x"); @@ -1125,6 +1137,7 @@ void ModelItemProperties::copyFromModelItem(const ModelItem& modelItem) { _animationFrameIndex = modelItem.getAnimationFrameIndex(); _animationFPS = modelItem.getAnimationFPS(); _glowLevel = modelItem.getGlowLevel(); + _sittingPoints = modelItem.getSittingPoints(); _id = modelItem.getID(); _idSet = true; diff --git a/libraries/models/src/ModelItem.h b/libraries/models/src/ModelItem.h index 9a558f2ef4..43aaca48a0 100644 --- a/libraries/models/src/ModelItem.h +++ b/libraries/models/src/ModelItem.h @@ -12,9 +12,10 @@ #ifndef hifi_ModelItem_h #define hifi_ModelItem_h -#include <glm/glm.hpp> #include <stdint.h> +#include <glm/glm.hpp> + #include <QtScript/QScriptEngine> #include <QtCore/QObject> @@ -22,6 +23,8 @@ #include <CollisionInfo.h> #include <SharedUtil.h> #include <OctreePacketData.h> +#include <FBXReader.h> + class ModelItem; class ModelEditPacketSender; @@ -122,7 +125,8 @@ private: float _animationFrameIndex; float _animationFPS; float _glowLevel; - + QVector<SittingPoint> _sittingPoints; + uint32_t _id; bool _idSet; quint64 _lastEdited; @@ -211,6 +215,7 @@ public: bool hasAnimation() const { return !_animationURL.isEmpty(); } const QString& getAnimationURL() const { return _animationURL; } float getGlowLevel() const { return _glowLevel; } + QVector<SittingPoint> getSittingPoints() const { return _sittingPoints; } ModelItemID getModelItemID() const { return ModelItemID(getID(), getCreatorTokenID(), getID() != UNKNOWN_MODEL_ID); } ModelItemProperties getProperties() const; @@ -254,6 +259,7 @@ public: void setAnimationIsPlaying(bool value) { _animationIsPlaying = value; } void setAnimationFPS(float value) { _animationFPS = value; } void setGlowLevel(float glowLevel) { _glowLevel = glowLevel; } + void setSittingPoints(QVector<SittingPoint> sittingPoints) { _sittingPoints = sittingPoints; } void setProperties(const ModelItemProperties& properties); @@ -300,6 +306,8 @@ protected: QString _modelURL; glm::quat _modelRotation; + QVector<SittingPoint> _sittingPoints; + float _glowLevel; uint32_t _creatorTokenID; diff --git a/libraries/models/src/ModelTree.cpp b/libraries/models/src/ModelTree.cpp index 466d4c5273..763f0a969e 100644 --- a/libraries/models/src/ModelTree.cpp +++ b/libraries/models/src/ModelTree.cpp @@ -117,7 +117,7 @@ void ModelTree::storeModel(const ModelItem& model, const SharedNodePointer& send // if we didn't find it in the tree, then store it... if (!theOperator.wasFound()) { AACube modelCube = model.getAACube(); - ModelTreeElement* element = (ModelTreeElement*)getOrCreateChildElementContaining(model.getAACube()); + ModelTreeElement* element = static_cast<ModelTreeElement*>(getOrCreateChildElementContaining(model.getAACube())); element->storeModel(model); // In the case where we stored it, we also need to mark the entire "path" down to the model as diff --git a/libraries/models/src/ModelTreeElement.cpp b/libraries/models/src/ModelTreeElement.cpp index 75b9670d0f..960d1dd4cb 100644 --- a/libraries/models/src/ModelTreeElement.cpp +++ b/libraries/models/src/ModelTreeElement.cpp @@ -330,6 +330,9 @@ bool ModelTreeElement::updateModel(const ModelItemID& modelID, const ModelItemPr } if (found) { thisModel.setProperties(properties); + if (_myTree->getGeometryForModel(thisModel)) { + thisModel.setSittingPoints(_myTree->getGeometryForModel(thisModel)->sittingPoints); + } markWithChangedTime(); // mark our element as changed.. const bool wantDebug = false; if (wantDebug) { diff --git a/libraries/models/src/ModelsScriptingInterface.cpp b/libraries/models/src/ModelsScriptingInterface.cpp index 7e08571fe5..bac1213071 100644 --- a/libraries/models/src/ModelsScriptingInterface.cpp +++ b/libraries/models/src/ModelsScriptingInterface.cpp @@ -160,10 +160,8 @@ ModelItemID ModelsScriptingInterface::findClosestModel(const glm::vec3& center, QVector<ModelItemID> ModelsScriptingInterface::findModels(const glm::vec3& center, float radius) const { QVector<ModelItemID> result; if (_modelTree) { - _modelTree->lockForRead(); QVector<const ModelItem*> models; _modelTree->findModels(center/(float)TREE_SCALE, radius/(float)TREE_SCALE, models); - _modelTree->unlock(); foreach (const ModelItem* model, models) { ModelItemID thisModelItemID(model->getID(), UNKNOWN_MODEL_TOKEN, true); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 95689f8e82..f603d21240 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -11,6 +11,7 @@ #include "NodeList.h" #include "PacketHeaders.h" +#include "UserActivityLogger.h" #include "DomainHandler.h" @@ -83,6 +84,7 @@ void DomainHandler::setHostname(const QString& hostname) { qDebug("Looking up DS hostname %s.", _hostname.toLocal8Bit().constData()); QHostInfo::lookupHost(_hostname, this, SLOT(completedHostnameLookup(const QHostInfo&))); + UserActivityLogger::getInstance().changedDomain(_hostname); emit hostnameChanged(_hostname); } } diff --git a/libraries/networking/src/PacketHeaders.cpp b/libraries/networking/src/PacketHeaders.cpp index e2bc46b3be..a5c05a6ae9 100644 --- a/libraries/networking/src/PacketHeaders.cpp +++ b/libraries/networking/src/PacketHeaders.cpp @@ -50,6 +50,8 @@ PacketVersion versionForPacketType(PacketType type) { case PacketTypeMicrophoneAudioNoEcho: case PacketTypeMicrophoneAudioWithEcho: case PacketTypeSilentAudioFrame: + return 2; + case PacketTypeMixedAudio: return 1; case PacketTypeAvatarData: return 3; diff --git a/libraries/networking/src/PacketHeaders.h b/libraries/networking/src/PacketHeaders.h index 0f87b0e607..83350a32d1 100644 --- a/libraries/networking/src/PacketHeaders.h +++ b/libraries/networking/src/PacketHeaders.h @@ -40,7 +40,7 @@ enum PacketType { PacketTypeCreateAssignment, PacketTypeDomainOAuthRequest, PacketTypeMuteEnvironment, - PacketTypeDataServerSend, // reusable + PacketTypeAudioStreamStats, PacketTypeDataServerConfirm, PacketTypeVoxelQuery, PacketTypeVoxelData, diff --git a/libraries/networking/src/SequenceNumberStats.cpp b/libraries/networking/src/SequenceNumberStats.cpp new file mode 100644 index 0000000000..15d3c0542e --- /dev/null +++ b/libraries/networking/src/SequenceNumberStats.cpp @@ -0,0 +1,184 @@ +// +// SequenceNumberStats.cpp +// libraries/networking/src +// +// Created by Yixin Wang on 6/25/2014 +// Copyright 2014 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 "SequenceNumberStats.h" + +#include <limits> + +SequenceNumberStats::SequenceNumberStats() + : _lastReceived(std::numeric_limits<quint16>::max()), + _missingSet(), + _numReceived(0), + _numUnreasonable(0), + _numEarly(0), + _numLate(0), + _numLost(0), + _numRecovered(0), + _numDuplicate(0), + _lastSenderUUID() +{ +} + +void SequenceNumberStats::reset() { + _missingSet.clear(); + _numReceived = 0; + _numUnreasonable = 0; + _numEarly = 0; + _numLate = 0; + _numLost = 0; + _numRecovered = 0; + _numDuplicate = 0; +} + +static const int UINT16_RANGE = std::numeric_limits<uint16_t>::max() + 1; +static const int MAX_REASONABLE_SEQUENCE_GAP = 1000; // this must be less than UINT16_RANGE / 2 for rollover handling to work + +void SequenceNumberStats::sequenceNumberReceived(quint16 incoming, QUuid senderUUID, const bool wantExtraDebugging) { + + // if the sender node has changed, reset all stats + if (senderUUID != _lastSenderUUID) { + qDebug() << "sequence number stats was reset due to new sender node"; + qDebug() << "previous:" << _lastSenderUUID << "current:" << senderUUID; + reset(); + _lastSenderUUID = senderUUID; + } + + // determine our expected sequence number... handle rollover appropriately + quint16 expected = _numReceived > 0 ? _lastReceived + (quint16)1 : incoming; + + _numReceived++; + + if (incoming == expected) { // on time + _lastReceived = incoming; + } else { // out of order + + if (wantExtraDebugging) { + qDebug() << "out of order... got:" << incoming << "expected:" << expected; + } + + int incomingInt = (int)incoming; + int expectedInt = (int)expected; + + // check if the gap between incoming and expected is reasonable, taking possible rollover into consideration + int absGap = std::abs(incomingInt - expectedInt); + if (absGap >= UINT16_RANGE - MAX_REASONABLE_SEQUENCE_GAP) { + // rollover likely occurred between incoming and expected. + // correct the larger of the two so that it's within [-UINT16_RANGE, -1] while the other remains within [0, UINT16_RANGE-1] + if (incomingInt > expectedInt) { + incomingInt -= UINT16_RANGE; + } else { + expectedInt -= UINT16_RANGE; + } + } else if (absGap > MAX_REASONABLE_SEQUENCE_GAP) { + // ignore packet if gap is unreasonable + qDebug() << "ignoring unreasonable sequence number:" << incoming + << "previous:" << _lastReceived; + _numUnreasonable++; + return; + } + + // now that rollover has been corrected for (if it occurred), incoming and expected can be + // compared to each other directly, though one of them might be negative + if (incomingInt > expectedInt) { // early + if (wantExtraDebugging) { + qDebug() << "this packet is earlier than expected..."; + qDebug() << ">>>>>>>> missing gap=" << (incomingInt - expectedInt); + } + + _numEarly++; + _numLost += (incomingInt - expectedInt); + + // add all sequence numbers that were skipped to the missing sequence numbers list + for (int missingInt = expectedInt; missingInt < incomingInt; missingInt++) { + _missingSet.insert((quint16)(missingInt < 0 ? missingInt + UINT16_RANGE : missingInt)); + } + + // prune missing sequence list if it gets too big; sequence numbers that are older than MAX_REASONABLE_SEQUENCE_GAP + // will be removed. + if (_missingSet.size() > MAX_REASONABLE_SEQUENCE_GAP) { + pruneMissingSet(wantExtraDebugging); + } + + _lastReceived = incoming; + } else { // late + if (wantExtraDebugging) { + qDebug() << "this packet is later than expected..."; + } + _numLate++; + + // remove this from missing sequence number if it's in there + if (_missingSet.remove(incoming)) { + if (wantExtraDebugging) { + qDebug() << "found it in _missingSet"; + } + _numLost--; + _numRecovered++; + } else { + if (wantExtraDebugging) { + qDebug() << "sequence:" << incoming << "was NOT found in _missingSet and is probably a duplicate"; + } + _numDuplicate++; + } + + // do not update _incomingLastSequence; it shouldn't become smaller + } + } +} + +void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) { + if (wantExtraDebugging) { + qDebug() << "pruning _missingSet! size:" << _missingSet.size(); + } + + // some older sequence numbers may be from before a rollover point; this must be handled. + // some sequence numbers in this list may be larger than _incomingLastSequence, indicating that they were received + // before the most recent rollover. + int cutoff = (int)_lastReceived - MAX_REASONABLE_SEQUENCE_GAP; + if (cutoff >= 0) { + quint16 nonRolloverCutoff = (quint16)cutoff; + QSet<quint16>::iterator i = _missingSet.begin(); + while (i != _missingSet.end()) { + quint16 missing = *i; + if (wantExtraDebugging) { + qDebug() << "checking item:" << missing << "is it in need of pruning?"; + qDebug() << "old age cutoff:" << nonRolloverCutoff; + } + + if (missing > _lastReceived || missing < nonRolloverCutoff) { + i = _missingSet.erase(i); + if (wantExtraDebugging) { + qDebug() << "pruning really old missing sequence:" << missing; + } + } else { + i++; + } + } + } else { + quint16 rolloverCutoff = (quint16)(cutoff + UINT16_RANGE); + QSet<quint16>::iterator i = _missingSet.begin(); + while (i != _missingSet.end()) { + quint16 missing = *i; + if (wantExtraDebugging) { + qDebug() << "checking item:" << missing << "is it in need of pruning?"; + qDebug() << "old age cutoff:" << rolloverCutoff; + } + + if (missing > _lastReceived && missing < rolloverCutoff) { + i = _missingSet.erase(i); + if (wantExtraDebugging) { + qDebug() << "pruning really old missing sequence:" << missing; + } + } else { + i++; + } + } + } +} diff --git a/libraries/networking/src/SequenceNumberStats.h b/libraries/networking/src/SequenceNumberStats.h new file mode 100644 index 0000000000..b2561552ef --- /dev/null +++ b/libraries/networking/src/SequenceNumberStats.h @@ -0,0 +1,53 @@ +// +// SequenceNumberStats.h +// libraries/networking/src +// +// Created by Yixin Wang on 6/25/2014 +// Copyright 2014 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_SequenceNumberStats_h +#define hifi_SequenceNumberStats_h + +#include "SharedUtil.h" +#include <quuid.h> + +class SequenceNumberStats { +public: + SequenceNumberStats(); + + void reset(); + + void sequenceNumberReceived(quint16 incoming, QUuid senderUUID = QUuid(), const bool wantExtraDebugging = false); + + quint32 getNumReceived() const { return _numReceived; } + quint32 getNumUnreasonable() const { return _numUnreasonable; } + quint32 getNumOutOfOrder() const { return _numEarly + _numLate; } + quint32 getNumEarly() const { return _numEarly; } + quint32 getNumLate() const { return _numLate; } + quint32 getNumLost() const { return _numLost; } + quint32 getNumRecovered() const { return _numRecovered; } + quint32 getNumDuplicate() const { return _numDuplicate; } + const QSet<quint16>& getMissingSet() const { return _missingSet; } + +private: + void pruneMissingSet(const bool wantExtraDebugging); + + quint16 _lastReceived; + QSet<quint16> _missingSet; + + quint32 _numReceived; + quint32 _numUnreasonable; + quint32 _numEarly; + quint32 _numLate; + quint32 _numLost; + quint32 _numRecovered; + quint32 _numDuplicate; + + QUuid _lastSenderUUID; +}; + +#endif // hifi_SequenceNumberStats_h diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp new file mode 100644 index 0000000000..aa18cb43ee --- /dev/null +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -0,0 +1,155 @@ +// +// UserActivityLogger.cpp +// +// +// Created by Clement on 5/21/14. +// Copyright 2014 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 "UserActivityLogger.h" + +#include <QEventLoop> +#include <QJsonDocument> +#include <QHttpMultiPart> +#include <QTimer> + +static const QString USER_ACTIVITY_URL = "/api/v1/user_activities"; + +UserActivityLogger& UserActivityLogger::getInstance() { + static UserActivityLogger sharedInstance; + return sharedInstance; +} + +UserActivityLogger::UserActivityLogger() { +} + +void UserActivityLogger::logAction(QString action, QJsonObject details, JSONCallbackParameters params) { + AccountManager& accountManager = AccountManager::getInstance(); + QHttpMultiPart* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + // Adding the action name + QHttpPart actionPart; + actionPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"action_name\""); + actionPart.setBody(QByteArray().append(action)); + multipart->append(actionPart); + + // If there are action details, add them to the multipart + if (!details.isEmpty()) { + QHttpPart detailsPart; + detailsPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data;" + " name=\"action_details\""); + detailsPart.setBody(QJsonDocument(details).toJson(QJsonDocument::Compact)); + multipart->append(detailsPart); + } + qDebug() << "Logging activity" << action; + + // if no callbacks specified, call our owns + if (params.isEmpty()) { + params.jsonCallbackReceiver = this; + params.jsonCallbackMethod = "requestFinished"; + params.errorCallbackReceiver = this; + params.errorCallbackMethod = "requestError"; + } + + accountManager.authenticatedRequest(USER_ACTIVITY_URL, + QNetworkAccessManager::PostOperation, + params, + NULL, + multipart); +} + +void UserActivityLogger::requestFinished(const QJsonObject& object) { + qDebug() << object; +} + +void UserActivityLogger::requestError(QNetworkReply::NetworkError error,const QString& string) { + qDebug() << error << ": " << string; +} + +void UserActivityLogger::launch(QString applicationVersion) { + const QString ACTION_NAME = "launch"; + QJsonObject actionDetails; + QString VERSION_KEY = "version"; + actionDetails.insert(VERSION_KEY, applicationVersion); + + logAction(ACTION_NAME, actionDetails); +} + +void UserActivityLogger::close(int delayTime) { + const QString ACTION_NAME = "close"; + + // In order to get the end of the session, we need to give the account manager enough time to send the packet. + QEventLoop loop; + // Here we connect the callbacks to stop the event loop + JSONCallbackParameters params; + params.jsonCallbackReceiver = &loop; + params.errorCallbackReceiver = &loop; + params.jsonCallbackMethod = "quit"; + params.errorCallbackMethod = "quit"; + // In case something goes wrong, we also setup a timer so that the delai is not greater than delayTime + QTimer timer; + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + // Now we can log it + logAction(ACTION_NAME, QJsonObject(), params); + timer.start(delayTime); + loop.exec(); +} + +void UserActivityLogger::changedDisplayName(QString displayName) { + const QString ACTION_NAME = "changed_display_name"; + QJsonObject actionDetails; + const QString DISPLAY_NAME = "display_name"; + + actionDetails.insert(DISPLAY_NAME, displayName); + + logAction(ACTION_NAME, actionDetails); +} + +void UserActivityLogger::changedModel(QString typeOfModel, QString modelURL) { + const QString ACTION_NAME = "changed_model"; + QJsonObject actionDetails; + const QString TYPE_OF_MODEL = "type_of_model"; + const QString MODEL_URL = "model_url"; + + actionDetails.insert(TYPE_OF_MODEL, typeOfModel); + actionDetails.insert(MODEL_URL, modelURL); + + logAction(ACTION_NAME, actionDetails); +} + +void UserActivityLogger::changedDomain(QString domainURL) { + const QString ACTION_NAME = "changed_domain"; + QJsonObject actionDetails; + const QString DOMAIN_URL = "domain_url"; + + actionDetails.insert(DOMAIN_URL, domainURL); + + logAction(ACTION_NAME, actionDetails); +} + +void UserActivityLogger::connectedDevice(QString typeOfDevice, QString deviceName) { + const QString ACTION_NAME = "connected_device"; + QJsonObject actionDetails; + const QString TYPE_OF_DEVICE = "type_of_device"; + const QString DEVICE_NAME = "device_name"; + + actionDetails.insert(TYPE_OF_DEVICE, typeOfDevice); + actionDetails.insert(DEVICE_NAME, deviceName); + + logAction(ACTION_NAME, actionDetails); + +} + +void UserActivityLogger::loadedScript(QString scriptName) { + const QString ACTION_NAME = "loaded_script"; + QJsonObject actionDetails; + const QString SCRIPT_NAME = "script_name"; + + actionDetails.insert(SCRIPT_NAME, scriptName); + + logAction(ACTION_NAME, actionDetails); + +} diff --git a/libraries/networking/src/UserActivityLogger.h b/libraries/networking/src/UserActivityLogger.h new file mode 100644 index 0000000000..4823143234 --- /dev/null +++ b/libraries/networking/src/UserActivityLogger.h @@ -0,0 +1,47 @@ +// +// UserActivityLogger.h +// +// +// Created by Clement on 5/21/14. +// Copyright 2014 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_UserActivityLogger_h +#define hifi_UserActivityLogger_h + +#include "AccountManager.h" + +#include <QObject> +#include <QString> +#include <QJsonObject> +#include <QNetworkReply> + +class UserActivityLogger : public QObject { + Q_OBJECT + +public: + static UserActivityLogger& getInstance(); + +public slots: + void logAction(QString action, QJsonObject details = QJsonObject(), JSONCallbackParameters params = JSONCallbackParameters()); + + void launch(QString applicationVersion); + void close(int delayTime); + void changedDisplayName(QString displayName); + void changedModel(QString typeOfModel, QString modelURL); + void changedDomain(QString domainURL); + void connectedDevice(QString typeOfDevice, QString deviceName); + void loadedScript(QString scriptName); + +private slots: + void requestFinished(const QJsonObject& object); + void requestError(QNetworkReply::NetworkError error,const QString& string); + +private: + UserActivityLogger(); +}; + +#endif // hifi_UserActivityLogger_h \ No newline at end of file diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index cbdc4753dc..2af86663f7 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -757,7 +757,7 @@ bool findShapeCollisionsOp(OctreeElement* element, void* extraData) { // coarse check against bounds AACube cube = element->getAACube(); cube.scale(TREE_SCALE); - if (!cube.expandedContains(args->shape->getPosition(), args->shape->getBoundingRadius())) { + if (!cube.expandedContains(args->shape->getTranslation(), args->shape->getBoundingRadius())) { return false; } if (!element->isLeaf()) { diff --git a/libraries/octree/src/OctreeEditPacketSender.cpp b/libraries/octree/src/OctreeEditPacketSender.cpp index 43e253b2da..d67306e8c7 100644 --- a/libraries/octree/src/OctreeEditPacketSender.cpp +++ b/libraries/octree/src/OctreeEditPacketSender.cpp @@ -354,7 +354,7 @@ void OctreeEditPacketSender::processNackPacket(const QByteArray& packet) { // read number of sequence numbers uint16_t numSequenceNumbers = (*(uint16_t*)dataAt); dataAt += sizeof(uint16_t); - + // read sequence numbers and queue packets for resend for (int i = 0; i < numSequenceNumbers; i++) { unsigned short int sequenceNumber = (*(unsigned short int*)dataAt); diff --git a/libraries/octree/src/OctreeSceneStats.cpp b/libraries/octree/src/OctreeSceneStats.cpp index 28445ec327..e585c8dfe6 100644 --- a/libraries/octree/src/OctreeSceneStats.cpp +++ b/libraries/octree/src/OctreeSceneStats.cpp @@ -21,10 +21,6 @@ #include "OctreeSceneStats.h" -const uint16_t MAX_MISSING_SEQUENCE = 100; /// how many items in our _missingSequenceNumbers before we start to prune them -const uint16_t MAX_MISSING_SEQUENCE_OLD_AGE = 1000; /// age we allow items in _missingSequenceNumbers to be before pruning - - const int samples = 100; OctreeSceneStats::OctreeSceneStats() : _isReadyToSend(false), @@ -39,14 +35,7 @@ OctreeSceneStats::OctreeSceneStats() : _incomingPacket(0), _incomingBytes(0), _incomingWastedBytes(0), - _incomingLastSequence(0), - _incomingLikelyLost(0), - _incomingRecovered(0), - _incomingEarly(0), - _incomingLate(0), - _incomingReallyLate(0), - _incomingPossibleDuplicate(0), - _missingSequenceNumbers(), + _incomingOctreeSequenceNumberStats(), _incomingFlightTimeAverage(samples), _jurisdictionRoot(NULL) { @@ -150,15 +139,8 @@ void OctreeSceneStats::copyFromOther(const OctreeSceneStats& other) { _incomingPacket = other._incomingPacket; _incomingBytes = other._incomingBytes; _incomingWastedBytes = other._incomingWastedBytes; - _incomingLastSequence = other._incomingLastSequence; - _incomingLikelyLost = other._incomingLikelyLost; - _incomingRecovered = other._incomingRecovered; - _incomingEarly = other._incomingEarly; - _incomingLate = other._incomingLate; - _incomingReallyLate = other._incomingReallyLate; - _incomingPossibleDuplicate = other._incomingPossibleDuplicate; - - _missingSequenceNumbers = other._missingSequenceNumbers; + + _incomingOctreeSequenceNumberStats = other._incomingOctreeSequenceNumberStats; } @@ -875,155 +857,8 @@ void OctreeSceneStats::trackIncomingOctreePacket(const QByteArray& packet, qDebug() << "ignoring unreasonable packet... flightTime:" << flightTime; return; // ignore any packets that are unreasonable } - - const int UINT16_RANGE = std::numeric_limits<uint16_t>::max() + 1; - - // determine our expected sequence number... handle rollover appropriately - OCTREE_PACKET_SEQUENCE expected = _incomingPacket > 0 ? _incomingLastSequence + (quint16)1 : sequence; - - const int USECS_PER_MSEC = 1000; - float flightTimeMsecs = flightTime / USECS_PER_MSEC; - _incomingFlightTimeAverage.updateAverage(flightTimeMsecs); - - // track out of order and possibly lost packets... - if (sequence == _incomingLastSequence) { - if (wantExtraDebugging) { - qDebug() << "last packet duplicate got:" << sequence << "_incomingLastSequence:" << _incomingLastSequence; - } - } else { - if (sequence != expected) { - if (wantExtraDebugging) { - qDebug() << "out of order... got:" << sequence << "expected:" << expected; - } - - int sequenceInt = (int)sequence; - int expectedInt = (int)expected; - - // if distance between sequence and expected are more than half of the total range of possible seq numbers, - // assume that a rollover occurred between the two. - // correct the larger one so it's in the range [-UINT16_RANGE, -1] while the other remains in [0, UINT16_RANGE-1] - // after doing so, sequenceInt and expectedInt can be correctly compared to each other, though one may be negative - if (std::abs(sequenceInt - expectedInt) > UINT16_RANGE / 2) { - if (sequenceInt > expectedInt) { - sequenceInt -= UINT16_RANGE; - } - else { - expectedInt -= UINT16_RANGE; - } - } - - // Guard against possible corrupted packets... with bad sequence numbers - const int MAX_RESONABLE_SEQUENCE_OFFSET = 2000; - const int MIN_RESONABLE_SEQUENCE_OFFSET = -2000; - - int sequenceOffset = (sequenceInt - expectedInt); - if (sequenceOffset > MAX_RESONABLE_SEQUENCE_OFFSET || sequenceOffset < MIN_RESONABLE_SEQUENCE_OFFSET) { - qDebug() << "ignoring unreasonable packet... sequence:" << sequence << "_incomingLastSequence:" << _incomingLastSequence; - return; // ignore any packets that are unreasonable - } - - // if the sequence is less than our expected, then this might be a packet - // that was delayed and so we should find it in our lostSequence list - if (sequenceInt < expectedInt) { - - // if no rollover between them: sequenceInt, expectedInt are both in range [0, UINT16_RANGE-1] - // if rollover between them: sequenceInt in [-UINT16_RANGE, -1], expectedInt in [0, UINT16_RANGE-1] - - if (wantExtraDebugging) { - qDebug() << "this packet is later than expected..."; - } - if (sequenceInt < expectedInt - MAX_MISSING_SEQUENCE_OLD_AGE) { - _incomingReallyLate++; - } - else { - _incomingLate++; - } - - if (_missingSequenceNumbers.contains(sequence)) { - if (wantExtraDebugging) { - qDebug() << "found it in _missingSequenceNumbers"; - } - _missingSequenceNumbers.remove(sequence); - _incomingLikelyLost--; - _incomingRecovered++; - } - else { - // if we're still in our pruning window, and we didn't find it in our missing list, - // than this is really unexpected and can probably only happen if the packet was a - // duplicate - if (sequenceInt >= expectedInt - MAX_MISSING_SEQUENCE_OLD_AGE) { - if (wantExtraDebugging) { - qDebug() << "sequence:" << sequence << "WAS NOT found in _missingSequenceNumbers, and not that old... (expected - MAX_MISSING_SEQUENCE_OLD_AGE):" - << (uint16_t)(expectedInt - MAX_MISSING_SEQUENCE_OLD_AGE); - } - _incomingPossibleDuplicate++; - } - } - - // don't update _incomingLastSequence in this case. - // only bump the last sequence if it was greater than our expected sequence, this will keep us from - // accidentally going backwards when an out of order (recovered) packet comes in - - } else { // sequenceInt > expectedInt - - // if no rollover between them: sequenceInt, expectedInt are both in range [0, UINT16_RANGE-1] - // if rollover between them: sequenceInt in [0, UINT16_RANGE-1], expectedInt in [-UINT16_RANGE, -1] - - if (wantExtraDebugging) { - qDebug() << "this packet is earlier than expected..."; - } - _incomingEarly++; - - // hmm... so, we either didn't get some packets, or this guy came early... - int missing = sequenceInt - expectedInt; - if (wantExtraDebugging) { - qDebug() << ">>>>>>>> missing gap=" << missing; - } - _incomingLikelyLost += missing; - for (int missingSequenceInt = expectedInt; missingSequenceInt < sequenceInt; missingSequenceInt++) { - OCTREE_PACKET_SEQUENCE missingSequence = missingSequenceInt >= 0 ? missingSequenceInt : missingSequenceInt + UINT16_RANGE; - _missingSequenceNumbers << missingSequence; - } - - _incomingLastSequence = sequence; - } - } else { // sequence = expected - - _incomingLastSequence = sequence; - } - } - - // do some garbage collecting on our _missingSequenceNumbers - if (_missingSequenceNumbers.size() > MAX_MISSING_SEQUENCE) { - if (wantExtraDebugging) { - qDebug() << "too many _missingSequenceNumbers:" << _missingSequenceNumbers.size(); - } - - int oldAgeCutoff = (int)_incomingLastSequence - MAX_MISSING_SEQUENCE_OLD_AGE; - - foreach(uint16_t missingItem, _missingSequenceNumbers) { - if (wantExtraDebugging) { - qDebug() << "checking item:" << missingItem << "is it in need of pruning?"; - qDebug() << "(_incomingLastSequence - MAX_MISSING_SEQUENCE_OLD_AGE):" - << (uint16_t)((int)_incomingLastSequence - MAX_MISSING_SEQUENCE_OLD_AGE); - } - - bool prune; - if (oldAgeCutoff >= 0) { - prune = (missingItem <= oldAgeCutoff || missingItem > _incomingLastSequence); - } - else { - prune = (missingItem <= oldAgeCutoff + UINT16_RANGE && missingItem > _incomingLastSequence); - } - - if (prune) { - if (wantExtraDebugging) { - qDebug() << "pruning really old missing sequence:" << missingItem; - } - _missingSequenceNumbers.remove(missingItem); - } - } - } + + _incomingOctreeSequenceNumberStats.sequenceNumberReceived(sequence); // track packets here... _incomingPacket++; diff --git a/libraries/octree/src/OctreeSceneStats.h b/libraries/octree/src/OctreeSceneStats.h index 1c468a8dc6..d7b65c63be 100644 --- a/libraries/octree/src/OctreeSceneStats.h +++ b/libraries/octree/src/OctreeSceneStats.h @@ -17,6 +17,7 @@ #include <SharedUtil.h> #include "JurisdictionMap.h" #include "OctreePacketData.h" +#include "SequenceNumberStats.h" #define GREENISH 0x40ff40d0 #define YELLOWISH 0xffef40c0 @@ -164,16 +165,9 @@ public: quint32 getIncomingPackets() const { return _incomingPacket; } quint64 getIncomingBytes() const { return _incomingBytes; } quint64 getIncomingWastedBytes() const { return _incomingWastedBytes; } - quint32 getIncomingOutOfOrder() const { return _incomingLate + _incomingEarly; } - quint32 getIncomingLikelyLost() const { return _incomingLikelyLost; } - quint32 getIncomingRecovered() const { return _incomingRecovered; } - quint32 getIncomingEarly() const { return _incomingEarly; } - quint32 getIncomingLate() const { return _incomingLate; } - quint32 getIncomingReallyLate() const { return _incomingReallyLate; } - quint32 getIncomingPossibleDuplicate() const { return _incomingPossibleDuplicate; } float getIncomingFlightTimeAverage() { return _incomingFlightTimeAverage.getAverage(); } - const QSet<OCTREE_PACKET_SEQUENCE>& getMissingSequenceNumbers() const { return _missingSequenceNumbers; } + const SequenceNumberStats& getIncomingOctreeSequenceNumberStats() const { return _incomingOctreeSequenceNumberStats; } private: @@ -268,14 +262,8 @@ private: quint64 _incomingBytes; quint64 _incomingWastedBytes; - quint16 _incomingLastSequence; /// last incoming sequence number - quint32 _incomingLikelyLost; /// count of packets likely lost, may be off by _incomingReallyLate count - quint32 _incomingRecovered; /// packets that were late, and we had in our missing list, we consider recovered - quint32 _incomingEarly; /// out of order earlier than expected - quint32 _incomingLate; /// out of order later than expected - quint32 _incomingReallyLate; /// out of order and later than MAX_MISSING_SEQUENCE_OLD_AGE late - quint32 _incomingPossibleDuplicate; /// out of order possibly a duplicate - QSet<OCTREE_PACKET_SEQUENCE> _missingSequenceNumbers; + SequenceNumberStats _incomingOctreeSequenceNumberStats; + SimpleMovingAverage _incomingFlightTimeAverage; // features related items diff --git a/libraries/particles/src/ParticleCollisionSystem.cpp b/libraries/particles/src/ParticleCollisionSystem.cpp index 358c5a1b84..d8d5887d97 100644 --- a/libraries/particles/src/ParticleCollisionSystem.cpp +++ b/libraries/particles/src/ParticleCollisionSystem.cpp @@ -202,14 +202,13 @@ void ParticleCollisionSystem::updateCollisionWithAvatars(Particle* particle) { foreach (const AvatarSharedPointer& avatarPointer, _avatars->getAvatarHash()) { AvatarData* avatar = avatarPointer.data(); - // use a very generous bounding radius since the arms can stretch - float totalRadius = 2.f * avatar->getBoundingRadius() + radius; + float totalRadius = avatar->getBoundingRadius() + radius; glm::vec3 relativePosition = center - avatar->getPosition(); if (glm::dot(relativePosition, relativePosition) > (totalRadius * totalRadius)) { continue; } - if (avatar->findParticleCollisions(center, radius, _collisions)) { + if (avatar->findSphereCollisions(center, radius, _collisions)) { int numCollisions = _collisions.size(); for (int i = 0; i < numCollisions; ++i) { CollisionInfo* collision = _collisions.getCollision(i); @@ -222,25 +221,6 @@ void ParticleCollisionSystem::updateCollisionWithAvatars(Particle* particle) { if (glm::dot(relativeVelocity, collision->_penetration) <= 0.f) { // only collide when particle and collision point are moving toward each other // (doing this prevents some "collision snagging" when particle penetrates the object) - - // HACK BEGIN: to allow paddle hands to "hold" particles we attenuate soft collisions against them. - if (collision->_type == COLLISION_TYPE_PADDLE_HAND) { - // NOTE: the physics are wrong (particles cannot roll) but it IS possible to catch a slow moving particle. - // TODO: make this less hacky when we have more per-collision details - float elasticity = ELASTICITY; - float attenuationFactor = glm::length(collision->_addedVelocity) / HALTING_SPEED; - float damping = DAMPING; - if (attenuationFactor < 1.f) { - collision->_addedVelocity *= attenuationFactor; - elasticity *= attenuationFactor; - // NOTE: the math below keeps the damping piecewise continuous, - // while ramping it up to 1 when attenuationFactor = 0 - damping = DAMPING + (1.f - attenuationFactor) * (1.f - DAMPING); - } - collision->_damping = damping; - } - // HACK END - updateCollisionSound(particle, collision->_penetration, COLLISION_FREQUENCY); collision->_penetration /= (float)(TREE_SCALE); particle->applyHardCollision(*collision); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 28b8f289e5..38cf8f0b09 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -323,8 +323,9 @@ void ScriptEngine::evaluate() { if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); - qDebug() << "Uncaught exception at line" << line << ":" << result.toString(); - emit errorMessage("Uncaught exception at line" + QString::number(line) + ":" + result.toString()); + qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString(); + emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + result.toString()); + _engine.clearExceptions(); } } @@ -333,7 +334,7 @@ QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileN bool hasUncaughtException = _engine.hasUncaughtException(); if (hasUncaughtException) { int line = _engine.uncaughtExceptionLineNumber(); - qDebug() << "Uncaught exception at line" << line << ": " << result.toString(); + qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ": " << result.toString(); } emit evaluationFinished(result, hasUncaughtException); _engine.clearExceptions(); @@ -357,14 +358,15 @@ void ScriptEngine::run() { init(); } _isRunning = true; + _isFinished = false; emit runningStateChanged(); QScriptValue result = _engine.evaluate(_scriptContents); if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); - - qDebug() << "Uncaught exception at line" << line << ":" << result.toString(); - emit errorMessage("Uncaught exception at line" + QString::number(line) + ":" + result.toString()); + qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString(); + emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + result.toString()); + _engine.clearExceptions(); } QElapsedTimer startTime; @@ -467,13 +469,17 @@ void ScriptEngine::run() { _numAvatarSoundSentBytes = 0; } } - + QByteArray audioPacket = byteArrayWithPopulatedHeader(silentFrame ? PacketTypeSilentAudioFrame : PacketTypeMicrophoneAudioNoEcho); QDataStream packetStream(&audioPacket, QIODevice::Append); + // pack a placeholder value for sequence number for now, will be packed when destination node is known + int numPreSequenceNumberBytes = audioPacket.size(); + packetStream << (quint16)0; + // use the orientation and position of this avatar for the source of this audio packetStream.writeRawData(reinterpret_cast<const char*>(&_avatarData->getPosition()), sizeof(glm::vec3)); glm::quat headOrientation = _avatarData->getHeadOrientation(); @@ -493,7 +499,19 @@ void ScriptEngine::run() { numAvailableSamples * sizeof(int16_t)); } - nodeList->broadcastToNodes(audioPacket, NodeSet() << NodeType::AudioMixer); + // write audio packet to AudioMixer nodes + NodeList* nodeList = NodeList::getInstance(); + foreach(const SharedNodePointer& node, nodeList->getNodeHash()) { + // only send to nodes of type AudioMixer + if (node->getType() == NodeType::AudioMixer) { + // pack sequence number + quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++; + memcpy(audioPacket.data() + numPreSequenceNumberBytes, &sequence, sizeof(quint16)); + + // send audio packet + nodeList->writeDatagram(audioPacket, node); + } + } } } @@ -504,8 +522,9 @@ void ScriptEngine::run() { if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); - qDebug() << "Uncaught exception at line" << line << ":" << _engine.uncaughtException().toString(); - emit errorMessage("Uncaught exception at line" + QString::number(line) + ":" + _engine.uncaughtException().toString()); + qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << _engine.uncaughtException().toString(); + emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + _engine.uncaughtException().toString()); + _engine.clearExceptions(); } } emit scriptEnding(); @@ -665,5 +684,10 @@ void ScriptEngine::include(const QString& includeFile) { int line = _engine.uncaughtExceptionLineNumber(); qDebug() << "Uncaught exception at (" << includeFile << ") line" << line << ":" << result.toString(); emit errorMessage("Uncaught exception at (" + includeFile + ") line" + QString::number(line) + ":" + result.toString()); + _engine.clearExceptions(); } } + +void ScriptEngine::nodeKilled(SharedNodePointer node) { + _outgoingScriptAudioSequenceNumbers.remove(node->getUUID()); +} diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index bf2ac40568..5b01b8124a 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -100,6 +100,8 @@ public slots: void include(const QString& includeFile); void print(const QString& message); + void nodeKilled(SharedNodePointer node); + signals: void update(float deltaTime); void scriptEnding(); @@ -146,6 +148,7 @@ private: ScriptUUID _uuidLibrary; AnimationCache _animationCache; + QHash<QUuid, quint16> _outgoingScriptAudioSequenceNumbers; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/shared/src/CapsuleShape.cpp b/libraries/shared/src/CapsuleShape.cpp index 5416ff92a6..12ab6ba479 100644 --- a/libraries/shared/src/CapsuleShape.cpp +++ b/libraries/shared/src/CapsuleShape.cpp @@ -18,9 +18,6 @@ #include "SharedUtil.h" -// default axis of CapsuleShape is Y-axis -const glm::vec3 localAxis(0.0f, 1.0f, 0.0f); - CapsuleShape::CapsuleShape() : Shape(Shape::CAPSULE_SHAPE), _radius(0.0f), _halfHeight(0.0f) {} CapsuleShape::CapsuleShape(float radius, float halfHeight) : Shape(Shape::CAPSULE_SHAPE), @@ -40,17 +37,17 @@ CapsuleShape::CapsuleShape(float radius, const glm::vec3& startPoint, const glm: /// \param[out] startPoint is the center of start cap void CapsuleShape::getStartPoint(glm::vec3& startPoint) const { - startPoint = _position - _rotation * glm::vec3(0.0f, _halfHeight, 0.0f); + startPoint = _translation - _rotation * glm::vec3(0.0f, _halfHeight, 0.0f); } /// \param[out] endPoint is the center of the end cap void CapsuleShape::getEndPoint(glm::vec3& endPoint) const { - endPoint = _position + _rotation * glm::vec3(0.0f, _halfHeight, 0.0f); + endPoint = _translation + _rotation * glm::vec3(0.0f, _halfHeight, 0.0f); } void CapsuleShape::computeNormalizedAxis(glm::vec3& axis) const { // default axis of a capsule is along the yAxis - axis = _rotation * glm::vec3(0.0f, 1.0f, 0.0f); + axis = _rotation * DEFAULT_CAPSULE_AXIS; } void CapsuleShape::setRadius(float radius) { @@ -71,17 +68,12 @@ void CapsuleShape::setRadiusAndHalfHeight(float radius, float halfHeight) { void CapsuleShape::setEndPoints(const glm::vec3& startPoint, const glm::vec3& endPoint) { glm::vec3 axis = endPoint - startPoint; - _position = 0.5f * (endPoint + startPoint); + _translation = 0.5f * (endPoint + startPoint); float height = glm::length(axis); if (height > EPSILON) { _halfHeight = 0.5f * height; axis /= height; - glm::vec3 yAxis(0.0f, 1.0f, 0.0f); - float angle = glm::angle(axis, yAxis); - if (angle > EPSILON) { - axis = glm::normalize(glm::cross(yAxis, axis)); - _rotation = glm::angleAxis(angle, axis); - } + computeNewRotation(axis); } updateBoundingRadius(); } @@ -94,3 +86,13 @@ bool CapsuleShape::findRayIntersection(const glm::vec3& rayStart, const glm::vec // TODO: implement the raycast to return inside surface intersection for the internal rayStart. return findRayCapsuleIntersection(rayStart, rayDirection, capsuleStart, capsuleEnd, _radius, distance); } + +// static +glm::quat CapsuleShape::computeNewRotation(const glm::vec3& newAxis) { + float angle = glm::angle(newAxis, DEFAULT_CAPSULE_AXIS); + if (angle > EPSILON) { + glm::vec3 rotationAxis = glm::normalize(glm::cross(DEFAULT_CAPSULE_AXIS, newAxis)); + return glm::angleAxis(angle, rotationAxis); + } + return glm::quat(); +} diff --git a/libraries/shared/src/CapsuleShape.h b/libraries/shared/src/CapsuleShape.h index fdd6c3eda6..8d84e32a97 100644 --- a/libraries/shared/src/CapsuleShape.h +++ b/libraries/shared/src/CapsuleShape.h @@ -14,7 +14,11 @@ #include "Shape.h" +#include "SharedUtil.h" + // default axis of CapsuleShape is Y-axis +const glm::vec3 DEFAULT_CAPSULE_AXIS(0.0f, 1.0f, 0.0f); + class CapsuleShape : public Shape { public: @@ -23,26 +27,33 @@ public: CapsuleShape(float radius, float halfHeight, const glm::vec3& position, const glm::quat& rotation); CapsuleShape(float radius, const glm::vec3& startPoint, const glm::vec3& endPoint); + virtual ~CapsuleShape() {} + float getRadius() const { return _radius; } - float getHalfHeight() const { return _halfHeight; } + virtual float getHalfHeight() const { return _halfHeight; } /// \param[out] startPoint is the center of start cap - void getStartPoint(glm::vec3& startPoint) const; + virtual void getStartPoint(glm::vec3& startPoint) const; /// \param[out] endPoint is the center of the end cap - void getEndPoint(glm::vec3& endPoint) const; + virtual void getEndPoint(glm::vec3& endPoint) const; - void computeNormalizedAxis(glm::vec3& axis) const; + virtual void computeNormalizedAxis(glm::vec3& axis) const; void setRadius(float radius); - void setHalfHeight(float height); - void setRadiusAndHalfHeight(float radius, float height); - void setEndPoints(const glm::vec3& startPoint, const glm::vec3& endPoint); + virtual void setHalfHeight(float height); + virtual void setRadiusAndHalfHeight(float radius, float height); + + /// Sets the endpoints and updates center, rotation, and halfHeight to agree. + virtual void setEndPoints(const glm::vec3& startPoint, const glm::vec3& endPoint); bool findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const; + virtual float getVolume() const { return (PI * _radius * _radius) * (1.3333333333f * _radius + getHalfHeight()); } + protected: - void updateBoundingRadius() { _boundingRadius = _radius + _halfHeight; } + virtual void updateBoundingRadius() { _boundingRadius = _radius + getHalfHeight(); } + static glm::quat computeNewRotation(const glm::vec3& newAxis); float _radius; float _halfHeight; diff --git a/libraries/shared/src/CollisionInfo.cpp b/libraries/shared/src/CollisionInfo.cpp index 38e3a4b2db..e862a22f4a 100644 --- a/libraries/shared/src/CollisionInfo.cpp +++ b/libraries/shared/src/CollisionInfo.cpp @@ -11,6 +11,20 @@ #include "CollisionInfo.h" +#include "Shape.h" +#include "SharedUtil.h" + +CollisionInfo::CollisionInfo() : + _data(NULL), + _intData(0), + _shapeA(NULL), + _shapeB(NULL), + _damping(0.f), + _elasticity(1.f), + _contactPoint(0.f), + _penetration(0.f), + _addedVelocity(0.f) { +} CollisionList::CollisionList(int maxSize) : _maxSize(maxSize), @@ -18,6 +32,29 @@ CollisionList::CollisionList(int maxSize) : _collisions.resize(_maxSize); } +void CollisionInfo::apply() { + assert(_shapeA); + // NOTE: Shape::computeEffectiveMass() has side effects: computes and caches partial Lagrangian coefficients + Shape* shapeA = const_cast<Shape*>(_shapeA); + float massA = shapeA->computeEffectiveMass(_penetration, _contactPoint); + float massB = MAX_SHAPE_MASS; + float totalMass = massA + massB; + if (_shapeB) { + Shape* shapeB = const_cast<Shape*>(_shapeB); + massB = shapeB->computeEffectiveMass(-_penetration, _contactPoint - _penetration); + totalMass = massA + massB; + if (totalMass < EPSILON) { + massA = massB = 1.0f; + totalMass = 2.0f; + } + // remember that _penetration points from A into B + shapeB->accumulateDelta(massA / totalMass, _penetration); + } + // NOTE: Shape::accumulateDelta() uses the coefficients from previous call to Shape::computeEffectiveMass() + // remember that _penetration points from A into B + shapeA->accumulateDelta(massB / totalMass, -_penetration); +} + CollisionInfo* CollisionList::getNewCollision() { // return pointer to existing CollisionInfo, or NULL of list is full return (_size < _maxSize) ? &(_collisions[_size++]) : NULL; @@ -38,17 +75,17 @@ CollisionInfo* CollisionList::getLastCollision() { } void CollisionList::clear() { - // we rely on the external context to properly set or clear the data members of a collision - // whenever it is used. + // we rely on the external context to properly set or clear the data members of CollisionInfos /* for (int i = 0; i < _size; ++i) { // we only clear the important stuff CollisionInfo& collision = _collisions[i]; - collision._type = COLLISION_TYPE_UNKNOWN; //collision._data = NULL; //collision._intData = 0; //collision._floatDAta = 0.0f; //collision._vecData = glm::vec3(0.0f); + //collision._shapeA = NULL; + //collision._shapeB = NULL; //collision._damping; //collision._elasticity; //collision._contactPoint; diff --git a/libraries/shared/src/CollisionInfo.h b/libraries/shared/src/CollisionInfo.h index 52d5298fde..1ab06e2ef5 100644 --- a/libraries/shared/src/CollisionInfo.h +++ b/libraries/shared/src/CollisionInfo.h @@ -17,16 +17,7 @@ #include <QVector> -enum CollisionType { - COLLISION_TYPE_UNKNOWN = 0, - COLLISION_TYPE_PADDLE_HAND, - COLLISION_TYPE_MODEL, - // _data = pointer to Model that owns joint - // _intData = joint index - COLLISION_TYPE_AACUBE, - // _floatData = cube side - // _vecData = cube center -}; +class Shape; const quint32 COLLISION_GROUP_ENVIRONMENT = 1U << 0; const quint32 COLLISION_GROUP_AVATARS = 1U << 1; @@ -41,38 +32,24 @@ const quint32 VALID_COLLISION_GROUPS = 0x0f; class CollisionInfo { public: - CollisionInfo() - : _type(0), - _data(NULL), - _intData(0), - _damping(0.f), - _elasticity(1.f), - _contactPoint(0.f), - _penetration(0.f), - _addedVelocity(0.f) { - } - - CollisionInfo(qint32 type) - : _type(type), - _data(NULL), - _intData(0), - _damping(0.f), - _elasticity(1.f), - _contactPoint(0.f), - _penetration(0.f), - _addedVelocity(0.f) { - } - + CollisionInfo(); ~CollisionInfo() {} - int _type; // type of Collision - - // the value of the *Data fields depend on the type + // TODO: Andrew to get rid of these data members void* _data; int _intData; float _floatData; glm::vec3 _vecData; + /// accumulates position changes for the shapes in this collision to resolve penetration + void apply(); + + Shape* getShapeA() const { return const_cast<Shape*>(_shapeA); } + Shape* getShapeB() const { return const_cast<Shape*>(_shapeB); } + + const Shape* _shapeA; // pointer to shapeA in this collision + const Shape* _shapeB; // pointer to shapeB in this collision + float _damping; // range [0,1] of friction coeficient float _elasticity; // range [0,1] of energy conservation glm::vec3 _contactPoint; // world-frame point on BodyA that is deepest into BodyB diff --git a/libraries/shared/src/ListShape.cpp b/libraries/shared/src/ListShape.cpp index dcea97826e..67ec32d4b1 100644 --- a/libraries/shared/src/ListShape.cpp +++ b/libraries/shared/src/ListShape.cpp @@ -14,7 +14,7 @@ // ListShapeEntry void ListShapeEntry::updateTransform(const glm::vec3& rootPosition, const glm::quat& rootRotation) { - _shape->setPosition(rootPosition + rootRotation * _localPosition); + _shape->setTranslation(rootPosition + rootRotation * _localPosition); _shape->setRotation(_localRotation * rootRotation); } @@ -24,9 +24,9 @@ ListShape::~ListShape() { clear(); } -void ListShape::setPosition(const glm::vec3& position) { +void ListShape::setTranslation(const glm::vec3& position) { _subShapeTransformsAreDirty = true; - Shape::setPosition(position); + Shape::setTranslation(position); } void ListShape::setRotation(const glm::quat& rotation) { @@ -44,7 +44,7 @@ const Shape* ListShape::getSubShape(int index) const { void ListShape::updateSubTransforms() { if (_subShapeTransformsAreDirty) { for (int i = 0; i < _subShapeEntries.size(); ++i) { - _subShapeEntries[i].updateTransform(_position, _rotation); + _subShapeEntries[i].updateTransform(_translation, _rotation); } _subShapeTransformsAreDirty = false; } diff --git a/libraries/shared/src/ListShape.h b/libraries/shared/src/ListShape.h index 17e7d7b2b6..bd150c8246 100644 --- a/libraries/shared/src/ListShape.h +++ b/libraries/shared/src/ListShape.h @@ -42,7 +42,7 @@ public: ~ListShape(); - void setPosition(const glm::vec3& position); + void setTranslation(const glm::vec3& position); void setRotation(const glm::quat& rotation); const Shape* getSubShape(int index) const; diff --git a/libraries/shared/src/PhysicsEntity.cpp b/libraries/shared/src/PhysicsEntity.cpp new file mode 100644 index 0000000000..37d1a88d67 --- /dev/null +++ b/libraries/shared/src/PhysicsEntity.cpp @@ -0,0 +1,211 @@ +// +// PhysicsEntity.cpp +// libraries/shared/src +// +// Created by Andrew Meadows 2014.06.11 +// Copyright 2014 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 "PhysicsEntity.h" + +#include "PhysicsSimulation.h" +#include "Shape.h" +#include "ShapeCollider.h" + +PhysicsEntity::PhysicsEntity() : + _translation(0.0f), + _rotation(), + _boundingRadius(0.0f), + _shapesAreDirty(true), + _enableShapes(false), + _simulation(NULL) { +} + +PhysicsEntity::~PhysicsEntity() { + if (_simulation) { + _simulation->removeEntity(this); + _simulation = NULL; + } +} + +void PhysicsEntity::setTranslation(const glm::vec3& translation) { + if (_translation != translation) { + _shapesAreDirty = !_shapes.isEmpty(); + _translation = translation; + } +} + +void PhysicsEntity::setRotation(const glm::quat& rotation) { + if (_rotation != rotation) { + _shapesAreDirty = !_shapes.isEmpty(); + _rotation = rotation; + } +} + +void PhysicsEntity::setShapeBackPointers() { + for (int i = 0; i < _shapes.size(); i++) { + Shape* shape = _shapes[i]; + if (shape) { + shape->setEntity(this); + } + } +} + +void PhysicsEntity::setEnableShapes(bool enable) { + if (enable != _enableShapes) { + clearShapes(); + _enableShapes = enable; + if (_enableShapes) { + buildShapes(); + } + } +} + +void PhysicsEntity::clearShapes() { + for (int i = 0; i < _shapes.size(); ++i) { + delete _shapes[i]; + } + _shapes.clear(); +} + +bool PhysicsEntity::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance) const { + int numShapes = _shapes.size(); + float minDistance = FLT_MAX; + for (int j = 0; j < numShapes; ++j) { + const Shape* shape = _shapes[j]; + float thisDistance = FLT_MAX; + if (shape && shape->findRayIntersection(origin, direction, thisDistance)) { + if (thisDistance < minDistance) { + minDistance = thisDistance; + } + } + } + if (minDistance < FLT_MAX) { + distance = minDistance; + return true; + } + return false; +} + +bool PhysicsEntity::findCollisions(const QVector<const Shape*> shapes, CollisionList& collisions) { + bool collided = false; + int numTheirShapes = shapes.size(); + for (int i = 0; i < numTheirShapes; ++i) { + const Shape* theirShape = shapes[i]; + if (!theirShape) { + continue; + } + int numOurShapes = _shapes.size(); + for (int j = 0; j < numOurShapes; ++j) { + const Shape* ourShape = _shapes.at(j); + if (ourShape && ShapeCollider::collideShapes(theirShape, ourShape, collisions)) { + collided = true; + } + } + } + return collided; +} + +bool PhysicsEntity::findSphereCollisions(const glm::vec3& sphereCenter, float sphereRadius, CollisionList& collisions) { + bool collided = false; + SphereShape sphere(sphereRadius, sphereCenter); + for (int i = 0; i < _shapes.size(); i++) { + Shape* shape = _shapes[i]; + if (!shape) { + continue; + } + if (ShapeCollider::collideShapes(&sphere, shape, collisions)) { + CollisionInfo* collision = collisions.getLastCollision(); + collision->_data = (void*)(this); + collision->_intData = i; + collided = true; + } + } + return collided; +} + +bool PhysicsEntity::findPlaneCollisions(const glm::vec4& plane, CollisionList& collisions) { + bool collided = false; + PlaneShape planeShape(plane); + for (int i = 0; i < _shapes.size(); i++) { + if (_shapes.at(i) && ShapeCollider::collideShapes(&planeShape, _shapes.at(i), collisions)) { + CollisionInfo* collision = collisions.getLastCollision(); + collision->_data = (void*)(this); + collision->_intData = i; + collided = true; + } + } + return collided; +} + +// ----------------------------------------------------------- +// TODO: enforce this maximum when shapes are actually built. The gotcha here is +// that the Model class (derived from PhysicsEntity) expects numShapes == numJoints, +// so we have to modify that code to be safe. +const int MAX_SHAPES_PER_ENTITY = 256; + +// the first 256 prime numbers +const int primes[256] = { + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, + 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, + 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, + 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, + 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, + 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, + 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, + 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, + 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, + 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, + 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, + 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, + 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, + 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, + 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, + 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, + 947, 953, 967, 971, 977, 983, 991, 997, 1009, 1013, + 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, + 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, + 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, + 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, + 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, + 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, + 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, + 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, + 1597, 1601, 1607, 1609, 1613, 1619 }; + +void PhysicsEntity::disableCollisions(int shapeIndexA, int shapeIndexB) { + if (shapeIndexA < MAX_SHAPES_PER_ENTITY && shapeIndexB < MAX_SHAPES_PER_ENTITY) { + _disabledCollisions.insert(primes[shapeIndexA] * primes[shapeIndexB]); + } +} + +bool PhysicsEntity::collisionsAreEnabled(int shapeIndexA, int shapeIndexB) const { + if (shapeIndexA < MAX_SHAPES_PER_ENTITY && shapeIndexB < MAX_SHAPES_PER_ENTITY) { + return !_disabledCollisions.contains(primes[shapeIndexA] * primes[shapeIndexB]); + } + return false; +} + +void PhysicsEntity::disableCurrentSelfCollisions() { + CollisionList collisions(10); + int numShapes = _shapes.size(); + for (int i = 0; i < numShapes; ++i) { + const Shape* shape = _shapes.at(i); + if (!shape) { + continue; + } + for (int j = i+1; j < numShapes; ++j) { + if (!collisionsAreEnabled(i, j)) { + continue; + } + const Shape* otherShape = _shapes.at(j); + if (otherShape && ShapeCollider::collideShapes(shape, otherShape, collisions)) { + disableCollisions(i, j); + collisions.clear(); + } + } + } +} diff --git a/libraries/shared/src/PhysicsEntity.h b/libraries/shared/src/PhysicsEntity.h new file mode 100644 index 0000000000..3407ac8421 --- /dev/null +++ b/libraries/shared/src/PhysicsEntity.h @@ -0,0 +1,78 @@ +// +// PhysicsEntity.h +// libraries/shared/src +// +// Created by Andrew Meadows 2014.05.30 +// Copyright 2014 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_PhysicsEntity_h +#define hifi_PhysicsEntity_h + +#include <QVector> +#include <QSet> + +#include <glm/glm.hpp> +#include <glm/gtc/quaternion.hpp> + +#include "CollisionInfo.h" + +class Shape; +class PhysicsSimulation; + +// PhysicsEntity is the base class for anything that owns one or more Shapes that collide in a +// PhysicsSimulation. Each CollisionInfo generated by a PhysicsSimulation has back pointers to the +// two Shapes involved, and those Shapes may (optionally) have valid back pointers to their PhysicsEntity. + +class PhysicsEntity { + +public: + PhysicsEntity(); + virtual ~PhysicsEntity(); + + void setTranslation(const glm::vec3& translation); + void setRotation(const glm::quat& rotation); + + const glm::vec3& getTranslation() const { return _translation; } + const glm::quat& getRotation() const { return _rotation; } + float getBoundingRadius() const { return _boundingRadius; } + + void setShapeBackPointers(); + + void setEnableShapes(bool enable); + + virtual void buildShapes() = 0; + virtual void clearShapes(); + const QVector<Shape*> getShapes() const { return _shapes; } + + PhysicsSimulation* getSimulation() const { return _simulation; } + + bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance) const; + bool findCollisions(const QVector<const Shape*> shapes, CollisionList& collisions); + bool findSphereCollisions(const glm::vec3& sphereCenter, float sphereRadius, CollisionList& collisions); + bool findPlaneCollisions(const glm::vec4& plane, CollisionList& collisions); + + void disableCollisions(int shapeIndexA, int shapeIndexB); + bool collisionsAreEnabled(int shapeIndexA, int shapeIndexB) const; + + void disableCurrentSelfCollisions(); + +protected: + glm::vec3 _translation; + glm::quat _rotation; + float _boundingRadius; + bool _shapesAreDirty; + bool _enableShapes; + QVector<Shape*> _shapes; + QSet<int> _disabledCollisions; + +private: + // PhysicsSimulation is a friend so that it can set the protected _simulation backpointer + friend class PhysicsSimulation; + PhysicsSimulation* _simulation; +}; + +#endif // hifi_PhysicsEntity_h diff --git a/libraries/shared/src/PhysicsSimulation.cpp b/libraries/shared/src/PhysicsSimulation.cpp new file mode 100644 index 0000000000..637a5e955c --- /dev/null +++ b/libraries/shared/src/PhysicsSimulation.cpp @@ -0,0 +1,244 @@ +// +// PhysicsSimulation.cpp +// interface/src/avatar +// +// Created by Andrew Meadows 2014.06.06 +// Copyright 2014 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 <glm/glm.hpp> +#include <iostream> + +#include "PhysicsSimulation.h" + +#include "PhysicsEntity.h" +#include "Ragdoll.h" +#include "SharedUtil.h" +#include "ShapeCollider.h" + +int MAX_DOLLS_PER_SIMULATION = 16; +int MAX_ENTITIES_PER_SIMULATION = 64; +int MAX_COLLISIONS_PER_SIMULATION = 256; + + +const int NUM_SHAPE_BITS = 6; +const int SHAPE_INDEX_MASK = (1 << (NUM_SHAPE_BITS + 1)) - 1; + +PhysicsSimulation::PhysicsSimulation() : _collisionList(MAX_COLLISIONS_PER_SIMULATION), + _numIterations(0), _numCollisions(0), _constraintError(0.0f), _stepTime(0) { +} + +PhysicsSimulation::~PhysicsSimulation() { + // entities have a backpointer to this simulator that must be cleaned up + int numEntities = _entities.size(); + for (int i = 0; i < numEntities; ++i) { + _entities[i]->_simulation = NULL; + } + _entities.clear(); + + // but Ragdolls do not + _dolls.clear(); +} + +bool PhysicsSimulation::addEntity(PhysicsEntity* entity) { + if (!entity) { + return false; + } + if (entity->_simulation == this) { + int numEntities = _entities.size(); + for (int i = 0; i < numEntities; ++i) { + if (entity == _entities.at(i)) { + // already in list + assert(entity->_simulation == this); + return true; + } + } + // belongs to some other simulation + return false; + } + int numEntities = _entities.size(); + if (numEntities > MAX_ENTITIES_PER_SIMULATION) { + // list is full + return false; + } + // add to list + entity->_simulation = this; + _entities.push_back(entity); + return true; +} + +void PhysicsSimulation::removeEntity(PhysicsEntity* entity) { + if (!entity || !entity->_simulation || !(entity->_simulation == this)) { + return; + } + int numEntities = _entities.size(); + for (int i = 0; i < numEntities; ++i) { + if (entity == _entities.at(i)) { + if (i == numEntities - 1) { + // remove it + _entities.pop_back(); + } else { + // swap the last for this one + PhysicsEntity* lastEntity = _entities[numEntities - 1]; + _entities.pop_back(); + _entities[i] = lastEntity; + } + entity->_simulation = NULL; + break; + } + } +} + +bool PhysicsSimulation::addRagdoll(Ragdoll* doll) { + if (!doll) { + return false; + } + int numDolls = _dolls.size(); + if (numDolls > MAX_DOLLS_PER_SIMULATION) { + // list is full + return false; + } + for (int i = 0; i < numDolls; ++i) { + if (doll == _dolls[i]) { + // already in list + return true; + } + } + // add to list + _dolls.push_back(doll); + return true; +} + +void PhysicsSimulation::removeRagdoll(Ragdoll* doll) { + int numDolls = _dolls.size(); + for (int i = 0; i < numDolls; ++i) { + if (doll == _dolls[i]) { + if (i == numDolls - 1) { + // remove it + _dolls.pop_back(); + } else { + // swap the last for this one + Ragdoll* lastDoll = _dolls[numDolls - 1]; + _dolls.pop_back(); + _dolls[i] = lastDoll; + } + break; + } + } +} +// TODO: Andrew to implement: +// DONE (1) joints pull points (SpecialCapsuleShape would help solve this) +// DONE (2) points slam shapes (SpecialCapsuleShape would help solve this) +// DONE (3) detect collisions +// DONE (4) collisions move points (SpecialCapsuleShape would help solve this) +// DONE (5) enforce constraints +// DONE (6) make sure MyAvatar creates shapes, adds to simulation with ragdoll support +// DONE (7) support for pairwise collision bypass +// DONE (8) process collisions +// DONE (8a) stubbery +// DONE (8b) shapes actually accumulate movement +// DONE (9) verify that avatar shapes self collide +// (10) slave rendered SkeletonModel to physical shapes +// (10a) give SkeletonModel duplicate JointState data +// (10b) figure out how to slave dupe JointStates to physical shapes +// (11) add and enforce angular contraints for joints +void PhysicsSimulation::stepForward(float deltaTime, float minError, int maxIterations, quint64 maxUsec) { + quint64 now = usecTimestampNow(); + quint64 startTime = now; + quint64 expiry = startTime + maxUsec; + + moveRagdolls(deltaTime); + + int numDolls = _dolls.size(); + _numCollisions = 0; + int iterations = 0; + float error = 0.0f; + do { + computeCollisions(); + processCollisions(); + + // enforce constraints + error = 0.0f; + for (int i = 0; i < numDolls; ++i) { + error = glm::max(error, _dolls[i]->enforceRagdollConstraints()); + } + ++iterations; + + now = usecTimestampNow(); + } while (_numCollisions != 0 && (iterations < maxIterations) && (error > minError) && (now < expiry)); + + _numIterations = iterations; + _constraintError = error; + _stepTime = usecTimestampNow()- startTime; + +#ifdef ANDREW_DEBUG + // temporary debug info for watching simulation performance + static int adebug = 0; ++adebug; + if (0 == (adebug % 100)) { + std::cout << "adebug Ni = " << _numIterations << " E = " << error << " t = " << _stepTime << std::endl; // adebug + } +#endif // ANDREW_DEBUG +} + +void PhysicsSimulation::moveRagdolls(float deltaTime) { + int numDolls = _dolls.size(); + for (int i = 0; i < numDolls; ++i) { + _dolls.at(i)->stepRagdollForward(deltaTime); + } +} + +void PhysicsSimulation::computeCollisions() { + _collisionList.clear(); + // TODO: keep track of QSet<PhysicsEntity*> collidedEntities; + int numEntities = _entities.size(); + for (int i = 0; i < numEntities; ++i) { + PhysicsEntity* entity = _entities.at(i); + const QVector<Shape*> shapes = entity->getShapes(); + int numShapes = shapes.size(); + // collide with self + for (int j = 0; j < numShapes; ++j) { + const Shape* shape = shapes.at(j); + if (!shape) { + continue; + } + for (int k = j+1; k < numShapes; ++k) { + const Shape* otherShape = shapes.at(k); + if (otherShape && entity->collisionsAreEnabled(j, k)) { + ShapeCollider::collideShapes(shape, otherShape, _collisionList); + } + } + } + + // collide with others + for (int j = i+1; j < numEntities; ++j) { + const QVector<Shape*> otherShapes = _entities.at(j)->getShapes(); + ShapeCollider::collideShapesWithShapes(shapes, otherShapes, _collisionList); + } + } + _numCollisions = _collisionList.size(); +} + +void PhysicsSimulation::processCollisions() { + // walk all collisions, accumulate movement on shapes, and build a list of affected shapes + QSet<Shape*> shapes; + int numCollisions = _collisionList.size(); + for (int i = 0; i < numCollisions; ++i) { + CollisionInfo* collision = _collisionList.getCollision(i); + collision->apply(); + // there is always a shapeA + shapes.insert(collision->getShapeA()); + // but need to check for valid shapeB + if (collision->_shapeB) { + shapes.insert(collision->getShapeB()); + } + } + // walk all affected shapes and apply accumulated movement + QSet<Shape*>::const_iterator shapeItr = shapes.constBegin(); + while (shapeItr != shapes.constEnd()) { + (*shapeItr)->applyAccumulatedDelta(); + ++shapeItr; + } +} diff --git a/libraries/shared/src/PhysicsSimulation.h b/libraries/shared/src/PhysicsSimulation.h new file mode 100644 index 0000000000..c611e06870 --- /dev/null +++ b/libraries/shared/src/PhysicsSimulation.h @@ -0,0 +1,60 @@ +// +// PhysicsSimulation.h +// interface/src/avatar +// +// Created by Andrew Meadows 2014.06.06 +// Copyright 2014 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_PhysicsSimulation +#define hifi_PhysicsSimulation + +#include <QVector> + +#include "CollisionInfo.h" + +class PhysicsEntity; +class Ragdoll; + +class PhysicsSimulation { +public: + + PhysicsSimulation(); + ~PhysicsSimulation(); + + /// \return true if entity was added to or is already in the list + bool addEntity(PhysicsEntity* entity); + + void removeEntity(PhysicsEntity* entity); + + /// \return true if doll was added to or is already in the list + bool addRagdoll(Ragdoll* doll); + + void removeRagdoll(Ragdoll* doll); + + /// \param minError constraint motion below this value is considered "close enough" + /// \param maxIterations max number of iterations before giving up + /// \param maxUsec max number of usec to spend enforcing constraints + /// \return distance of largest movement + void stepForward(float deltaTime, float minError, int maxIterations, quint64 maxUsec); + + void moveRagdolls(float deltaTime); + void computeCollisions(); + void processCollisions(); + +private: + CollisionList _collisionList; + QVector<PhysicsEntity*> _entities; + QVector<Ragdoll*> _dolls; + + // some stats + int _numIterations; + int _numCollisions; + float _constraintError; + quint64 _stepTime; +}; + +#endif // hifi_PhysicsSimulation diff --git a/libraries/shared/src/PlaneShape.cpp b/libraries/shared/src/PlaneShape.cpp index e9563c6d8b..15ea281510 100644 --- a/libraries/shared/src/PlaneShape.cpp +++ b/libraries/shared/src/PlaneShape.cpp @@ -18,7 +18,7 @@ PlaneShape::PlaneShape(const glm::vec4& coefficients) : Shape(Shape::PLANE_SHAPE) { glm::vec3 normal = glm::vec3(coefficients); - _position = -normal * coefficients.w; + _translation = -normal * coefficients.w; float angle = acosf(glm::dot(normal, UNROTATED_NORMAL)); if (angle > EPSILON) { @@ -36,7 +36,7 @@ glm::vec3 PlaneShape::getNormal() const { glm::vec4 PlaneShape::getCoefficients() const { glm::vec3 normal = _rotation * UNROTATED_NORMAL; - return glm::vec4(normal.x, normal.y, normal.z, -glm::dot(normal, _position)); + return glm::vec4(normal.x, normal.y, normal.z, -glm::dot(normal, _translation)); } bool PlaneShape::findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const { @@ -44,9 +44,9 @@ bool PlaneShape::findRayIntersection(const glm::vec3& rayStart, const glm::vec3& float denominator = glm::dot(n, rayDirection); if (fabsf(denominator) < EPSILON) { // line is parallel to plane - return glm::dot(_position - rayStart, n) < EPSILON; + return glm::dot(_translation - rayStart, n) < EPSILON; } else { - float d = glm::dot(_position - rayStart, n) / denominator; + float d = glm::dot(_translation - rayStart, n) / denominator; if (d > 0.0f) { // ray points toward plane distance = d; diff --git a/libraries/shared/src/Ragdoll.cpp b/libraries/shared/src/Ragdoll.cpp new file mode 100644 index 0000000000..1d24e74864 --- /dev/null +++ b/libraries/shared/src/Ragdoll.cpp @@ -0,0 +1,121 @@ +// +// Ragdoll.cpp +// libraries/shared/src +// +// Created by Andrew Meadows 2014.05.30 +// Copyright 2014 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 "Ragdoll.h" + +#include "CapsuleShape.h" +#include "CollisionInfo.h" +#include "SharedUtil.h" +#include "SphereShape.h" + +// ---------------------------------------------------------------------------- +// VerletPoint +// ---------------------------------------------------------------------------- +void VerletPoint::accumulateDelta(const glm::vec3& delta) { + _accumulatedDelta += delta; + ++_numDeltas; +} + +void VerletPoint::applyAccumulatedDelta() { + if (_numDeltas > 0) { + _position += _accumulatedDelta / (float)_numDeltas; + _accumulatedDelta = glm::vec3(0.0f); + _numDeltas = 0; + } +} + +// ---------------------------------------------------------------------------- +// FixedConstraint +// ---------------------------------------------------------------------------- +FixedConstraint::FixedConstraint(VerletPoint* point, const glm::vec3& anchor) : _point(point), _anchor(anchor) { +} + +float FixedConstraint::enforce() { + assert(_point != NULL); + // TODO: use fast approximate sqrt here + float distance = glm::distance(_anchor, _point->_position); + _point->_position = _anchor; + return distance; +} + +void FixedConstraint::setPoint(VerletPoint* point) { + assert(point); + _point = point; + _point->_mass = MAX_SHAPE_MASS; +} + +void FixedConstraint::setAnchor(const glm::vec3& anchor) { + _anchor = anchor; +} + +// ---------------------------------------------------------------------------- +// DistanceConstraint +// ---------------------------------------------------------------------------- +DistanceConstraint::DistanceConstraint(VerletPoint* startPoint, VerletPoint* endPoint) : _distance(-1.0f) { + _points[0] = startPoint; + _points[1] = endPoint; + _distance = glm::distance(_points[0]->_position, _points[1]->_position); +} + +DistanceConstraint::DistanceConstraint(const DistanceConstraint& other) { + _distance = other._distance; + _points[0] = other._points[0]; + _points[1] = other._points[1]; +} + +void DistanceConstraint::setDistance(float distance) { + _distance = fabsf(distance); +} + +float DistanceConstraint::enforce() { + // TODO: use a fast distance approximation + float newDistance = glm::distance(_points[0]->_position, _points[1]->_position); + glm::vec3 direction(0.0f, 1.0f, 0.0f); + if (newDistance > EPSILON) { + direction = (_points[0]->_position - _points[1]->_position) / newDistance; + } + glm::vec3 center = 0.5f * (_points[0]->_position + _points[1]->_position); + _points[0]->_position = center + (0.5f * _distance) * direction; + _points[1]->_position = center - (0.5f * _distance) * direction; + return glm::abs(newDistance - _distance); +} + +// ---------------------------------------------------------------------------- +// Ragdoll +// ---------------------------------------------------------------------------- + +Ragdoll::Ragdoll() { +} + +Ragdoll::~Ragdoll() { + clearRagdollConstraintsAndPoints(); +} + +void Ragdoll::clearRagdollConstraintsAndPoints() { + int numConstraints = _ragdollConstraints.size(); + for (int i = 0; i < numConstraints; ++i) { + delete _ragdollConstraints[i]; + } + _ragdollConstraints.clear(); + _ragdollPoints.clear(); +} + +float Ragdoll::enforceRagdollConstraints() { + float maxDistance = 0.0f; + const int numConstraints = _ragdollConstraints.size(); + for (int i = 0; i < numConstraints; ++i) { + DistanceConstraint* c = static_cast<DistanceConstraint*>(_ragdollConstraints[i]); + //maxDistance = glm::max(maxDistance, _ragdollConstraints[i]->enforce()); + maxDistance = glm::max(maxDistance, c->enforce()); + } + return maxDistance; +} + diff --git a/libraries/shared/src/Ragdoll.h b/libraries/shared/src/Ragdoll.h new file mode 100644 index 0000000000..59c1291725 --- /dev/null +++ b/libraries/shared/src/Ragdoll.h @@ -0,0 +1,107 @@ +// +// Ragdoll.h +// libraries/shared/src +// +// Created by Andrew Meadows 2014.05.30 +// Copyright 2014 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_Ragdoll_h +#define hifi_Ragdoll_h + +#include <glm/glm.hpp> +#include <glm/gtx/quaternion.hpp> + +#include <QVector> + +class Shape; + +// TODO: Andrew to move VerletPoint class to its own file +class VerletPoint { +public: + VerletPoint() : _position(0.0f), _lastPosition(0.0f), _mass(1.0f), _accumulatedDelta(0.0f), _numDeltas(0) {} + + void accumulateDelta(const glm::vec3& delta); + void applyAccumulatedDelta(); + + glm::vec3 getAccumulatedDelta() const { + glm::vec3 foo(0.0f); + if (_numDeltas > 0) { + foo = _accumulatedDelta / (float)_numDeltas; + } + return foo; + } + + glm::vec3 _position; + glm::vec3 _lastPosition; + float _mass; + +private: + glm::vec3 _accumulatedDelta; + int _numDeltas; +}; + +class Constraint { +public: + Constraint() {} + virtual ~Constraint() {} + + /// Enforce contraint by moving relevant points. + /// \return max distance of point movement + virtual float enforce() = 0; + +protected: + int _type; +}; + +class FixedConstraint : public Constraint { +public: + FixedConstraint(VerletPoint* point, const glm::vec3& anchor); + float enforce(); + void setPoint(VerletPoint* point); + void setAnchor(const glm::vec3& anchor); +private: + VerletPoint* _point; + glm::vec3 _anchor; +}; + +class DistanceConstraint : public Constraint { +public: + DistanceConstraint(VerletPoint* startPoint, VerletPoint* endPoint); + DistanceConstraint(const DistanceConstraint& other); + float enforce(); + void setDistance(float distance); + float getDistance() const { return _distance; } +private: + float _distance; + VerletPoint* _points[2]; +}; + +class Ragdoll { +public: + + Ragdoll(); + virtual ~Ragdoll(); + + virtual void stepRagdollForward(float deltaTime) = 0; + + /// \return max distance of point movement + float enforceRagdollConstraints(); + + // both const and non-const getPoints() + const QVector<VerletPoint>& getRagdollPoints() const { return _ragdollPoints; } + QVector<VerletPoint>& getRagdollPoints() { return _ragdollPoints; } + +protected: + void clearRagdollConstraintsAndPoints(); + virtual void initRagdollPoints() = 0; + virtual void buildRagdollConstraints() = 0; + + QVector<VerletPoint> _ragdollPoints; + QVector<Constraint*> _ragdollConstraints; +}; + +#endif // hifi_Ragdoll_h diff --git a/libraries/shared/src/Shape.h b/libraries/shared/src/Shape.h index 3926f6cd07..09ed30a116 100644 --- a/libraries/shared/src/Shape.h +++ b/libraries/shared/src/Shape.h @@ -15,47 +15,76 @@ #include <glm/glm.hpp> #include <glm/gtc/quaternion.hpp> +class PhysicsEntity; + +const float MAX_SHAPE_MASS = 1.0e18f; // something less than sqrt(FLT_MAX) class Shape { public: + enum Type{ UNKNOWN_SHAPE = 0, SPHERE_SHAPE, CAPSULE_SHAPE, PLANE_SHAPE, - BOX_SHAPE, LIST_SHAPE }; - Shape() : _type(UNKNOWN_SHAPE), _boundingRadius(0.f), _position(0.f), _rotation() { } + Shape() : _type(UNKNOWN_SHAPE), _owningEntity(NULL), _boundingRadius(0.f), _translation(0.f), _rotation(), _mass(MAX_SHAPE_MASS) { } virtual ~Shape() {} int getType() const { return _type; } - float getBoundingRadius() const { return _boundingRadius; } - const glm::vec3& getPosition() const { return _position; } - const glm::quat& getRotation() const { return _rotation; } - virtual void setPosition(const glm::vec3& position) { _position = position; } + void setEntity(PhysicsEntity* entity) { _owningEntity = entity; } + PhysicsEntity* getEntity() const { return _owningEntity; } + + float getBoundingRadius() const { return _boundingRadius; } + + virtual const glm::quat& getRotation() const { return _rotation; } virtual void setRotation(const glm::quat& rotation) { _rotation = rotation; } + virtual void setTranslation(const glm::vec3& translation) { _translation = translation; } + virtual const glm::vec3& getTranslation() const { return _translation; } + + virtual void setMass(float mass) { _mass = mass; } + virtual float getMass() const { return _mass; } + virtual bool findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const = 0; + /// \param penetration of collision + /// \param contactPoint of collision + /// \return the effective mass for the collision + /// For most shapes has side effects: computes and caches the partial Lagrangian coefficients which will be + /// used in the next accumulateDelta() call. + virtual float computeEffectiveMass(const glm::vec3& penetration, const glm::vec3& contactPoint) { return _mass; } + + /// \param relativeMassFactor the final ingredient for partial Lagrangian coefficients from computeEffectiveMass() + /// \param penetration the delta movement + virtual void accumulateDelta(float relativeMassFactor, const glm::vec3& penetration) {} + + virtual void applyAccumulatedDelta() {} + + /// \return volume of shape in cubic meters + virtual float getVolume() const { return 1.0; } + protected: // these ctors are protected (used by derived classes only) - Shape(Type type) : _type(type), _boundingRadius(0.f), _position(0.f), _rotation() {} + Shape(Type type) : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(0.f), _rotation() {} Shape(Type type, const glm::vec3& position) - : _type(type), _boundingRadius(0.f), _position(position), _rotation() {} + : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation() {} Shape(Type type, const glm::vec3& position, const glm::quat& rotation) - : _type(type), _boundingRadius(0.f), _position(position), _rotation(rotation) {} + : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation(rotation) {} void setBoundingRadius(float radius) { _boundingRadius = radius; } int _type; + PhysicsEntity* _owningEntity; float _boundingRadius; - glm::vec3 _position; + glm::vec3 _translation; glm::quat _rotation; + float _mass; }; #endif // hifi_Shape_h diff --git a/libraries/shared/src/ShapeCollider.cpp b/libraries/shared/src/ShapeCollider.cpp index bbedeb401d..ffb51660e2 100644 --- a/libraries/shared/src/ShapeCollider.cpp +++ b/libraries/shared/src/ShapeCollider.cpp @@ -24,7 +24,6 @@ namespace ShapeCollider { bool collideShapes(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - // ATM we only have two shape types so we just check every case. // TODO: make a fast lookup for correct method int typeA = shapeA->getType(); int typeB = shapeB->getType(); @@ -74,7 +73,7 @@ bool collideShapesCoarse(const QVector<const Shape*>& shapesA, const QVector<con tempCollisions.clear(); foreach (const Shape* shapeA, shapesA) { foreach (const Shape* shapeB, shapesB) { - ShapeCollider::collideShapes(shapeA, shapeB, tempCollisions); + collideShapes(shapeA, shapeB, tempCollisions); } } if (tempCollisions.size() > 0) { @@ -87,11 +86,52 @@ bool collideShapesCoarse(const QVector<const Shape*>& shapesA, const QVector<con } collision._penetration = totalPenetration; collision._contactPoint = averageContactPoint / (float)(tempCollisions.size()); + // there are no valid shape pointers for this collision so we set them NULL + collision._shapeA = NULL; + collision._shapeB = NULL; return true; } return false; } +bool collideShapeWithShapes(const Shape* shapeA, const QVector<Shape*>& shapes, int startIndex, CollisionList& collisions) { + bool collided = false; + if (shapeA) { + int numShapes = shapes.size(); + for (int i = startIndex; i < numShapes; ++i) { + const Shape* shapeB = shapes.at(i); + if (!shapeB) { + continue; + } + if (collideShapes(shapeA, shapeB, collisions)) { + collided = true; + if (collisions.isFull()) { + break; + } + } + } + } + return collided; +} + +bool collideShapesWithShapes(const QVector<Shape*>& shapesA, const QVector<Shape*>& shapesB, CollisionList& collisions) { + bool collided = false; + int numShapesA = shapesA.size(); + for (int i = 0; i < numShapesA; ++i) { + Shape* shapeA = shapesA.at(i); + if (!shapeA) { + continue; + } + if (collideShapeWithShapes(shapeA, shapesB, 0, collisions)) { + collided = true; + if (collisions.isFull()) { + break; + } + } + } + return collided; +} + bool collideShapeWithAACube(const Shape* shapeA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { int typeA = shapeA->getType(); if (typeA == Shape::SPHERE_SHAPE) { @@ -116,7 +156,7 @@ bool collideShapeWithAACube(const Shape* shapeA, const glm::vec3& cubeCenter, fl } bool sphereSphere(const SphereShape* sphereA, const SphereShape* sphereB, CollisionList& collisions) { - glm::vec3 BA = sphereB->getPosition() - sphereA->getPosition(); + glm::vec3 BA = sphereB->getTranslation() - sphereA->getTranslation(); float distanceSquared = glm::dot(BA, BA); float totalRadius = sphereA->getRadius() + sphereB->getRadius(); if (distanceSquared < totalRadius * totalRadius) { @@ -132,10 +172,11 @@ bool sphereSphere(const SphereShape* sphereA, const SphereShape* sphereB, Collis // penetration points from A into B CollisionInfo* collision = collisions.getNewCollision(); if (collision) { - collision->_type = COLLISION_TYPE_UNKNOWN; collision->_penetration = BA * (totalRadius - distance); // contactPoint is on surface of A - collision->_contactPoint = sphereA->getPosition() + sphereA->getRadius() * BA; + collision->_contactPoint = sphereA->getTranslation() + sphereA->getRadius() * BA; + collision->_shapeA = sphereA; + collision->_shapeB = sphereB; return true; } } @@ -144,7 +185,7 @@ bool sphereSphere(const SphereShape* sphereA, const SphereShape* sphereB, Collis bool sphereCapsule(const SphereShape* sphereA, const CapsuleShape* capsuleB, CollisionList& collisions) { // find sphereA's closest approach to axis of capsuleB - glm::vec3 BA = capsuleB->getPosition() - sphereA->getPosition(); + glm::vec3 BA = capsuleB->getTranslation() - sphereA->getTranslation(); glm::vec3 capsuleAxis; capsuleB->computeNormalizedAxis(capsuleAxis); float axialDistance = - glm::dot(BA, capsuleAxis); @@ -179,8 +220,9 @@ bool sphereCapsule(const SphereShape* sphereA, const CapsuleShape* capsuleB, Col // penetration points from A into B collision->_penetration = (totalRadius - radialDistance) * radialAxis; // points from A into B // contactPoint is on surface of sphereA - collision->_contactPoint = sphereA->getPosition() + sphereA->getRadius() * radialAxis; - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_contactPoint = sphereA->getTranslation() + sphereA->getRadius() * radialAxis; + collision->_shapeA = sphereA; + collision->_shapeB = capsuleB; } else { // A is on B's axis, so the penetration is undefined... if (absAxialDistance > capsuleB->getHalfHeight()) { @@ -201,8 +243,9 @@ bool sphereCapsule(const SphereShape* sphereA, const CapsuleShape* capsuleB, Col float sign = (axialDistance > 0.0f) ? -1.0f : 1.0f; collision->_penetration = (sign * (totalRadius + capsuleB->getHalfHeight() - absAxialDistance)) * capsuleAxis; // contactPoint is on surface of sphereA - collision->_contactPoint = sphereA->getPosition() + (sign * sphereA->getRadius()) * capsuleAxis; - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_contactPoint = sphereA->getTranslation() + (sign * sphereA->getRadius()) * capsuleAxis; + collision->_shapeA = sphereA; + collision->_shapeB = capsuleB; } return true; } @@ -211,14 +254,15 @@ bool sphereCapsule(const SphereShape* sphereA, const CapsuleShape* capsuleB, Col bool spherePlane(const SphereShape* sphereA, const PlaneShape* planeB, CollisionList& collisions) { glm::vec3 penetration; - if (findSpherePlanePenetration(sphereA->getPosition(), sphereA->getRadius(), planeB->getCoefficients(), penetration)) { + if (findSpherePlanePenetration(sphereA->getTranslation(), sphereA->getRadius(), planeB->getCoefficients(), penetration)) { CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { return false; // collision list is full } collision->_penetration = penetration; - collision->_contactPoint = sphereA->getPosition() + sphereA->getRadius() * glm::normalize(penetration); - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_contactPoint = sphereA->getTranslation() + sphereA->getRadius() * glm::normalize(penetration); + collision->_shapeA = sphereA; + collision->_shapeB = planeB; return true; } return false; @@ -226,7 +270,7 @@ bool spherePlane(const SphereShape* sphereA, const PlaneShape* planeB, Collision bool capsuleSphere(const CapsuleShape* capsuleA, const SphereShape* sphereB, CollisionList& collisions) { // find sphereB's closest approach to axis of capsuleA - glm::vec3 AB = capsuleA->getPosition() - sphereB->getPosition(); + glm::vec3 AB = capsuleA->getTranslation() - sphereB->getTranslation(); glm::vec3 capsuleAxis; capsuleA->computeNormalizedAxis(capsuleAxis); float axialDistance = - glm::dot(AB, capsuleAxis); @@ -242,14 +286,14 @@ bool capsuleSphere(const CapsuleShape* capsuleA, const SphereShape* sphereB, Col } // closestApproach = point on capsuleA's axis that is closest to sphereB's center - glm::vec3 closestApproach = capsuleA->getPosition() + axialDistance * capsuleAxis; + glm::vec3 closestApproach = capsuleA->getTranslation() + axialDistance * capsuleAxis; if (absAxialDistance > capsuleA->getHalfHeight()) { // sphere hits capsule on a cap // --> recompute radialAxis and closestApproach float sign = (axialDistance > 0.0f) ? 1.0f : -1.0f; - closestApproach = capsuleA->getPosition() + (sign * capsuleA->getHalfHeight()) * capsuleAxis; - radialAxis = closestApproach - sphereB->getPosition(); + closestApproach = capsuleA->getTranslation() + (sign * capsuleA->getHalfHeight()) * capsuleAxis; + radialAxis = closestApproach - sphereB->getTranslation(); radialDistance2 = glm::length2(radialAxis); if (radialDistance2 > totalRadius2) { return false; @@ -268,7 +312,8 @@ bool capsuleSphere(const CapsuleShape* capsuleA, const SphereShape* sphereB, Col collision->_penetration = (radialDistance - totalRadius) * radialAxis; // points from A into B // contactPoint is on surface of capsuleA collision->_contactPoint = closestApproach - capsuleA->getRadius() * radialAxis; - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_shapeA = capsuleA; + collision->_shapeB = sphereB; } else { // A is on B's axis, so the penetration is undefined... if (absAxialDistance > capsuleA->getHalfHeight()) { @@ -289,7 +334,8 @@ bool capsuleSphere(const CapsuleShape* capsuleA, const SphereShape* sphereB, Col collision->_penetration = (sign * (totalRadius + capsuleA->getHalfHeight() - absAxialDistance)) * capsuleAxis; // contactPoint is on surface of sphereA collision->_contactPoint = closestApproach + (sign * capsuleA->getRadius()) * capsuleAxis; - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_shapeA = capsuleA; + collision->_shapeB = sphereB; } } return true; @@ -302,8 +348,8 @@ bool capsuleCapsule(const CapsuleShape* capsuleA, const CapsuleShape* capsuleB, capsuleA->computeNormalizedAxis(axisA); glm::vec3 axisB; capsuleB->computeNormalizedAxis(axisB); - glm::vec3 centerA = capsuleA->getPosition(); - glm::vec3 centerB = capsuleB->getPosition(); + glm::vec3 centerA = capsuleA->getTranslation(); + glm::vec3 centerB = capsuleB->getTranslation(); // NOTE: The formula for closest approach between two lines is: // d = [(B - A) . (a - (a.b)b)] / (1 - (a.b)^2) @@ -361,7 +407,8 @@ bool capsuleCapsule(const CapsuleShape* capsuleA, const CapsuleShape* capsuleB, collision->_penetration = BA * (totalRadius - distance); // contactPoint is on surface of A collision->_contactPoint = centerA + distanceA * axisA + capsuleA->getRadius() * BA; - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_shapeA = capsuleA; + collision->_shapeB = capsuleB; return true; } } else { @@ -427,7 +474,8 @@ bool capsuleCapsule(const CapsuleShape* capsuleA, const CapsuleShape* capsuleB, // average the internal pair, and then do the math from centerB collision->_contactPoint = centerB + (0.5f * (points[1] + points[2])) * axisB + (capsuleA->getRadius() - distance) * BA; - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_shapeA = capsuleA; + collision->_shapeB = capsuleB; return true; } } @@ -447,7 +495,8 @@ bool capsulePlane(const CapsuleShape* capsuleA, const PlaneShape* planeB, Collis collision->_penetration = penetration; glm::vec3 deepestEnd = (glm::dot(start, glm::vec3(plane)) < glm::dot(end, glm::vec3(plane))) ? start : end; collision->_contactPoint = deepestEnd + capsuleA->getRadius() * glm::normalize(penetration); - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_shapeA = capsuleA; + collision->_shapeB = planeB; return true; } return false; @@ -455,15 +504,16 @@ bool capsulePlane(const CapsuleShape* capsuleA, const PlaneShape* planeB, Collis bool planeSphere(const PlaneShape* planeA, const SphereShape* sphereB, CollisionList& collisions) { glm::vec3 penetration; - if (findSpherePlanePenetration(sphereB->getPosition(), sphereB->getRadius(), planeA->getCoefficients(), penetration)) { + if (findSpherePlanePenetration(sphereB->getTranslation(), sphereB->getRadius(), planeA->getCoefficients(), penetration)) { CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { return false; // collision list is full } collision->_penetration = -penetration; - collision->_contactPoint = sphereB->getPosition() + + collision->_contactPoint = sphereB->getTranslation() + (sphereB->getRadius() / glm::length(penetration) - 1.0f) * penetration; - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_shapeA = planeA; + collision->_shapeB = sphereB; return true; } return false; @@ -482,7 +532,8 @@ bool planeCapsule(const PlaneShape* planeA, const CapsuleShape* capsuleB, Collis collision->_penetration = -penetration; glm::vec3 deepestEnd = (glm::dot(start, glm::vec3(plane)) < glm::dot(end, glm::vec3(plane))) ? start : end; collision->_contactPoint = deepestEnd + (capsuleB->getRadius() / glm::length(penetration) - 1.0f) * penetration; - collision->_type = COLLISION_TYPE_UNKNOWN; + collision->_shapeA = planeA; + collision->_shapeB = capsuleB; return true; } return false; @@ -668,15 +719,15 @@ bool sphereAACube(const glm::vec3& sphereCenter, float sphereRadius, const glm:: direction /= lengthDirection; // compute collision details - collision->_type = COLLISION_TYPE_AACUBE; collision->_floatData = cubeSide; collision->_vecData = cubeCenter; collision->_penetration = (halfCubeSide * lengthDirection + sphereRadius - maxBA * glm::dot(BA, direction)) * direction; collision->_contactPoint = sphereCenter + sphereRadius * direction; } - collision->_type = COLLISION_TYPE_AACUBE; collision->_floatData = cubeSide; collision->_vecData = cubeCenter; + collision->_shapeA = NULL; + collision->_shapeB = NULL; return true; } else if (sphereRadius + halfCubeSide > distance) { // NOTE: for cocentric approximation we collide sphere and cube as two spheres which means @@ -688,9 +739,10 @@ bool sphereAACube(const glm::vec3& sphereCenter, float sphereRadius, const glm:: // contactPoint is on surface of A collision->_contactPoint = sphereCenter + collision->_penetration; - collision->_type = COLLISION_TYPE_AACUBE; collision->_floatData = cubeSide; collision->_vecData = cubeCenter; + collision->_shapeA = NULL; + collision->_shapeB = NULL; return true; } } @@ -726,6 +778,8 @@ bool sphereAACube_StarkAngles(const glm::vec3& sphereCenter, float sphereRadius, collision->_penetration = glm::dot(surfaceAB, direction) * direction; // contactPoint is on surface of A collision->_contactPoint = sphereCenter + sphereRadius * direction; + collision->_shapeA = NULL; + collision->_shapeB = NULL; return true; } } @@ -738,6 +792,8 @@ bool sphereAACube_StarkAngles(const glm::vec3& sphereCenter, float sphereRadius, collision->_penetration = (sphereRadius + 0.5f * cubeSide) * glm::vec3(0.0f, -1.0f, 0.0f); // contactPoint is on surface of A collision->_contactPoint = sphereCenter + collision->_penetration; + collision->_shapeA = NULL; + collision->_shapeB = NULL; return true; } } @@ -746,21 +802,21 @@ bool sphereAACube_StarkAngles(const glm::vec3& sphereCenter, float sphereRadius, */ bool sphereAACube(const SphereShape* sphereA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { - return sphereAACube(sphereA->getPosition(), sphereA->getRadius(), cubeCenter, cubeSide, collisions); + return sphereAACube(sphereA->getTranslation(), sphereA->getRadius(), cubeCenter, cubeSide, collisions); } bool capsuleAACube(const CapsuleShape* capsuleA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { // find nerest approach of capsule line segment to cube glm::vec3 capsuleAxis; capsuleA->computeNormalizedAxis(capsuleAxis); - float offset = glm::dot(cubeCenter - capsuleA->getPosition(), capsuleAxis); + float offset = glm::dot(cubeCenter - capsuleA->getTranslation(), capsuleAxis); float halfHeight = capsuleA->getHalfHeight(); if (offset > halfHeight) { offset = halfHeight; } else if (offset < -halfHeight) { offset = -halfHeight; } - glm::vec3 nearestApproach = capsuleA->getPosition() + offset * capsuleAxis; + glm::vec3 nearestApproach = capsuleA->getTranslation() + offset * capsuleAxis; // collide nearest approach like a sphere at that point return sphereAACube(nearestApproach, capsuleA->getRadius(), cubeCenter, cubeSide, collisions); } diff --git a/libraries/shared/src/ShapeCollider.h b/libraries/shared/src/ShapeCollider.h index 8261aceaf3..b1be75fa40 100644 --- a/libraries/shared/src/ShapeCollider.h +++ b/libraries/shared/src/ShapeCollider.h @@ -12,6 +12,8 @@ #ifndef hifi_ShapeCollider_h #define hifi_ShapeCollider_h +#include <QVector> + #include "CapsuleShape.h" #include "CollisionInfo.h" #include "ListShape.h" @@ -33,6 +35,9 @@ namespace ShapeCollider { /// \return true if any shapes collide bool collideShapesCoarse(const QVector<const Shape*>& shapesA, const QVector<const Shape*>& shapesB, CollisionInfo& collision); + bool collideShapeWithShapes(const Shape* shapeA, const QVector<Shape*>& shapes, int startIndex, CollisionList& collisions); + bool collideShapesWithShapes(const QVector<Shape*>& shapesA, const QVector<Shape*>& shapesB, CollisionList& collisions); + /// \param shapeA a pointer to a shape (cannot be NULL) /// \param cubeCenter center of cube /// \param cubeSide lenght of side of cube diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index dbbfb02365..e5c2a0afc9 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -68,7 +68,7 @@ float randFloat(); int randIntInRange (int min, int max); float randFloatInRange (float min,float max); float randomSign(); /// \return -1.0 or 1.0 -unsigned char randomColorValue(int minimum); +unsigned char randomColorValue(int minimum = 0); bool randomBoolean(); glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float alpha); diff --git a/libraries/shared/src/SphereShape.cpp b/libraries/shared/src/SphereShape.cpp index 49137fac43..c77b0c97fb 100644 --- a/libraries/shared/src/SphereShape.cpp +++ b/libraries/shared/src/SphereShape.cpp @@ -17,14 +17,14 @@ bool SphereShape::findRayIntersection(const glm::vec3& rayStart, const glm::vec3 float r2 = _boundingRadius * _boundingRadius; // compute closest approach (CA) - float a = glm::dot(_position - rayStart, rayDirection); // a = distance from ray-start to CA - float b2 = glm::distance2(_position, rayStart + a * rayDirection); // b2 = squared distance from sphere-center to CA + float a = glm::dot(_translation - rayStart, rayDirection); // a = distance from ray-start to CA + float b2 = glm::distance2(_translation, rayStart + a * rayDirection); // b2 = squared distance from sphere-center to CA if (b2 > r2) { // ray does not hit sphere return false; } float c = sqrtf(r2 - b2); // c = distance from CA to sphere surface along rayDirection - float d2 = glm::distance2(rayStart, _position); // d2 = squared distance from sphere-center to ray-start + float d2 = glm::distance2(rayStart, _translation); // d2 = squared distance from sphere-center to ray-start if (a < 0.0f) { // ray points away from sphere-center if (d2 > r2) { diff --git a/libraries/shared/src/SphereShape.h b/libraries/shared/src/SphereShape.h index e87b8acab1..d2f2a8596f 100644 --- a/libraries/shared/src/SphereShape.h +++ b/libraries/shared/src/SphereShape.h @@ -14,6 +14,8 @@ #include "Shape.h" +#include "SharedUtil.h" + class SphereShape : public Shape { public: SphereShape() : Shape(Shape::SPHERE_SHAPE) {} @@ -26,11 +28,15 @@ public: _boundingRadius = radius; } + virtual ~SphereShape() {} + float getRadius() const { return _boundingRadius; } void setRadius(float radius) { _boundingRadius = radius; } bool findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const; + + float getVolume() const { return 1.3333333333f * PI * _boundingRadius * _boundingRadius * _boundingRadius; } }; #endif // hifi_SphereShape_h diff --git a/libraries/shared/src/VerletCapsuleShape.cpp b/libraries/shared/src/VerletCapsuleShape.cpp new file mode 100644 index 0000000000..3ac4899682 --- /dev/null +++ b/libraries/shared/src/VerletCapsuleShape.cpp @@ -0,0 +1,166 @@ +// +// VerletCapsuleShape.cpp +// libraries/shared/src +// +// Created by Andrew Meadows on 2014.06.16 +// Copyright 2014 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 "VerletCapsuleShape.h" + +#include "Ragdoll.h" // for VerletPoint +#include "SharedUtil.h" + +VerletCapsuleShape::VerletCapsuleShape(VerletPoint* startPoint, VerletPoint* endPoint) : + CapsuleShape(), _startPoint(startPoint), _endPoint(endPoint), _startLagrangeCoef(0.5f), _endLagrangeCoef(0.5f) { + assert(startPoint); + assert(endPoint); + _halfHeight = 0.5f * glm::distance(_startPoint->_position, _endPoint->_position); + updateBoundingRadius(); +} + +VerletCapsuleShape::VerletCapsuleShape(float radius, VerletPoint* startPoint, VerletPoint* endPoint) : + CapsuleShape(radius, 1.0f), _startPoint(startPoint), _endPoint(endPoint), + _startLagrangeCoef(0.5f), _endLagrangeCoef(0.5f) { + assert(startPoint); + assert(endPoint); + _halfHeight = 0.5f * glm::distance(_startPoint->_position, _endPoint->_position); + updateBoundingRadius(); +} + +const glm::quat& VerletCapsuleShape::getRotation() const { + // NOTE: The "rotation" of this shape must be computed on the fly, + // which makes this method MUCH more more expensive than you might expect. + glm::vec3 axis; + computeNormalizedAxis(axis); + VerletCapsuleShape* thisCapsule = const_cast<VerletCapsuleShape*>(this); + thisCapsule->_rotation = computeNewRotation(axis); + return _rotation; +} + +void VerletCapsuleShape::setRotation(const glm::quat& rotation) { + // NOTE: this method will update the verlet points, which is probably not + // what you want to do. Only call this method if you know what you're doing. + + // update points such that they have the same center but a different axis + glm::vec3 center = getTranslation(); + float halfHeight = getHalfHeight(); + glm::vec3 axis = rotation * DEFAULT_CAPSULE_AXIS; + _startPoint->_position = center - halfHeight * axis; + _endPoint->_position = center + halfHeight * axis; +} + +void VerletCapsuleShape::setTranslation(const glm::vec3& position) { + // NOTE: this method will update the verlet points, which is probably not + // what you want to do. Only call this method if you know what you're doing. + + // update the points such that their center is at position + glm::vec3 movement = position - getTranslation(); + _startPoint->_position += movement; + _endPoint->_position += movement; +} + +const glm::vec3& VerletCapsuleShape::getTranslation() const { + // the "translation" of this shape must be computed on the fly + VerletCapsuleShape* thisCapsule = const_cast<VerletCapsuleShape*>(this); + thisCapsule->_translation = 0.5f * (_startPoint->_position + _endPoint->_position); + return _translation; +} + +float VerletCapsuleShape::computeEffectiveMass(const glm::vec3& penetration, const glm::vec3& contactPoint) { + glm::vec3 startLeg = _startPoint->_position - contactPoint; + glm::vec3 endLeg = _endPoint->_position - contactPoint; + + // TODO: use fast approximate distance calculations here + float startLength = glm::length(startLeg); + float endlength = glm::length(endLeg); + + // The raw coefficient is proportional to the other leg's length multiplied by the dot-product + // of the penetration and this leg direction. We don't worry about the common penetration length + // because it is normalized out later. + float startCoef = glm::abs(glm::dot(startLeg, penetration)) * endlength / (startLength + EPSILON); + float endCoef = glm::abs(glm::dot(endLeg, penetration)) * startLength / (endlength + EPSILON); + + float maxCoef = glm::max(startCoef, endCoef); + if (maxCoef > EPSILON) { + // One of these coeficients will be 1.0, the other will be less --> + // one endpoint will move the full amount while the other will move less. + _startLagrangeCoef = startCoef / maxCoef; + _endLagrangeCoef = endCoef / maxCoef; + assert(!glm::isnan(_startLagrangeCoef)); + assert(!glm::isnan(_startLagrangeCoef)); + } else { + // The coefficients are the same --> the collision will move both equally + // as if the object were solid. + _startLagrangeCoef = 1.0f; + _endLagrangeCoef = 1.0f; + } + // the effective mass is the weighted sum of the two endpoints + return _startLagrangeCoef * _startPoint->_mass + _endLagrangeCoef * _endPoint->_mass; +} + +void VerletCapsuleShape::accumulateDelta(float relativeMassFactor, const glm::vec3& penetration) { + assert(!glm::isnan(relativeMassFactor)); + _startPoint->accumulateDelta(relativeMassFactor * _startLagrangeCoef * penetration); + _endPoint->accumulateDelta(relativeMassFactor * _endLagrangeCoef * penetration); +} + +void VerletCapsuleShape::applyAccumulatedDelta() { + _startPoint->applyAccumulatedDelta(); + _endPoint->applyAccumulatedDelta(); +} + +// virtual +float VerletCapsuleShape::getHalfHeight() const { + return 0.5f * glm::distance(_startPoint->_position, _endPoint->_position); +} + +// virtual +void VerletCapsuleShape::getStartPoint(glm::vec3& startPoint) const { + startPoint = _startPoint->_position; +} + +// virtual +void VerletCapsuleShape::getEndPoint(glm::vec3& endPoint) const { + endPoint = _endPoint->_position; +} + +// virtual +void VerletCapsuleShape::computeNormalizedAxis(glm::vec3& axis) const { + glm::vec3 unormalizedAxis = _endPoint->_position - _startPoint->_position; + float fullLength = glm::length(unormalizedAxis); + if (fullLength > EPSILON) { + axis = unormalizedAxis / fullLength; + } else { + // the axis is meaningless, but we fill it with a normalized direction + // just in case the calling context assumes it really is normalized. + axis = glm::vec3(0.0f, 1.0f, 0.0f); + } +} + +// virtual +void VerletCapsuleShape::setHalfHeight(float halfHeight) { + // push points along axis so they are 2*halfHeight apart + glm::vec3 center = getTranslation(); + glm::vec3 axis; + computeNormalizedAxis(axis); + _startPoint->_position = center - halfHeight * axis; + _endPoint->_position = center + halfHeight * axis; + _boundingRadius = _radius + halfHeight; +} + +// virtual +void VerletCapsuleShape::setRadiusAndHalfHeight(float radius, float halfHeight) { + _radius = radius; + setHalfHeight(halfHeight); +} + +// virtual +void VerletCapsuleShape::setEndPoints(const glm::vec3& startPoint, const glm::vec3& endPoint) { + _startPoint->_position = startPoint; + _endPoint->_position = endPoint; + updateBoundingRadius(); +} diff --git a/libraries/shared/src/VerletCapsuleShape.h b/libraries/shared/src/VerletCapsuleShape.h new file mode 100644 index 0000000000..1fd84f5b1e --- /dev/null +++ b/libraries/shared/src/VerletCapsuleShape.h @@ -0,0 +1,83 @@ +// +// VerletCapsuleShape.h +// libraries/shared/src +// +// Created by Andrew Meadows on 2014.06.16 +// Copyright 2014 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_VerletCapsuleShape_h +#define hifi_VerletCapsuleShape_h + +#include "CapsuleShape.h" + + +// The VerletCapsuleShape is similar to a regular CapsuleShape, except it keeps a pointer +// to its endpoints which are owned by some other data structure (a verlet simulation system). +// This makes it easier for the points to be moved around by constraints in the system +// as well as collisions with the shape, however it has some drawbacks: +// +// (1) The Shape::_translation and ::_rotation data members are not used (wasted) +// +// (2) A VerletShape doesn't own the points that it uses, so you must be careful not to +// leave dangling pointers around. +// +// (3) Some const methods of VerletCapsuleShape are much more expensive than you might think. +// For example getHalfHeight() and setHalfHeight() methods must do extra computation. In +// particular setRotation() is significantly more expensive than for the CapsuleShape. +// Not too expensive to use when setting up shapes, but you woudln't want to use it deep +// down in a hot simulation loop, such as when processing collision results. Best to +// just let the verlet simulation do its thing and not try to constantly force a rotation. + +class VerletPoint; + +class VerletCapsuleShape : public CapsuleShape { +public: + VerletCapsuleShape(VerletPoint* startPoint, VerletPoint* endPoint); + VerletCapsuleShape(float radius, VerletPoint* startPoint, VerletPoint* endPoint); + + // virtual overrides from Shape + const glm::quat& getRotation() const; + void setRotation(const glm::quat& rotation); + void setTranslation(const glm::vec3& position); + const glm::vec3& getTranslation() const; + float computeEffectiveMass(const glm::vec3& penetration, const glm::vec3& contactPoint); + void accumulateDelta(float relativeMassFactor, const glm::vec3& penetration); + void applyAccumulatedDelta(); + + //float getRadius() const { return _radius; } + virtual float getHalfHeight() const; + + /// \param[out] startPoint is the center of start cap + void getStartPoint(glm::vec3& startPoint) const; + + /// \param[out] endPoint is the center of the end cap + void getEndPoint(glm::vec3& endPoint) const; + + /// \param[out] axis is a normalized vector that points from start to end + void computeNormalizedAxis(glm::vec3& axis) const; + + //void setRadius(float radius); + void setHalfHeight(float halfHeight); + void setRadiusAndHalfHeight(float radius, float halfHeight); + void setEndPoints(const glm::vec3& startPoint, const glm::vec3& endPoint); + + //void assignEndPoints(glm::vec3* startPoint, glm::vec3* endPoint); + +protected: + // NOTE: VerletCapsuleShape does NOT own the data in its points. + VerletPoint* _startPoint; + VerletPoint* _endPoint; + + // The LagrangeCoef's are numerical weights for distributing collision movement + // between the relevant VerletPoints associated with this shape. They are functions + // of the movement parameters and are computed (and cached) in computeEffectiveMass() + // and then used in the subsequent accumulateDelta(). + float _startLagrangeCoef; + float _endLagrangeCoef; +}; + +#endif // hifi_VerletCapsuleShape_h diff --git a/libraries/shared/src/VerletSphereShape.cpp b/libraries/shared/src/VerletSphereShape.cpp new file mode 100644 index 0000000000..10c40c6611 --- /dev/null +++ b/libraries/shared/src/VerletSphereShape.cpp @@ -0,0 +1,50 @@ +// +// VerletSphereShape.cpp +// libraries/shared/src +// +// Created by Andrew Meadows on 2014.06.16 +// Copyright 2014 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 "VerletSphereShape.h" + +#include "Ragdoll.h" // for VerletPoint + +VerletSphereShape::VerletSphereShape(VerletPoint* centerPoint) : SphereShape() { + assert(centerPoint); + _point = centerPoint; +} + +VerletSphereShape::VerletSphereShape(float radius, VerletPoint* centerPoint) : SphereShape(radius) { + assert(centerPoint); + _point = centerPoint; +} + +// virtual from Shape class +void VerletSphereShape::setTranslation(const glm::vec3& position) { + _point->_position = position; + _point->_lastPosition = position; +} + +// virtual from Shape class +const glm::vec3& VerletSphereShape::getTranslation() const { + return _point->_position; +} + +// virtual +float VerletSphereShape::computeEffectiveMass(const glm::vec3& penetration, const glm::vec3& contactPoint) { + return _point->_mass; +} + +// virtual +void VerletSphereShape::accumulateDelta(float relativeMassFactor, const glm::vec3& penetration) { + _point->accumulateDelta(relativeMassFactor * penetration); +} + +// virtual +void VerletSphereShape::applyAccumulatedDelta() { + _point->applyAccumulatedDelta(); +} diff --git a/libraries/shared/src/VerletSphereShape.h b/libraries/shared/src/VerletSphereShape.h new file mode 100644 index 0000000000..65da3b2597 --- /dev/null +++ b/libraries/shared/src/VerletSphereShape.h @@ -0,0 +1,47 @@ +// +// VerletSphereShape.h +// libraries/shared/src +// +// Created by Andrew Meadows on 2014.06.16 +// Copyright 2014 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_VerletSphereShape_h +#define hifi_VerletSphereShape_h + +#include "SphereShape.h" + +// The VerletSphereShape is similar to a regular SphereShape, except it keeps a pointer +// to its center which is owned by some other data structure (a verlet simulation system). +// This makes it easier for the points to be moved around by constraints in the system +// as well as collisions with the shape, however it has some drawbacks: +// +// (1) The Shape::_translation data member is not used (wasted) +// +// (2) A VerletShape doesn't own the points that it uses, so you must be careful not to +// leave dangling pointers around. + +class VerletPoint; + +class VerletSphereShape : public SphereShape { +public: + VerletSphereShape(VerletPoint* point); + + VerletSphereShape(float radius, VerletPoint* centerPoint); + + // virtual overrides from Shape + void setTranslation(const glm::vec3& position); + const glm::vec3& getTranslation() const; + float computeEffectiveMass(const glm::vec3& penetration, const glm::vec3& contactPoint); + void accumulateDelta(float relativeMassFactor, const glm::vec3& penetration); + void applyAccumulatedDelta(); + +protected: + // NOTE: VerletSphereShape does NOT own its _point + VerletPoint* _point; +}; + +#endif // hifi_VerletSphereShape_h diff --git a/tests/audio/CMakeLists.txt b/tests/audio/CMakeLists.txt new file mode 100644 index 0000000000..5c5178cb71 --- /dev/null +++ b/tests/audio/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 2.8) + +if (WIN32) + cmake_policy (SET CMP0020 NEW) +endif (WIN32) + +set(TARGET_NAME audio-tests) + +set(ROOT_DIR ../..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +#find_package(Qt5Network REQUIRED) +#find_package(Qt5Script REQUIRED) +#find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE) + +include(${MACRO_DIR}/AutoMTC.cmake) +auto_mtc(${TARGET_NAME} ${ROOT_DIR}) + +#qt5_use_modules(${TARGET_NAME} Network Script Widgets) + +#include glm +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} ${ROOT_DIR}) + +# link in the shared libraries +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(audio ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) + +IF (WIN32) + target_link_libraries(${TARGET_NAME} Winmm Ws2_32) +ENDIF(WIN32) + diff --git a/tests/audio/src/AudioRingBufferTests.cpp b/tests/audio/src/AudioRingBufferTests.cpp new file mode 100644 index 0000000000..506e81e13e --- /dev/null +++ b/tests/audio/src/AudioRingBufferTests.cpp @@ -0,0 +1,146 @@ +// +// AudioRingBufferTests.cpp +// tests/audio/src +// +// Created by Yixin Wang on 6/24/2014 +// Copyright 2014 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 "AudioRingBufferTests.h" + +#include "SharedUtil.h" + +void AudioRingBufferTests::assertBufferSize(const AudioRingBuffer& buffer, int samples) { + if (buffer.samplesAvailable() != samples) { + qDebug("Unexpected num samples available! Exptected: %d Actual: %d\n", samples, buffer.samplesAvailable()); + } +} + +void AudioRingBufferTests::runAllTests() { + + int16_t writeData[10000]; + for (int i = 0; i < 10000; i++) { writeData[i] = i; } + int writeIndexAt; + + int16_t readData[10000]; + int readIndexAt; + + + AudioRingBuffer ringBuffer(10); // makes buffer of 100 int16_t samples + for (int T = 0; T < 300; T++) { + + writeIndexAt = 0; + readIndexAt = 0; + + // write 73 samples, 73 samples in buffer + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 73) / sizeof(int16_t); + assertBufferSize(ringBuffer, 73); + + // read 43 samples, 30 samples in buffer + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 43) / sizeof(int16_t); + assertBufferSize(ringBuffer, 30); + + // write 70 samples, 100 samples in buffer (full) + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 70) / sizeof(int16_t); + assertBufferSize(ringBuffer, 100); + + // read 100 samples, 0 samples in buffer (empty) + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 100) / sizeof(int16_t); + assertBufferSize(ringBuffer, 0); + + + // verify 143 samples of read data + for (int i = 0; i < 143; i++) { + if (readData[i] != i) { + qDebug("first readData[%d] incorrect! Expcted: %d Actual: %d", i, i, readData[i]); + return; + } + } + + + writeIndexAt = 0; + readIndexAt = 0; + + // write 59 samples, 59 samples in buffer + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 59) / sizeof(int16_t); + assertBufferSize(ringBuffer, 59); + + // write 99 samples, 100 samples in buffer + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 99) / sizeof(int16_t); + assertBufferSize(ringBuffer, 100); + + // read 100 samples, 0 samples in buffer + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 100) / sizeof(int16_t); + assertBufferSize(ringBuffer, 0); + + // verify 100 samples of read data + for (int i = 0; i < 100; i++) { + readData[i] = writeIndexAt - 100 + i; + } + + + + + writeIndexAt = 0; + readIndexAt = 0; + + // write 77 samples, 77 samples in buffer + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 77) / sizeof(int16_t); + assertBufferSize(ringBuffer, 77); + + // write 24 samples, 100 samples in buffer (overwrote one sample: "0") + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 24) / sizeof(int16_t); + assertBufferSize(ringBuffer, 100); + + // write 29 silent samples, 100 samples in buffer, make sure non were added + int samplesWritten; + if ((samplesWritten = ringBuffer.addSilentFrame(29)) != 0) { + qDebug("addSilentFrame(29) incorrect! Expected: 0 Actual: %d", samplesWritten); + return; + } + assertBufferSize(ringBuffer, 100); + + // read 3 samples, 97 samples in buffer (expect to read "1", "2", "3") + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 3) / sizeof(int16_t); + for (int i = 0; i < 3; i++) { + if (readData[i] != i + 1) { + qDebug("Second readData[%d] incorrect! Expcted: %d Actual: %d", i, i + 1, readData[i]); + return; + } + } + assertBufferSize(ringBuffer, 97); + + // write 4 silent samples, 100 samples in buffer + if ((samplesWritten = ringBuffer.addSilentFrame(4) / sizeof(int16_t)) != 3) { + qDebug("addSilentFrame(4) incorrect! Exptected: 3 Actual: %d", samplesWritten); + return; + } + assertBufferSize(ringBuffer, 100); + + // read back 97 samples (the non-silent samples), 3 samples in buffer (expect to read "4" thru "100") + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 97) / sizeof(int16_t); + for (int i = 3; i < 100; i++) { + if (readData[i] != i + 1) { + qDebug("third readData[%d] incorrect! Expcted: %d Actual: %d", i, i + 1, readData[i]); + return; + } + } + assertBufferSize(ringBuffer, 3); + + // read back 3 silent samples, 0 samples in buffer + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 3) / sizeof(int16_t); + for (int i = 100; i < 103; i++) { + if (readData[i] != 0) { + qDebug("Fourth readData[%d] incorrect! Expcted: %d Actual: %d", i, 0, readData[i]); + return; + } + } + assertBufferSize(ringBuffer, 0); + } + + qDebug() << "PASSED"; +} + diff --git a/tests/audio/src/AudioRingBufferTests.h b/tests/audio/src/AudioRingBufferTests.h new file mode 100644 index 0000000000..20cbe74699 --- /dev/null +++ b/tests/audio/src/AudioRingBufferTests.h @@ -0,0 +1,25 @@ +// +// AudioRingBufferTests.h +// tests/audio/src +// +// Created by Yixin Wang on 6/24/2014 +// Copyright 2014 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_AudioRingBufferTests_h +#define hifi_AudioRingBufferTests_h + +#include "AudioRingBuffer.h" + + +namespace AudioRingBufferTests { + + void runAllTests(); + + void assertBufferSize(const AudioRingBuffer& buffer, int samples); +}; + +#endif // hifi_AudioRingBufferTests_h diff --git a/tests/audio/src/main.cpp b/tests/audio/src/main.cpp new file mode 100644 index 0000000000..10f1a2e522 --- /dev/null +++ b/tests/audio/src/main.cpp @@ -0,0 +1,19 @@ +// +// main.cpp +// tests/audio/src +// +// Copyright 2014 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 "AudioRingBufferTests.h" +#include <stdio.h> + +int main(int argc, char** argv) { + AudioRingBufferTests::runAllTests(); + printf("all tests passed. press enter to exit\n"); + getchar(); + return 0; +} diff --git a/tests/metavoxels/CMakeLists.txt b/tests/metavoxels/CMakeLists.txt index 5f11c7290e..ca141c5137 100644 --- a/tests/metavoxels/CMakeLists.txt +++ b/tests/metavoxels/CMakeLists.txt @@ -27,6 +27,7 @@ include_glm(${TARGET_NAME} "${ROOT_DIR}") # link in the shared libraries include(${MACRO_DIR}/LinkHifiLibrary.cmake) link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") IF (WIN32) diff --git a/tests/metavoxels/src/MetavoxelTests.cpp b/tests/metavoxels/src/MetavoxelTests.cpp index c9bce27cc3..61ab664310 100644 --- a/tests/metavoxels/src/MetavoxelTests.cpp +++ b/tests/metavoxels/src/MetavoxelTests.cpp @@ -28,10 +28,127 @@ MetavoxelTests::MetavoxelTests(int& argc, char** argv) : QCoreApplication(argc, argv) { } +static bool testSpanList() { + SpanList list; + + if (list.getTotalSet() != 0 || !list.getSpans().isEmpty()) { + qDebug() << "Failed empty state test."; + return true; + } + + if (list.set(-5, 15) != 10 || list.getTotalSet() != 0 || !list.getSpans().isEmpty()) { + qDebug() << "Failed initial front set."; + return true; + } + + if (list.set(5, 15) != 0 || list.getTotalSet() != 15 || list.getSpans().size() != 1 || + list.getSpans().at(0).unset != 5 || list.getSpans().at(0).set != 15) { + qDebug() << "Failed initial middle set."; + return true; + } + + if (list.set(25, 5) != 0 || list.getTotalSet() != 20 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 5 || list.getSpans().at(0).set != 15 || + list.getSpans().at(1).unset != 5 || list.getSpans().at(1).set != 5) { + qDebug() << "Failed initial end set."; + return true; + } + + if (list.set(1, 3) != 0 || list.getTotalSet() != 23 || list.getSpans().size() != 3 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 15 || + list.getSpans().at(2).unset != 5 || list.getSpans().at(2).set != 5) { + qDebug() << "Failed second front set."; + return true; + } + SpanList threeSet = list; + + if (list.set(20, 5) != 0 || list.getTotalSet() != 28 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 25) { + qDebug() << "Failed minimal join last two."; + return true; + } + + list = threeSet; + if (list.set(5, 25) != 0 || list.getTotalSet() != 28 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 25) { + qDebug() << "Failed maximal join last two."; + return true; + } + + list = threeSet; + if (list.set(10, 18) != 0 || list.getTotalSet() != 28 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 25) { + qDebug() << "Failed middle join last two."; + return true; + } + + list = threeSet; + if (list.set(10, 18) != 0 || list.getTotalSet() != 28 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 25) { + qDebug() << "Failed middle join last two."; + return true; + } + + list = threeSet; + if (list.set(2, 26) != 0 || list.getTotalSet() != 29 || list.getSpans().size() != 1 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 29) { + qDebug() << "Failed middle join three."; + return true; + } + + list = threeSet; + if (list.set(0, 2) != 4 || list.getTotalSet() != 20 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 15 || + list.getSpans().at(1).unset != 5 || list.getSpans().at(1).set != 5) { + qDebug() << "Failed front advance."; + return true; + } + + list = threeSet; + if (list.set(-10, 15) != 20 || list.getTotalSet() != 5 || list.getSpans().size() != 1 || + list.getSpans().at(0).unset != 5 || list.getSpans().at(0).set != 5) { + qDebug() << "Failed middle advance."; + return true; + } + + list = threeSet; + if (list.set(-10, 38) != 30 || list.getTotalSet() != 0 || list.getSpans().size() != 0) { + qDebug() << "Failed end advance."; + return true; + } + + list = threeSet; + if (list.set(-10, 100) != 90 || list.getTotalSet() != 0 || list.getSpans().size() != 0) { + qDebug() << "Failed clobber advance."; + return true; + } + + list = threeSet; + if (list.set(21, 3) != 0 || list.getTotalSet() != 26 || list.getSpans().size() != 4 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 15 || + list.getSpans().at(2).unset != 1 || list.getSpans().at(2).set != 3 || + list.getSpans().at(3).unset != 1 || list.getSpans().at(3).set != 5) { + qDebug() << "Failed adding fourth."; + return true; + } + + return false; +} + static int datagramsSent = 0; static int datagramsReceived = 0; static int bytesSent = 0; static int bytesReceived = 0; +static int maxDatagramsPerPacket = 0; +static int maxBytesPerPacket = 0; +static int groupsSent = 0; +static int maxPacketsPerGroup = 0; static int highPriorityMessagesSent = 0; static int highPriorityMessagesReceived = 0; static int unreliableMessagesSent = 0; @@ -45,6 +162,8 @@ static int sharedObjectsDestroyed = 0; static int objectMutationsPerformed = 0; static int scriptObjectsCreated = 0; static int scriptMutationsPerformed = 0; +static int metavoxelMutationsPerformed = 0; +static int spannerMutationsPerformed = 0; static QByteArray createRandomBytes(int minimumSize, int maximumSize) { QByteArray bytes(randIntInRange(minimumSize, maximumSize), 0); @@ -321,44 +440,122 @@ static bool testSerialization(Bitstream::MetadataType metadataType) { } bool MetavoxelTests::run() { - - qDebug() << "Running transmission tests..."; - qDebug(); - + LimitedNodeList::createInstance(); + // seed the random number generator so that our tests are reproducible srand(0xBAAAAABE); - // create two endpoints with the same header - QByteArray datagramHeader("testheader"); - Endpoint alice(datagramHeader), bob(datagramHeader); - - alice.setOther(&bob); - bob.setOther(&alice); - - // perform a large number of simulation iterations + // check for an optional command line argument specifying a single test + QStringList arguments = this->arguments(); + int test = (arguments.size() > 1) ? arguments.at(1).toInt() : 0; + + if (test == 0 || test == 1) { + qDebug() << "Running SpanList test..."; + qDebug(); + + if (testSpanList()) { + return true; + } + } + const int SIMULATION_ITERATIONS = 10000; - for (int i = 0; i < SIMULATION_ITERATIONS; i++) { - if (alice.simulate(i) || bob.simulate(i)) { + if (test == 0 || test == 2) { + qDebug() << "Running transmission test..."; + qDebug(); + + // create two endpoints with the same header + TestEndpoint alice, bob; + + alice.setOther(&bob); + bob.setOther(&alice); + + // perform a large number of simulation iterations + for (int i = 0; i < SIMULATION_ITERATIONS; i++) { + if (alice.simulate(i) || bob.simulate(i)) { + return true; + } + } + + qDebug() << "Sent" << highPriorityMessagesSent << "high priority messages, received" << highPriorityMessagesReceived; + qDebug() << "Sent" << unreliableMessagesSent << "unreliable messages, received" << unreliableMessagesReceived; + qDebug() << "Sent" << reliableMessagesSent << "reliable messages, received" << reliableMessagesReceived; + qDebug() << "Sent" << streamedBytesSent << "streamed bytes, received" << streamedBytesReceived; + qDebug() << "Sent" << datagramsSent << "datagrams with" << bytesSent << "bytes, received" << + datagramsReceived << "with" << bytesReceived << "bytes"; + qDebug() << "Max" << maxDatagramsPerPacket << "datagrams," << maxBytesPerPacket << "bytes per packet"; + qDebug() << "Created" << sharedObjectsCreated << "shared objects, destroyed" << sharedObjectsDestroyed; + qDebug() << "Performed" << objectMutationsPerformed << "object mutations"; + qDebug() << "Created" << scriptObjectsCreated << "script objects, mutated" << scriptMutationsPerformed; + qDebug(); + } + + if (test == 0 || test == 3) { + qDebug() << "Running congestion control test..."; + qDebug(); + + // clear the stats + streamedBytesSent = streamedBytesReceived = datagramsSent = bytesSent = 0; + datagramsReceived = bytesReceived = maxDatagramsPerPacket = maxBytesPerPacket = 0; + + // create two endpoints with the same header + TestEndpoint alice(TestEndpoint::CONGESTION_MODE), bob(TestEndpoint::CONGESTION_MODE); + + alice.setOther(&bob); + bob.setOther(&alice); + + // perform a large number of simulation iterations + for (int i = 0; i < SIMULATION_ITERATIONS; i++) { + if (alice.simulate(i) || bob.simulate(i)) { + return true; + } + } + + qDebug() << "Sent" << streamedBytesSent << "streamed bytes, received" << streamedBytesReceived; + qDebug() << "Sent" << datagramsSent << "datagrams in" << groupsSent << "groups with" << bytesSent << + "bytes, received" << datagramsReceived << "with" << bytesReceived << "bytes"; + qDebug() << "Max" << maxDatagramsPerPacket << "datagrams," << maxBytesPerPacket << "bytes per packet"; + qDebug() << "Max" << maxPacketsPerGroup << "packets per group"; + qDebug() << "Average" << (bytesReceived / datagramsReceived) << "bytes per datagram," << + (datagramsSent / groupsSent) << "datagrams per group"; + qDebug() << "Speed:" << (bytesReceived / SIMULATION_ITERATIONS) << "bytes per iteration"; + qDebug() << "Efficiency:" << ((float)streamedBytesReceived / bytesReceived); + } + + if (test == 0 || test == 4) { + qDebug() << "Running serialization test..."; + qDebug(); + + if (testSerialization(Bitstream::HASH_METADATA) || testSerialization(Bitstream::FULL_METADATA)) { return true; } } - qDebug() << "Sent" << highPriorityMessagesSent << "high priority messages, received" << highPriorityMessagesReceived; - qDebug() << "Sent" << unreliableMessagesSent << "unreliable messages, received" << unreliableMessagesReceived; - qDebug() << "Sent" << reliableMessagesSent << "reliable messages, received" << reliableMessagesReceived; - qDebug() << "Sent" << streamedBytesSent << "streamed bytes, received" << streamedBytesReceived; - qDebug() << "Sent" << datagramsSent << "datagrams with" << bytesSent << "bytes, received" << - datagramsReceived << "with" << bytesReceived << "bytes"; - qDebug() << "Created" << sharedObjectsCreated << "shared objects, destroyed" << sharedObjectsDestroyed; - qDebug() << "Performed" << objectMutationsPerformed << "object mutations"; - qDebug() << "Created" << scriptObjectsCreated << "script objects, mutated" << scriptMutationsPerformed; - qDebug(); + if (test == 0 || test == 5) { + qDebug() << "Running metavoxel data test..."; + qDebug(); - qDebug() << "Running serialization tests..."; - qDebug(); + // clear the stats + datagramsSent = bytesSent = datagramsReceived = bytesReceived = maxDatagramsPerPacket = maxBytesPerPacket = 0; - if (testSerialization(Bitstream::HASH_METADATA) || testSerialization(Bitstream::FULL_METADATA)) { - return true; + // create client and server endpoints + TestEndpoint client(TestEndpoint::METAVOXEL_CLIENT_MODE); + TestEndpoint server(TestEndpoint::METAVOXEL_SERVER_MODE); + + client.setOther(&server); + server.setOther(&client); + + // simulate + for (int i = 0; i < SIMULATION_ITERATIONS; i++) { + if (client.simulate(i) || server.simulate(i)) { + return true; + } + } + + qDebug() << "Sent" << datagramsSent << "datagrams with" << bytesSent << "bytes, received" << + datagramsReceived << "with" << bytesReceived << "bytes"; + qDebug() << "Max" << maxDatagramsPerPacket << "datagrams," << maxBytesPerPacket << "bytes per packet"; + qDebug() << "Performed" << metavoxelMutationsPerformed << "metavoxel mutations," << spannerMutationsPerformed << + "spanner mutations"; } qDebug() << "All tests passed!"; @@ -375,47 +572,137 @@ static SharedObjectPointer createRandomSharedObject() { } } -Endpoint::Endpoint(const QByteArray& datagramHeader) : - _sequencer(new DatagramSequencer(datagramHeader, this)), +class RandomVisitor : public MetavoxelVisitor { +public: + + int leafCount; + + RandomVisitor(); + virtual int visit(MetavoxelInfo& info); +}; + +RandomVisitor::RandomVisitor() : + MetavoxelVisitor(QVector<AttributePointer>(), + QVector<AttributePointer>() << AttributeRegistry::getInstance()->getColorAttribute()), + leafCount(0) { +} + +const float MAXIMUM_LEAF_SIZE = 0.5f; +const float MINIMUM_LEAF_SIZE = 0.25f; + +int RandomVisitor::visit(MetavoxelInfo& info) { + if (info.size > MAXIMUM_LEAF_SIZE || (info.size > MINIMUM_LEAF_SIZE && randomBoolean())) { + return DEFAULT_ORDER; + } + info.outputValues[0] = OwnedAttributeValue(_outputs.at(0), encodeInline<QRgb>(qRgb(randomColorValue(), + randomColorValue(), randomColorValue()))); + leafCount++; + return STOP_RECURSION; +} + +class TestSendRecord : public PacketRecord { +public: + + TestSendRecord(const MetavoxelLOD& lod = MetavoxelLOD(), const MetavoxelData& data = MetavoxelData(), + const SharedObjectPointer& localState = SharedObjectPointer(), int packetNumber = 0); + + const SharedObjectPointer& getLocalState() const { return _localState; } + int getPacketNumber() const { return _packetNumber; } + +private: + + SharedObjectPointer _localState; + int _packetNumber; + +}; + +TestSendRecord::TestSendRecord(const MetavoxelLOD& lod, const MetavoxelData& data, + const SharedObjectPointer& localState, int packetNumber) : + PacketRecord(lod, data), + _localState(localState), + _packetNumber(packetNumber) { +} + +class TestReceiveRecord : public PacketRecord { +public: + + TestReceiveRecord(const MetavoxelLOD& lod = MetavoxelLOD(), const MetavoxelData& data = MetavoxelData(), + const SharedObjectPointer& remoteState = SharedObjectPointer()); + + const SharedObjectPointer& getRemoteState() const { return _remoteState; } + +private: + + SharedObjectPointer _remoteState; +}; + +TestReceiveRecord::TestReceiveRecord(const MetavoxelLOD& lod, + const MetavoxelData& data, const SharedObjectPointer& remoteState) : + PacketRecord(lod, data), + _remoteState(remoteState) { +} + +TestEndpoint::TestEndpoint(Mode mode) : + Endpoint(SharedNodePointer(), new TestSendRecord(), new TestReceiveRecord()), + _mode(mode), _highPriorityMessagesToSend(0.0f), _reliableMessagesToSend(0.0f) { - connect(_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendDatagram(const QByteArray&))); - connect(_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readMessage(Bitstream&))); - connect(_sequencer, SIGNAL(receivedHighPriorityMessage(const QVariant&)), + connect(&_sequencer, SIGNAL(receivedHighPriorityMessage(const QVariant&)), SLOT(handleHighPriorityMessage(const QVariant&))); - connect(_sequencer, SIGNAL(sendAcknowledged(int)), SLOT(clearSendRecordsBefore(int))); - connect(_sequencer, SIGNAL(receiveAcknowledged(int)), SLOT(clearReceiveRecordsBefore(int))); - - // insert the baseline send record - SendRecord sendRecord = { 0 }; - _sendRecords.append(sendRecord); - - // insert the baseline receive record - ReceiveRecord receiveRecord = { 0 }; - _receiveRecords.append(receiveRecord); - + if (mode == METAVOXEL_CLIENT_MODE) { + _lod = MetavoxelLOD(glm::vec3(), 0.01f); + return; + } + if (mode == METAVOXEL_SERVER_MODE) { + _data.expand(); + _data.expand(); + + RandomVisitor visitor; + _data.guide(visitor); + qDebug() << "Created" << visitor.leafCount << "base leaves"; + + _data.insert(AttributeRegistry::getInstance()->getSpannersAttribute(), new Sphere()); + + _sphere = new Sphere(); + static_cast<Transformable*>(_sphere.data())->setScale(0.01f); + _data.insert(AttributeRegistry::getInstance()->getSpannersAttribute(), _sphere); + return; + } // create the object that represents out delta-encoded state _localState = new TestSharedObjectA(); - connect(_sequencer->getReliableInputChannel(), SIGNAL(receivedMessage(const QVariant&)), + connect(_sequencer.getReliableInputChannel(), SIGNAL(receivedMessage(const QVariant&)), SLOT(handleReliableMessage(const QVariant&))); - ReliableChannel* secondInput = _sequencer->getReliableInputChannel(1); + ReliableChannel* secondInput = _sequencer.getReliableInputChannel(1); secondInput->setMessagesEnabled(false); connect(&secondInput->getBuffer(), SIGNAL(readyRead()), SLOT(readReliableChannel())); // enqueue a large amount of data in a low-priority channel - ReliableChannel* output = _sequencer->getReliableOutputChannel(1); + ReliableChannel* output = _sequencer.getReliableOutputChannel(1); output->setPriority(0.25f); output->setMessagesEnabled(false); - const int MIN_STREAM_BYTES = 100000; - const int MAX_STREAM_BYTES = 200000; - QByteArray bytes = createRandomBytes(MIN_STREAM_BYTES, MAX_STREAM_BYTES); + QByteArray bytes; + if (mode == CONGESTION_MODE) { + const int HUGE_STREAM_BYTES = 60 * 1024 * 1024; + bytes = createRandomBytes(HUGE_STREAM_BYTES, HUGE_STREAM_BYTES); + + // initialize the pipeline + for (int i = 0; i < 10; i++) { + _pipeline.append(ByteArrayVector()); + } + _remainingPipelineCapacity = 100 * 1024; + + } else { + const int MIN_STREAM_BYTES = 100000; + const int MAX_STREAM_BYTES = 200000; + bytes = createRandomBytes(MIN_STREAM_BYTES, MAX_STREAM_BYTES); + } _dataStreamed.append(bytes); output->getBuffer().write(bytes); - streamedBytesSent += bytes.size(); + streamedBytesSent += bytes.size(); } static QVariant createRandomMessage() { @@ -512,12 +799,42 @@ static bool messagesEqual(const QVariant& firstMessage, const QVariant& secondMe } } -bool Endpoint::simulate(int iterationNumber) { +class MutateVisitor : public MetavoxelVisitor { +public: + + MutateVisitor(); + virtual int visit(MetavoxelInfo& info); + +private: + + int _mutationsRemaining; +}; + +MutateVisitor::MutateVisitor() : + MetavoxelVisitor(QVector<AttributePointer>(), + QVector<AttributePointer>() << AttributeRegistry::getInstance()->getColorAttribute()), + _mutationsRemaining(randIntInRange(2, 4)) { +} + +int MutateVisitor::visit(MetavoxelInfo& info) { + if (_mutationsRemaining <= 0) { + return STOP_RECURSION; + } + if (info.size > MAXIMUM_LEAF_SIZE || (info.size > MINIMUM_LEAF_SIZE && randomBoolean())) { + return encodeRandomOrder(); + } + info.outputValues[0] = OwnedAttributeValue(_outputs.at(0), encodeInline<QRgb>(qRgb(randomColorValue(), + randomColorValue(), randomColorValue()))); + _mutationsRemaining--; + metavoxelMutationsPerformed++; + return STOP_RECURSION; +} + +bool TestEndpoint::simulate(int iterationNumber) { // update/send our delayed datagrams - for (QList<QPair<QByteArray, int> >::iterator it = _delayedDatagrams.begin(); it != _delayedDatagrams.end(); ) { + for (QList<ByteArrayIntPair>::iterator it = _delayedDatagrams.begin(); it != _delayedDatagrams.end(); ) { if (it->second-- == 1) { - _other->_sequencer->receivedDatagram(it->first); - datagramsReceived++; + _other->parseData(it->first); it = _delayedDatagrams.erase(it); } else { @@ -525,86 +842,272 @@ bool Endpoint::simulate(int iterationNumber) { } } - // enqueue some number of high priority messages - const float MIN_HIGH_PRIORITY_MESSAGES = 0.0f; - const float MAX_HIGH_PRIORITY_MESSAGES = 2.0f; - _highPriorityMessagesToSend += randFloatInRange(MIN_HIGH_PRIORITY_MESSAGES, MAX_HIGH_PRIORITY_MESSAGES); - while (_highPriorityMessagesToSend >= 1.0f) { - QVariant message = createRandomMessage(); - _highPriorityMessagesSent.append(message); - _sequencer->sendHighPriorityMessage(message); - highPriorityMessagesSent++; - _highPriorityMessagesToSend -= 1.0f; + int oldDatagramsSent = datagramsSent; + int oldBytesSent = bytesSent; + if (_mode == CONGESTION_MODE) { + // cycle our pipeline + ByteArrayVector datagrams = _pipeline.takeLast(); + _pipeline.prepend(ByteArrayVector()); + foreach (const QByteArray& datagram, datagrams) { + _sequencer.receivedDatagram(datagram); + datagramsReceived++; + bytesReceived += datagram.size(); + _remainingPipelineCapacity += datagram.size(); + } + int packetCount = _sequencer.startPacketGroup(); + groupsSent++; + maxPacketsPerGroup = qMax(maxPacketsPerGroup, packetCount); + for (int i = 0; i < packetCount; i++) { + oldDatagramsSent = datagramsSent; + oldBytesSent = bytesSent; + + Bitstream& out = _sequencer.startPacket(); + out << QVariant(); + _sequencer.endPacket(); + + maxDatagramsPerPacket = qMax(maxDatagramsPerPacket, datagramsSent - oldDatagramsSent); + maxBytesPerPacket = qMax(maxBytesPerPacket, bytesSent - oldBytesSent); + + // record the send + _sendRecords.append(maybeCreateSendRecord()); + } + return false; + + } else if (_mode == METAVOXEL_CLIENT_MODE) { + Bitstream& out = _sequencer.startPacket(); + + ClientStateMessage state = { _lod }; + out << QVariant::fromValue(state); + _sequencer.endPacket(); + + // record the send + _sendRecords.append(maybeCreateSendRecord()); + + } else if (_mode == METAVOXEL_SERVER_MODE) { + // make a random change + MutateVisitor visitor; + _data.guide(visitor); + + // perhaps mutate the spanner + if (randomBoolean()) { + SharedObjectPointer oldSphere = _sphere; + _sphere = _sphere->clone(true); + Sphere* newSphere = static_cast<Sphere*>(_sphere.data()); + if (randomBoolean()) { + newSphere->setColor(QColor(randomColorValue(), randomColorValue(), randomColorValue())); + } else { + newSphere->setTranslation(newSphere->getTranslation() + glm::vec3(randFloatInRange(-0.01f, 0.01f), + randFloatInRange(-0.01f, 0.01f), randFloatInRange(-0.01f, 0.01f))); + } + _data.replace(AttributeRegistry::getInstance()->getSpannersAttribute(), oldSphere, _sphere); + spannerMutationsPerformed++; + } + + // wait until we have a valid lod before sending + if (!_lod.isValid()) { + return false; + } + Bitstream& out = _sequencer.startPacket(); + out << QVariant::fromValue(MetavoxelDeltaMessage()); + PacketRecord* sendRecord = getLastAcknowledgedSendRecord(); + _data.writeDelta(sendRecord->getData(), sendRecord->getLOD(), out, _lod); + _sequencer.endPacket(); + + // record the send + _sendRecords.append(maybeCreateSendRecord()); + + } else { + // enqueue some number of high priority messages + const float MIN_HIGH_PRIORITY_MESSAGES = 0.0f; + const float MAX_HIGH_PRIORITY_MESSAGES = 2.0f; + _highPriorityMessagesToSend += randFloatInRange(MIN_HIGH_PRIORITY_MESSAGES, MAX_HIGH_PRIORITY_MESSAGES); + while (_highPriorityMessagesToSend >= 1.0f) { + QVariant message = createRandomMessage(); + _highPriorityMessagesSent.append(message); + _sequencer.sendHighPriorityMessage(message); + highPriorityMessagesSent++; + _highPriorityMessagesToSend -= 1.0f; + } + + // and some number of reliable messages + const float MIN_RELIABLE_MESSAGES = 0.0f; + const float MAX_RELIABLE_MESSAGES = 4.0f; + _reliableMessagesToSend += randFloatInRange(MIN_RELIABLE_MESSAGES, MAX_RELIABLE_MESSAGES); + while (_reliableMessagesToSend >= 1.0f) { + QVariant message = createRandomMessage(); + _reliableMessagesSent.append(message); + _sequencer.getReliableOutputChannel()->sendMessage(message); + reliableMessagesSent++; + _reliableMessagesToSend -= 1.0f; + } + + // tweak the local state + _localState = mutate(_localState); + + // send a packet + try { + Bitstream& out = _sequencer.startPacket(); + SequencedTestMessage message = { iterationNumber, createRandomMessage(), _localState }; + _unreliableMessagesSent.append(message); + unreliableMessagesSent++; + out << message; + _sequencer.endPacket(); + + } catch (const QString& message) { + qDebug() << message; + return true; + } + + // record the send + _sendRecords.append(maybeCreateSendRecord()); } - - // and some number of reliable messages - const float MIN_RELIABLE_MESSAGES = 0.0f; - const float MAX_RELIABLE_MESSAGES = 4.0f; - _reliableMessagesToSend += randFloatInRange(MIN_RELIABLE_MESSAGES, MAX_RELIABLE_MESSAGES); - while (_reliableMessagesToSend >= 1.0f) { - QVariant message = createRandomMessage(); - _reliableMessagesSent.append(message); - _sequencer->getReliableOutputChannel()->sendMessage(message); - reliableMessagesSent++; - _reliableMessagesToSend -= 1.0f; - } - - // tweak the local state - _localState = mutate(_localState); - - // send a packet - try { - Bitstream& out = _sequencer->startPacket(); - SequencedTestMessage message = { iterationNumber, createRandomMessage(), _localState }; - _unreliableMessagesSent.append(message); - unreliableMessagesSent++; - out << message; - _sequencer->endPacket(); - - } catch (const QString& message) { - qDebug() << message; - return true; - } - - // record the send - SendRecord record = { _sequencer->getOutgoingPacketNumber(), _localState }; - _sendRecords.append(record); - + maxDatagramsPerPacket = qMax(maxDatagramsPerPacket, datagramsSent - oldDatagramsSent); + maxBytesPerPacket = qMax(maxBytesPerPacket, bytesSent - oldBytesSent); return false; } -void Endpoint::sendDatagram(const QByteArray& datagram) { +int TestEndpoint::parseData(const QByteArray& packet) { + if (_mode == CONGESTION_MODE) { + if (packet.size() <= _remainingPipelineCapacity) { + // have to copy the datagram; the one we're passed is a reference to a shared buffer + _pipeline[0].append(QByteArray(packet.constData(), packet.size())); + _remainingPipelineCapacity -= packet.size(); + } + } else { + _sequencer.receivedDatagram(packet); + datagramsReceived++; + bytesReceived += packet.size(); + } + return packet.size(); +} + +void TestEndpoint::sendDatagram(const QByteArray& datagram) { datagramsSent++; bytesSent += datagram.size(); // some datagrams are dropped const float DROP_PROBABILITY = 0.1f; - if (randFloat() < DROP_PROBABILITY) { + float probabilityMultiplier = (_mode == CONGESTION_MODE) ? 0.01f : 1.0f; + if (randFloat() < DROP_PROBABILITY * probabilityMultiplier) { return; } // some are received out of order const float REORDER_PROBABILITY = 0.1f; - if (randFloat() < REORDER_PROBABILITY) { + if (randFloat() < REORDER_PROBABILITY * probabilityMultiplier) { const int MIN_DELAY = 1; const int MAX_DELAY = 5; // have to copy the datagram; the one we're passed is a reference to a shared buffer - _delayedDatagrams.append(QPair<QByteArray, int>(QByteArray(datagram.constData(), datagram.size()), + _delayedDatagrams.append(ByteArrayIntPair(QByteArray(datagram.constData(), datagram.size()), randIntInRange(MIN_DELAY, MAX_DELAY))); // and some are duplicated const float DUPLICATE_PROBABILITY = 0.01f; - if (randFloat() > DUPLICATE_PROBABILITY) { + if (randFloat() > DUPLICATE_PROBABILITY * probabilityMultiplier) { return; } } - _other->_sequencer->receivedDatagram(datagram); - datagramsReceived++; - bytesReceived += datagram.size(); + _other->parseData(datagram); } -void Endpoint::handleHighPriorityMessage(const QVariant& message) { +void TestEndpoint::readMessage(Bitstream& in) { + if (_mode == CONGESTION_MODE) { + QVariant message; + in >> message; + + // record the receipt + _receiveRecords.append(maybeCreateReceiveRecord()); + return; + } + if (_mode == METAVOXEL_CLIENT_MODE) { + QVariant message; + in >> message; + handleMessage(message, in); + + // deep-compare data to sent version + int packetNumber = _sequencer.getIncomingPacketNumber(); + foreach (PacketRecord* record, _other->_sendRecords) { + TestSendRecord* sendRecord = static_cast<TestSendRecord*>(record); + if (sendRecord->getPacketNumber() == packetNumber) { + if (!sendRecord->getData().deepEquals(_data, getLastAcknowledgedSendRecord()->getLOD())) { + qDebug() << "Sent/received metavoxel data mismatch."; + exit(true); + } + break; + } + } + + // record the receipt + _receiveRecords.append(maybeCreateReceiveRecord()); + return; + } + if (_mode == METAVOXEL_SERVER_MODE) { + QVariant message; + in >> message; + handleMessage(message, in); + + // record the receipt + _receiveRecords.append(maybeCreateReceiveRecord()); + return; + } + + SequencedTestMessage message; + in >> message; + + _remoteState = message.state; + + // record the receipt + _receiveRecords.append(maybeCreateReceiveRecord()); + + for (QList<SequencedTestMessage>::iterator it = _other->_unreliableMessagesSent.begin(); + it != _other->_unreliableMessagesSent.end(); it++) { + if (it->sequenceNumber == message.sequenceNumber) { + if (!messagesEqual(it->submessage, message.submessage)) { + qDebug() << "Sent/received unreliable message mismatch."; + exit(true); + } + if (!it->state->equals(message.state)) { + qDebug() << "Delta-encoded object mismatch."; + exit(true); + } + _other->_unreliableMessagesSent.erase(_other->_unreliableMessagesSent.begin(), it + 1); + unreliableMessagesReceived++; + return; + } + } + qDebug() << "Received unsent/already sent unreliable message."; + exit(true); +} + +void TestEndpoint::handleMessage(const QVariant& message, Bitstream& in) { + int userType = message.userType(); + if (userType == ClientStateMessage::Type) { + ClientStateMessage state = message.value<ClientStateMessage>(); + _lod = state.lod; + + } else if (userType == MetavoxelDeltaMessage::Type) { + PacketRecord* receiveRecord = getLastAcknowledgedReceiveRecord(); + _data.readDelta(receiveRecord->getData(), receiveRecord->getLOD(), in, getLastAcknowledgedSendRecord()->getLOD()); + + } else if (userType == QMetaType::QVariantList) { + foreach (const QVariant& element, message.toList()) { + handleMessage(element, in); + } + } +} + +PacketRecord* TestEndpoint::maybeCreateSendRecord() const { + return new TestSendRecord(_lod, (_mode == METAVOXEL_CLIENT_MODE) ? MetavoxelData() : _data, + _localState, _sequencer.getOutgoingPacketNumber()); +} + +PacketRecord* TestEndpoint::maybeCreateReceiveRecord() const { + return new TestReceiveRecord(getLastAcknowledgedSendRecord()->getLOD(), + (_mode == METAVOXEL_SERVER_MODE) ? MetavoxelData() : _data, _remoteState); +} + +void TestEndpoint::handleHighPriorityMessage(const QVariant& message) { if (message.userType() == ClearSharedObjectMessage::Type) { return; } @@ -618,34 +1121,7 @@ void Endpoint::handleHighPriorityMessage(const QVariant& message) { highPriorityMessagesReceived++; } -void Endpoint::readMessage(Bitstream& in) { - SequencedTestMessage message; - in >> message; - - _remoteState = message.state; - - // record the receipt - ReceiveRecord record = { _sequencer->getIncomingPacketNumber(), message.state }; - _receiveRecords.append(record); - - for (QList<SequencedTestMessage>::iterator it = _other->_unreliableMessagesSent.begin(); - it != _other->_unreliableMessagesSent.end(); it++) { - if (it->sequenceNumber == message.sequenceNumber) { - if (!messagesEqual(it->submessage, message.submessage)) { - throw QString("Sent/received unreliable message mismatch."); - } - if (!it->state->equals(message.state)) { - throw QString("Delta-encoded object mismatch."); - } - _other->_unreliableMessagesSent.erase(_other->_unreliableMessagesSent.begin(), it + 1); - unreliableMessagesReceived++; - return; - } - } - throw QString("Received unsent/already sent unreliable message."); -} - -void Endpoint::handleReliableMessage(const QVariant& message) { +void TestEndpoint::handleReliableMessage(const QVariant& message) { if (message.userType() == ClearSharedObjectMessage::Type || message.userType() == ClearMainChannelSharedObjectMessage::Type) { return; @@ -660,8 +1136,8 @@ void Endpoint::handleReliableMessage(const QVariant& message) { reliableMessagesReceived++; } -void Endpoint::readReliableChannel() { - CircularBuffer& buffer = _sequencer->getReliableInputChannel(1)->getBuffer(); +void TestEndpoint::readReliableChannel() { + CircularBuffer& buffer = _sequencer.getReliableInputChannel(1)->getBuffer(); QByteArray bytes = buffer.read(buffer.bytesAvailable()); if (_other->_dataStreamed.size() < bytes.size()) { throw QString("Received unsent/already sent streamed data."); @@ -674,14 +1150,6 @@ void Endpoint::readReliableChannel() { streamedBytesReceived += bytes.size(); } -void Endpoint::clearSendRecordsBefore(int index) { - _sendRecords.erase(_sendRecords.begin(), _sendRecords.begin() + index + 1); -} - -void Endpoint::clearReceiveRecordsBefore(int index) { - _receiveRecords.erase(_receiveRecords.begin(), _receiveRecords.begin() + index + 1); -} - TestSharedObjectA::TestSharedObjectA(float foo, TestEnum baz, TestFlags bong) : _foo(foo), _baz(baz), diff --git a/tests/metavoxels/src/MetavoxelTests.h b/tests/metavoxels/src/MetavoxelTests.h index ac9eda2659..476a8c6295 100644 --- a/tests/metavoxels/src/MetavoxelTests.h +++ b/tests/metavoxels/src/MetavoxelTests.h @@ -15,7 +15,7 @@ #include <QCoreApplication> #include <QVariantList> -#include <DatagramSequencer.h> +#include <Endpoint.h> #include <ScriptCache.h> class SequencedTestMessage; @@ -34,53 +34,60 @@ public: }; /// Represents a simulated endpoint. -class Endpoint : public QObject { +class TestEndpoint : public Endpoint { Q_OBJECT public: - Endpoint(const QByteArray& datagramHeader); + enum Mode { BASIC_PEER_MODE, CONGESTION_MODE, METAVOXEL_SERVER_MODE, METAVOXEL_CLIENT_MODE }; + + TestEndpoint(Mode mode = BASIC_PEER_MODE); - void setOther(Endpoint* other) { _other = other; } + void setOther(TestEndpoint* other) { _other = other; } /// Perform a simulation step. /// \return true if failure was detected bool simulate(int iterationNumber); -private slots: + virtual int parseData(const QByteArray& packet); + +protected: - void sendDatagram(const QByteArray& datagram); + virtual void sendDatagram(const QByteArray& data); + virtual void readMessage(Bitstream& in); + + virtual void handleMessage(const QVariant& message, Bitstream& in); + + virtual PacketRecord* maybeCreateSendRecord() const; + virtual PacketRecord* maybeCreateReceiveRecord() const; + +private slots: + void handleHighPriorityMessage(const QVariant& message); - void readMessage(Bitstream& in); void handleReliableMessage(const QVariant& message); void readReliableChannel(); - void clearSendRecordsBefore(int index); - void clearReceiveRecordsBefore(int index); - private: - class SendRecord { - public: - int packetNumber; - SharedObjectPointer localState; - }; - - class ReceiveRecord { - public: - int packetNumber; - SharedObjectPointer remoteState; - }; - - DatagramSequencer* _sequencer; - QList<SendRecord> _sendRecords; - QList<ReceiveRecord> _receiveRecords; + Mode _mode; SharedObjectPointer _localState; SharedObjectPointer _remoteState; - Endpoint* _other; - QList<QPair<QByteArray, int> > _delayedDatagrams; + MetavoxelData _data; + MetavoxelLOD _lod; + + SharedObjectPointer _sphere; + + TestEndpoint* _other; + + typedef QPair<QByteArray, int> ByteArrayIntPair; + QList<ByteArrayIntPair> _delayedDatagrams; + + typedef QVector<QByteArray> ByteArrayVector; + QList<ByteArrayVector> _pipeline; + int _remainingPipelineCapacity; + float _highPriorityMessagesToSend; QVariantList _highPriorityMessagesSent; QList<SequencedTestMessage> _unreliableMessagesSent; diff --git a/tests/networking/CMakeLists.txt b/tests/networking/CMakeLists.txt new file mode 100644 index 0000000000..2e094d2ce7 --- /dev/null +++ b/tests/networking/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 2.8) + +if (WIN32) + cmake_policy (SET CMP0020 NEW) +endif (WIN32) + +set(TARGET_NAME networking-tests) + +set(ROOT_DIR ../..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +#find_package(Qt5Network REQUIRED) +#find_package(Qt5Script REQUIRED) +#find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE) + +include(${MACRO_DIR}/AutoMTC.cmake) +auto_mtc(${TARGET_NAME} ${ROOT_DIR}) + +#qt5_use_modules(${TARGET_NAME} Network Script Widgets) + +#include glm +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} ${ROOT_DIR}) + +# link in the shared libraries +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) + +IF (WIN32) + target_link_libraries(${TARGET_NAME} Winmm Ws2_32) +ENDIF(WIN32) + diff --git a/tests/networking/src/SequenceNumberStatsTests.cpp b/tests/networking/src/SequenceNumberStatsTests.cpp new file mode 100644 index 0000000000..89a14deb20 --- /dev/null +++ b/tests/networking/src/SequenceNumberStatsTests.cpp @@ -0,0 +1,267 @@ +// +// AudioRingBufferTests.cpp +// tests/networking/src +// +// Created by Yixin Wang on 6/24/2014 +// Copyright 2014 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 "SequenceNumberStatsTests.h" + +#include "SharedUtil.h" +#include <limits> + + +void SequenceNumberStatsTests::runAllTests() { + + rolloverTest(); + earlyLateTest(); + duplicateTest(); + pruneTest(); +} + +const int UINT16_RANGE = std::numeric_limits<quint16>::max() + 1; + + +void SequenceNumberStatsTests::rolloverTest() { + + SequenceNumberStats stats; + + // insert enough samples to cause 3 rollovers + quint16 seq = 79; // start on some random number + + for (int R = 0; R < 2; R++) { + for (int i = 0; i < 3 * UINT16_RANGE; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + + assert(stats.getNumDuplicate() == 0); + assert(stats.getNumEarly() == 0); + assert(stats.getNumLate() == 0); + assert(stats.getNumLost() == 0); + assert(stats.getNumReceived() == i + 1); + assert(stats.getNumRecovered() == 0); + } + stats.reset(); + } +} + +void SequenceNumberStatsTests::earlyLateTest() { + + SequenceNumberStats stats; + quint16 seq = 65530; + int numSent = 0; + + int numEarly = 0; + int numLate = 0; + int numLost = 0; + int numRecovered = 0; + + for (int R = 0; R < 2; R++) { + for (int T = 0; T < 10000; T++) { + + // insert 7 consecutive + for (int i = 0; i < 7; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + assert(stats.getNumDuplicate() == 0); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == numRecovered); + } + + // skip 10 + quint16 skipped = seq; + seq = seq + (quint16)10; + + // insert 36 consecutive + numEarly++; + numLost += 10; + for (int i = 0; i < 36; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + assert(stats.getNumDuplicate() == 0); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == numRecovered); + } + + // send ones we skipped + for (int i = 0; i < 10; i++) { + stats.sequenceNumberReceived(skipped); + skipped = skipped + (quint16)1; + numSent++; + numLate++; + numLost--; + numRecovered++; + + assert(stats.getNumDuplicate() == 0); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == numRecovered); + } + } + stats.reset(); + } +} + +void SequenceNumberStatsTests::duplicateTest() { + + SequenceNumberStats stats; + quint16 seq = 12345; + int numSent = 0; + + int numDuplicate = 0; + int numEarly = 0; + int numLate = 0; + int numLost = 0; + + for (int R = 0; R < 2; R++) { + for (int T = 0; T < 10000; T++) { + + quint16 duplicate = seq; + + // insert 7 consecutive + for (int i = 0; i < 7; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + assert(stats.getNumDuplicate() == numDuplicate); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == 0); + } + + // skip 10 + seq = seq + (quint16)10; + + + quint16 duplicate2 = seq; + + numEarly++; + numLost += 10; + // insert 36 consecutive + for (int i = 0; i < 36; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + assert(stats.getNumDuplicate() == numDuplicate); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == 0); + } + + // send 5 duplicates from before skip + for (int i = 0; i < 5; i++) { + stats.sequenceNumberReceived(duplicate); + duplicate = duplicate + (quint16)1; + numSent++; + numDuplicate++; + numLate++; + + assert(stats.getNumDuplicate() == numDuplicate); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == 0); + } + + // send 5 duplicates from after skip + for (int i = 0; i < 5; i++) { + stats.sequenceNumberReceived(duplicate2); + duplicate2 = duplicate2 + (quint16)1; + numSent++; + numDuplicate++; + numLate++; + + assert(stats.getNumDuplicate() == numDuplicate); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == 0); + } + } + stats.reset(); + } +} + +void SequenceNumberStatsTests::pruneTest() { + + SequenceNumberStats stats; + quint16 seq = 54321; + int numSent = 0; + + int numEarly = 0; + int numLost = 0; + + for (int R = 0; R < 2; R++) { + for (int T = 0; T < 1000; T++) { + // insert 1 seq + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + // skip 1000 seq + seq = seq + (quint16)1000; + quint16 highestSkipped = seq - (quint16)1; + + // insert 1 seq + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + numEarly++; + numLost += 1000; + + // skip 10 seq + seq = seq + (quint16)10; + quint16 highestSkipped2 = seq - (quint16)1; + + // insert 1 seq + // insert 1 seq + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + numEarly++; + numLost += 10; + + const QSet<quint16>& missingSet = stats.getMissingSet(); + assert(missingSet.size() <= 1000); + + for (int i = 0; i < 10; i++) { + assert(missingSet.contains(highestSkipped2)); + highestSkipped2 = highestSkipped2 - (quint16)1; + } + + for (int i = 0; i < 989; i++) { + assert(missingSet.contains(highestSkipped)); + highestSkipped = highestSkipped - (quint16)1; + } + for (int i = 0; i < 11; i++) { + assert(!missingSet.contains(highestSkipped)); + highestSkipped = highestSkipped - (quint16)1; + } + } + stats.reset(); + } +} diff --git a/tests/networking/src/SequenceNumberStatsTests.h b/tests/networking/src/SequenceNumberStatsTests.h new file mode 100644 index 0000000000..53a0b66480 --- /dev/null +++ b/tests/networking/src/SequenceNumberStatsTests.h @@ -0,0 +1,28 @@ +// +// AudioRingBufferTests.h +// tests/networking/src +// +// Created by Yixin Wang on 6/24/2014 +// Copyright 2014 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_SequenceNumberStatsTests_h +#define hifi_SequenceNumberStatsTests_h + +#include "SequenceNumberStatsTests.h" +#include "SequenceNumberStats.h" + +namespace SequenceNumberStatsTests { + + void runAllTests(); + + void rolloverTest(); + void earlyLateTest(); + void duplicateTest(); + void pruneTest(); +}; + +#endif // hifi_SequenceNumberStatsTests_h diff --git a/tests/networking/src/main.cpp b/tests/networking/src/main.cpp new file mode 100644 index 0000000000..91a59a0e41 --- /dev/null +++ b/tests/networking/src/main.cpp @@ -0,0 +1,19 @@ +// +// main.cpp +// tests/networking/src +// +// Copyright 2014 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 "SequenceNumberStatsTests.h" +#include <stdio.h> + +int main(int argc, char** argv) { + SequenceNumberStatsTests::runAllTests(); + printf("tests passed! press enter to exit"); + getchar(); + return 0; +} diff --git a/tests/octree/CMakeLists.txt b/tests/octree/CMakeLists.txt index cbdfd02054..9c5e031d74 100644 --- a/tests/octree/CMakeLists.txt +++ b/tests/octree/CMakeLists.txt @@ -12,9 +12,9 @@ set(MACRO_DIR ${ROOT_DIR}/cmake/macros) # setup for find modules set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") -#find_package(Qt5Network REQUIRED) -#find_package(Qt5Script REQUIRED) -#find_package(Qt5Widgets REQUIRED) +find_package(Qt5Network REQUIRED) +find_package(Qt5Script REQUIRED) +find_package(Qt5Widgets REQUIRED) include(${MACRO_DIR}/SetupHifiProject.cmake) setup_hifi_project(${TARGET_NAME} TRUE) @@ -22,7 +22,7 @@ setup_hifi_project(${TARGET_NAME} TRUE) include(${MACRO_DIR}/AutoMTC.cmake) auto_mtc(${TARGET_NAME} ${ROOT_DIR}) -#qt5_use_modules(${TARGET_NAME} Network Script Widgets) +qt5_use_modules(${TARGET_NAME} Network Script Widgets) #include glm include(${MACRO_DIR}/IncludeGLM.cmake) @@ -30,10 +30,20 @@ include_glm(${TARGET_NAME} ${ROOT_DIR}) # link in the shared libraries include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(models ${TARGET_NAME} ${ROOT_DIR}) link_hifi_library(octree ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(audio ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(animation ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(fbx ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) IF (WIN32) + # add a definition for ssize_t so that windows doesn't bail + add_definitions(-Dssize_t=long) + #target_link_libraries(${TARGET_NAME} Winmm Ws2_32) + target_link_libraries(${TARGET_NAME} wsock32.lib) ENDIF(WIN32) + diff --git a/tests/octree/src/ModelTests.cpp b/tests/octree/src/ModelTests.cpp new file mode 100644 index 0000000000..2cca4b43f6 --- /dev/null +++ b/tests/octree/src/ModelTests.cpp @@ -0,0 +1,264 @@ +// +// ModelTests.h +// tests/octree/src +// +// Created by Brad Hefta-Gaub on 06/04/2014. +// Copyright 2014 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 +// +// TODO: +// * need to add expected results and accumulation of test success/failure +// + +#include <QDebug> + +#include <Octree.h> +#include <ModelItem.h> +#include <ModelTree.h> +#include <ModelTreeElement.h> +#include <OctreeConstants.h> +#include <PropertyFlags.h> +#include <SharedUtil.h> + +#include "ModelTests.h" + +void ModelTests::modelTreeTests(bool verbose) { + int testsTaken = 0; + int testsPassed = 0; + int testsFailed = 0; + + if (verbose) { + qDebug() << "******************************************************************************************"; + } + + qDebug() << "ModelTests::modelTreeTests()"; + + // Tree, id, and model properties used in many tests below... + ModelTree tree; + uint32_t id = 1; + ModelItemID modelID(id); + modelID.isKnownID = false; // this is a temporary workaround to allow local tree models to be added with known IDs + ModelItemProperties properties; + float oneMeter = 1.0f; + float halfMeter = oneMeter / 2.0f; + float halfOfDomain = TREE_SCALE * 0.5f; + glm::vec3 positionNearOriginInMeters(oneMeter, oneMeter, oneMeter); // when using properties, these are in meter not tree units + glm::vec3 positionAtCenterInMeters(halfOfDomain, halfOfDomain, halfOfDomain); + glm::vec3 positionNearOriginInTreeUnits = positionNearOriginInMeters / (float)TREE_SCALE; + glm::vec3 positionAtCenterInTreeUnits = positionAtCenterInMeters / (float)TREE_SCALE; + + { + testsTaken++; + QString testName = "add model to tree and search"; + if (verbose) { + qDebug() << "Test" << testsTaken <<":" << qPrintable(testName); + } + + properties.setPosition(positionAtCenterInMeters); + properties.setRadius(halfMeter); + properties.setModelURL("https://s3-us-west-1.amazonaws.com/highfidelity-public/ozan/theater.fbx"); + + tree.addModel(modelID, properties); + + float targetRadius = oneMeter * 2.0 / (float)TREE_SCALE; // in tree units + const ModelItem* foundModelByRadius = tree.findClosestModel(positionAtCenterInTreeUnits, targetRadius); + const ModelItem* foundModelByID = tree.findModelByID(id); + + if (verbose) { + qDebug() << "foundModelByRadius=" << foundModelByRadius; + qDebug() << "foundModelByID=" << foundModelByID; + } + + bool passed = foundModelByRadius && foundModelByID && (foundModelByRadius == foundModelByID); + if (passed) { + testsPassed++; + } else { + testsFailed++; + qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName); + } + } + + modelID.isKnownID = true; // this is a temporary workaround to allow local tree models to be added with known IDs + + { + testsTaken++; + QString testName = "change position of model in tree"; + if (verbose) { + qDebug() << "Test" << testsTaken <<":" << qPrintable(testName); + } + + glm::vec3 newPosition = positionNearOriginInMeters; + + properties.setPosition(newPosition); + + tree.updateModel(modelID, properties); + + float targetRadius = oneMeter * 2.0 / (float)TREE_SCALE; // in tree units + const ModelItem* foundModelByRadius = tree.findClosestModel(positionNearOriginInTreeUnits, targetRadius); + const ModelItem* foundModelByID = tree.findModelByID(id); + + if (verbose) { + qDebug() << "foundModelByRadius=" << foundModelByRadius; + qDebug() << "foundModelByID=" << foundModelByID; + } + + // NOTE: This test is currently expected to fail in the production code. There's a bug in ModelTree::updateModel() + // that does not update the actual location of the model into the correct element when modified locally. So this + // test will fail. There's a new optimized and correctly working version of updateModel() that fixes this problem. + bool passed = foundModelByRadius && foundModelByID && (foundModelByRadius == foundModelByID); + if (passed) { + testsPassed++; + qDebug() << "NOTE: Expected to FAIL - Test" << testsTaken <<":" << qPrintable(testName); + } else { + testsFailed++; + qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName); + qDebug() << "NOTE: Expected to FAIL - Test" << testsTaken <<":" << qPrintable(testName); + } + } + + { + testsTaken++; + QString testName = "change position of model in tree back to center"; + if (verbose) { + qDebug() << "Test" << testsTaken <<":" << qPrintable(testName); + } + + glm::vec3 newPosition = positionAtCenterInMeters; + + properties.setPosition(newPosition); + + tree.updateModel(modelID, properties); + + float targetRadius = oneMeter * 2.0 / (float)TREE_SCALE; // in tree units + const ModelItem* foundModelByRadius = tree.findClosestModel(positionAtCenterInTreeUnits, targetRadius); + const ModelItem* foundModelByID = tree.findModelByID(id); + + if (verbose) { + qDebug() << "foundModelByRadius=" << foundModelByRadius; + qDebug() << "foundModelByID=" << foundModelByID; + } + + bool passed = foundModelByRadius && foundModelByID && (foundModelByRadius == foundModelByID); + if (passed) { + testsPassed++; + } else { + testsFailed++; + qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName); + } + } + + { + testsTaken++; + QString testName = "Performance - findClosestModel() 1,000,000 times"; + if (verbose) { + qDebug() << "Test" << testsTaken <<":" << qPrintable(testName); + } + + float targetRadius = oneMeter * 2.0 / (float)TREE_SCALE; // in tree units + const int TEST_ITERATIONS = 1000000; + quint64 start = usecTimestampNow(); + const ModelItem* foundModelByRadius = NULL; + for (int i = 0; i < TEST_ITERATIONS; i++) { + foundModelByRadius = tree.findClosestModel(positionAtCenterInTreeUnits, targetRadius); + } + quint64 end = usecTimestampNow(); + + if (verbose) { + qDebug() << "foundModelByRadius=" << foundModelByRadius; + } + + bool passed = foundModelByRadius; + if (passed) { + testsPassed++; + } else { + testsFailed++; + qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName); + } + float USECS_PER_MSECS = 1000.0f; + float elapsedInMSecs = (float)(end - start) / USECS_PER_MSECS; + qDebug() << "TIME - Test" << testsTaken <<":" << qPrintable(testName) << "elapsed=" << elapsedInMSecs << "msecs"; + } + + { + testsTaken++; + QString testName = "Performance - findModelByID() 1,000,000 times"; + if (verbose) { + qDebug() << "Test" << testsTaken <<":" << qPrintable(testName); + } + + const int TEST_ITERATIONS = 1000000; + quint64 start = usecTimestampNow(); + const ModelItem* foundModelByID = NULL; + for (int i = 0; i < TEST_ITERATIONS; i++) { + foundModelByID = tree.findModelByID(id); + } + quint64 end = usecTimestampNow(); + + if (verbose) { + qDebug() << "foundModelByID=" << foundModelByID; + } + + bool passed = foundModelByID; + if (passed) { + testsPassed++; + } else { + testsFailed++; + qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName); + } + float USECS_PER_MSECS = 1000.0f; + float elapsedInMSecs = (float)(end - start) / USECS_PER_MSECS; + qDebug() << "TIME - Test" << testsTaken <<":" << qPrintable(testName) << "elapsed=" << elapsedInMSecs << "msecs"; + } + + { + testsTaken++; + QString testName = "Performance - add model to tree 10,000 times"; + if (verbose) { + qDebug() << "Test" << testsTaken <<":" << qPrintable(testName); + } + + const int TEST_ITERATIONS = 10000; + quint64 start = usecTimestampNow(); + for (int i = 0; i < TEST_ITERATIONS; i++) { + uint32_t id = i + 2; // make sure it doesn't collide with previous model ids + ModelItemID modelID(id); + modelID.isKnownID = false; // this is a temporary workaround to allow local tree models to be added with known IDs + + float randomX = randFloatInRange(0.0f ,(float)TREE_SCALE); + float randomY = randFloatInRange(0.0f ,(float)TREE_SCALE); + float randomZ = randFloatInRange(0.0f ,(float)TREE_SCALE); + glm::vec3 randomPositionInMeters(randomX,randomY,randomZ); + + properties.setPosition(randomPositionInMeters); + properties.setRadius(halfMeter); + properties.setModelURL("https://s3-us-west-1.amazonaws.com/highfidelity-public/ozan/theater.fbx"); + + tree.addModel(modelID, properties); + } + quint64 end = usecTimestampNow(); + + bool passed = true; + if (passed) { + testsPassed++; + } else { + testsFailed++; + qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName); + } + float USECS_PER_MSECS = 1000.0f; + float elapsedInMSecs = (float)(end - start) / USECS_PER_MSECS; + qDebug() << "TIME - Test" << testsTaken <<":" << qPrintable(testName) << "elapsed=" << elapsedInMSecs << "msecs"; + } + + qDebug() << " tests passed:" << testsPassed << "out of" << testsTaken; + if (verbose) { + qDebug() << "******************************************************************************************"; + } +} + + +void ModelTests::runAllTests(bool verbose) { + modelTreeTests(verbose); +} + diff --git a/tests/octree/src/ModelTests.h b/tests/octree/src/ModelTests.h new file mode 100644 index 0000000000..dd764edf9d --- /dev/null +++ b/tests/octree/src/ModelTests.h @@ -0,0 +1,20 @@ +// +// ModelTests.h +// tests/octree/src +// +// Created by Brad Hefta-Gaub on 06/04/2014. +// Copyright 2014 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_ModelTests_h +#define hifi_ModelTests_h + +namespace ModelTests { + void modelTreeTests(bool verbose = false); + void runAllTests(bool verbose = false); +} + +#endif // hifi_ModelTests_h diff --git a/tests/octree/src/main.cpp b/tests/octree/src/main.cpp index de7b3926ae..590df268c3 100644 --- a/tests/octree/src/main.cpp +++ b/tests/octree/src/main.cpp @@ -8,11 +8,13 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "ModelTests.h" #include "OctreeTests.h" #include "AABoxCubeTests.h" int main(int argc, char** argv) { OctreeTests::runAllTests(); AABoxCubeTests::runAllTests(); + ModelTests::runAllTests(true); return 0; } diff --git a/tests/physics/src/ShapeColliderTests.cpp b/tests/physics/src/ShapeColliderTests.cpp index 608e012998..bde29ea588 100644 --- a/tests/physics/src/ShapeColliderTests.cpp +++ b/tests/physics/src/ShapeColliderTests.cpp @@ -123,8 +123,8 @@ void ShapeColliderTests::sphereTouchesSphere() { } // contactPoint is on surface of sphereA - glm::vec3 AtoB = sphereB.getPosition() - sphereA.getPosition(); - glm::vec3 expectedContactPoint = sphereA.getPosition() + radiusA * glm::normalize(AtoB); + glm::vec3 AtoB = sphereB.getTranslation() - sphereA.getTranslation(); + glm::vec3 expectedContactPoint = sphereA.getTranslation() + radiusA * glm::normalize(AtoB); inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); if (fabs(inaccuracy) > EPSILON) { std::cout << __FILE__ << ":" << __LINE__ @@ -153,8 +153,8 @@ void ShapeColliderTests::sphereTouchesSphere() { } // contactPoint is on surface of sphereA - glm::vec3 BtoA = sphereA.getPosition() - sphereB.getPosition(); - glm::vec3 expectedContactPoint = sphereB.getPosition() + radiusB * glm::normalize(BtoA); + glm::vec3 BtoA = sphereA.getTranslation() - sphereB.getTranslation(); + glm::vec3 expectedContactPoint = sphereB.getTranslation() + radiusB * glm::normalize(BtoA); inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); if (fabs(inaccuracy) > EPSILON) { std::cout << __FILE__ << ":" << __LINE__ @@ -182,7 +182,7 @@ void ShapeColliderTests::sphereMissesCapsule() { glm::quat rotation = glm::angleAxis(angle, axis); glm::vec3 translation(15.1f, -27.1f, -38.6f); capsuleB.setRotation(rotation); - capsuleB.setPosition(translation); + capsuleB.setTranslation(translation); CollisionList collisions(16); @@ -193,7 +193,7 @@ void ShapeColliderTests::sphereMissesCapsule() { for (int i = 0; i < numberOfSteps; ++i) { // translate sphereA into world-frame glm::vec3 localPosition = localStartPosition + ((float)i * delta) * yAxis; - sphereA.setPosition(rotation * localPosition + translation); + sphereA.setTranslation(rotation * localPosition + translation); // sphereA agains capsuleB if (ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions)) @@ -236,7 +236,7 @@ void ShapeColliderTests::sphereTouchesCapsule() { int numCollisions = 0; { // sphereA collides with capsuleB's cylindrical wall - sphereA.setPosition(radialOffset * xAxis); + sphereA.setTranslation(radialOffset * xAxis); if (!ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions)) { @@ -258,7 +258,7 @@ void ShapeColliderTests::sphereTouchesCapsule() { } // contactPoint is on surface of sphereA - glm::vec3 expectedContactPoint = sphereA.getPosition() - radiusA * xAxis; + glm::vec3 expectedContactPoint = sphereA.getTranslation() - radiusA * xAxis; inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); if (fabs(inaccuracy) > EPSILON) { std::cout << __FILE__ << ":" << __LINE__ @@ -287,8 +287,8 @@ void ShapeColliderTests::sphereTouchesCapsule() { } // contactPoint is on surface of capsuleB - glm::vec3 BtoA = sphereA.getPosition() - capsuleB.getPosition(); - glm::vec3 closestApproach = capsuleB.getPosition() + glm::dot(BtoA, yAxis) * yAxis; + glm::vec3 BtoA = sphereA.getTranslation() - capsuleB.getTranslation(); + glm::vec3 closestApproach = capsuleB.getTranslation() + glm::dot(BtoA, yAxis) * yAxis; expectedContactPoint = closestApproach + radiusB * glm::normalize(BtoA - closestApproach); inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); if (fabs(inaccuracy) > EPSILON) { @@ -299,7 +299,7 @@ void ShapeColliderTests::sphereTouchesCapsule() { } { // sphereA hits end cap at axis glm::vec3 axialOffset = (halfHeightB + alpha * radiusA + beta * radiusB) * yAxis; - sphereA.setPosition(axialOffset * yAxis); + sphereA.setTranslation(axialOffset * yAxis); if (!ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions)) { @@ -321,7 +321,7 @@ void ShapeColliderTests::sphereTouchesCapsule() { } // contactPoint is on surface of sphereA - glm::vec3 expectedContactPoint = sphereA.getPosition() - radiusA * yAxis; + glm::vec3 expectedContactPoint = sphereA.getTranslation() - radiusA * yAxis; inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); if (fabs(inaccuracy) > EPSILON) { std::cout << __FILE__ << ":" << __LINE__ @@ -362,7 +362,7 @@ void ShapeColliderTests::sphereTouchesCapsule() { } { // sphereA hits start cap at axis glm::vec3 axialOffset = - (halfHeightB + alpha * radiusA + beta * radiusB) * yAxis; - sphereA.setPosition(axialOffset * yAxis); + sphereA.setTranslation(axialOffset * yAxis); if (!ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions)) { @@ -384,7 +384,7 @@ void ShapeColliderTests::sphereTouchesCapsule() { } // contactPoint is on surface of sphereA - glm::vec3 expectedContactPoint = sphereA.getPosition() + radiusA * yAxis; + glm::vec3 expectedContactPoint = sphereA.getTranslation() + radiusA * yAxis; inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); if (fabs(inaccuracy) > EPSILON) { std::cout << __FILE__ << ":" << __LINE__ @@ -441,12 +441,12 @@ void ShapeColliderTests::capsuleMissesCapsule() { float totalHalfLength = totalRadius + halfHeightA + halfHeightB; CapsuleShape capsuleA(radiusA, halfHeightA); - CapsuleShape capsuleB(radiusA, halfHeightA); + CapsuleShape capsuleB(radiusB, halfHeightB); CollisionList collisions(16); // side by side - capsuleB.setPosition((1.01f * totalRadius) * xAxis); + capsuleB.setTranslation((1.01f * totalRadius) * xAxis); if (ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) { std::cout << __FILE__ << ":" << __LINE__ @@ -461,7 +461,7 @@ void ShapeColliderTests::capsuleMissesCapsule() { } // end to end - capsuleB.setPosition((1.01f * totalHalfLength) * xAxis); + capsuleB.setTranslation((1.01f * totalHalfLength) * xAxis); if (ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) { std::cout << __FILE__ << ":" << __LINE__ @@ -478,7 +478,7 @@ void ShapeColliderTests::capsuleMissesCapsule() { // rotate B and move it to the side glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); capsuleB.setRotation(rotation); - capsuleB.setPosition((1.01f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); + capsuleB.setTranslation((1.01f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); if (ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) { std::cout << __FILE__ << ":" << __LINE__ @@ -516,7 +516,7 @@ void ShapeColliderTests::capsuleTouchesCapsule() { int numCollisions = 0; { // side by side - capsuleB.setPosition((0.99f * totalRadius) * xAxis); + capsuleB.setTranslation((0.99f * totalRadius) * xAxis); if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) { std::cout << __FILE__ << ":" << __LINE__ @@ -536,7 +536,7 @@ void ShapeColliderTests::capsuleTouchesCapsule() { } { // end to end - capsuleB.setPosition((0.99f * totalHalfLength) * yAxis); + capsuleB.setTranslation((0.99f * totalHalfLength) * yAxis); if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) { @@ -559,7 +559,7 @@ void ShapeColliderTests::capsuleTouchesCapsule() { { // rotate B and move it to the side glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); capsuleB.setRotation(rotation); - capsuleB.setPosition((0.99f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); + capsuleB.setTranslation((0.99f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) { @@ -584,7 +584,7 @@ void ShapeColliderTests::capsuleTouchesCapsule() { glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); capsuleB.setRotation(rotation); glm::vec3 positionB = ((totalRadius + capsuleB.getHalfHeight()) - overlap) * xAxis; - capsuleB.setPosition(positionB); + capsuleB.setTranslation(positionB); // capsuleA vs capsuleB if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) @@ -605,7 +605,7 @@ void ShapeColliderTests::capsuleTouchesCapsule() { << " actual = " << collision->_penetration; } - glm::vec3 expectedContactPoint = capsuleA.getPosition() + radiusA * xAxis; + glm::vec3 expectedContactPoint = capsuleA.getTranslation() + radiusA * xAxis; inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); if (fabs(inaccuracy) > EPSILON) { std::cout << __FILE__ << ":" << __LINE__ @@ -633,7 +633,7 @@ void ShapeColliderTests::capsuleTouchesCapsule() { << std::endl; } - expectedContactPoint = capsuleB.getPosition() - (radiusB + halfHeightB) * xAxis; + expectedContactPoint = capsuleB.getTranslation() - (radiusB + halfHeightB) * xAxis; inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); if (fabs(inaccuracy) > EPSILON) { std::cout << __FILE__ << ":" << __LINE__ @@ -649,7 +649,7 @@ void ShapeColliderTests::capsuleTouchesCapsule() { glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); capsuleB.setRotation(rotation); glm::vec3 positionB = (totalRadius - overlap) * zAxis + shift * yAxis; - capsuleB.setPosition(positionB); + capsuleB.setTranslation(positionB); // capsuleA vs capsuleB if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) @@ -671,7 +671,7 @@ void ShapeColliderTests::capsuleTouchesCapsule() { << std::endl; } - glm::vec3 expectedContactPoint = capsuleA.getPosition() + radiusA * zAxis + shift * yAxis; + glm::vec3 expectedContactPoint = capsuleA.getTranslation() + radiusA * zAxis + shift * yAxis; inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); if (fabs(inaccuracy) > EPSILON) { std::cout << __FILE__ << ":" << __LINE__ @@ -708,7 +708,7 @@ void ShapeColliderTests::sphereTouchesAACubeFaces() { float overlap = 0.25f; float sphereOffset = 0.5f * cubeSide + sphereRadius - overlap; sphereCenter = cubeCenter + sphereOffset * axis; - sphere.setPosition(sphereCenter); + sphere.setTranslation(sphereCenter); if (!ShapeCollider::sphereAACube(&sphere, cubeCenter, cubeSide, collisions)){ std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should collide with cube. axis = " << axis << std::endl; @@ -741,7 +741,7 @@ void ShapeColliderTests::sphereTouchesAACubeFaces() { float overlap = 1.25f * sphereRadius; float sphereOffset = 0.5f * cubeSide + sphereRadius - overlap; sphereCenter = cubeCenter + sphereOffset * axis; - sphere.setPosition(sphereCenter); + sphere.setTranslation(sphereCenter); if (!ShapeCollider::sphereAACube(&sphere, cubeCenter, cubeSide, collisions)){ std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should collide with cube." @@ -815,7 +815,7 @@ void ShapeColliderTests::sphereTouchesAACubeEdges() { float overlap = 0.25f; sphereCenter = cubeCenter + (lengthAxis * 0.5f * cubeSide + sphereRadius - overlap) * axis; - sphere.setPosition(sphereCenter); + sphere.setTranslation(sphereCenter); if (!ShapeCollider::sphereAACube(&sphere, cubeCenter, cubeSide, collisions)){ std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should collide with cube. axis = " << axis << std::endl; @@ -857,42 +857,42 @@ void ShapeColliderTests::sphereMissesAACube() { // top sphereCenter = cubeCenter + sphereOffset * yAxis; - sphere.setPosition(sphereCenter); + sphere.setTranslation(sphereCenter); if (ShapeCollider::sphereAACube(&sphere, cubeCenter, cubeSide, collisions)){ std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should NOT collide with cube" << std::endl; } // bottom sphereCenter = cubeCenter - sphereOffset * yAxis; - sphere.setPosition(sphereCenter); + sphere.setTranslation(sphereCenter); if (ShapeCollider::sphereAACube(&sphere, cubeCenter, cubeSide, collisions)){ std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should NOT collide with cube" << std::endl; } // left sphereCenter = cubeCenter + sphereOffset * xAxis; - sphere.setPosition(sphereCenter); + sphere.setTranslation(sphereCenter); if (ShapeCollider::sphereAACube(&sphere, cubeCenter, cubeSide, collisions)){ std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should NOT collide with cube" << std::endl; } // right sphereCenter = cubeCenter - sphereOffset * xAxis; - sphere.setPosition(sphereCenter); + sphere.setTranslation(sphereCenter); if (ShapeCollider::sphereAACube(&sphere, cubeCenter, cubeSide, collisions)){ std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should NOT collide with cube" << std::endl; } // forward sphereCenter = cubeCenter + sphereOffset * zAxis; - sphere.setPosition(sphereCenter); + sphere.setTranslation(sphereCenter); if (ShapeCollider::sphereAACube(&sphere, cubeCenter, cubeSide, collisions)){ std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should NOT collide with cube" << std::endl; } // back sphereCenter = cubeCenter - sphereOffset * zAxis; - sphere.setPosition(sphereCenter); + sphere.setTranslation(sphereCenter); if (ShapeCollider::sphereAACube(&sphere, cubeCenter, cubeSide, collisions)){ std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should NOT collide with cube" << std::endl; } @@ -955,7 +955,7 @@ void ShapeColliderTests::rayHitsSphere() { rayDirection = rotation * unrotatedRayDirection; sphere.setRadius(radius); - sphere.setPosition(rotation * translation); + sphere.setTranslation(rotation * translation); float distance = FLT_MAX; if (!sphere.findRayIntersection(rayStart, rayDirection, distance)) { @@ -994,7 +994,7 @@ void ShapeColliderTests::rayBarelyHitsSphere() { rayStart = rotation * (rayStart + translation); rayDirection = rotation * rayDirection; - sphere.setPosition(rotation * translation); + sphere.setTranslation(rotation * translation); // ...and test again distance = FLT_MAX; @@ -1032,7 +1032,7 @@ void ShapeColliderTests::rayBarelyMissesSphere() { rayStart = rotation * (rayStart + translation); rayDirection = rotation * rayDirection; - sphere.setPosition(rotation * translation); + sphere.setTranslation(rotation * translation); // ...and test again distance = FLT_MAX; @@ -1183,10 +1183,10 @@ void ShapeColliderTests::rayMissesCapsule() { void ShapeColliderTests::rayHitsPlane() { // make a simple plane - float planeDistanceFromOrigin = 3.579; + float planeDistanceFromOrigin = 3.579f; glm::vec3 planePosition(0.0f, planeDistanceFromOrigin, 0.0f); PlaneShape plane; - plane.setPosition(planePosition); + plane.setTranslation(planePosition); // make a simple ray float startDistance = 1.234f; @@ -1209,7 +1209,7 @@ void ShapeColliderTests::rayHitsPlane() { glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); glm::quat rotation = glm::angleAxis(angle, axis); - plane.setPosition(rotation * planePosition); + plane.setTranslation(rotation * planePosition); plane.setRotation(rotation); rayStart = rotation * rayStart; rayDirection = rotation * rayDirection; @@ -1228,10 +1228,10 @@ void ShapeColliderTests::rayHitsPlane() { void ShapeColliderTests::rayMissesPlane() { // make a simple plane - float planeDistanceFromOrigin = 3.579; + float planeDistanceFromOrigin = 3.579f; glm::vec3 planePosition(0.0f, planeDistanceFromOrigin, 0.0f); PlaneShape plane; - plane.setPosition(planePosition); + plane.setTranslation(planePosition); { // parallel rays should miss float startDistance = 1.234f; @@ -1251,7 +1251,7 @@ void ShapeColliderTests::rayMissesPlane() { glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); glm::quat rotation = glm::angleAxis(angle, axis); - plane.setPosition(rotation * planePosition); + plane.setTranslation(rotation * planePosition); plane.setRotation(rotation); rayStart = rotation * rayStart; rayDirection = rotation * rayDirection; @@ -1283,7 +1283,7 @@ void ShapeColliderTests::rayMissesPlane() { glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); glm::quat rotation = glm::angleAxis(angle, axis); - plane.setPosition(rotation * planePosition); + plane.setTranslation(rotation * planePosition); plane.setRotation(rotation); rayStart = rotation * rayStart; rayDirection = rotation * rayDirection; diff --git a/tests/physics/src/VerletShapeTests.cpp b/tests/physics/src/VerletShapeTests.cpp new file mode 100644 index 0000000000..3a3bd43278 --- /dev/null +++ b/tests/physics/src/VerletShapeTests.cpp @@ -0,0 +1,769 @@ +// +// VerletShapeTests.cpp +// tests/physics/src +// +// Created by Andrew Meadows on 02/21/2014. +// Copyright 2014 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 <stdio.h> +#include <iostream> +#include <math.h> + +#include <glm/glm.hpp> +#include <glm/gtx/quaternion.hpp> + +#include <CollisionInfo.h> +#include <Ragdoll.h> // for VerletPoint +#include <ShapeCollider.h> +#include <SharedUtil.h> +#include <VerletCapsuleShape.h> +#include <VerletSphereShape.h> +#include <StreamUtils.h> + +#include "VerletShapeTests.h" + +const glm::vec3 origin(0.0f); +static const glm::vec3 xAxis(1.0f, 0.0f, 0.0f); +static const glm::vec3 yAxis(0.0f, 1.0f, 0.0f); +static const glm::vec3 zAxis(0.0f, 0.0f, 1.0f); + +void VerletShapeTests::setSpherePosition() { + float radius = 1.0f; + glm::vec3 offset(1.23f, 4.56f, 7.89f); + VerletPoint point; + VerletSphereShape sphere(radius, &point); + + point._position = glm::vec3(0.f); + float d = glm::distance(glm::vec3(0.0f), sphere.getTranslation()); + if (d != 0.0f) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should be at origin" << std::endl; + } + + point._position = offset; + d = glm::distance(glm::vec3(0.0f), sphere.getTranslation()); + if (d != glm::length(offset)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should be at offset" << std::endl; + } +} + +void VerletShapeTests::sphereMissesSphere() { + // non-overlapping spheres of unequal size + + float radiusA = 7.0f; + float radiusB = 3.0f; + float alpha = 1.2f; + float beta = 1.3f; + glm::vec3 offsetDirection = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); + float offsetDistance = alpha * radiusA + beta * radiusB; + + // create points for the sphere centers + VerletPoint points[2]; + + // give pointers to the spheres + VerletSphereShape sphereA(radiusA, (points + 0)); + VerletSphereShape sphereB(radiusB, (points + 1)); + + // set the positions of the spheres by slamming the points directly + points[0]._position = origin; + points[1]._position = offsetDistance * offsetDirection; + + CollisionList collisions(16); + + // collide A to B... + { + bool touching = ShapeCollider::collideShapes(&sphereA, &sphereB, collisions); + if (touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should NOT touch" << std::endl; + } + } + + // collide B to A... + { + bool touching = ShapeCollider::collideShapes(&sphereB, &sphereA, collisions); + if (touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should NOT touch" << std::endl; + } + } + + // also test shapeShape + { + bool touching = ShapeCollider::collideShapes(&sphereB, &sphereA, collisions); + if (touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should NOT touch" << std::endl; + } + } + + if (collisions.size() > 0) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected empty collision list but size is " << collisions.size() + << std::endl; + } +} + +void VerletShapeTests::sphereTouchesSphere() { + // overlapping spheres of unequal size + float radiusA = 7.0f; + float radiusB = 3.0f; + float alpha = 0.2f; + float beta = 0.3f; + glm::vec3 offsetDirection = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); + float offsetDistance = alpha * radiusA + beta * radiusB; + float expectedPenetrationDistance = (1.0f - alpha) * radiusA + (1.0f - beta) * radiusB; + glm::vec3 expectedPenetration = expectedPenetrationDistance * offsetDirection; + + // create two points for the sphere centers + VerletPoint points[2]; + + // give pointers to the spheres + VerletSphereShape sphereA(radiusA, points+0); + VerletSphereShape sphereB(radiusB, points+1); + + // set the positions of the spheres by slamming the points directly + points[0]._position = origin; + points[1]._position = offsetDistance * offsetDirection; + + CollisionList collisions(16); + int numCollisions = 0; + + // collide A to B... + { + bool touching = ShapeCollider::collideShapes(&sphereA, &sphereB, collisions); + if (!touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should touch" << std::endl; + } else { + ++numCollisions; + } + + // verify state of collisions + if (numCollisions != collisions.size()) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected collisions size of " << numCollisions << " but actual size is " << collisions.size() + << std::endl; + } + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + if (!collision) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: null collision" << std::endl; + } + + // penetration points from sphereA into sphereB + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration; + } + + // contactPoint is on surface of sphereA + glm::vec3 AtoB = sphereB.getTranslation() - sphereA.getTranslation(); + glm::vec3 expectedContactPoint = sphereA.getTranslation() + radiusA * glm::normalize(AtoB); + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint; + } + } + + // collide B to A... + { + bool touching = ShapeCollider::collideShapes(&sphereB, &sphereA, collisions); + if (!touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should touch" << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into sphereB + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + float inaccuracy = glm::length(collision->_penetration + expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration; + } + + // contactPoint is on surface of sphereA + glm::vec3 BtoA = sphereA.getTranslation() - sphereB.getTranslation(); + glm::vec3 expectedContactPoint = sphereB.getTranslation() + radiusB * glm::normalize(BtoA); + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint; + } + } +} + +void VerletShapeTests::sphereMissesCapsule() { + // non-overlapping sphere and capsule + float radiusA = 1.5f; + float radiusB = 2.3f; + float totalRadius = radiusA + radiusB; + float halfHeightB = 1.7f; + float axialOffset = totalRadius + 1.1f * halfHeightB; + float radialOffset = 1.2f * radiusA + 1.3f * radiusB; + + // create points for the sphere + capsule + VerletPoint points[3]; + for (int i = 0; i < 3; ++i) { + points[i]._position = glm::vec3(0.0f); + } + + // give the points to the shapes + VerletSphereShape sphereA(radiusA, points); + VerletCapsuleShape capsuleB(radiusB, points+1, points+2); + capsuleB.setHalfHeight(halfHeightB); + + // give the capsule some arbitrary transform + float angle = 37.8f; + glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); + glm::quat rotation = glm::angleAxis(angle, axis); + glm::vec3 translation(15.1f, -27.1f, -38.6f); + capsuleB.setRotation(rotation); + capsuleB.setTranslation(translation); + + CollisionList collisions(16); + + // walk sphereA along the local yAxis next to, but not touching, capsuleB + glm::vec3 localStartPosition(radialOffset, axialOffset, 0.0f); + int numberOfSteps = 10; + float delta = 1.3f * (totalRadius + halfHeightB) / (numberOfSteps - 1); + for (int i = 0; i < numberOfSteps; ++i) { + // translate sphereA into world-frame + glm::vec3 localPosition = localStartPosition + ((float)i * delta) * yAxis; + sphereA.setTranslation(rotation * localPosition + translation); + + // sphereA agains capsuleB + if (ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should NOT touch" + << std::endl; + } + + // capsuleB against sphereA + if (ShapeCollider::collideShapes(&capsuleB, &sphereA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should NOT touch" + << std::endl; + } + } + + if (collisions.size() > 0) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected empty collision list but size is " << collisions.size() + << std::endl; + } +} + +void VerletShapeTests::sphereTouchesCapsule() { + // overlapping sphere and capsule + float radiusA = 2.0f; + float radiusB = 1.0f; + float totalRadius = radiusA + radiusB; + float halfHeightB = 2.0f; + float alpha = 0.5f; + float beta = 0.5f; + float radialOffset = alpha * radiusA + beta * radiusB; + + // create points for the sphere + capsule + VerletPoint points[3]; + for (int i = 0; i < 3; ++i) { + points[i]._position = glm::vec3(0.0f); + } + + // give the points to the shapes + VerletSphereShape sphereA(radiusA, points); + VerletCapsuleShape capsuleB(radiusB, points+1, points+2); + capsuleB.setHalfHeight(halfHeightB); + + CollisionList collisions(16); + int numCollisions = 0; + + { // sphereA collides with capsuleB's cylindrical wall + sphereA.setTranslation(radialOffset * xAxis); + + if (!ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = (radialOffset - totalRadius) * xAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration; + } + + // contactPoint is on surface of sphereA + glm::vec3 expectedContactPoint = sphereA.getTranslation() - radiusA * xAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint; + } + + // capsuleB collides with sphereA + if (!ShapeCollider::collideShapes(&capsuleB, &sphereA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and sphere should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + collision = collisions.getCollision(numCollisions - 1); + expectedPenetration = - (radialOffset - totalRadius) * xAxis; + inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration; + } + + // contactPoint is on surface of capsuleB + glm::vec3 BtoA = sphereA.getTranslation() - capsuleB.getTranslation(); + glm::vec3 closestApproach = capsuleB.getTranslation() + glm::dot(BtoA, yAxis) * yAxis; + expectedContactPoint = closestApproach + radiusB * glm::normalize(BtoA - closestApproach); + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint; + } + } + { // sphereA hits end cap at axis + glm::vec3 axialOffset = (halfHeightB + alpha * radiusA + beta * radiusB) * yAxis; + sphereA.setTranslation(axialOffset * yAxis); + + if (!ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = - ((1.0f - alpha) * radiusA + (1.0f - beta) * radiusB) * yAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration; + } + + // contactPoint is on surface of sphereA + glm::vec3 expectedContactPoint = sphereA.getTranslation() - radiusA * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint; + } + + // capsuleB collides with sphereA + if (!ShapeCollider::collideShapes(&capsuleB, &sphereA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and sphere should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + collision = collisions.getCollision(numCollisions - 1); + expectedPenetration = ((1.0f - alpha) * radiusA + (1.0f - beta) * radiusB) * yAxis; + inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration; + } + + // contactPoint is on surface of capsuleB + glm::vec3 endPoint; + capsuleB.getEndPoint(endPoint); + expectedContactPoint = endPoint + radiusB * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint; + } + } + { // sphereA hits start cap at axis + glm::vec3 axialOffset = - (halfHeightB + alpha * radiusA + beta * radiusB) * yAxis; + sphereA.setTranslation(axialOffset * yAxis); + + if (!ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = ((1.0f - alpha) * radiusA + (1.0f - beta) * radiusB) * yAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration; + } + + // contactPoint is on surface of sphereA + glm::vec3 expectedContactPoint = sphereA.getTranslation() + radiusA * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint; + } + + // capsuleB collides with sphereA + if (!ShapeCollider::collideShapes(&capsuleB, &sphereA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and sphere should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + collision = collisions.getCollision(numCollisions - 1); + expectedPenetration = - ((1.0f - alpha) * radiusA + (1.0f - beta) * radiusB) * yAxis; + inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration; + } + + // contactPoint is on surface of capsuleB + glm::vec3 startPoint; + capsuleB.getStartPoint(startPoint); + expectedContactPoint = startPoint - radiusB * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint; + } + } + if (collisions.size() != numCollisions) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected " << numCollisions << " collisions but actual number is " << collisions.size() + << std::endl; + } +} + +void VerletShapeTests::capsuleMissesCapsule() { + // non-overlapping capsules + float radiusA = 2.0f; + float halfHeightA = 3.0f; + float radiusB = 3.0f; + float halfHeightB = 4.0f; + + float totalRadius = radiusA + radiusB; + float totalHalfLength = totalRadius + halfHeightA + halfHeightB; + + // create points for the shapes + VerletPoint points[4]; + for (int i = 0; i < 4; ++i) { + points[i]._position = glm::vec3(0.0f); + } + + // give the points to the shapes + VerletCapsuleShape capsuleA(radiusA, points+0, points+1); + VerletCapsuleShape capsuleB(radiusB, points+2, points+3); + capsuleA.setHalfHeight(halfHeightA); + capsuleA.setHalfHeight(halfHeightB); + + CollisionList collisions(16); + + // side by side + capsuleB.setTranslation((1.01f * totalRadius) * xAxis); + if (ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + if (ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + + // end to end + capsuleB.setTranslation((1.01f * totalHalfLength) * xAxis); + if (ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + if (ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + + // rotate B and move it to the side + glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); + capsuleB.setRotation(rotation); + capsuleB.setTranslation((1.01f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); + if (ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + if (ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + + if (collisions.size() > 0) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected empty collision list but size is " << collisions.size() + << std::endl; + } +} + +void VerletShapeTests::capsuleTouchesCapsule() { + // overlapping capsules + float radiusA = 2.0f; + float halfHeightA = 3.0f; + float radiusB = 3.0f; + float halfHeightB = 4.0f; + + float totalRadius = radiusA + radiusB; + float totalHalfLength = totalRadius + halfHeightA + halfHeightB; + + // create points for the shapes + VerletPoint points[4]; + for (int i = 0; i < 4; ++i) { + points[i]._position = glm::vec3(0.0f); + } + + // give the points to the shapes + VerletCapsuleShape capsuleA(radiusA, points+0, points+1); + VerletCapsuleShape capsuleB(radiusB, points+2, points+3); + capsuleA.setHalfHeight(halfHeightA); + capsuleB.setHalfHeight(halfHeightB); + + CollisionList collisions(16); + int numCollisions = 0; + + { // side by side + capsuleB.setTranslation((0.99f * totalRadius) * xAxis); + if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + if (!ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + } + + { // end to end + capsuleB.setTranslation((0.99f * totalHalfLength) * yAxis); + + if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + if (!ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + } + + { // rotate B and move it to the side + glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); + capsuleB.setRotation(rotation); + capsuleB.setTranslation((0.99f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); + + if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + if (!ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + } + + { // again, but this time check collision details + float overlap = 0.1f; + glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); + capsuleB.setRotation(rotation); + glm::vec3 positionB = ((totalRadius + capsuleB.getHalfHeight()) - overlap) * xAxis; + capsuleB.setTranslation(positionB); + + // capsuleA vs capsuleB + if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = overlap * xAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration; + } + + glm::vec3 expectedContactPoint = capsuleA.getTranslation() + radiusA * xAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint; + } + + // capsuleB vs capsuleA + if (!ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + collision = collisions.getCollision(numCollisions - 1); + expectedPenetration = - overlap * xAxis; + inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + expectedContactPoint = capsuleB.getTranslation() - (radiusB + halfHeightB) * xAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + } + + { // collide cylinder wall against cylinder wall + float overlap = 0.137f; + float shift = 0.317f * halfHeightA; + glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); + capsuleB.setRotation(rotation); + glm::vec3 positionB = (totalRadius - overlap) * zAxis + shift * yAxis; + capsuleB.setTranslation(positionB); + + // capsuleA vs capsuleB + if (!ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = overlap * zAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + glm::vec3 expectedContactPoint = capsuleA.getTranslation() + radiusA * zAxis + shift * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + } +} + +void VerletShapeTests::runAllTests() { + setSpherePosition(); + sphereMissesSphere(); + sphereTouchesSphere(); + + sphereMissesCapsule(); + sphereTouchesCapsule(); + + capsuleMissesCapsule(); + capsuleTouchesCapsule(); +} diff --git a/tests/physics/src/VerletShapeTests.h b/tests/physics/src/VerletShapeTests.h new file mode 100644 index 0000000000..36e2fe0cbd --- /dev/null +++ b/tests/physics/src/VerletShapeTests.h @@ -0,0 +1,30 @@ +// +// VerletShapeTests.h +// tests/physics/src +// +// Created by Andrew Meadows on 2014.06.18 +// Copyright 2014 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_VerletShapeTests_h +#define hifi_VerletShapeTests_h + +namespace VerletShapeTests { + void setSpherePosition(); + + void sphereMissesSphere(); + void sphereTouchesSphere(); + + void sphereMissesCapsule(); + void sphereTouchesCapsule(); + + void capsuleMissesCapsule(); + void capsuleTouchesCapsule(); + + void runAllTests(); +} + +#endif // hifi_VerletShapeTests_h diff --git a/tests/physics/src/main.cpp b/tests/physics/src/main.cpp index ca98f4d546..086bff4dcd 100644 --- a/tests/physics/src/main.cpp +++ b/tests/physics/src/main.cpp @@ -9,8 +9,10 @@ // #include "ShapeColliderTests.h" +#include "VerletShapeTests.h" int main(int argc, char** argv) { ShapeColliderTests::runAllTests(); + VerletShapeTests::runAllTests(); return 0; } diff --git a/tools/bitstream2json/CMakeLists.txt b/tools/bitstream2json/CMakeLists.txt index fde80b4d33..d5b82adbd9 100644 --- a/tools/bitstream2json/CMakeLists.txt +++ b/tools/bitstream2json/CMakeLists.txt @@ -18,9 +18,14 @@ include(${MACRO_DIR}/SetupHifiProject.cmake) setup_hifi_project(${TARGET_NAME} TRUE) link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") include(${MACRO_DIR}/IncludeGLM.cmake) include_glm(${TARGET_NAME} "${ROOT_DIR}") +IF (WIN32) + target_link_libraries(${TARGET_NAME} Winmm Ws2_32) +ENDIF(WIN32) + target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets Qt5::Script) diff --git a/tools/json2bitstream/CMakeLists.txt b/tools/json2bitstream/CMakeLists.txt index 51382d5858..b93c57b582 100644 --- a/tools/json2bitstream/CMakeLists.txt +++ b/tools/json2bitstream/CMakeLists.txt @@ -18,9 +18,14 @@ include(${MACRO_DIR}/SetupHifiProject.cmake) setup_hifi_project(${TARGET_NAME} TRUE) link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") include(${MACRO_DIR}/IncludeGLM.cmake) include_glm(${TARGET_NAME} "${ROOT_DIR}") +IF (WIN32) + target_link_libraries(${TARGET_NAME} Winmm Ws2_32) +ENDIF(WIN32) + target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets Qt5::Script)