mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-13 21:08:00 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into 19734
This commit is contained in:
commit
fca3a6aa13
192 changed files with 9627 additions and 2750 deletions
2
BUILD.md
2
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -71,6 +71,8 @@ private:
|
|||
ModelTreeHeadlessViewer _modelViewer;
|
||||
|
||||
MixedAudioRingBuffer _receivedAudioBuffer;
|
||||
SequenceNumberStats _incomingMixedAudioSequenceNumberStats;
|
||||
|
||||
AvatarHashMap _avatarHashMap;
|
||||
};
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"-->
|
26
domain-server/resources/web/js/form2js.min.js
vendored
Executable file
26
domain-server/resources/web/js/form2js.min.js
vendored
Executable file
|
@ -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})
|
72
domain-server/resources/web/js/settings.js
Normal file
72
domain-server/resources/web/js/settings.js
Normal file
|
@ -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();
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
7
domain-server/resources/web/js/underscore-1.5.0.min.js
vendored
Normal file
7
domain-server/resources/web/js/underscore-1.5.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20
domain-server/resources/web/settings/describe.json
Normal file
20
domain-server/resources/web/settings/describe.json
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
domain-server/resources/web/settings/index.shtml
Normal file
46
domain-server/resources/web/settings/index.shtml
Normal file
|
@ -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"-->
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
179
domain-server/src/DomainServerSettingsManager.cpp
Normal file
179
domain-server/src/DomainServerSettingsManager.cpp
Normal file
|
@ -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.");
|
||||
}
|
||||
}
|
35
domain-server/src/DomainServerSettingsManager.h
Normal file
35
domain-server/src/DomainServerSettingsManager.h
Normal file
|
@ -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
|
72
examples/concertCamera.js
Normal file
72
examples/concertCamera.js
Normal file
|
@ -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);
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
38
examples/inWorldTestTone.js
Normal file
38
examples/inWorldTestTone.js
Normal file
|
@ -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);
|
||||
|
|
@ -195,6 +195,8 @@ function keyReleaseEvent(event) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function mousePressEvent(event) {
|
||||
if (alt && !isActive) {
|
||||
mouseLastX = event.x;
|
||||
|
|
210
examples/sit.js
210
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
8
interface/external/oculus/readme.txt
vendored
8
interface/external/oculus/readme.txt
vendored
|
@ -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.
|
|
@ -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);
|
||||
}
|
||||
|
|
63
interface/resources/shaders/oculus.vert
Normal file
63
interface/resources/shaders/oculus.vert
Normal file
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(); }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
209
interface/src/ScriptsModel.cpp
Normal file
209
interface/src/ScriptsModel.cpp
Normal file
|
@ -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();
|
||||
}
|
62
interface/src/ScriptsModel.h
Normal file
62
interface/src/ScriptsModel.h
Normal file
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -71,6 +71,7 @@ private:
|
|||
float _lastDistance;
|
||||
|
||||
#endif
|
||||
bool _hydrasConnected;
|
||||
quint64 _lastMovement;
|
||||
glm::vec3 _amountMoved;
|
||||
|
||||
|
|
|
@ -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/";
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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());
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ protected:
|
|||
|
||||
virtual void keyPressEvent(QKeyEvent *event);
|
||||
virtual void showEvent(QShowEvent* event);
|
||||
virtual bool event(QEvent* event);
|
||||
|
||||
private:
|
||||
#ifdef HAVE_QXMPP
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -74,9 +74,11 @@ public slots:
|
|||
private slots:
|
||||
void applyFilter(const QString& filter);
|
||||
void resizeView();
|
||||
void enableSearchBar();
|
||||
|
||||
private:
|
||||
ModelHandler* _handler;
|
||||
QLineEdit* _searchBar;
|
||||
QTreeView _view;
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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/>" <<
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -42,6 +42,7 @@ private slots:
|
|||
void setHeadUrl(QString modelUrl);
|
||||
void setSkeletonUrl(QString modelUrl);
|
||||
void openSnapshotLocationBrowser();
|
||||
void openScriptsLocationBrowser();
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
136
interface/src/ui/overlays/BillboardOverlay.cpp
Normal file
136
interface/src/ui/overlays/BillboardOverlay.cpp
Normal file
|
@ -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;
|
||||
}
|
46
interface/src/ui/overlays/BillboardOverlay.h
Normal file
46
interface/src/ui/overlays/BillboardOverlay.h
Normal file
|
@ -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
|
|
@ -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() {
|
||||
|
|
115
interface/src/ui/overlays/ModelOverlay.cpp
Normal file
115
interface/src/ui/overlays/ModelOverlay.cpp
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
38
interface/src/ui/overlays/ModelOverlay.h
Normal file
38
interface/src/ui/overlays/ModelOverlay.h
Normal file
|
@ -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
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue