diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index a8343e94ad..b78d4f81f9 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -28,6 +28,7 @@ link_hifi_library(audio ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(avatars ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") @@ -51,4 +52,4 @@ IF (WIN32) target_link_libraries(${TARGET_NAME} Winmm Ws2_32) ENDIF(WIN32) -target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets Qt5::Script "${GNUTLS_LIBRARY}") \ No newline at end of file +target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets Qt5::Script "${GNUTLS_LIBRARY}") diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index f48c4b9401..e39cb39307 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -11,8 +11,10 @@ #include #include +#include #include #include +#include #include #include @@ -20,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +33,8 @@ Agent::Agent(const QByteArray& packet) : ThreadedAssignment(packet), _voxelEditSender(), _particleEditSender(), - _receivedAudioBuffer(NETWORK_BUFFER_LENGTH_SAMPLES_STEREO) + _receivedAudioBuffer(NETWORK_BUFFER_LENGTH_SAMPLES_STEREO), + _avatarHashMap() { // be the parent of the script engine so it gets moved when we do _scriptEngine.setParent(this); @@ -128,6 +132,16 @@ void Agent::readPendingDatagrams() { // let this continue through to the NodeList so it updates last heard timestamp // for the sending audio mixer NodeList::getInstance()->processNodeData(senderSockAddr, receivedPacket); + } else if (datagramPacketType == PacketTypeBulkAvatarData + || datagramPacketType == PacketTypeAvatarIdentity + || datagramPacketType == PacketTypeAvatarBillboard + || datagramPacketType == PacketTypeKillAvatar) { + // let the avatar hash map process it + _avatarHashMap.processAvatarMixerDatagram(receivedPacket, nodeList->sendingNodeForPacket(receivedPacket)); + + // let this continue through to the NodeList so it updates last heard timestamp + // for the sending avatar-mixer + NodeList::getInstance()->processNodeData(senderSockAddr, receivedPacket); } else { NodeList::getInstance()->processNodeData(senderSockAddr, receivedPacket); } @@ -148,22 +162,32 @@ void Agent::run() { << NodeType::ParticleServer); // figure out the URL for the script for this agent assignment - QString scriptURLString("http://%1:8080/assignment/%2"); - scriptURLString = scriptURLString.arg(NodeList::getInstance()->getDomainHandler().getIP().toString(), - uuidStringWithoutCurlyBraces(_uuid)); - + QUrl scriptURL; + if (_payload.isEmpty()) { + scriptURL = QUrl(QString("http://%1:8080/assignment/%2") + .arg(NodeList::getInstance()->getDomainHandler().getIP().toString(), + uuidStringWithoutCurlyBraces(_uuid))); + } else { + scriptURL = QUrl(_payload); + } + QNetworkAccessManager *networkManager = new QNetworkAccessManager(this); - QNetworkReply *reply = networkManager->get(QNetworkRequest(QUrl(scriptURLString))); + QNetworkReply *reply = networkManager->get(QNetworkRequest(scriptURL)); + QNetworkDiskCache* cache = new QNetworkDiskCache(networkManager); + QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + cache->setCacheDirectory(!cachePath.isEmpty() ? cachePath : "agentCache"); + networkManager->setCache(cache); - qDebug() << "Downloading script at" << scriptURLString; + qDebug() << "Downloading script at" << scriptURL.toString(); QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); - // let the AvatarData class use our QNetworkAcessManager + // let the AvatarData and ResourceCache classes use our QNetworkAccessManager AvatarData::setNetworkAccessManager(networkManager); + ResourceCache::setNetworkAccessManager(networkManager); QString scriptContents(reply->readAll()); @@ -178,6 +202,7 @@ void Agent::run() { // give this AvatarData object to the script engine _scriptEngine.setAvatarData(&scriptedAvatar, "Avatar"); + _scriptEngine.setAvatarHashMap(&_avatarHashMap, "AvatarList"); // register ourselves to the script engine _scriptEngine.registerGlobalObject("Agent", this); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 2dcd7e3107..9f6a8089cf 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -65,6 +66,7 @@ private: VoxelTreeHeadlessViewer _voxelViewer; MixedAudioRingBuffer _receivedAudioBuffer; + AvatarHashMap _avatarHashMap; }; #endif // hifi_Agent_h diff --git a/domain-server/resources/web/assignment/css/style.css b/domain-server/resources/web/assignment/css/style.css index b6c26ca9fd..51813a4d73 100644 --- a/domain-server/resources/web/assignment/css/style.css +++ b/domain-server/resources/web/assignment/css/style.css @@ -42,13 +42,13 @@ body { background-color: #28FF57; } -#instance-field { +#extra-fields { position: absolute; right: 20px; - top: 40px; + top: 30px; } -#instance-field input { +#extra-fields input { width: 80px; } diff --git a/domain-server/resources/web/assignment/index.shtml b/domain-server/resources/web/assignment/index.shtml index 64a6d8825f..27c84be985 100644 --- a/domain-server/resources/web/assignment/index.shtml +++ b/domain-server/resources/web/assignment/index.shtml @@ -14,8 +14,13 @@ Run -
- +
+
+ +
+
+ +
diff --git a/domain-server/resources/web/assignment/js/assignment.js b/domain-server/resources/web/assignment/js/assignment.js index a04c8192f4..71f4373251 100644 --- a/domain-server/resources/web/assignment/js/assignment.js +++ b/domain-server/resources/web/assignment/js/assignment.js @@ -22,9 +22,14 @@ $(document).ready(function(){ + '--' + boundary + '--\r\n'; var headers = {}; + if ($('#instance-field input').val()) { headers['ASSIGNMENT-INSTANCES'] = $('#instance-field input').val(); } + + if ($('#pool-field input').val()) { + headers['ASSIGNMENT-POOL'] = $('#pool-field input').val(); + } // post form to assignment in order to create an assignment $.ajax({ diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 969b83f64f..9dbb1e478e 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -187,7 +187,7 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { } } - QSet parsedTypes(QSet() << Assignment::AgentType); + QSet parsedTypes; parseAssignmentConfigs(parsedTypes); populateDefaultStaticAssignmentsExcludingTypes(parsedTypes); @@ -222,12 +222,19 @@ void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) { QVariant mapValue = _argumentVariantMap[variantMapKeys[configIndex]]; + QJsonArray assignmentArray; if (mapValue.type() == QVariant::String) { QJsonDocument deserializedDocument = QJsonDocument::fromJson(mapValue.toString().toUtf8()); - createStaticAssignmentsForType(assignmentType, deserializedDocument.array()); + assignmentArray = deserializedDocument.array(); } else { - createStaticAssignmentsForType(assignmentType, mapValue.toJsonValue().toArray()); + assignmentArray = mapValue.toJsonValue().toArray(); + } + + if (assignmentType != Assignment::AgentType) { + createStaticAssignmentsForType(assignmentType, assignmentArray); + } else { + createScriptedAssignmentsFromArray(assignmentArray); } excludedTypes.insert(assignmentType); @@ -242,6 +249,42 @@ void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment _staticAssignmentHash.insert(newAssignment->getUUID(), SharedAssignmentPointer(newAssignment)); } +void DomainServer::createScriptedAssignmentsFromArray(const QJsonArray &configArray) { + foreach(const QJsonValue& jsonValue, configArray) { + if (jsonValue.isObject()) { + QJsonObject jsonObject = jsonValue.toObject(); + + // make sure we were passed a URL, otherwise this is an invalid scripted assignment + const QString ASSIGNMENT_URL_KEY = "url"; + QString assignmentURL = jsonObject[ASSIGNMENT_URL_KEY].toString(); + + if (!assignmentURL.isEmpty()) { + // check the json for a pool + const QString ASSIGNMENT_POOL_KEY = "pool"; + QString assignmentPool = jsonObject[ASSIGNMENT_POOL_KEY].toString(); + + // check for a number of instances, if not passed then default is 1 + const QString ASSIGNMENT_INSTANCES_KEY = "instances"; + int numInstances = jsonObject[ASSIGNMENT_INSTANCES_KEY].toInt(); + numInstances = (numInstances == 0 ? 1 : numInstances); + + for (int i = 0; i < numInstances; i++) { + // add a scripted assignment to the queue for this instance + Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, + Assignment::AgentType, + assignmentPool); + scriptAssignment->setPayload(assignmentURL.toUtf8()); + + qDebug() << "Adding scripted assignment to queue -" << *scriptAssignment; + qDebug() << "URL for script is" << assignmentURL; + + _assignmentQueue.enqueue(SharedAssignmentPointer(scriptAssignment)); + } + } + } + } +} + void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const QJsonArray& configArray) { // we have a string for config for this type qDebug() << "Parsing config for assignment type" << type; @@ -284,8 +327,10 @@ void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const Q void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet& excludedTypes) { // enumerate over all assignment types and see if we've already excluded it - for (int defaultedType = Assignment::AudioMixerType; defaultedType != Assignment::AllTypes; defaultedType++) { - if (!excludedTypes.contains((Assignment::Type) defaultedType)) { + for (Assignment::Type defaultedType = Assignment::AudioMixerType; + defaultedType != Assignment::AllTypes; + defaultedType = static_cast(static_cast(defaultedType) + 1)) { + if (!excludedTypes.contains(defaultedType) && defaultedType != Assignment::AgentType) { // type has not been set from a command line or config file config, use the default // by clearing whatever exists and writing a single default assignment with no payload Assignment* newAssignment = new Assignment(Assignment::CreateCommand, (Assignment::Type) defaultedType); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 0153fac49d..9bd10b8c60 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -66,6 +66,7 @@ private: void parseAssignmentConfigs(QSet& excludedTypes); void addStaticAssignmentToAssignmentHash(Assignment* newAssignment); + void createScriptedAssignmentsFromArray(const QJsonArray& configArray); void createStaticAssignmentsForType(Assignment::Type type, const QJsonArray& configArray); void populateDefaultStaticAssignmentsExcludingTypes(const QSet& excludedTypes); diff --git a/examples/audioReflectorTools.js b/examples/audioReflectorTools.js index 3cc6a1a21e..76869de578 100644 --- a/examples/audioReflectorTools.js +++ b/examples/audioReflectorTools.js @@ -19,6 +19,8 @@ var reflectiveScale = 100.0; var diffusionScale = 100.0; var absorptionScale = 100.0; var combFilterScale = 50.0; +var originalScale = 2.0; +var echoesScale = 2.0; // these three properties are bound together, if you change one, the others will also change var reflectiveRatio = AudioReflector.getReflectiveRatio(); @@ -421,6 +423,84 @@ var absorptionThumb = Overlays.addOverlay("image", { alpha: 1 }); +var originalY = topY; +topY += sliderHeight; + +var originalLabel = Overlays.addOverlay("text", { + x: 40, + y: originalY, + width: 60, + height: sliderHeight, + color: { red: 0, green: 0, blue: 0}, + textColor: { red: 255, green: 255, blue: 255}, + topMargin: 6, + leftMargin: 5, + text: "Original\nMix:" + }); + + +var originalSlider = Overlays.addOverlay("image", { + // alternate form of expressing bounds + bounds: { x: 100, y: originalY, width: 150, height: sliderHeight}, + subImage: { x: 46, y: 0, width: 200, height: 71 }, + imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/slider.png", + color: { red: 255, green: 255, blue: 255}, + alpha: 1 + }); + + +var originalMinThumbX = 110; +var originalMaxThumbX = originalMinThumbX + 110; +var originalThumbX = originalMinThumbX + ((originalMaxThumbX - originalMinThumbX) * (AudioReflector.getOriginalSourceAttenuation() / originalScale)); +var originalThumb = Overlays.addOverlay("image", { + x: originalThumbX, + y: originalY+9, + width: 18, + height: 17, + imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/thumb.png", + color: { red: 128, green: 128, blue: 0}, + alpha: 1 + }); + +var echoesY = topY; +topY += sliderHeight; + +var echoesLabel = Overlays.addOverlay("text", { + x: 40, + y: echoesY, + width: 60, + height: sliderHeight, + color: { red: 0, green: 0, blue: 0}, + textColor: { red: 255, green: 255, blue: 255}, + topMargin: 6, + leftMargin: 5, + text: "Echoes\nMix:" + }); + + +var echoesSlider = Overlays.addOverlay("image", { + // alternate form of expressing bounds + bounds: { x: 100, y: echoesY, width: 150, height: sliderHeight}, + subImage: { x: 46, y: 0, width: 200, height: 71 }, + imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/slider.png", + color: { red: 255, green: 255, blue: 255}, + alpha: 1 + }); + + +var echoesMinThumbX = 110; +var echoesMaxThumbX = echoesMinThumbX + 110; +var echoesThumbX = echoesMinThumbX + ((echoesMaxThumbX - echoesMinThumbX) * (AudioReflector.getEchoesAttenuation() / echoesScale)); +var echoesThumb = Overlays.addOverlay("image", { + x: echoesThumbX, + y: echoesY+9, + width: 18, + height: 17, + imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/thumb.png", + color: { red: 128, green: 128, blue: 0}, + alpha: 1 + }); + // When our script shuts down, we should clean up all of our overlays function scriptEnding() { @@ -460,6 +540,14 @@ function scriptEnding() { Overlays.deleteOverlay(absorptionThumb); Overlays.deleteOverlay(absorptionSlider); + Overlays.deleteOverlay(echoesLabel); + Overlays.deleteOverlay(echoesThumb); + Overlays.deleteOverlay(echoesSlider); + + Overlays.deleteOverlay(originalLabel); + Overlays.deleteOverlay(originalThumb); + Overlays.deleteOverlay(originalSlider); + } Script.scriptEnding.connect(scriptEnding); @@ -483,6 +571,8 @@ var movingSliderLocalFactor = false; var movingSliderReflective = false; var movingSliderDiffusion = false; var movingSliderAbsorption = false; +var movingSliderOriginal = false; +var movingSliderEchoes = false; var thumbClickOffsetX = 0; function mouseMoveEvent(event) { @@ -546,7 +636,6 @@ function mouseMoveEvent(event) { var combFilter = ((newThumbX - combFilterMinThumbX) / (combFilterMaxThumbX - combFilterMinThumbX)) * combFilterScale; AudioReflector.setCombFilterWindow(combFilter); } - if (movingSliderLocalFactor) { newThumbX = event.x - thumbClickOffsetX; if (newThumbX < localFactorMinThumbX) { @@ -598,6 +687,30 @@ function mouseMoveEvent(event) { var diffusion = ((newThumbX - diffusionMinThumbX) / (diffusionMaxThumbX - diffusionMinThumbX)) * diffusionScale; setDiffusionRatio(diffusion); } + if (movingSliderEchoes) { + newThumbX = event.x - thumbClickOffsetX; + if (newThumbX < echoesMinThumbX) { + newThumbX = echoesMminThumbX; + } + if (newThumbX > echoesMaxThumbX) { + newThumbX = echoesMaxThumbX; + } + Overlays.editOverlay(echoesThumb, { x: newThumbX } ); + var echoes = ((newThumbX - echoesMinThumbX) / (echoesMaxThumbX - echoesMinThumbX)) * echoesScale; + AudioReflector.setEchoesAttenuation(echoes); + } + if (movingSliderOriginal) { + newThumbX = event.x - thumbClickOffsetX; + if (newThumbX < originalMinThumbX) { + newThumbX = originalMminThumbX; + } + if (newThumbX > originalMaxThumbX) { + newThumbX = originalMaxThumbX; + } + Overlays.editOverlay(originalThumb, { x: newThumbX } ); + var original = ((newThumbX - originalMinThumbX) / (originalMaxThumbX - originalMinThumbX)) * originalScale; + AudioReflector.setOriginalSourceAttenuation(original); + } } @@ -640,7 +753,16 @@ function mousePressEvent(event) { movingSliderReflective = true; thumbClickOffsetX = event.x - reflectiveThumbX; } + if (clickedOverlay == originalThumb) { + movingSliderOriginal = true; + thumbClickOffsetX = event.x - originalThumbX; + } + if (clickedOverlay == echoesThumb) { + movingSliderEchoes = true; + thumbClickOffsetX = event.x - echoesThumbX; + } } + function mouseReleaseEvent(event) { if (movingSliderDelay) { movingSliderDelay = false; @@ -672,14 +794,12 @@ function mouseReleaseEvent(event) { AudioReflector.setCombFilterWindow(combFilter); combFilterThumbX = newThumbX; } - if (movingSliderLocalFactor) { movingSliderLocalFactor = false; var localFactor = ((newThumbX - localFactorMinThumbX) / (localFactorMaxThumbX - localFactorMinThumbX)) * localFactorScale; AudioReflector.setLocalAudioAttenuationFactor(localFactor); localFactorThumbX = newThumbX; } - if (movingSliderReflective) { movingSliderReflective = false; var reflective = ((newThumbX - reflectiveMinThumbX) / (reflectiveMaxThumbX - reflectiveMinThumbX)) * reflectiveScale; @@ -687,7 +807,6 @@ function mouseReleaseEvent(event) { reflectiveThumbX = newThumbX; updateRatioSliders(); } - if (movingSliderDiffusion) { movingSliderDiffusion = false; var diffusion = ((newThumbX - diffusionMinThumbX) / (diffusionMaxThumbX - diffusionMinThumbX)) * diffusionScale; @@ -695,7 +814,6 @@ function mouseReleaseEvent(event) { diffusionThumbX = newThumbX; updateRatioSliders(); } - if (movingSliderAbsorption) { movingSliderAbsorption = false; var absorption = ((newThumbX - absorptionMinThumbX) / (absorptionMaxThumbX - absorptionMinThumbX)) * absorptionScale; @@ -703,6 +821,18 @@ function mouseReleaseEvent(event) { absorptionThumbX = newThumbX; updateRatioSliders(); } + if (movingSliderEchoes) { + movingSliderEchoes = false; + var echoes = ((newThumbX - echoesMinThumbX) / (echoesMaxThumbX - echoesMinThumbX)) * echoesScale; + AudioReflector.setEchoesAttenuation(echoes); + echoesThumbX = newThumbX; + } + if (movingSliderOriginal) { + movingSliderOriginal = false; + var original = ((newThumbX - originalMinThumbX) / (originalMaxThumbX - originalMinThumbX)) * originalScale; + AudioReflector.setOriginalSourceAttenuation(original); + originalThumbX = newThumbX; + } } Controller.mouseMoveEvent.connect(mouseMoveEvent); diff --git a/examples/crazylegs.js b/examples/crazylegs.js index 6311aea6e4..b0f8e937bc 100644 --- a/examples/crazylegs.js +++ b/examples/crazylegs.js @@ -15,12 +15,12 @@ var AMPLITUDE = 45.0; var cumulativeTime = 0.0; -print("# Joint list start"); var jointList = MyAvatar.getJointNames(); +var jointMappings = "\n# Joint list start"; for (var i = 0; i < jointList.length; i++) { - print("jointIndex = " + jointList[i] + " = " + i); + jointMappings = jointMappings + "\njointIndex = " + jointList[i] + " = " + i; } -print("# Joint list end"); +print(jointMappings + "\n# Joint list end"); Script.update.connect(function(deltaTime) { cumulativeTime += deltaTime; diff --git a/examples/dancing_bot.js b/examples/dancing_bot.js new file mode 100644 index 0000000000..3572cc5a11 --- /dev/null +++ b/examples/dancing_bot.js @@ -0,0 +1,49 @@ +// +// dancing_bot.js +// examples +// +// Created by Andrzej Kapolka on 4/16/14. +// Copyright 2014 High Fidelity, Inc. +// +// This is an example script that demonstrates an NPC avatar running an FBX animation loop. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var animation = AnimationCache.getAnimation("http://www.fungibleinsight.com/faces/gangnam_style_2.fbx"); + +Avatar.skeletonModelURL = "http://www.fungibleinsight.com/faces/beta.fst"; + +Agent.isAvatar = true; + +var jointMapping; + +var frameIndex = 0.0; + +var FRAME_RATE = 30.0; // frames per second + +Script.update.connect(function(deltaTime) { + if (!jointMapping) { + var avatarJointNames = Avatar.jointNames; + var animationJointNames = animation.jointNames; + if (avatarJointNames === 0 || animationJointNames.length === 0) { + return; + } + jointMapping = new Array(avatarJointNames.length); + for (var i = 0; i < avatarJointNames.length; i++) { + jointMapping[i] = animationJointNames.indexOf(avatarJointNames[i]); + } + } + frameIndex += deltaTime * FRAME_RATE; + var frames = animation.frames; + var rotations = frames[Math.floor(frameIndex) % frames.length].rotations; + for (var j = 0; j < jointMapping.length; j++) { + var rotationIndex = jointMapping[j]; + if (rotationIndex != -1) { + Avatar.setJointData(j, rotations[rotationIndex]); + } + } +}); + + diff --git a/examples/editVoxels.js b/examples/editVoxels.js index 31e8da8e74..ee86d9b599 100644 --- a/examples/editVoxels.js +++ b/examples/editVoxels.js @@ -64,19 +64,92 @@ colors[8] = { red: 31, green: 64, blue: 64 }; var numColors = 9; var whichColor = -1; // Starting color is 'Copy' mode -// Create sounds for adding, deleting, recoloring voxels -var addSound1 = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Voxels/voxel+create+2.raw"); -var addSound2 = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Voxels/voxel+create+4.raw"); - -var addSound3 = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Voxels/voxel+create+3.raw"); -var deleteSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Voxels/voxel+delete+2.raw"); -var changeColorSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Voxels/voxel+edit+2.raw"); -var clickSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Switches+and+sliders/toggle+switch+-+medium.raw"); +// Create sounds for for every script actions that require one var audioOptions = new AudioInjectionOptions(); - -audioOptions.volume = 0.5; +audioOptions.volume = 1.0; audioOptions.position = Vec3.sum(MyAvatar.position, { x: 0, y: 1, z: 0 } ); // start with audio slightly above the avatar +function SoundArray() { + this.audioOptions = audioOptions + this.sounds = new Array(); + this.addSound = function (soundURL) { + this.sounds[this.sounds.length] = new Sound(soundURL); + } + this.play = function (index) { + if (0 <= index && index < this.sounds.length) { + Audio.playSound(this.sounds[index], this.audioOptions); + } else { + print("[ERROR] editVoxels.js:randSound.play() : Index " + index + " out of range."); + } + } + this.playRandom = function () { + if (this.sounds.length > 0) { + rand = Math.floor(Math.random() * this.sounds.length); + Audio.playSound(this.sounds[rand], this.audioOptions); + } else { + print("[ERROR] editVoxels.js:randSound.playRandom() : Array is empty."); + } + } +} + +var addVoxelSound = new SoundArray(); +addVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Add/VA+1.raw"); +addVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Add/VA+2.raw"); +addVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Add/VA+3.raw"); +addVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Add/VA+4.raw"); +addVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Add/VA+5.raw"); +addVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Add/VA+6.raw"); + +var delVoxelSound = new SoundArray(); +delVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Del/VD+A1.raw"); +delVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Del/VD+A2.raw"); +delVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Del/VD+A3.raw"); +delVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Del/VD+B1.raw"); +delVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Del/VD+B2.raw"); +delVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Del/VD+B3.raw"); + +var resizeVoxelSound = new SoundArray(); +resizeVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Size/V+Size+Minus.raw"); +resizeVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Voxel+Size/V+Size+Plus.raw"); +var voxelSizeMinus = 0; +var voxelSizePlus = 1; + +var swatchesSound = new SoundArray(); +swatchesSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Swatches/Swatch+1.raw"); +swatchesSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Swatches/Swatch+2.raw"); +swatchesSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Swatches/Swatch+3.raw"); +swatchesSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Swatches/Swatch+4.raw"); +swatchesSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Swatches/Swatch+5.raw"); +swatchesSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Swatches/Swatch+6.raw"); +swatchesSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Swatches/Swatch+7.raw"); +swatchesSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Swatches/Swatch+8.raw"); +swatchesSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Swatches/Swatch+9.raw"); + +var undoSound = new SoundArray(); +undoSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Undo/Undo+1.raw"); +undoSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Undo/Undo+2.raw"); +undoSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Undo/Undo+3.raw"); + +var scriptInitSound = new SoundArray(); +scriptInitSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Script+Init/Script+Init+A.raw"); +scriptInitSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Script+Init/Script+Init+B.raw"); +scriptInitSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Script+Init/Script+Init+C.raw"); +scriptInitSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Script+Init/Script+Init+D.raw"); + +var modeSwitchSound = new SoundArray(); +modeSwitchSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Mode+Switch/Mode+1.raw"); +modeSwitchSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Mode+Switch/Mode+2.raw"); +modeSwitchSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Mode+Switch/Mode+3.raw"); + +var initialVoxelSound = new SoundArray(); +initialVoxelSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Initial+Voxel/Initial+V.raw"); + +var colorInheritSound = new SoundArray(); +colorInheritSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Color+Inherit/Inherit+A.raw"); +colorInheritSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Color+Inherit/Inherit+B.raw"); +colorInheritSound.addSound("https://highfidelity-public.s3.amazonaws.com/sounds/Voxel+Editing/Color+Inherit/Inherit+C.raw"); + + var editToolsOn = true; // starts out off // previewAsVoxel - by default, we will preview adds/deletes/recolors as just 4 lines on the intersecting face. But if you @@ -379,8 +452,17 @@ function calcThumbFromScale(scale) { if (thumbStep > pointerVoxelScaleSteps) { thumbStep = pointerVoxelScaleSteps; } + var oldThumbX = thumbX; thumbX = (thumbDeltaPerStep * (thumbStep - 1)) + minThumbX; Overlays.editOverlay(thumb, { x: thumbX + sliderX } ); + + if (thumbX > oldThumbX) { + resizeVoxelSound.play(voxelSizePlus); + print("Plus"); + } else if (thumbX < oldThumbX) { + resizeVoxelSound.play(voxelSizeMinus); + print("Minus"); + } } function calcScaleFromThumb(newThumbX) { @@ -443,15 +525,6 @@ var recolorToolSelected = false; var eyedropperToolSelected = false; var pasteMode = false; -function playRandomAddSound(audioOptions) { - if (Math.random() < 0.33) { - Audio.playSound(addSound1, audioOptions); - } else if (Math.random() < 0.5) { - Audio.playSound(addSound2, audioOptions); - } else { - Audio.playSound(addSound3, audioOptions); - } -} function calculateVoxelFromIntersection(intersection, operation) { //print("calculateVoxelFromIntersection() operation="+operation); @@ -744,7 +817,7 @@ function trackKeyReleaseEvent(event) { moveTools(); setAudioPosition(); // make sure we set the audio position before playing sounds showPreviewGuides(); - Audio.playSound(clickSound, audioOptions); + scriptInitSound.playRandom(); } if (event.text == "ALT") { @@ -808,18 +881,21 @@ function mousePressEvent(event) { Overlays.editOverlay(thumb, { imageURL: toolIconUrl + "voxel-size-slider-handle.svg", }); } else if (clickedOverlay == voxelTool) { + modeSwitchSound.play(0); voxelToolSelected = true; recolorToolSelected = false; eyedropperToolSelected = false; moveTools(); clickedOnSomething = true; } else if (clickedOverlay == recolorTool) { + modeSwitchSound.play(1); voxelToolSelected = false; recolorToolSelected = true; eyedropperToolSelected = false; moveTools(); clickedOnSomething = true; } else if (clickedOverlay == eyedropperTool) { + modeSwitchSound.play(2); voxelToolSelected = false; recolorToolSelected = false; eyedropperToolSelected = true; @@ -846,6 +922,7 @@ function mousePressEvent(event) { whichColor = s; moveTools(); clickedOnSomething = true; + swatchesSound.play(whichColor); break; } } @@ -888,7 +965,7 @@ function mousePressEvent(event) { // Delete voxel voxelDetails = calculateVoxelFromIntersection(intersection,"delete"); Voxels.eraseVoxel(voxelDetails.x, voxelDetails.y, voxelDetails.z, voxelDetails.s); - Audio.playSound(deleteSound, audioOptions); + delVoxelSound.playRandom(); Overlays.editOverlay(voxelPreview, { visible: false }); } else if (eyedropperToolSelected || trackAsEyedropper) { if (whichColor != -1) { @@ -896,6 +973,7 @@ function mousePressEvent(event) { colors[whichColor].green = intersection.voxel.green; colors[whichColor].blue = intersection.voxel.blue; moveTools(); + swatchesSound.play(whichColor); } } else if (recolorToolSelected || trackAsRecolor) { @@ -903,10 +981,9 @@ function mousePressEvent(event) { voxelDetails = calculateVoxelFromIntersection(intersection,"recolor"); // doing this erase then set will make sure we only recolor just the target voxel - Voxels.eraseVoxel(voxelDetails.x, voxelDetails.y, voxelDetails.z, voxelDetails.s); Voxels.setVoxel(voxelDetails.x, voxelDetails.y, voxelDetails.z, voxelDetails.s, colors[whichColor].red, colors[whichColor].green, colors[whichColor].blue); - Audio.playSound(changeColorSound, audioOptions); + swatchesSound.play(whichColor); Overlays.editOverlay(voxelPreview, { visible: false }); } else if (voxelToolSelected) { // Add voxel on face @@ -930,7 +1007,7 @@ function mousePressEvent(event) { lastVoxelColor = { red: newColor.red, green: newColor.green, blue: newColor.blue }; lastVoxelScale = voxelDetails.s; - playRandomAddSound(audioOptions); + addVoxelSound.playRandom(); Overlays.editOverlay(voxelPreview, { visible: false }); dragStart = { x: event.x, y: event.y }; @@ -946,12 +1023,12 @@ function keyPressEvent(event) { if (event.text == "`") { print("Color = Copy"); whichColor = -1; - Audio.playSound(clickSound, audioOptions); + colorInheritSound.playRandom(); moveTools(); } else if ((nVal > 0) && (nVal <= numColors)) { whichColor = nVal - 1; print("Color = " + (whichColor + 1)); - Audio.playSound(clickSound, audioOptions); + swatchesSound.play(whichColor); moveTools(); } else if (event.text == "0") { // Create a brand new 1 meter voxel in front of your avatar @@ -969,8 +1046,12 @@ function keyPressEvent(event) { Voxels.eraseVoxel(newVoxel.x, newVoxel.y, newVoxel.z, newVoxel.s); Voxels.setVoxel(newVoxel.x, newVoxel.y, newVoxel.z, newVoxel.s, newVoxel.red, newVoxel.green, newVoxel.blue); setAudioPosition(); - playRandomAddSound(audioOptions); + initialVoxelSound.playRandom(); + } else if (event.text == "z") { + undoSound.playRandom(); } + + } trackKeyPressEvent(event); // used by preview support diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 6295095b43..557af35e12 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -120,6 +120,7 @@ include(${MACRO_DIR}/LinkHifiLibrary.cmake) link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}") diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bd7a82b439..438722df17 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1679,8 +1679,12 @@ void Application::init() { _audioReflector.setMyAvatar(getAvatar()); _audioReflector.setVoxels(_voxels.getTree()); _audioReflector.setAudio(getAudio()); + _audioReflector.setAvatarManager(&_avatarManager); + connect(getAudio(), &Audio::processInboundAudio, &_audioReflector, &AudioReflector::processInboundAudio,Qt::DirectConnection); connect(getAudio(), &Audio::processLocalAudio, &_audioReflector, &AudioReflector::processLocalAudio,Qt::DirectConnection); + connect(getAudio(), &Audio::preProcessOriginalInboundAudio, &_audioReflector, + &AudioReflector::preProcessOriginalInboundAudio,Qt::DirectConnection); // save settings when avatar changes connect(_myAvatar, &MyAvatar::transformChanged, this, &Application::bumpSettings); @@ -3397,6 +3401,7 @@ void Application::loadScript(const QString& scriptName) { scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("AnimationCache", &_animationCache); scriptEngine->registerGlobalObject("AudioReflector", &_audioReflector); QThread* workerThread = new QThread(this); diff --git a/interface/src/Application.h b/interface/src/Application.h index 2656ff47ce..b8f8a0c0ff 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -470,6 +470,7 @@ private: QSet _keysPressed; GeometryCache _geometryCache; + AnimationCache _animationCache; TextureCache _textureCache; GlowEffect _glowEffect; diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index daa7c036eb..830e2fe69b 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -784,6 +784,7 @@ void Audio::processReceivedAudio(const QByteArray& audioByteArray) { _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); addSpatialAudioToBuffer(sampleTime, buffer, numNetworkOutputSamples); } diff --git a/interface/src/Audio.h b/interface/src/Audio.h index 3b19d98146..96def43dd2 100644 --- a/interface/src/Audio.h +++ b/interface/src/Audio.h @@ -99,6 +99,7 @@ public slots: signals: bool muteToggled(); + void preProcessOriginalInboundAudio(unsigned int sampleTime, QByteArray& samples, const QAudioFormat& format); void processInboundAudio(unsigned int sampleTime, const QByteArray& samples, const QAudioFormat& format); void processLocalAudio(unsigned int sampleTime, const QByteArray& samples, const QAudioFormat& format); diff --git a/interface/src/AudioReflector.cpp b/interface/src/AudioReflector.cpp index 52558f1d59..e66735c403 100644 --- a/interface/src/AudioReflector.cpp +++ b/interface/src/AudioReflector.cpp @@ -25,6 +25,8 @@ const float SLIGHTLY_SHORT = 0.999f; // slightly inside the distance so we're on const float DEFAULT_ABSORPTION_RATIO = 0.125; // 12.5% is absorbed const float DEFAULT_DIFFUSION_RATIO = 0.125; // 12.5% is diffused +const float DEFAULT_ORIGINAL_ATTENUATION = 1.0f; +const float DEFAULT_ECHO_ATTENUATION = 1.0f; AudioReflector::AudioReflector(QObject* parent) : QObject(parent), @@ -36,6 +38,8 @@ AudioReflector::AudioReflector(QObject* parent) : _diffusionFanout(DEFAULT_DIFFUSION_FANOUT), _absorptionRatio(DEFAULT_ABSORPTION_RATIO), _diffusionRatio(DEFAULT_DIFFUSION_RATIO), + _originalSourceAttenuation(DEFAULT_ORIGINAL_ATTENUATION), + _allEchoesAttenuation(DEFAULT_ECHO_ATTENUATION), _withDiffusion(false), _lastPreDelay(DEFAULT_PRE_DELAY), _lastSoundMsPerMeter(DEFAULT_MS_DELAY_PER_METER), @@ -43,20 +47,29 @@ AudioReflector::AudioReflector(QObject* parent) : _lastLocalAudioAttenuationFactor(DEFAULT_LOCAL_ATTENUATION_FACTOR), _lastDiffusionFanout(DEFAULT_DIFFUSION_FANOUT), _lastAbsorptionRatio(DEFAULT_ABSORPTION_RATIO), - _lastDiffusionRatio(DEFAULT_DIFFUSION_RATIO) + _lastDiffusionRatio(DEFAULT_DIFFUSION_RATIO), + _lastDontDistanceAttenuate(false), + _lastAlternateDistanceAttenuate(false) { _reflections = 0; _diffusionPathCount = 0; - _averageAttenuation = 0.0f; - _maxAttenuation = 0.0f; - _minAttenuation = 0.0f; - _averageDelay = 0; - _maxDelay = 0; - _minDelay = 0; + _officialAverageAttenuation = _averageAttenuation = 0.0f; + _officialMaxAttenuation = _maxAttenuation = 0.0f; + _officialMinAttenuation = _minAttenuation = 0.0f; + _officialAverageDelay = _averageDelay = 0; + _officialMaxDelay = _maxDelay = 0; + _officialMinDelay = _minDelay = 0; + _inboundEchoesCount = 0; + _inboundEchoesSuppressedCount = 0; + _localEchoesCount = 0; + _localEchoesSuppressedCount = 0; } bool AudioReflector::haveAttributesChanged() { bool withDiffusion = Menu::getInstance()->isOptionChecked(MenuOption::AudioSpatialProcessingWithDiffusions); + bool dontDistanceAttenuate = Menu::getInstance()->isOptionChecked(MenuOption::AudioSpatialProcessingDontDistanceAttenuate); + bool alternateDistanceAttenuate = Menu::getInstance()->isOptionChecked( + MenuOption::AudioSpatialProcessingAlternateDistanceAttenuate); bool attributesChange = (_withDiffusion != withDiffusion || _lastPreDelay != _preDelay @@ -64,7 +77,9 @@ bool AudioReflector::haveAttributesChanged() { || _lastDistanceAttenuationScalingFactor != _distanceAttenuationScalingFactor || _lastDiffusionFanout != _diffusionFanout || _lastAbsorptionRatio != _absorptionRatio - || _lastDiffusionRatio != _diffusionRatio); + || _lastDiffusionRatio != _diffusionRatio + || _lastDontDistanceAttenuate != dontDistanceAttenuate + || _lastAlternateDistanceAttenuate != alternateDistanceAttenuate); if (attributesChange) { _withDiffusion = withDiffusion; @@ -74,6 +89,8 @@ bool AudioReflector::haveAttributesChanged() { _lastDiffusionFanout = _diffusionFanout; _lastAbsorptionRatio = _absorptionRatio; _lastDiffusionRatio = _diffusionRatio; + _lastDontDistanceAttenuate = dontDistanceAttenuate; + _lastAlternateDistanceAttenuate = alternateDistanceAttenuate; } return attributesChange; @@ -107,19 +124,47 @@ float AudioReflector::getDelayFromDistance(float distance) { // attenuation = from the Audio Mixer float AudioReflector::getDistanceAttenuationCoefficient(float distance) { - const float DISTANCE_SCALE = 2.5f; - const float GEOMETRIC_AMPLITUDE_SCALAR = 0.3f; - const float DISTANCE_LOG_BASE = 2.5f; - const float DISTANCE_SCALE_LOG = logf(DISTANCE_SCALE) / logf(DISTANCE_LOG_BASE); + + + bool doDistanceAttenuation = !Menu::getInstance()->isOptionChecked( + MenuOption::AudioSpatialProcessingDontDistanceAttenuate); + + bool originalFormula = !Menu::getInstance()->isOptionChecked( + MenuOption::AudioSpatialProcessingAlternateDistanceAttenuate); - float distanceSquareToSource = distance * distance; + + float distanceCoefficient = 1.0f; + + if (doDistanceAttenuation) { + + if (originalFormula) { + const float DISTANCE_SCALE = 2.5f; + const float GEOMETRIC_AMPLITUDE_SCALAR = 0.3f; + const float DISTANCE_LOG_BASE = 2.5f; + const float DISTANCE_SCALE_LOG = logf(DISTANCE_SCALE) / logf(DISTANCE_LOG_BASE); + + float distanceSquareToSource = distance * distance; - // calculate the distance coefficient using the distance to this node - float distanceCoefficient = powf(GEOMETRIC_AMPLITUDE_SCALAR, - DISTANCE_SCALE_LOG + - (0.5f * logf(distanceSquareToSource) / logf(DISTANCE_LOG_BASE)) - 1); - - distanceCoefficient = std::min(1.0f, distanceCoefficient * getDistanceAttenuationScalingFactor()); + // calculate the distance coefficient using the distance to this node + distanceCoefficient = powf(GEOMETRIC_AMPLITUDE_SCALAR, + DISTANCE_SCALE_LOG + + (0.5f * logf(distanceSquareToSource) / logf(DISTANCE_LOG_BASE)) - 1); + distanceCoefficient = std::min(1.0f, distanceCoefficient * getDistanceAttenuationScalingFactor()); + } else { + + // From Fred: If we wanted something that would produce a tail that could go up to 5 seconds in a + // really big room, that would suggest the sound still has to be in the audible after traveling about + // 1500 meters. If it’s a sound of average volume, we probably have about 30 db, or 5 base2 orders + // of magnitude we can drop down before the sound becomes inaudible. (That’s approximate headroom + // based on a few sloppy assumptions.) So we could try a factor like 1 / (2^(D/300)) for starters. + // 1 / (2^(D/300)) + const float DISTANCE_BASE = 2.0f; + const float DISTANCE_DENOMINATOR = 300.0f; + const float DISTANCE_NUMERATOR = 300.0f; + distanceCoefficient = DISTANCE_NUMERATOR / powf(DISTANCE_BASE, (distance / DISTANCE_DENOMINATOR )); + distanceCoefficient = std::min(1.0f, distanceCoefficient * getDistanceAttenuationScalingFactor()); + } + } return distanceCoefficient; } @@ -236,11 +281,13 @@ void AudioReflector::injectAudiblePoint(AudioSource source, const AudiblePoint& rightSample = originalSamplesData[(sample * NUMBER_OF_CHANNELS) + 1]; } - attenuatedLeftSamplesData[sample * NUMBER_OF_CHANNELS] = leftSample * leftEarAttenuation; + attenuatedLeftSamplesData[sample * NUMBER_OF_CHANNELS] = + leftSample * leftEarAttenuation * _allEchoesAttenuation; attenuatedLeftSamplesData[sample * NUMBER_OF_CHANNELS + 1] = 0; attenuatedRightSamplesData[sample * NUMBER_OF_CHANNELS] = 0; - attenuatedRightSamplesData[sample * NUMBER_OF_CHANNELS + 1] = rightSample * rightEarAttenuation; + attenuatedRightSamplesData[sample * NUMBER_OF_CHANNELS + 1] = + rightSample * rightEarAttenuation * _allEchoesAttenuation; } // now inject the attenuated array with the appropriate delay @@ -249,9 +296,25 @@ void AudioReflector::injectAudiblePoint(AudioSource source, const AudiblePoint& _audio->addSpatialAudioToBuffer(sampleTimeLeft, attenuatedLeftSamples, totalNumberOfSamples); _audio->addSpatialAudioToBuffer(sampleTimeRight, attenuatedRightSamples, totalNumberOfSamples); + + _injectedEchoes++; } } + +void AudioReflector::preProcessOriginalInboundAudio(unsigned int sampleTime, + QByteArray& samples, const QAudioFormat& format) { + + if (_originalSourceAttenuation != 1.0f) { + int numberOfSamples = (samples.size() / sizeof(int16_t)); + int16_t* sampleData = (int16_t*)samples.data(); + for (int i = 0; i < numberOfSamples; i++) { + sampleData[i] = sampleData[i] * _originalSourceAttenuation; + } + } + +} + void AudioReflector::processLocalAudio(unsigned int sampleTime, const QByteArray& samples, const QAudioFormat& format) { if (Menu::getInstance()->isOptionChecked(MenuOption::AudioSpatialProcessingProcessLocalAudio)) { const int NUM_CHANNELS_INPUT = 1; @@ -272,6 +335,8 @@ void AudioReflector::processLocalAudio(unsigned int sampleTime, const QByteArray _localAudioDelays.clear(); _localEchoesSuppressed.clear(); echoAudio(LOCAL_AUDIO, sampleTime, stereoInputData, outputFormat); + _localEchoesCount = _localAudioDelays.size(); + _localEchoesSuppressedCount = _localEchoesSuppressed.size(); } } } @@ -280,9 +345,13 @@ void AudioReflector::processInboundAudio(unsigned int sampleTime, const QByteArr _inboundAudioDelays.clear(); _inboundEchoesSuppressed.clear(); echoAudio(INBOUND_AUDIO, sampleTime, samples, format); + _inboundEchoesCount = _inboundAudioDelays.size(); + _inboundEchoesSuppressedCount = _inboundEchoesSuppressed.size(); } void AudioReflector::echoAudio(AudioSource source, unsigned int sampleTime, const QByteArray& samples, const QAudioFormat& format) { + QMutexLocker locker(&_mutex); + _maxDelay = 0; _maxAttenuation = 0.0f; _minDelay = std::numeric_limits::max(); @@ -292,14 +361,20 @@ void AudioReflector::echoAudio(AudioSource source, unsigned int sampleTime, cons _totalAttenuation = 0.0f; _attenuationCount = 0; - QMutexLocker locker(&_mutex); - // depending on if we're processing local or external audio, pick the correct points vector QVector& audiblePoints = source == INBOUND_AUDIO ? _inboundAudiblePoints : _localAudiblePoints; + int injectCalls = 0; + _injectedEchoes = 0; foreach(const AudiblePoint& audiblePoint, audiblePoints) { + injectCalls++; injectAudiblePoint(source, audiblePoint, samples, sampleTime, format.sampleRate()); } + + /* + qDebug() << "injectCalls=" << injectCalls; + qDebug() << "_injectedEchoes=" << _injectedEchoes; + */ _averageDelay = _delayCount == 0 ? 0 : _totalDelay / _delayCount; _averageAttenuation = _attenuationCount == 0 ? 0 : _totalAttenuation / _attenuationCount; @@ -308,6 +383,14 @@ void AudioReflector::echoAudio(AudioSource source, unsigned int sampleTime, cons _minDelay = 0.0f; _minAttenuation = 0.0f; } + + _officialMaxDelay = _maxDelay; + _officialMinDelay = _minDelay; + _officialMaxAttenuation = _maxAttenuation; + _officialMinAttenuation = _minAttenuation; + _officialAverageDelay = _averageDelay; + _officialAverageAttenuation = _averageAttenuation; + } void AudioReflector::drawVector(const glm::vec3& start, const glm::vec3& end, const glm::vec3& color) { @@ -359,6 +442,19 @@ void AudioReflector::addAudioPath(AudioSource source, const glm::vec3& origin, c audioPaths.push_back(path); } +// NOTE: This is a prototype of an eventual utility that will identify the speaking sources for the inbound audio +// stream. It's not currently called but will be added soon. +void AudioReflector::identifyAudioSources() { + // looking for audio sources.... + foreach (const AvatarSharedPointer& avatarPointer, _avatarManager->getAvatarHash()) { + Avatar* avatar = static_cast(avatarPointer.data()); + if (!avatar->isInitialized()) { + continue; + } + qDebug() << "avatar["<< avatar <<"] loudness:" << avatar->getAudioLoudness(); + } +} + void AudioReflector::calculateAllReflections() { // only recalculate when we've moved, or if the attributes have changed // TODO: what about case where new voxels are added in front of us??? diff --git a/interface/src/AudioReflector.h b/interface/src/AudioReflector.h index 2408b70a96..582345e064 100644 --- a/interface/src/AudioReflector.h +++ b/interface/src/AudioReflector.h @@ -15,6 +15,7 @@ #include "Audio.h" #include "avatar/MyAvatar.h" +#include "avatar/AvatarManager.h" enum AudioSource { LOCAL_AUDIO, @@ -69,25 +70,27 @@ public: void setVoxels(VoxelTree* voxels) { _voxels = voxels; } void setMyAvatar(MyAvatar* myAvatar) { _myAvatar = myAvatar; } void setAudio(Audio* audio) { _audio = audio; } + void setAvatarManager(AvatarManager* avatarManager) { _avatarManager = avatarManager; } void render(); /// must be called in the application render loop + void preProcessOriginalInboundAudio(unsigned int sampleTime, QByteArray& samples, const QAudioFormat& format); void processInboundAudio(unsigned int sampleTime, const QByteArray& samples, const QAudioFormat& format); void processLocalAudio(unsigned int sampleTime, const QByteArray& samples, const QAudioFormat& format); public slots: // statistics int getReflections() const { return _reflections; } - float getAverageDelayMsecs() const { return _averageDelay; } - float getAverageAttenuation() const { return _averageAttenuation; } - float getMaxDelayMsecs() const { return _maxDelay; } - float getMaxAttenuation() const { return _maxAttenuation; } - float getMinDelayMsecs() const { return _minDelay; } - float getMinAttenuation() const { return _minAttenuation; } + float getAverageDelayMsecs() const { return _officialAverageDelay; } + float getAverageAttenuation() const { return _officialAverageAttenuation; } + float getMaxDelayMsecs() const { return _officialMaxDelay; } + float getMaxAttenuation() const { return _officialMaxAttenuation; } + float getMinDelayMsecs() const { return _officialMinDelay; } + float getMinAttenuation() const { return _officialMinAttenuation; } float getDelayFromDistance(float distance); int getDiffusionPathCount() const { return _diffusionPathCount; } - int getEchoesInjected() const { return _inboundAudioDelays.size() + _localAudioDelays.size(); } - int getEchoesSuppressed() const { return _inboundEchoesSuppressed.size() + _localEchoesSuppressed.size(); } + int getEchoesInjected() const { return _inboundEchoesCount + _localEchoesCount; } + int getEchoesSuppressed() const { return _inboundEchoesSuppressedCount + _localEchoesSuppressedCount; } /// ms of delay added to all echos float getPreDelay() const { return _preDelay; } @@ -126,12 +129,19 @@ public slots: float getReflectiveRatio() const { return (1.0f - (_absorptionRatio + _diffusionRatio)); } void setReflectiveRatio(float ratio); + // wet/dry mix - these don't affect any reflection calculations, only the final mix volumes + float getOriginalSourceAttenuation() const { return _originalSourceAttenuation; } + void setOriginalSourceAttenuation(float value) { _originalSourceAttenuation = value; } + float getEchoesAttenuation() const { return _allEchoesAttenuation; } + void setEchoesAttenuation(float value) { _allEchoesAttenuation = value; } + signals: private: VoxelTree* _voxels; // used to access voxel scene MyAvatar* _myAvatar; // access to listener Audio* _audio; // access to audio API + AvatarManager* _avatarManager; // access to avatar manager API // Helpers for drawing void drawVector(const glm::vec3& start, const glm::vec3& end, const glm::vec3& color); @@ -147,11 +157,18 @@ private: float _averageDelay; float _maxDelay; float _minDelay; + float _officialAverageDelay; + float _officialMaxDelay; + float _officialMinDelay; int _attenuationCount; float _totalAttenuation; float _averageAttenuation; float _maxAttenuation; float _minAttenuation; + float _officialAverageAttenuation; + float _officialMaxAttenuation; + float _officialMinAttenuation; + glm::vec3 _listenerPosition; glm::vec3 _origin; @@ -161,11 +178,15 @@ private: QVector _inboundAudiblePoints; /// the audible points that have been calculated from the inbound audio paths QMap _inboundAudioDelays; /// delay times for currently injected audio points QVector _inboundEchoesSuppressed; /// delay times for currently injected audio points + int _inboundEchoesCount; + int _inboundEchoesSuppressedCount; QVector _localAudioPaths; /// audio paths we're processing for local audio QVector _localAudiblePoints; /// the audible points that have been calculated from the local audio paths QMap _localAudioDelays; /// delay times for currently injected audio points QVector _localEchoesSuppressed; /// delay times for currently injected audio points + int _localEchoesCount; + int _localEchoesSuppressedCount; // adds a sound source to begin an audio path trace, these can be the initial sound sources with their directional properties, // as well as diffusion sound sources @@ -182,6 +203,7 @@ private: void calculateAllReflections(); int countDiffusionPaths(); glm::vec3 getFaceNormal(BoxFace face); + void identifyAudioSources(); void injectAudiblePoint(AudioSource source, const AudiblePoint& audiblePoint, const QByteArray& samples, unsigned int sampleTime, int sampleRate); void echoAudio(AudioSource source, unsigned int sampleTime, const QByteArray& samples, const QAudioFormat& format); @@ -197,13 +219,16 @@ private: float _distanceAttenuationScalingFactor; float _localAudioAttenuationFactor; float _combFilterWindow; - int _diffusionFanout; // number of points of diffusion from each reflection point // all elements have the same material for now... float _absorptionRatio; float _diffusionRatio; float _reflectiveRatio; + + // wet/dry mix - these don't affect any reflection calculations, only the final mix volumes + float _originalSourceAttenuation; /// each sample of original signal will be multiplied by this + float _allEchoesAttenuation; /// each sample of all echo signals will be multiplied by this // remember the last known values at calculation bool haveAttributesChanged(); @@ -216,6 +241,10 @@ private: int _lastDiffusionFanout; float _lastAbsorptionRatio; float _lastDiffusionRatio; + bool _lastDontDistanceAttenuate; + bool _lastAlternateDistanceAttenuate; + + int _injectedEchoes; }; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 85c3d5b0c3..b05e5c91bc 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -429,6 +429,14 @@ Menu::Menu() : Qt::CTRL | Qt::SHIFT | Qt::Key_A, true); + addCheckableActionToQMenuAndActionHash(spatialAudioMenu, MenuOption::AudioSpatialProcessingDontDistanceAttenuate, + Qt::CTRL | Qt::SHIFT | Qt::Key_Y, + false); + + addCheckableActionToQMenuAndActionHash(spatialAudioMenu, MenuOption::AudioSpatialProcessingAlternateDistanceAttenuate, + Qt::CTRL | Qt::SHIFT | Qt::Key_U, + false); + addActionToQMenuAndActionHash(developerMenu, MenuOption::PasteToVoxel, Qt::CTRL | Qt::SHIFT | Qt::Key_V, this, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index a62f54b0c6..c17c9cc507 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -268,6 +268,10 @@ namespace MenuOption { const QString AudioSpatialProcessingSlightlyRandomSurfaces = "Slightly Random Surfaces"; const QString AudioSpatialProcessingStereoSource = "Stereo Source"; const QString AudioSpatialProcessingWithDiffusions = "With Diffusions"; + const QString AudioSpatialProcessingDontDistanceAttenuate = "Don't calculate distance attenuation"; + const QString AudioSpatialProcessingAlternateDistanceAttenuate = "Alternate distance attenuation"; + + const QString Avatars = "Avatars"; const QString Bandwidth = "Bandwidth Display"; diff --git a/interface/src/ModelUploader.cpp b/interface/src/ModelUploader.cpp index edbc6c0ad9..7f4ed0d36e 100644 --- a/interface/src/ModelUploader.cpp +++ b/interface/src/ModelUploader.cpp @@ -22,9 +22,9 @@ #include -#include "Application.h" -#include "renderer/FBXReader.h" +#include +#include "Application.h" #include "ModelUploader.h" @@ -306,14 +306,14 @@ bool ModelUploader::addTextures(const QString& texdir, const QString fbxFile) { foreach (FBXMesh mesh, geometry.meshes) { foreach (FBXMeshPart part, mesh.parts) { - if (!part.diffuseFilename.isEmpty()) { - if (!addPart(texdir + "/" + part.diffuseFilename, + if (!part.diffuseTexture.filename.isEmpty()) { + if (!addPart(texdir + "/" + part.diffuseTexture.filename, QString("texture%1").arg(++_texturesCount))) { return false; } } - if (!part.normalFilename.isEmpty()) { - if (!addPart(texdir + "/" + part.normalFilename, + if (!part.normalTexture.filename.isEmpty()) { + if (!addPart(texdir + "/" + part.normalTexture.filename, QString("texture%1").arg(++_texturesCount))) { return false; } diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index cecc363daa..1dae3a4fd6 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -78,112 +78,6 @@ float angle_to(glm::vec3 head_pos, glm::vec3 source_pos, float render_yaw, float return atan2(head_pos.x - source_pos.x, head_pos.z - source_pos.z) + render_yaw + head_yaw; } -// Helper function returns the positive angle (in radians) between two 3D vectors -float angleBetween(const glm::vec3& v1, const glm::vec3& v2) { - return acosf((glm::dot(v1, v2)) / (glm::length(v1) * glm::length(v2))); -} - -// Helper function return the rotation from the first vector onto the second -glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2) { - float angle = angleBetween(v1, v2); - if (glm::isnan(angle) || angle < EPSILON) { - return glm::quat(); - } - glm::vec3 axis; - if (angle > 179.99f * RADIANS_PER_DEGREE) { // 180 degree rotation; must use another axis - axis = glm::cross(v1, glm::vec3(1.0f, 0.0f, 0.0f)); - float axisLength = glm::length(axis); - if (axisLength < EPSILON) { // parallel to x; y will work - axis = glm::normalize(glm::cross(v1, glm::vec3(0.0f, 1.0f, 0.0f))); - } else { - axis /= axisLength; - } - } else { - axis = glm::normalize(glm::cross(v1, v2)); - } - return glm::angleAxis(angle, axis); -} - - - -glm::vec3 extractTranslation(const glm::mat4& matrix) { - return glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]); -} - -void setTranslation(glm::mat4& matrix, const glm::vec3& translation) { - matrix[3][0] = translation.x; - matrix[3][1] = translation.y; - matrix[3][2] = translation.z; -} - -glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal) { - // uses the iterative polar decomposition algorithm described by Ken Shoemake at - // http://www.cs.wisc.edu/graphics/Courses/838-s2002/Papers/polar-decomp.pdf - // code adapted from Clyde, https://github.com/threerings/clyde/blob/master/src/main/java/com/threerings/math/Matrix4f.java - - // start with the contents of the upper 3x3 portion of the matrix - glm::mat3 upper = glm::mat3(matrix); - if (!assumeOrthogonal) { - for (int i = 0; i < 10; i++) { - // store the results of the previous iteration - glm::mat3 previous = upper; - - // compute average of the matrix with its inverse transpose - float sd00 = previous[1][1] * previous[2][2] - previous[2][1] * previous[1][2]; - float sd10 = previous[0][1] * previous[2][2] - previous[2][1] * previous[0][2]; - float sd20 = previous[0][1] * previous[1][2] - previous[1][1] * previous[0][2]; - float det = previous[0][0] * sd00 + previous[2][0] * sd20 - previous[1][0] * sd10; - if (fabs(det) == 0.0f) { - // determinant is zero; matrix is not invertible - break; - } - float hrdet = 0.5f / det; - upper[0][0] = +sd00 * hrdet + previous[0][0] * 0.5f; - upper[1][0] = -sd10 * hrdet + previous[1][0] * 0.5f; - upper[2][0] = +sd20 * hrdet + previous[2][0] * 0.5f; - - upper[0][1] = -(previous[1][0] * previous[2][2] - previous[2][0] * previous[1][2]) * hrdet + previous[0][1] * 0.5f; - upper[1][1] = +(previous[0][0] * previous[2][2] - previous[2][0] * previous[0][2]) * hrdet + previous[1][1] * 0.5f; - upper[2][1] = -(previous[0][0] * previous[1][2] - previous[1][0] * previous[0][2]) * hrdet + previous[2][1] * 0.5f; - - upper[0][2] = +(previous[1][0] * previous[2][1] - previous[2][0] * previous[1][1]) * hrdet + previous[0][2] * 0.5f; - upper[1][2] = -(previous[0][0] * previous[2][1] - previous[2][0] * previous[0][1]) * hrdet + previous[1][2] * 0.5f; - upper[2][2] = +(previous[0][0] * previous[1][1] - previous[1][0] * previous[0][1]) * hrdet + previous[2][2] * 0.5f; - - // compute the difference; if it's small enough, we're done - glm::mat3 diff = upper - previous; - if (diff[0][0] * diff[0][0] + diff[1][0] * diff[1][0] + diff[2][0] * diff[2][0] + diff[0][1] * diff[0][1] + - diff[1][1] * diff[1][1] + diff[2][1] * diff[2][1] + diff[0][2] * diff[0][2] + diff[1][2] * diff[1][2] + - diff[2][2] * diff[2][2] < EPSILON) { - break; - } - } - } - - // now that we have a nice orthogonal matrix, we can extract the rotation quaternion - // using the method described in http://en.wikipedia.org/wiki/Rotation_matrix#Conversions - float x2 = fabs(1.0f + upper[0][0] - upper[1][1] - upper[2][2]); - float y2 = fabs(1.0f - upper[0][0] + upper[1][1] - upper[2][2]); - float z2 = fabs(1.0f - upper[0][0] - upper[1][1] + upper[2][2]); - float w2 = fabs(1.0f + upper[0][0] + upper[1][1] + upper[2][2]); - return glm::normalize(glm::quat(0.5f * sqrtf(w2), - 0.5f * sqrtf(x2) * (upper[1][2] >= upper[2][1] ? 1.0f : -1.0f), - 0.5f * sqrtf(y2) * (upper[2][0] >= upper[0][2] ? 1.0f : -1.0f), - 0.5f * sqrtf(z2) * (upper[0][1] >= upper[1][0] ? 1.0f : -1.0f))); -} - -glm::vec3 extractScale(const glm::mat4& matrix) { - return glm::vec3(glm::length(matrix[0]), glm::length(matrix[1]), glm::length(matrix[2])); -} - -float extractUniformScale(const glm::mat4& matrix) { - return extractUniformScale(extractScale(matrix)); -} - -float extractUniformScale(const glm::vec3& scale) { - return (scale.x + scale.y + scale.z) / 3.0f; -} - // Draw a 3D vector floating in space void drawVector(glm::vec3 * vector) { glDisable(GL_LIGHTING); @@ -629,4 +523,4 @@ bool pointInSphere(glm::vec3& point, glm::vec3& sphereCenter, double sphereRadiu return true; } return false; -} \ No newline at end of file +} diff --git a/interface/src/Util.h b/interface/src/Util.h index ac680645a9..4bd1ed604c 100644 --- a/interface/src/Util.h +++ b/interface/src/Util.h @@ -44,22 +44,6 @@ void drawVector(glm::vec3* vector); void printVector(glm::vec3 vec); -float angleBetween(const glm::vec3& v1, const glm::vec3& v2); - -glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2); - -glm::vec3 extractTranslation(const glm::mat4& matrix); - -void setTranslation(glm::mat4& matrix, const glm::vec3& translation); - -glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal = false); - -glm::vec3 extractScale(const glm::mat4& matrix); - -float extractUniformScale(const glm::mat4& matrix); - -float extractUniformScale(const glm::vec3& scale); - double diffclock(timeval *clock1,timeval *clock2); void renderCollisionOverlay(int width, int height, float magnitude, float red = 0, float blue = 0, float green = 0); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index e9d804d227..8cfab7da03 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -56,7 +56,6 @@ Avatar::Avatar() : _mouseRayOrigin(0.0f, 0.0f, 0.0f), _mouseRayDirection(0.0f, 0.0f, 0.0f), _moving(false), - _owningAvatarMixer(), _collisionFlags(0), _initialized(false), _shouldRenderBillboard(true) @@ -685,6 +684,11 @@ void Avatar::setBillboard(const QByteArray& billboard) { } int Avatar::parseDataAtOffset(const QByteArray& packet, int offset) { + if (!_initialized) { + // now that we have data for this Avatar we are go for init + init(); + } + // change in position implies movement glm::vec3 oldPosition = _position; diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 4d68f2168b..ecf1be4899 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -99,9 +99,6 @@ public: /// Returns the distance to use as a LOD parameter. float getLODDistance() const; - - Node* getOwningAvatarMixer() { return _owningAvatarMixer.data(); } - void setOwningAvatarMixer(const QWeakPointer& owningAvatarMixer) { _owningAvatarMixer = owningAvatarMixer; } bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance) const; @@ -177,7 +174,6 @@ protected: glm::vec3 _mouseRayDirection; float _stringLength; bool _moving; ///< set when position is changing - QWeakPointer _owningAvatarMixer; uint32_t _collisionFlags; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 5798f33d0f..59f31388f8 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -51,14 +51,16 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { // simulate avatars AvatarHash::iterator avatarIterator = _avatarHash.begin(); while (avatarIterator != _avatarHash.end()) { - Avatar* avatar = static_cast(avatarIterator.value().data()); - if (avatar == static_cast(_myAvatar.data()) || !avatar->isInitialized()) { + AvatarSharedPointer sharedAvatar = avatarIterator.value(); + Avatar* avatar = reinterpret_cast(sharedAvatar.data()); + + if (sharedAvatar == _myAvatar || !avatar->isInitialized()) { // DO NOT update _myAvatar! Its update has already been done earlier in the main loop. // DO NOT update uninitialized Avatars ++avatarIterator; continue; } - if (avatar->getOwningAvatarMixer()) { + if (!shouldKillAvatar(sharedAvatar)) { // this avatar's mixer is still around, go ahead and simulate it avatar->simulate(deltaTime); avatar->setMouseRay(mouseOrigin, mouseDirection); @@ -127,127 +129,12 @@ void AvatarManager::renderAvatarFades(const glm::vec3& cameraPosition, Avatar::R } } -AvatarSharedPointer AvatarManager::matchingOrNewAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { - AvatarSharedPointer matchingAvatar = _avatarHash.value(sessionUUID); - - if (!matchingAvatar) { - // construct a new Avatar for this node - Avatar* avatar = new Avatar(); - avatar->setOwningAvatarMixer(mixerWeakPointer); - - // insert the new avatar into our hash - matchingAvatar = AvatarSharedPointer(avatar); - _avatarHash.insert(sessionUUID, matchingAvatar); - - qDebug() << "Adding avatar with sessionUUID " << sessionUUID << "to AvatarManager hash."; - } - - return matchingAvatar; -} - -void AvatarManager::processAvatarMixerDatagram(const QByteArray& datagram, const QWeakPointer& mixerWeakPointer) { - switch (packetTypeForPacket(datagram)) { - case PacketTypeBulkAvatarData: - processAvatarDataPacket(datagram, mixerWeakPointer); - break; - case PacketTypeAvatarIdentity: - processAvatarIdentityPacket(datagram, mixerWeakPointer); - break; - case PacketTypeAvatarBillboard: - processAvatarBillboardPacket(datagram, mixerWeakPointer); - break; - case PacketTypeKillAvatar: - processKillAvatar(datagram); - break; - default: - break; - } -} - -void AvatarManager::processAvatarDataPacket(const QByteArray &datagram, const QWeakPointer &mixerWeakPointer) { - int bytesRead = numBytesForPacketHeader(datagram); - - // enumerate over all of the avatars in this packet - // only add them if mixerWeakPointer points to something (meaning that mixer is still around) - while (bytesRead < datagram.size() && mixerWeakPointer.data()) { - QUuid sessionUUID = QUuid::fromRfc4122(datagram.mid(bytesRead, NUM_BYTES_RFC4122_UUID)); - bytesRead += NUM_BYTES_RFC4122_UUID; - - AvatarSharedPointer matchingAvatarData = matchingOrNewAvatar(sessionUUID, mixerWeakPointer); - - // have the matching (or new) avatar parse the data from the packet - bytesRead += matchingAvatarData->parseDataAtOffset(datagram, bytesRead); - - Avatar* matchingAvatar = reinterpret_cast(matchingAvatarData.data()); - - if (!matchingAvatar->isInitialized()) { - // now that we have AvatarData for this Avatar we are go for init - matchingAvatar->init(); - } - } -} - -void AvatarManager::processAvatarIdentityPacket(const QByteArray &packet, const QWeakPointer& mixerWeakPointer) { - // setup a data stream to parse the packet - QDataStream identityStream(packet); - identityStream.skipRawData(numBytesForPacketHeader(packet)); - - QUuid sessionUUID; - - while (!identityStream.atEnd()) { - - QUrl faceMeshURL, skeletonURL; - QString displayName; - identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> displayName; - - // mesh URL for a UUID, find avatar in our list - AvatarSharedPointer matchingAvatar = matchingOrNewAvatar(sessionUUID, mixerWeakPointer); - if (matchingAvatar) { - Avatar* avatar = static_cast(matchingAvatar.data()); - - if (avatar->getFaceModelURL() != faceMeshURL) { - avatar->setFaceModelURL(faceMeshURL); - } - - if (avatar->getSkeletonModelURL() != skeletonURL) { - avatar->setSkeletonModelURL(skeletonURL); - } - - if (avatar->getDisplayName() != displayName) { - avatar->setDisplayName(displayName); - } - } - } -} - -void AvatarManager::processAvatarBillboardPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer) { - int headerSize = numBytesForPacketHeader(packet); - QUuid sessionUUID = QUuid::fromRfc4122(QByteArray::fromRawData(packet.constData() + headerSize, NUM_BYTES_RFC4122_UUID)); - - AvatarSharedPointer matchingAvatar = matchingOrNewAvatar(sessionUUID, mixerWeakPointer); - if (matchingAvatar) { - Avatar* avatar = static_cast(matchingAvatar.data()); - QByteArray billboard = packet.mid(headerSize + NUM_BYTES_RFC4122_UUID); - if (avatar->getBillboard() != billboard) { - avatar->setBillboard(billboard); - } - } -} - -void AvatarManager::processKillAvatar(const QByteArray& datagram) { - // read the node id - QUuid sessionUUID = QUuid::fromRfc4122(datagram.mid(numBytesForPacketHeader(datagram), NUM_BYTES_RFC4122_UUID)); - - // remove the avatar with that UUID from our hash, if it exists - AvatarHash::iterator matchedAvatar = _avatarHash.find(sessionUUID); - if (matchedAvatar != _avatarHash.end()) { - erase(matchedAvatar); - } +AvatarSharedPointer AvatarManager::newSharedAvatar() { + return AvatarSharedPointer(new Avatar()); } AvatarHash::iterator AvatarManager::erase(const AvatarHash::iterator& iterator) { if (iterator.key() != MY_AVATAR_KEY) { - qDebug() << "Removing Avatar with UUID" << iterator.key() << "from AvatarManager hash."; if (reinterpret_cast(iterator.value().data())->isInitialized()) { _avatarFades.push_back(iterator.value()); } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 78491b3a5d..048844ddf2 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -22,7 +22,7 @@ class MyAvatar; -class AvatarManager : public QObject, public AvatarHashMap { +class AvatarManager : public AvatarHashMap { Q_OBJECT public: AvatarManager(QObject* parent = 0); @@ -35,23 +35,15 @@ public: void renderAvatars(Avatar::RenderMode renderMode, bool selfAvatarOnly = false); void clearOtherAvatars(); - -public slots: - void processAvatarMixerDatagram(const QByteArray& datagram, const QWeakPointer& mixerWeakPointer); private: AvatarManager(const AvatarManager& other); - - AvatarSharedPointer matchingOrNewAvatar(const QUuid& nodeUUID, const QWeakPointer& mixerWeakPointer); - - void processAvatarDataPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); - void processAvatarIdentityPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); - void processAvatarBillboardPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); - void processKillAvatar(const QByteArray& datagram); void simulateAvatarFades(float deltaTime); void renderAvatarFades(const glm::vec3& cameraPosition, Avatar::RenderMode renderMode); + AvatarSharedPointer newSharedAvatar(); + // virtual override AvatarHash::iterator erase(const AvatarHash::iterator& iterator); diff --git a/interface/src/devices/Faceplus.cpp b/interface/src/devices/Faceplus.cpp index f7f2f1f1bd..f155b994e7 100644 --- a/interface/src/devices/Faceplus.cpp +++ b/interface/src/devices/Faceplus.cpp @@ -15,9 +15,10 @@ #include #endif +#include + #include "Application.h" #include "Faceplus.h" -#include "renderer/FBXReader.h" static int floatVectorMetaTypeId = qRegisterMetaType >(); diff --git a/interface/src/devices/Visage.cpp b/interface/src/devices/Visage.cpp index e2f40e4741..a467d2d4a8 100644 --- a/interface/src/devices/Visage.cpp +++ b/interface/src/devices/Visage.cpp @@ -13,9 +13,10 @@ #include +#include + #include "Application.h" #include "Visage.h" -#include "renderer/FBXReader.h" // this has to go after our normal includes, because its definition of HANDLE conflicts with Qt's #ifdef HAVE_VISAGE diff --git a/interface/src/renderer/GeometryCache.cpp b/interface/src/renderer/GeometryCache.cpp index 7b4eef1ac1..3a410ac5e2 100644 --- a/interface/src/renderer/GeometryCache.cpp +++ b/interface/src/renderer/GeometryCache.cpp @@ -543,14 +543,14 @@ void NetworkGeometry::setGeometry(const FBXGeometry& geometry) { int totalIndices = 0; foreach (const FBXMeshPart& part, mesh.parts) { NetworkMeshPart networkPart; - if (!part.diffuseFilename.isEmpty()) { + if (!part.diffuseTexture.filename.isEmpty()) { networkPart.diffuseTexture = Application::getInstance()->getTextureCache()->getTexture( - _textureBase.resolved(QUrl(part.diffuseFilename)), false, mesh.isEye); + _textureBase.resolved(QUrl(part.diffuseTexture.filename)), false, mesh.isEye, part.diffuseTexture.content); networkPart.diffuseTexture->setLoadPriorities(_loadPriorities); } - if (!part.normalFilename.isEmpty()) { + if (!part.normalTexture.filename.isEmpty()) { networkPart.normalTexture = Application::getInstance()->getTextureCache()->getTexture( - _textureBase.resolved(QUrl(part.normalFilename)), true); + _textureBase.resolved(QUrl(part.normalTexture.filename)), true, false, part.normalTexture.content); networkPart.normalTexture->setLoadPriorities(_loadPriorities); } networkMesh.parts.append(networkPart); diff --git a/interface/src/renderer/GeometryCache.h b/interface/src/renderer/GeometryCache.h index c2d276fb5e..0ad4f73904 100644 --- a/interface/src/renderer/GeometryCache.h +++ b/interface/src/renderer/GeometryCache.h @@ -20,7 +20,7 @@ #include -#include "FBXReader.h" +#include class Model; class NetworkGeometry; diff --git a/interface/src/renderer/TextureCache.cpp b/interface/src/renderer/TextureCache.cpp index 3f523cf4bb..f31e4f9060 100644 --- a/interface/src/renderer/TextureCache.cpp +++ b/interface/src/renderer/TextureCache.cpp @@ -105,13 +105,22 @@ GLuint TextureCache::getBlueTextureID() { return _blueTextureID; } -QSharedPointer TextureCache::getTexture(const QUrl& url, bool normalMap, bool dilatable) { +/// Extra data for creating textures. +class TextureExtra { +public: + bool normalMap; + const QByteArray& content; +}; + +QSharedPointer TextureCache::getTexture(const QUrl& url, bool normalMap, + bool dilatable, const QByteArray& content) { if (!dilatable) { - return ResourceCache::getResource(url, QUrl(), false, &normalMap).staticCast(); + TextureExtra extra = { normalMap, content }; + return ResourceCache::getResource(url, QUrl(), false, &extra).staticCast(); } QSharedPointer texture = _dilatableNetworkTextures.value(url); if (texture.isNull()) { - texture = QSharedPointer(new DilatableNetworkTexture(url), &Resource::allReferencesCleared); + texture = QSharedPointer(new DilatableNetworkTexture(url, content), &Resource::allReferencesCleared); texture->setSelf(texture); texture->setCache(this); _dilatableNetworkTextures.insert(url, texture); @@ -215,7 +224,9 @@ bool TextureCache::eventFilter(QObject* watched, QEvent* event) { QSharedPointer TextureCache::createResource(const QUrl& url, const QSharedPointer& fallback, bool delayLoad, const void* extra) { - return QSharedPointer(new NetworkTexture(url, *(const bool*)extra), &Resource::allReferencesCleared); + const TextureExtra* textureExtra = static_cast(extra); + return QSharedPointer(new NetworkTexture(url, textureExtra->normalMap, textureExtra->content), + &Resource::allReferencesCleared); } QOpenGLFramebufferObject* TextureCache::createFramebufferObject() { @@ -238,8 +249,8 @@ Texture::~Texture() { glDeleteTextures(1, &_id); } -NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap) : - Resource(url), +NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap, const QByteArray& content) : + Resource(url, !content.isEmpty()), _translucent(false) { if (!url.isValid()) { @@ -250,12 +261,19 @@ NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap) : glBindTexture(GL_TEXTURE_2D, getID()); loadSingleColorTexture(normalMap ? OPAQUE_BLUE : OPAQUE_WHITE); glBindTexture(GL_TEXTURE_2D, 0); + + // if we have content, load it after we have our self pointer + if (!content.isEmpty()) { + _startedLoading = true; + QMetaObject::invokeMethod(this, "loadContent", Qt::QueuedConnection, Q_ARG(const QByteArray&, content)); + } } class ImageReader : public QRunnable { public: - ImageReader(const QWeakPointer& texture, QNetworkReply* reply); + ImageReader(const QWeakPointer& texture, QNetworkReply* reply, const QUrl& url = QUrl(), + const QByteArray& content = QByteArray()); virtual void run(); @@ -263,27 +281,37 @@ private: QWeakPointer _texture; QNetworkReply* _reply; + QUrl _url; + QByteArray _content; }; -ImageReader::ImageReader(const QWeakPointer& texture, QNetworkReply* reply) : +ImageReader::ImageReader(const QWeakPointer& texture, QNetworkReply* reply, + const QUrl& url, const QByteArray& content) : _texture(texture), - _reply(reply) { + _reply(reply), + _url(url), + _content(content) { } void ImageReader::run() { QSharedPointer texture = _texture.toStrongRef(); if (texture.isNull()) { - _reply->deleteLater(); + if (_reply) { + _reply->deleteLater(); + } return; } - QUrl url = _reply->url(); - QImage image = QImage::fromData(_reply->readAll()); - _reply->deleteLater(); + if (_reply) { + _url = _reply->url(); + _content = _reply->readAll(); + _reply->deleteLater(); + } + QImage image = QImage::fromData(_content); // enforce a fixed maximum const int MAXIMUM_SIZE = 1024; if (image.width() > MAXIMUM_SIZE || image.height() > MAXIMUM_SIZE) { - qDebug() << "Image greater than maximum size:" << url << image.width() << image.height(); + qDebug() << "Image greater than maximum size:" << _url << image.width() << image.height(); image = image.scaled(MAXIMUM_SIZE, MAXIMUM_SIZE, Qt::KeepAspectRatio); } @@ -315,7 +343,7 @@ void ImageReader::run() { } int imageArea = image.width() * image.height(); if (opaquePixels == imageArea) { - qDebug() << "Image with alpha channel is completely opaque:" << url; + qDebug() << "Image with alpha channel is completely opaque:" << _url; image = image.convertToFormat(QImage::Format_RGB888); } QMetaObject::invokeMethod(texture.data(), "setImage", Q_ARG(const QImage&, image), @@ -327,6 +355,10 @@ void NetworkTexture::downloadFinished(QNetworkReply* reply) { QThreadPool::globalInstance()->start(new ImageReader(_self, reply)); } +void NetworkTexture::loadContent(const QByteArray& content) { + QThreadPool::globalInstance()->start(new ImageReader(_self, NULL, _url, content)); +} + void NetworkTexture::setImage(const QImage& image, bool translucent) { _translucent = translucent; @@ -348,8 +380,8 @@ void NetworkTexture::imageLoaded(const QImage& image) { // nothing by default } -DilatableNetworkTexture::DilatableNetworkTexture(const QUrl& url) : - NetworkTexture(url, false), +DilatableNetworkTexture::DilatableNetworkTexture(const QUrl& url, const QByteArray& content) : + NetworkTexture(url, false, content), _innerRadius(0), _outerRadius(0) { diff --git a/interface/src/renderer/TextureCache.h b/interface/src/renderer/TextureCache.h index e66044d843..f4444b6dfc 100644 --- a/interface/src/renderer/TextureCache.h +++ b/interface/src/renderer/TextureCache.h @@ -44,7 +44,8 @@ public: GLuint getBlueTextureID(); /// Loads a texture from the specified URL. - QSharedPointer getTexture(const QUrl& url, bool normalMap = false, bool dilatable = false); + QSharedPointer getTexture(const QUrl& url, bool normalMap = false, bool dilatable = false, + const QByteArray& content = QByteArray()); /// Returns a pointer to the primary framebuffer object. This render target includes a depth component, and is /// used for scene rendering. @@ -115,7 +116,7 @@ class NetworkTexture : public Resource, public Texture { public: - NetworkTexture(const QUrl& url, bool normalMap); + NetworkTexture(const QUrl& url, bool normalMap, const QByteArray& content); /// Checks whether it "looks like" this texture is translucent /// (majority of pixels neither fully opaque or fully transparent). @@ -124,10 +125,12 @@ public: protected: virtual void downloadFinished(QNetworkReply* reply); - virtual void imageLoaded(const QImage& image); - + + Q_INVOKABLE void loadContent(const QByteArray& content); Q_INVOKABLE void setImage(const QImage& image, bool translucent); + virtual void imageLoaded(const QImage& image); + private: bool _translucent; @@ -139,7 +142,7 @@ class DilatableNetworkTexture : public NetworkTexture { public: - DilatableNetworkTexture(const QUrl& url); + DilatableNetworkTexture(const QUrl& url, const QByteArray& content); /// Returns a pointer to a texture with the requested amount of dilation. QSharedPointer getDilatedTexture(float dilation); diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 64616cbdf8..a2d615e04d 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -343,7 +343,7 @@ void Stats::display( lines = _expanded ? 12 : 3; if (_expanded && Menu::getInstance()->isOptionChecked(MenuOption::AudioSpatialProcessing)) { - lines += 8; // spatial audio processing adds 1 spacing line and 7 extra lines of info + lines += 9; // spatial audio processing adds 1 spacing line and 8 extra lines of info } drawBackground(backgroundColor, horizontalOffset, 0, glWidget->width() - horizontalOffset, lines * STATS_PELS_PER_LINE + 10); @@ -540,11 +540,19 @@ void Stats::display( verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, 0.10f, 0.f, 2.f, reflectionsStatus, color); + + bool distanceAttenuationDisabled = Menu::getInstance()->isOptionChecked( + MenuOption::AudioSpatialProcessingDontDistanceAttenuate); - sprintf(reflectionsStatus, "Attenuation: average %5.3f, max %5.3f, min %5.3f, Factor: %5.3f", + bool alternateDistanceAttenuationEnabled = Menu::getInstance()->isOptionChecked( + MenuOption::AudioSpatialProcessingAlternateDistanceAttenuate); + + sprintf(reflectionsStatus, "Attenuation: average %5.3f, max %5.3f, min %5.3f, %s: %5.3f", audioReflector->getAverageAttenuation(), audioReflector->getMaxAttenuation(), audioReflector->getMinAttenuation(), + (distanceAttenuationDisabled ? "Distance Factor [DISABLED]" : + alternateDistanceAttenuationEnabled ? "Distance Factor [ALTERNATE]" : "Distance Factor [STANARD]"), audioReflector->getDistanceAttenuationScalingFactor()); verticalOffset += STATS_PELS_PER_LINE; @@ -585,6 +593,13 @@ void Stats::display( verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, 0.10f, 0.f, 2.f, reflectionsStatus, color); + sprintf(reflectionsStatus, "Wet/Dry Mix: Original: %5.3f Echoes: %5.3f", + audioReflector->getOriginalSourceAttenuation(), + audioReflector->getEchoesAttenuation()); + + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, 0.10f, 0.f, 2.f, reflectionsStatus, color); + } } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 2f412b5e9a..2e716296ff 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -51,7 +51,9 @@ AvatarData::AvatarData() : _displayNameTargetAlpha(0.0f), _displayNameAlpha(0.0f), _billboard(), - _errorLogExpiry(0) + _errorLogExpiry(0), + _owningAvatarMixer(), + _lastUpdateTimer() { } @@ -193,6 +195,10 @@ bool AvatarData::shouldLogError(const quint64& now) { // read data in packet starting at byte offset and return number of bytes parsed int AvatarData::parseDataAtOffset(const QByteArray& packet, int offset) { + + // reset the last heard timer since we have new data for this AvatarData + _lastUpdateTimer.restart(); + // lazily allocate memory for HeadData in case we're not an Avatar instance if (!_headData) { _headData = new HeadData(this); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 0ac12649cf..d1a63c9a58 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -32,6 +32,7 @@ typedef unsigned long long quint64; #include #include +#include #include #include #include @@ -44,6 +45,7 @@ typedef unsigned long long quint64; #include #include +#include #include "HeadData.h" #include "HandData.h" @@ -99,6 +101,8 @@ class AvatarData : public QObject { Q_PROPERTY(QString skeletonModelURL READ getSkeletonModelURLFromScript WRITE setSkeletonModelURLFromScript) Q_PROPERTY(QString billboardURL READ getBillboardURL WRITE setBillboardFromURL) + Q_PROPERTY(QStringList jointNames READ getJointNames) + Q_PROPERTY(QUuid sessionUUID READ getSessionUUID); public: AvatarData(); @@ -218,6 +222,11 @@ public: QString getSkeletonModelURLFromScript() const { return _skeletonModelURL.toString(); } void setSkeletonModelURLFromScript(const QString& skeletonModelString) { setSkeletonModelURL(QUrl(skeletonModelString)); } + Node* getOwningAvatarMixer() { return _owningAvatarMixer.data(); } + void setOwningAvatarMixer(const QWeakPointer& owningAvatarMixer) { _owningAvatarMixer = owningAvatarMixer; } + + QElapsedTimer& getLastUpdateTimer() { return _lastUpdateTimer; } + virtual float getBoundingRadius() const { return 1.f; } static void setNetworkAccessManager(QNetworkAccessManager* sharedAccessManager) { networkAccessManager = sharedAccessManager; } @@ -276,7 +285,10 @@ protected: static QNetworkAccessManager* networkAccessManager; quint64 _errorLogExpiry; ///< time in future when to log an error - + + QWeakPointer _owningAvatarMixer; + QElapsedTimer _lastUpdateTimer; + /// Loads the joint indices, names from the FST file (if any) virtual void updateJointMappings(); diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 1c2cd4bf92..6b17a3fab8 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -9,11 +9,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "AvatarHashMap.h" AvatarHashMap::AvatarHashMap() : _avatarHash() { + } void AvatarHashMap::insert(const QUuid& id, AvatarSharedPointer avatar) { @@ -22,6 +25,150 @@ void AvatarHashMap::insert(const QUuid& id, AvatarSharedPointer avatar) { } AvatarHash::iterator AvatarHashMap::erase(const AvatarHash::iterator& iterator) { + qDebug() << "Removing Avatar with UUID" << iterator.key() << "from AvatarHashMap."; return _avatarHash.erase(iterator); } +const qint64 AVATAR_SILENCE_THRESHOLD_MSECS = 5 * 1000; + +bool AvatarHashMap::shouldKillAvatar(const AvatarSharedPointer& sharedAvatar) { + return (sharedAvatar->getOwningAvatarMixer() == NULL + || sharedAvatar->getLastUpdateTimer().elapsed() > AVATAR_SILENCE_THRESHOLD_MSECS); +} + +void AvatarHashMap::processAvatarMixerDatagram(const QByteArray& datagram, const QWeakPointer& mixerWeakPointer) { + switch (packetTypeForPacket(datagram)) { + case PacketTypeBulkAvatarData: + processAvatarDataPacket(datagram, mixerWeakPointer); + break; + case PacketTypeAvatarIdentity: + processAvatarIdentityPacket(datagram, mixerWeakPointer); + break; + case PacketTypeAvatarBillboard: + processAvatarBillboardPacket(datagram, mixerWeakPointer); + break; + case PacketTypeKillAvatar: + processKillAvatar(datagram); + break; + default: + break; + } +} + +bool AvatarHashMap::containsAvatarWithDisplayName(const QString& displayName) { + + AvatarHash::iterator avatarIterator = _avatarHash.begin(); + while (avatarIterator != _avatarHash.end()) { + AvatarSharedPointer sharedAvatar = avatarIterator.value(); + if (avatarIterator.value()->getDisplayName() == displayName) { + // this is a match + // check if this avatar should still be around + if (!shouldKillAvatar(sharedAvatar)) { + // we have a match, return true + return true; + } else { + // we should remove this avatar, do that now + erase(avatarIterator); + } + + break; + } else { + ++avatarIterator; + } + } + + // return false, no match + return false; +} + +AvatarSharedPointer AvatarHashMap::newSharedAvatar() { + return AvatarSharedPointer(new AvatarData()); +} + +AvatarSharedPointer AvatarHashMap::matchingOrNewAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { + AvatarSharedPointer matchingAvatar = _avatarHash.value(sessionUUID); + + if (!matchingAvatar) { + // insert the new avatar into our hash + matchingAvatar = newSharedAvatar(); + + qDebug() << "Adding avatar with sessionUUID " << sessionUUID << "to AvatarHashMap."; + _avatarHash.insert(sessionUUID, matchingAvatar); + + matchingAvatar->setOwningAvatarMixer(mixerWeakPointer); + } + + return matchingAvatar; +} + +void AvatarHashMap::processAvatarDataPacket(const QByteArray &datagram, const QWeakPointer &mixerWeakPointer) { + int bytesRead = numBytesForPacketHeader(datagram); + + // enumerate over all of the avatars in this packet + // only add them if mixerWeakPointer points to something (meaning that mixer is still around) + while (bytesRead < datagram.size() && mixerWeakPointer.data()) { + QUuid sessionUUID = QUuid::fromRfc4122(datagram.mid(bytesRead, NUM_BYTES_RFC4122_UUID)); + bytesRead += NUM_BYTES_RFC4122_UUID; + + AvatarSharedPointer matchingAvatarData = matchingOrNewAvatar(sessionUUID, mixerWeakPointer); + + // have the matching (or new) avatar parse the data from the packet + bytesRead += matchingAvatarData->parseDataAtOffset(datagram, bytesRead); + } +} + +void AvatarHashMap::processAvatarIdentityPacket(const QByteArray &packet, const QWeakPointer& mixerWeakPointer) { + // setup a data stream to parse the packet + QDataStream identityStream(packet); + identityStream.skipRawData(numBytesForPacketHeader(packet)); + + QUuid sessionUUID; + + while (!identityStream.atEnd()) { + + QUrl faceMeshURL, skeletonURL; + QString displayName; + identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> displayName; + + // mesh URL for a UUID, find avatar in our list + AvatarSharedPointer matchingAvatar = matchingOrNewAvatar(sessionUUID, mixerWeakPointer); + if (matchingAvatar) { + + if (matchingAvatar->getFaceModelURL() != faceMeshURL) { + matchingAvatar->setFaceModelURL(faceMeshURL); + } + + if (matchingAvatar->getSkeletonModelURL() != skeletonURL) { + matchingAvatar->setSkeletonModelURL(skeletonURL); + } + + if (matchingAvatar->getDisplayName() != displayName) { + matchingAvatar->setDisplayName(displayName); + } + } + } +} + +void AvatarHashMap::processAvatarBillboardPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer) { + int headerSize = numBytesForPacketHeader(packet); + QUuid sessionUUID = QUuid::fromRfc4122(QByteArray::fromRawData(packet.constData() + headerSize, NUM_BYTES_RFC4122_UUID)); + + AvatarSharedPointer matchingAvatar = matchingOrNewAvatar(sessionUUID, mixerWeakPointer); + if (matchingAvatar) { + QByteArray billboard = packet.mid(headerSize + NUM_BYTES_RFC4122_UUID); + if (matchingAvatar->getBillboard() != billboard) { + matchingAvatar->setBillboard(billboard); + } + } +} + +void AvatarHashMap::processKillAvatar(const QByteArray& datagram) { + // read the node id + QUuid sessionUUID = QUuid::fromRfc4122(datagram.mid(numBytesForPacketHeader(datagram), NUM_BYTES_RFC4122_UUID)); + + // remove the avatar with that UUID from our hash, if it exists + AvatarHash::iterator matchedAvatar = _avatarHash.find(sessionUUID); + if (matchedAvatar != _avatarHash.end()) { + erase(matchedAvatar); + } +} \ No newline at end of file diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index aee9cd09f1..542a2d62ab 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -16,12 +16,15 @@ #include #include +#include + #include "AvatarData.h" typedef QSharedPointer AvatarSharedPointer; typedef QHash AvatarHash; -class AvatarHashMap { +class AvatarHashMap : public QObject { + Q_OBJECT public: AvatarHashMap(); @@ -29,9 +32,23 @@ public: int size() const { return _avatarHash.size(); } virtual void insert(const QUuid& id, AvatarSharedPointer avatar); + +public slots: + void processAvatarMixerDatagram(const QByteArray& datagram, const QWeakPointer& mixerWeakPointer); + bool containsAvatarWithDisplayName(const QString& displayName); protected: virtual AvatarHash::iterator erase(const AvatarHash::iterator& iterator); + + bool shouldKillAvatar(const AvatarSharedPointer& sharedAvatar); + + virtual AvatarSharedPointer newSharedAvatar(); + AvatarSharedPointer matchingOrNewAvatar(const QUuid& nodeUUID, const QWeakPointer& mixerWeakPointer); + + void processAvatarDataPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); + void processAvatarIdentityPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); + void processAvatarBillboardPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); + void processKillAvatar(const QByteArray& datagram); AvatarHash _avatarHash; }; diff --git a/libraries/fbx/CMakeLists.txt b/libraries/fbx/CMakeLists.txt new file mode 100644 index 0000000000..10dd3f49f5 --- /dev/null +++ b/libraries/fbx/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 2.8) + +if (WIN32) + cmake_policy (SET CMP0020 NEW) +endif (WIN32) + +set(ROOT_DIR ../..) +set(MACRO_DIR "${ROOT_DIR}/cmake/macros") + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +set(TARGET_NAME fbx) + +include(${MACRO_DIR}/SetupHifiLibrary.cmake) +setup_hifi_library(${TARGET_NAME}) + +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} "${ROOT_DIR}") + +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") + +# link ZLIB and GnuTLS +find_package(ZLIB) +find_package(GnuTLS REQUIRED) + +# add a definition for ssize_t so that windows doesn't bail on gnutls.h +if (WIN32) + add_definitions(-Dssize_t=long) +endif () + + +include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}" "${GNUTLS_INCLUDE_DIR}") +target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets "${GNUTLS_LIBRARY}") diff --git a/interface/src/renderer/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp similarity index 92% rename from interface/src/renderer/FBXReader.cpp rename to libraries/fbx/src/FBXReader.cpp index ff3fa8667f..7692d81eb9 100644 --- a/interface/src/renderer/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -22,14 +22,14 @@ #include #include -#include - #include +#include #include +#include + #include #include "FBXReader.h" -#include "Util.h" using namespace std; @@ -72,6 +72,8 @@ bool FBXGeometry::hasBlendedMeshes() const { } static int fbxGeometryMetaTypeId = qRegisterMetaType(); +static int fbxAnimationFrameMetaTypeId = qRegisterMetaType(); +static int fbxAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); template QVariant readBinaryArray(QDataStream& in) { quint32 arrayLength; @@ -444,6 +446,34 @@ QVector getIntVector(const QVariantList& properties, int index) { return vector; } +QVector getLongVector(const QVariantList& properties, int index) { + if (index >= properties.size()) { + return QVector(); + } + QVector vector = properties.at(index).value >(); + if (!vector.isEmpty()) { + return vector; + } + for (; index < properties.size(); index++) { + vector.append(properties.at(index).toLongLong()); + } + return vector; +} + +QVector getFloatVector(const QVariantList& properties, int index) { + if (index >= properties.size()) { + return QVector(); + } + QVector vector = properties.at(index).value >(); + if (!vector.isEmpty()) { + return vector; + } + for (; index < properties.size(); index++) { + vector.append(properties.at(index).toFloat()); + } + return vector; +} + QVector getDoubleVector(const QVariantList& properties, int index) { if (index >= properties.size()) { return QVector(); @@ -478,8 +508,7 @@ glm::vec3 parseVec3(const QString& string) { QString processID(const QString& id) { // Blender (at least) prepends a type to the ID, so strip it out - int index = id.indexOf("::"); - return (index == -1) ? id : id.mid(index + 2); + return id.mid(id.lastIndexOf(':') + 1); } QString getID(const QVariantList& properties, int index = 0) { @@ -901,6 +930,19 @@ public: float averageRadius; // average distance from mesh points to averageVertex }; +class AnimationCurve { +public: + QVector values; +}; + +FBXTexture getTexture(const QString& textureID, const QHash& textureFilenames, + const QHash& textureContent) { + FBXTexture texture; + texture.filename = textureFilenames.value(textureID); + texture.content = textureContent.value(texture.filename); + return texture; +} + FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) { QHash meshes; QVector blendshapes; @@ -908,10 +950,16 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) QMultiHash childMap; QHash models; QHash clusters; + QHash animationCurves; QHash textureFilenames; + QHash textureContent; QHash materials; QHash diffuseTextures; QHash bumpTextures; + QHash localRotations; + QHash xComponents; + QHash yComponents; + QHash zComponents; QVariantHash joints = mapping.value("joint").toHash(); QString jointEyeLeftName = processID(getString(joints.value("jointEyeLeft", "jointEyeLeft"))); @@ -974,7 +1022,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) QString name; if (object.properties.size() == 3) { name = object.properties.at(1).toString(); - name = name.left(name.indexOf(QChar('\0'))); + name = processID(name.left(name.indexOf(QChar('\0')))); } else { name = getID(object.properties); @@ -1124,7 +1172,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) model.postRotation = glm::quat(glm::radians(postRotation)); model.postTransform = glm::translate(-rotationPivot) * glm::translate(scalePivot) * glm::scale(scale) * glm::translate(-scalePivot); - // NOTE: anbgles from the FBX file are in degrees + // NOTE: angles from the FBX file are in degrees // so we convert them to radians for the FBXModel class model.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f, rotationMinY ? rotationMin.y : -180.0f, rotationMinZ ? rotationMin.z : -180.0f)); @@ -1141,6 +1189,21 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) textureFilenames.insert(getID(object.properties), filename); } } + } else if (object.name == "Video") { + QByteArray filename; + QByteArray content; + foreach (const FBXNode& subobject, object.children) { + if (subobject.name == "RelativeFilename") { + filename = subobject.properties.at(0).toByteArray(); + filename = filename.mid(qMax(filename.lastIndexOf('\\'), filename.lastIndexOf('/')) + 1); + + } else if (subobject.name == "Content" && !subobject.properties.isEmpty()) { + content = subobject.properties.at(0).toByteArray(); + } + } + if (!content.isEmpty()) { + textureContent.insert(filename, content); + } } else if (object.name == "Material") { Material material = { glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(1.0f, 1.0f, 1.0f), 96.0f }; foreach (const FBXNode& subobject, object.children) { @@ -1204,6 +1267,14 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) blendshapeChannelIndices.insert(id, index); } } + } else if (object.name == "AnimationCurve") { + AnimationCurve curve; + foreach (const FBXNode& subobject, object.children) { + if (subobject.name == "KeyValueFloat") { + curve.values = getFloatVector(subobject.properties, 0); + } + } + animationCurves.insert(getID(object.properties), curve); } } } else if (child.name == "Connections") { @@ -1214,8 +1285,20 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) if (type.contains("diffuse")) { diffuseTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); - } else if (type.contains("bump")) { + } else if (type.contains("bump") || type.contains("normal")) { bumpTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + + } else if (type == "lcl rotation") { + localRotations.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + + } else if (type == "d|x") { + xComponents.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + + } else if (type == "d|y") { + yComponents.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + + } else if (type == "d|z") { + zComponents.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } } parentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); @@ -1239,7 +1322,8 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(), mapping.value("ry").toFloat(), mapping.value("rz").toFloat()))); geometry.offset = glm::translate(glm::vec3(mapping.value("tx").toFloat(), mapping.value("ty").toFloat(), - mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); + mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * + glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); // get the list of models in depth-first traversal order QVector modelIDs; @@ -1277,6 +1361,17 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) appendModelIDs(parentMap.value(topID), childMap, models, remainingModels, modelIDs); } + // figure the number of animation frames from the curves + int frameCount = 0; + foreach (const AnimationCurve& curve, animationCurves) { + frameCount = qMax(frameCount, curve.values.size()); + } + for (int i = 0; i < frameCount; i++) { + FBXAnimationFrame frame; + frame.rotations.resize(modelIDs.size()); + geometry.animationFrames.append(frame); + } + // convert the models to joints QVariantList freeJoints = mapping.values("freeJoint"); foreach (const QString& modelID, modelIDs) { @@ -1286,7 +1381,8 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) joint.parentIndex = model.parentIndex; // get the indices of all ancestors starting with the first free one (if any) - joint.freeLineage.append(geometry.joints.size()); + int jointIndex = geometry.joints.size(); + joint.freeLineage.append(jointIndex); int lastFreeIndex = joint.isFree ? 0 : -1; for (int index = joint.parentIndex; index != -1; index = geometry.joints.at(index).parentIndex) { if (geometry.joints.at(index).isFree) { @@ -1325,6 +1421,18 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) joint.shapeType = Shape::UNKNOWN_SHAPE; geometry.joints.append(joint); geometry.jointIndices.insert(model.name, geometry.joints.size()); + + QString rotationID = localRotations.value(modelID); + AnimationCurve xCurve = animationCurves.value(xComponents.value(rotationID)); + AnimationCurve yCurve = animationCurves.value(yComponents.value(rotationID)); + AnimationCurve zCurve = animationCurves.value(zComponents.value(rotationID)); + glm::vec3 defaultValues = glm::degrees(safeEulerAngles(joint.rotation)); + for (int i = 0; i < frameCount; i++) { + geometry.animationFrames[i].rotations[jointIndex] = glm::quat(glm::radians(glm::vec3( + xCurve.values.isEmpty() ? defaultValues.x : xCurve.values.at(i % xCurve.values.size()), + yCurve.values.isEmpty() ? defaultValues.y : yCurve.values.at(i % yCurve.values.size()), + zCurve.values.isEmpty() ? defaultValues.z : zCurve.values.at(i % zCurve.values.size())))); + } } // for each joint we allocate a JointShapeInfo in which we'll store collision shape info QVector jointShapeInfos; @@ -1377,23 +1485,23 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) if (materials.contains(childID)) { Material material = materials.value(childID); - QByteArray diffuseFilename; + FBXTexture diffuseTexture; QString diffuseTextureID = diffuseTextures.value(childID); if (!diffuseTextureID.isNull()) { - diffuseFilename = textureFilenames.value(diffuseTextureID); - + diffuseTexture = getTexture(diffuseTextureID, textureFilenames, textureContent); + // FBX files generated by 3DSMax have an intermediate texture parent, apparently foreach (const QString& childTextureID, childMap.values(diffuseTextureID)) { if (textureFilenames.contains(childTextureID)) { - diffuseFilename = textureFilenames.value(childTextureID); + diffuseTexture = getTexture(diffuseTextureID, textureFilenames, textureContent); } } } - QByteArray normalFilename; + FBXTexture normalTexture; QString bumpTextureID = bumpTextures.value(childID); if (!bumpTextureID.isNull()) { - normalFilename = textureFilenames.value(bumpTextureID); + normalTexture = getTexture(bumpTextureID, textureFilenames, textureContent); generateTangents = true; } @@ -1403,21 +1511,21 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) part.diffuseColor = material.diffuse; part.specularColor = material.specular; part.shininess = material.shininess; - if (!diffuseFilename.isNull()) { - part.diffuseFilename = diffuseFilename; + if (!diffuseTexture.filename.isNull()) { + part.diffuseTexture = diffuseTexture; } - if (!normalFilename.isNull()) { - part.normalFilename = normalFilename; + if (!normalTexture.filename.isNull()) { + part.normalTexture = normalTexture; } } } materialIndex++; } else if (textureFilenames.contains(childID)) { - QByteArray filename = textureFilenames.value(childID); + FBXTexture texture = getTexture(childID, textureFilenames, textureContent); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { if (extracted.partMaterialTextures.at(j).second == textureIndex) { - extracted.mesh.parts[j].diffuseFilename = filename; + extracted.mesh.parts[j].diffuseTexture = texture; } } textureIndex++; diff --git a/interface/src/renderer/FBXReader.h b/libraries/fbx/src/FBXReader.h similarity index 91% rename from interface/src/renderer/FBXReader.h rename to libraries/fbx/src/FBXReader.h index 366ab12180..e437961385 100644 --- a/interface/src/renderer/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -105,6 +105,14 @@ public: glm::mat4 inverseBindMatrix; }; +/// A texture map in an FBX document. +class FBXTexture { +public: + + QByteArray filename; + QByteArray content; +}; + /// A single part of a mesh (with the same material). class FBXMeshPart { public: @@ -116,8 +124,8 @@ public: glm::vec3 specularColor; float shininess; - QByteArray diffuseFilename; - QByteArray normalFilename; + FBXTexture diffuseTexture; + FBXTexture normalTexture; }; /// A single mesh (with optional blendshapes) extracted from an FBX document. @@ -141,6 +149,16 @@ public: QVector blendshapes; }; +/// A single animation frame extracted from an FBX document. +class FBXAnimationFrame { +public: + + QVector rotations; +}; + +Q_DECLARE_METATYPE(FBXAnimationFrame) +Q_DECLARE_METATYPE(QVector) + /// An attachment to an FBX document. class FBXAttachment { public: @@ -185,6 +203,8 @@ public: Extents bindExtents; Extents meshExtents; + QVector animationFrames; + QVector attachments; int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; } diff --git a/libraries/particles/CMakeLists.txt b/libraries/particles/CMakeLists.txt index 26d2b7fc26..1cb60756a2 100644 --- a/libraries/particles/CMakeLists.txt +++ b/libraries/particles/CMakeLists.txt @@ -23,6 +23,7 @@ include_glm(${TARGET_NAME} "${ROOT_DIR}") include(${MACRO_DIR}/LinkHifiLibrary.cmake) link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") # link ZLIB and GnuTLS @@ -35,4 +36,4 @@ if (WIN32) endif () include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}" "${GNUTLS_INCLUDE_DIR}") -target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets "${GNUTLS_LIBRARY}") \ No newline at end of file +target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets "${GNUTLS_LIBRARY}") diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 20569e2fe0..48d13e7742 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -24,6 +24,7 @@ include(${MACRO_DIR}/LinkHifiLibrary.cmake) link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}") # link ZLIB @@ -36,4 +37,4 @@ if (WIN32) endif () include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}" "${GNUTLS_INCLUDE_DIR}") -target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" "${GNUTLS_LIBRARY}" Qt5::Widgets) \ No newline at end of file +target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" "${GNUTLS_LIBRARY}" Qt5::Widgets) diff --git a/libraries/script-engine/src/AnimationCache.cpp b/libraries/script-engine/src/AnimationCache.cpp new file mode 100644 index 0000000000..8e1493f075 --- /dev/null +++ b/libraries/script-engine/src/AnimationCache.cpp @@ -0,0 +1,102 @@ +// +// AnimationCache.cpp +// libraries/script-engine/src/ +// +// Created by Andrzej Kapolka on 4/14/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include + +#include "AnimationCache.h" + +static int animationPointerMetaTypeId = qRegisterMetaType(); + +AnimationCache::AnimationCache(QObject* parent) : + ResourceCache(parent) { +} + +AnimationPointer AnimationCache::getAnimation(const QUrl& url) { + if (QThread::currentThread() != thread()) { + AnimationPointer result; + QMetaObject::invokeMethod(this, "getAnimation", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(AnimationPointer, result), Q_ARG(const QUrl&, url)); + return result; + } + return getResource(url).staticCast(); +} + +QSharedPointer AnimationCache::createResource(const QUrl& url, const QSharedPointer& fallback, + bool delayLoad, const void* extra) { + return QSharedPointer(new Animation(url), &Resource::allReferencesCleared); +} + +Animation::Animation(const QUrl& url) : + Resource(url) { +} + +class AnimationReader : public QRunnable { +public: + + AnimationReader(const QWeakPointer& animation, QNetworkReply* reply); + + virtual void run(); + +private: + + QWeakPointer _animation; + QNetworkReply* _reply; +}; + +AnimationReader::AnimationReader(const QWeakPointer& animation, QNetworkReply* reply) : + _animation(animation), + _reply(reply) { +} + +void AnimationReader::run() { + QSharedPointer animation = _animation.toStrongRef(); + if (!animation.isNull()) { + QMetaObject::invokeMethod(animation.data(), "setGeometry", + Q_ARG(const FBXGeometry&, readFBX(_reply->readAll(), QVariantHash()))); + } + _reply->deleteLater(); +} + +QStringList Animation::getJointNames() const { + if (QThread::currentThread() != thread()) { + QStringList result; + QMetaObject::invokeMethod(const_cast(this), "getJointNames", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QStringList, result)); + return result; + } + QStringList names; + foreach (const FBXJoint& joint, _geometry.joints) { + names.append(joint.name); + } + return names; +} + +QVector Animation::getFrames() const { + if (QThread::currentThread() != thread()) { + QVector result; + QMetaObject::invokeMethod(const_cast(this), "getFrames", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QVector, result)); + return result; + } + return _geometry.animationFrames; +} + +void Animation::setGeometry(const FBXGeometry& geometry) { + _geometry = geometry; + finishedLoading(true); +} + +void Animation::downloadFinished(QNetworkReply* reply) { + // send the reader off to the thread pool + QThreadPool::globalInstance()->start(new AnimationReader(_self, reply)); +} + diff --git a/libraries/script-engine/src/AnimationCache.h b/libraries/script-engine/src/AnimationCache.h new file mode 100644 index 0000000000..23183adf10 --- /dev/null +++ b/libraries/script-engine/src/AnimationCache.h @@ -0,0 +1,68 @@ +// +// AnimationCache.h +// libraries/script-engine/src/ +// +// Created by Andrzej Kapolka on 4/14/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// 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_AnimationCache_h +#define hifi_AnimationCache_h + +#include + +#include + +class Animation; + +typedef QSharedPointer AnimationPointer; + +/// Scriptable interface for FBX animation loading. +class AnimationCache : public ResourceCache { + Q_OBJECT + +public: + + AnimationCache(QObject* parent = NULL); + + Q_INVOKABLE AnimationPointer getAnimation(const QString& url) { return getAnimation(QUrl(url)); } + + Q_INVOKABLE AnimationPointer getAnimation(const QUrl& url); + +protected: + + virtual QSharedPointer createResource(const QUrl& url, + const QSharedPointer& fallback, bool delayLoad, const void* extra); +}; + +Q_DECLARE_METATYPE(AnimationPointer) + +/// An animation loaded from the network. +class Animation : public Resource { + Q_OBJECT + +public: + + Animation(const QUrl& url); + + const FBXGeometry& getGeometry() const { return _geometry; } + + Q_INVOKABLE QStringList getJointNames() const; + + Q_INVOKABLE QVector getFrames() const; + +protected: + + Q_INVOKABLE void setGeometry(const FBXGeometry& geometry); + + virtual void downloadFinished(QNetworkReply* reply); + +private: + + FBXGeometry _geometry; +}; + +#endif // hifi_AnimationCache_h diff --git a/libraries/script-engine/src/AnimationObject.cpp b/libraries/script-engine/src/AnimationObject.cpp new file mode 100644 index 0000000000..ede1e82623 --- /dev/null +++ b/libraries/script-engine/src/AnimationObject.cpp @@ -0,0 +1,36 @@ +// +// AnimationObject.cpp +// libraries/script-engine/src/ +// +// Created by Andrzej Kapolka on 4/17/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "AnimationCache.h" +#include "AnimationObject.h" + +QStringList AnimationObject::getJointNames() const { + return qscriptvalue_cast(thisObject())->getJointNames(); +} + +QVector AnimationObject::getFrames() const { + return qscriptvalue_cast(thisObject())->getFrames(); +} + +QVector AnimationFrameObject::getRotations() const { + return qscriptvalue_cast(thisObject()).rotations; +} + +void registerAnimationTypes(QScriptEngine* engine) { + qScriptRegisterSequenceMetaType >(engine); + engine->setDefaultPrototype(qMetaTypeId(), engine->newQObject( + new AnimationFrameObject(), QScriptEngine::ScriptOwnership)); + engine->setDefaultPrototype(qMetaTypeId(), engine->newQObject( + new AnimationObject(), QScriptEngine::ScriptOwnership)); +} + diff --git a/libraries/script-engine/src/AnimationObject.h b/libraries/script-engine/src/AnimationObject.h new file mode 100644 index 0000000000..078fc31fb3 --- /dev/null +++ b/libraries/script-engine/src/AnimationObject.h @@ -0,0 +1,47 @@ +// +// AnimationObject.h +// libraries/script-engine/src/ +// +// Created by Andrzej Kapolka on 4/17/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// 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_AnimationObject_h +#define hifi_AnimationObject_h + +#include +#include + +#include + +class QScriptEngine; + +/// Scriptable wrapper for animation pointers. +class AnimationObject : public QObject, protected QScriptable { + Q_OBJECT + Q_PROPERTY(QStringList jointNames READ getJointNames) + Q_PROPERTY(QVector frames READ getFrames) + +public: + + Q_INVOKABLE QStringList getJointNames() const; + + Q_INVOKABLE QVector getFrames() const; +}; + +/// Scriptable wrapper for animation frames. +class AnimationFrameObject : public QObject, protected QScriptable { + Q_OBJECT + Q_PROPERTY(QVector rotations READ getRotations) + +public: + + Q_INVOKABLE QVector getRotations() const; +}; + +void registerAnimationTypes(QScriptEngine* engine); + +#endif // hifi_AnimationObject_h diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index b5291a6a2f..684c55fbb0 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -28,6 +28,7 @@ #include +#include "AnimationObject.h" #include "MenuItemProperties.h" #include "LocalVoxels.h" #include "ScriptEngine.h" @@ -64,7 +65,8 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam _fileNameString(fileNameString), _quatLibrary(), _vec3Library(), - _uuidLibrary() + _uuidLibrary(), + _animationCache(this) { } @@ -88,7 +90,8 @@ ScriptEngine::ScriptEngine(const QUrl& scriptURL, _fileNameString(), _quatLibrary(), _vec3Library(), - _uuidLibrary() + _uuidLibrary(), + _animationCache(this) { QString scriptURLString = scriptURL.toString(); _fileNameString = scriptURLString; @@ -153,6 +156,14 @@ void ScriptEngine::setAvatarData(AvatarData* avatarData, const QString& objectNa registerGlobalObject(objectName, _avatarData); } +void ScriptEngine::setAvatarHashMap(AvatarHashMap* avatarHashMap, const QString& objectName) { + // remove the old Avatar property, if it exists + _engine.globalObject().setProperty(objectName, QScriptValue()); + + // give the script engine the new avatar hash map + registerGlobalObject(objectName, avatarHashMap); +} + bool ScriptEngine::setScriptContents(const QString& scriptContents, const QString& fileNameString) { if (_isRunning) { return false; @@ -180,11 +191,13 @@ void ScriptEngine::init() { registerVoxelMetaTypes(&_engine); registerEventTypes(&_engine); registerMenuItemProperties(&_engine); + registerAnimationTypes(&_engine); qScriptRegisterMetaType(&_engine, ParticlePropertiesToScriptValue, ParticlePropertiesFromScriptValue); qScriptRegisterMetaType(&_engine, ParticleIDtoScriptValue, ParticleIDfromScriptValue); qScriptRegisterSequenceMetaType >(&_engine); qScriptRegisterSequenceMetaType >(&_engine); + qScriptRegisterSequenceMetaType >(&_engine); qScriptRegisterSequenceMetaType >(&_engine); QScriptValue soundConstructorValue = _engine.newFunction(soundConstructor); @@ -204,7 +217,8 @@ void ScriptEngine::init() { registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); registerGlobalObject("Uuid", &_uuidLibrary); - + registerGlobalObject("AnimationCache", &_animationCache); + registerGlobalObject("Voxels", &_voxelsScriptingInterface); QScriptValue treeScaleValue = _engine.newVariant(QVariant(TREE_SCALE)); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 9719c83107..941c6bbe27 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -22,7 +22,9 @@ #include #include +#include +#include "AnimationCache.h" #include "AbstractControllerScriptingInterface.h" #include "Quat.h" #include "ScriptUUID.h" @@ -62,6 +64,7 @@ public: bool isAvatar() const { return _isAvatar; } void setAvatarData(AvatarData* avatarData, const QString& objectName); + void setAvatarHashMap(AvatarHashMap* avatarHashMap, const QString& objectName); bool isListeningToAudioStream() const { return _isListeningToAudioStream; } void setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; } @@ -125,6 +128,7 @@ private: Quat _quatLibrary; Vec3 _vec3Library; ScriptUUID _uuidLibrary; + AnimationCache _animationCache; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/shared/src/ResourceCache.cpp b/libraries/shared/src/ResourceCache.cpp index 30a725c010..04b6265513 100644 --- a/libraries/shared/src/ResourceCache.cpp +++ b/libraries/shared/src/ResourceCache.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -174,6 +175,10 @@ float Resource::getLoadPriority() { } void Resource::allReferencesCleared() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "allReferencesCleared"); + return; + } if (_cache) { // create and reinsert new shared pointer QSharedPointer self(this, &Resource::allReferencesCleared); diff --git a/libraries/shared/src/ResourceCache.h b/libraries/shared/src/ResourceCache.h index da217516e1..0cfabd26fc 100644 --- a/libraries/shared/src/ResourceCache.h +++ b/libraries/shared/src/ResourceCache.h @@ -123,7 +123,7 @@ public: void setCache(ResourceCache* cache) { _cache = cache; } - void allReferencesCleared(); + Q_INVOKABLE void allReferencesCleared(); protected slots: diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 956f78204f..f4e4b28f93 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -663,6 +663,110 @@ glm::vec3 safeEulerAngles(const glm::quat& q) { } } +// Helper function returns the positive angle (in radians) between two 3D vectors +float angleBetween(const glm::vec3& v1, const glm::vec3& v2) { + return acosf((glm::dot(v1, v2)) / (glm::length(v1) * glm::length(v2))); +} + +// Helper function return the rotation from the first vector onto the second +glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2) { + float angle = angleBetween(v1, v2); + if (glm::isnan(angle) || angle < EPSILON) { + return glm::quat(); + } + glm::vec3 axis; + if (angle > 179.99f * RADIANS_PER_DEGREE) { // 180 degree rotation; must use another axis + axis = glm::cross(v1, glm::vec3(1.0f, 0.0f, 0.0f)); + float axisLength = glm::length(axis); + if (axisLength < EPSILON) { // parallel to x; y will work + axis = glm::normalize(glm::cross(v1, glm::vec3(0.0f, 1.0f, 0.0f))); + } else { + axis /= axisLength; + } + } else { + axis = glm::normalize(glm::cross(v1, v2)); + } + return glm::angleAxis(angle, axis); +} + +glm::vec3 extractTranslation(const glm::mat4& matrix) { + return glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]); +} + +void setTranslation(glm::mat4& matrix, const glm::vec3& translation) { + matrix[3][0] = translation.x; + matrix[3][1] = translation.y; + matrix[3][2] = translation.z; +} + +glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal) { + // uses the iterative polar decomposition algorithm described by Ken Shoemake at + // http://www.cs.wisc.edu/graphics/Courses/838-s2002/Papers/polar-decomp.pdf + // code adapted from Clyde, https://github.com/threerings/clyde/blob/master/src/main/java/com/threerings/math/Matrix4f.java + + // start with the contents of the upper 3x3 portion of the matrix + glm::mat3 upper = glm::mat3(matrix); + if (!assumeOrthogonal) { + for (int i = 0; i < 10; i++) { + // store the results of the previous iteration + glm::mat3 previous = upper; + + // compute average of the matrix with its inverse transpose + float sd00 = previous[1][1] * previous[2][2] - previous[2][1] * previous[1][2]; + float sd10 = previous[0][1] * previous[2][2] - previous[2][1] * previous[0][2]; + float sd20 = previous[0][1] * previous[1][2] - previous[1][1] * previous[0][2]; + float det = previous[0][0] * sd00 + previous[2][0] * sd20 - previous[1][0] * sd10; + if (fabs(det) == 0.0f) { + // determinant is zero; matrix is not invertible + break; + } + float hrdet = 0.5f / det; + upper[0][0] = +sd00 * hrdet + previous[0][0] * 0.5f; + upper[1][0] = -sd10 * hrdet + previous[1][0] * 0.5f; + upper[2][0] = +sd20 * hrdet + previous[2][0] * 0.5f; + + upper[0][1] = -(previous[1][0] * previous[2][2] - previous[2][0] * previous[1][2]) * hrdet + previous[0][1] * 0.5f; + upper[1][1] = +(previous[0][0] * previous[2][2] - previous[2][0] * previous[0][2]) * hrdet + previous[1][1] * 0.5f; + upper[2][1] = -(previous[0][0] * previous[1][2] - previous[1][0] * previous[0][2]) * hrdet + previous[2][1] * 0.5f; + + upper[0][2] = +(previous[1][0] * previous[2][1] - previous[2][0] * previous[1][1]) * hrdet + previous[0][2] * 0.5f; + upper[1][2] = -(previous[0][0] * previous[2][1] - previous[2][0] * previous[0][1]) * hrdet + previous[1][2] * 0.5f; + upper[2][2] = +(previous[0][0] * previous[1][1] - previous[1][0] * previous[0][1]) * hrdet + previous[2][2] * 0.5f; + + // compute the difference; if it's small enough, we're done + glm::mat3 diff = upper - previous; + if (diff[0][0] * diff[0][0] + diff[1][0] * diff[1][0] + diff[2][0] * diff[2][0] + diff[0][1] * diff[0][1] + + diff[1][1] * diff[1][1] + diff[2][1] * diff[2][1] + diff[0][2] * diff[0][2] + diff[1][2] * diff[1][2] + + diff[2][2] * diff[2][2] < EPSILON) { + break; + } + } + } + + // now that we have a nice orthogonal matrix, we can extract the rotation quaternion + // using the method described in http://en.wikipedia.org/wiki/Rotation_matrix#Conversions + float x2 = fabs(1.0f + upper[0][0] - upper[1][1] - upper[2][2]); + float y2 = fabs(1.0f - upper[0][0] + upper[1][1] - upper[2][2]); + float z2 = fabs(1.0f - upper[0][0] - upper[1][1] + upper[2][2]); + float w2 = fabs(1.0f + upper[0][0] + upper[1][1] + upper[2][2]); + return glm::normalize(glm::quat(0.5f * sqrtf(w2), + 0.5f * sqrtf(x2) * (upper[1][2] >= upper[2][1] ? 1.0f : -1.0f), + 0.5f * sqrtf(y2) * (upper[2][0] >= upper[0][2] ? 1.0f : -1.0f), + 0.5f * sqrtf(z2) * (upper[0][1] >= upper[1][0] ? 1.0f : -1.0f))); +} + +glm::vec3 extractScale(const glm::mat4& matrix) { + return glm::vec3(glm::length(matrix[0]), glm::length(matrix[1]), glm::length(matrix[2])); +} + +float extractUniformScale(const glm::mat4& matrix) { + return extractUniformScale(extractScale(matrix)); +} + +float extractUniformScale(const glm::vec3& scale) { + return (scale.x + scale.y + scale.z) / 3.0f; +} + bool isNaN(float value) { return value != value; } diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index a2f98549ad..a8403d617c 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -167,6 +167,22 @@ int unpackFloatVec3FromSignedTwoByteFixed(const unsigned char* sourceBuffer, glm /// \return vec3 with euler angles in radians glm::vec3 safeEulerAngles(const glm::quat& q); +float angleBetween(const glm::vec3& v1, const glm::vec3& v2); + +glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2); + +glm::vec3 extractTranslation(const glm::mat4& matrix); + +void setTranslation(glm::mat4& matrix, const glm::vec3& translation); + +glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal = false); + +glm::vec3 extractScale(const glm::mat4& matrix); + +float extractUniformScale(const glm::mat4& matrix); + +float extractUniformScale(const glm::vec3& scale); + /// \return bool are two orientations similar to each other const float ORIENTATION_SIMILAR_ENOUGH = 5.0f; // 10 degrees in any direction bool isSimilarOrientation(const glm::quat& orientionA, const glm::quat& orientionB,