diff --git a/examples/editModels.js b/examples/editModels.js index d430800748..f55ea4bbbe 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -60,6 +60,413 @@ var jointList = MyAvatar.getJointNames(); var mode = 0; +var exportMenu = null; + +var ExportMenu = function(opts) { + var self = this; + + var windowDimensions = Controller.getViewportDimensions(); + var pos = { x: windowDimensions.x / 2, y: windowDimensions.y - 100 }; + + this._onClose = opts.onClose || function() {}; + this._position = { x: 0.0, y: 0.0, z: 0.0 }; + this._scale = 1.0; + + var minScale = 1; + var maxScale = 32768; + var titleWidth = 120; + var locationWidth = 100; + var scaleWidth = 144; + var exportWidth = 100; + var cancelWidth = 100; + var margin = 4; + var height = 30; + var outerHeight = height + (2 * margin); + var buttonColor = { red: 128, green: 128, blue: 128}; + + var SCALE_MINUS = scaleWidth * 40.0 / 100.0; + var SCALE_PLUS = scaleWidth * 63.0 / 100.0; + + var fullWidth = locationWidth + scaleWidth + exportWidth + cancelWidth + (2 * margin); + var offset = fullWidth / 2; + pos.x -= offset; + + var background= Overlays.addOverlay("text", { + x: pos.x, + y: pos.y, + opacity: 1, + width: fullWidth, + height: outerHeight, + backgroundColor: { red: 200, green: 200, blue: 200 }, + text: "", + }); + + var titleText = Overlays.addOverlay("text", { + x: pos.x, + y: pos.y - height, + font: { size: 14 }, + width: titleWidth, + height: height, + backgroundColor: { red: 255, green: 255, blue: 255 }, + color: { red: 255, green: 255, blue: 255 }, + text: "Export Models" + }); + + var locationButton = Overlays.addOverlay("text", { + x: pos.x + margin, + y: pos.y + margin, + width: locationWidth, + height: height, + color: { red: 255, green: 255, blue: 255 }, + text: "0, 0, 0", + }); + var scaleOverlay = Overlays.addOverlay("image", { + x: pos.x + margin + locationWidth, + y: pos.y + margin, + width: scaleWidth, + height: height, + subImage: { x: 0, y: 3, width: 144, height: height}, + imageURL: toolIconUrl + "voxel-size-selector.svg", + alpha: 0.9, + }); + var scaleViewWidth = 40; + var scaleView = Overlays.addOverlay("text", { + x: pos.x + margin + locationWidth + SCALE_MINUS, + y: pos.y + margin, + width: scaleViewWidth, + height: height, + alpha: 0.0, + color: { red: 255, green: 255, blue: 255 }, + text: "1" + }); + var exportButton = Overlays.addOverlay("text", { + x: pos.x + margin + locationWidth + scaleWidth, + y: pos.y + margin, + width: exportWidth, + height: height, + color: { red: 0, green: 255, blue: 255 }, + text: "Export" + }); + var cancelButton = Overlays.addOverlay("text", { + x: pos.x + margin + locationWidth + scaleWidth + exportWidth, + y: pos.y + margin, + width: cancelWidth, + height: height, + color: { red: 255, green: 255, blue: 255 }, + text: "Cancel" + }); + + var voxelPreview = Overlays.addOverlay("cube", { + position: { x: 0, y: 0, z: 0}, + size: this._scale, + color: { red: 255, green: 255, blue: 0}, + alpha: 1, + solid: false, + visible: true, + lineWidth: 4 + }); + + this.parsePosition = function(str) { + var parts = str.split(','); + if (parts.length == 3) { + var x = parseFloat(parts[0]); + var y = parseFloat(parts[1]); + var z = parseFloat(parts[2]); + if (isFinite(x) && isFinite(y) && isFinite(z)) { + return { x: x, y: y, z: z }; + } + } + return null; + }; + + this.showPositionPrompt = function() { + var positionStr = self._position.x + ", " + self._position.y + ", " + self._position.z; + while (1) { + positionStr = Window.prompt("Position to export form:", positionStr); + if (positionStr == null) { + break; + } + var position = self.parsePosition(positionStr); + if (position != null) { + self.setPosition(position.x, position.y, position.z); + break; + } + Window.alert("The position you entered was invalid."); + } + }; + + this.setScale = function(scale) { + self._scale = Math.min(maxScale, Math.max(minScale, scale)); + Overlays.editOverlay(scaleView, { text: self._scale }); + Overlays.editOverlay(voxelPreview, { size: self._scale }); + } + + this.decreaseScale = function() { + self.setScale(self._scale /= 2); + } + + this.increaseScale = function() { + self.setScale(self._scale *= 2); + } + + this.exportModels = function() { + var x = self._position.x; + var y = self._position.y; + var z = self._position.z; + var s = self._scale; + var filename = "models__" + Window.location.hostname + "__" + x + "_" + y + "_" + z + "_" + s + "__.svo"; + filename = Window.save("Select where to save", filename, "*.svo") + if (filename) { + var success = Clipboard.exportModels(filename, x, y, z, s); + if (!success) { + Window.alert("Export failed: no models found in selected area."); + } + } + self.close(); + }; + + this.getPosition = function() { + return self._position; + }; + + this.setPosition = function(x, y, z) { + self._position = { x: x, y: y, z: z }; + var positionStr = x + ", " + y + ", " + z; + Overlays.editOverlay(locationButton, { text: positionStr }); + Overlays.editOverlay(voxelPreview, { position: self._position }); + + }; + + this.mouseReleaseEvent = function(event) { + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + + if (clickedOverlay == locationButton) { + self.showPositionPrompt(); + } else if (clickedOverlay == exportButton) { + self.exportModels(); + } else if (clickedOverlay == cancelButton) { + self.close(); + } else if (clickedOverlay == scaleOverlay) { + var x = event.x - pos.x - margin - locationWidth; + print(x); + if (x < SCALE_MINUS) { + self.decreaseScale(); + } else if (x > SCALE_PLUS) { + self.increaseScale(); + } + } + }; + + this.close = function() { + this.cleanup(); + this._onClose(); + }; + + this.cleanup = function() { + Overlays.deleteOverlay(background); + Overlays.deleteOverlay(titleText); + Overlays.deleteOverlay(locationButton); + Overlays.deleteOverlay(exportButton); + Overlays.deleteOverlay(cancelButton); + Overlays.deleteOverlay(voxelPreview); + Overlays.deleteOverlay(scaleOverlay); + Overlays.deleteOverlay(scaleView); + }; + + print("CONNECTING!"); + Controller.mouseReleaseEvent.connect(this.mouseReleaseEvent); +}; + +var ModelImporter = function(opts) { + var self = this; + + var height = 30; + var margin = 4; + var outerHeight = height + (2 * margin); + var titleWidth = 120; + var cancelWidth = 100; + var fullWidth = titleWidth + cancelWidth + (2 * margin); + + var localModels = Overlays.addOverlay("localmodels", { + position: { x: 1, y: 1, z: 1 }, + scale: 1, + visible: false + }); + var importScale = 1; + var importBoundaries = Overlays.addOverlay("cube", { + position: { x: 0, y: 0, z: 0 }, + size: 1, + color: { red: 128, blue: 128, green: 128 }, + lineWidth: 4, + solid: false, + visible: false + }); + + var pos = { x: windowDimensions.x / 2 - (fullWidth / 2), y: windowDimensions.y - 100 }; + + var background = Overlays.addOverlay("text", { + x: pos.x, + y: pos.y, + opacity: 1, + width: fullWidth, + height: outerHeight, + backgroundColor: { red: 200, green: 200, blue: 200 }, + visible: false, + text: "", + }); + + var titleText = Overlays.addOverlay("text", { + x: pos.x + margin, + y: pos.y + margin, + font: { size: 14 }, + width: titleWidth, + height: height, + backgroundColor: { red: 255, green: 255, blue: 255 }, + color: { red: 255, green: 255, blue: 255 }, + visible: false, + text: "Import Models" + }); + var cancelButton = Overlays.addOverlay("text", { + x: pos.x + margin + titleWidth, + y: pos.y + margin, + width: cancelWidth, + height: height, + color: { red: 255, green: 255, blue: 255 }, + visible: false, + text: "Close" + }); + this._importing = false; + + this.setImportVisible = function(visible) { + Overlays.editOverlay(importBoundaries, { visible: visible }); + Overlays.editOverlay(localModels, { visible: visible }); + Overlays.editOverlay(cancelButton, { visible: visible }); + Overlays.editOverlay(titleText, { visible: visible }); + Overlays.editOverlay(background, { visible: visible }); + }; + + var importPosition = { x: 0, y: 0, z: 0 }; + this.moveImport = function(position) { + importPosition = position; + Overlays.editOverlay(localModels, { + position: { x: importPosition.x, y: importPosition.y, z: importPosition.z } + }); + Overlays.editOverlay(importBoundaries, { + position: { x: importPosition.x, y: importPosition.y, z: importPosition.z } + }); + } + + this.mouseMoveEvent = function(event) { + if (self._importing) { + var pickRay = Camera.computePickRay(event.x, event.y); + var intersection = Voxels.findRayIntersection(pickRay); + + var distance = 2;// * self._scale; + + if (false) {//intersection.intersects) { + var intersectionDistance = Vec3.length(Vec3.subtract(pickRay.origin, intersection.intersection)); + if (intersectionDistance < distance) { + distance = intersectionDistance * 0.99; + } + + } + + var targetPosition = { + x: pickRay.origin.x + (pickRay.direction.x * distance), + y: pickRay.origin.y + (pickRay.direction.y * distance), + z: pickRay.origin.z + (pickRay.direction.z * distance) + }; + + if (targetPosition.x < 0) targetPosition.x = 0; + if (targetPosition.y < 0) targetPosition.y = 0; + if (targetPosition.z < 0) targetPosition.z = 0; + + var nudgeFactor = 1; + var newPosition = { + x: Math.floor(targetPosition.x / nudgeFactor) * nudgeFactor, + y: Math.floor(targetPosition.y / nudgeFactor) * nudgeFactor, + z: Math.floor(targetPosition.z / nudgeFactor) * nudgeFactor + } + + self.moveImport(newPosition); + } + } + + this.mouseReleaseEvent = function(event) { + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + + if (clickedOverlay == cancelButton) { + self._importing = false; + self.setImportVisible(false); + } + }; + + // Would prefer to use {4} for the coords, but it would only capture the last digit. + var fileRegex = /__(.+)__(\d+(?:\.\d+)?)_(\d+(?:\.\d+)?)_(\d+(?:\.\d+)?)_(\d+(?:\.\d+)?)__/; + this.doImport = function() { + if (!self._importing) { + var filename = Window.browse("Select models to import", "", "*.svo") + if (filename) { + parts = fileRegex.exec(filename); + if (parts == null) { + Window.alert("The file you selected does not contain source domain or location information"); + } else { + var hostname = parts[1]; + var x = parts[2]; + var y = parts[3]; + var z = parts[4]; + var s = parts[5]; + importScale = s; + if (hostname != location.hostname) { + if (!Window.confirm(("These models were not originally exported from this domain. Continue?"))) { + return; + } + } else { + if (Window.confirm(("Would you like to import back to the source location?"))) { + var success = Clipboard.importModels(filename); + if (success) { + Clipboard.pasteModels(x, y, z, 1); + } else { + Window.alert("There was an error importing the model file."); + } + return; + } + } + } + var success = Clipboard.importModels(filename); + if (success) { + self._importing = true; + self.setImportVisible(true); + Overlays.editOverlay(importBoundaries, { size: s }); + } else { + Window.alert("There was an error importing the model file."); + } + } + } + } + + this.paste = function() { + if (self._importing) { + // self._importing = false; + // self.setImportVisible(false); + Clipboard.pasteModels(importPosition.x, importPosition.y, importPosition.z, 1); + } + } + + this.cleanup = function() { + Overlays.deleteOverlay(localModels); + Overlays.deleteOverlay(importBoundaries); + Overlays.deleteOverlay(cancelButton); + Overlays.deleteOverlay(titleText); + Overlays.deleteOverlay(background); + } + + Controller.mouseReleaseEvent.connect(this.mouseReleaseEvent); + Controller.mouseMoveEvent.connect(this.mouseMoveEvent); +}; + +var modelImporter = new ModelImporter(); + function isLocked(properties) { // special case to lock the ground plane model in hq. if (location.hostname == "hq.highfidelity.io" && @@ -1069,7 +1476,7 @@ function mouseReleaseEvent(event) { var modelMenuAddedDelete = false; function setupModelMenus() { print("setupModelMenus()"); - // add our menuitems + // adj our menuitems Menu.addMenuItem({ menuName: "Edit", menuItemName: "Models", isSeparator: true, beforeItem: "Physics" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Edit Properties...", shortcutKeyEvent: { text: "`" }, afterItem: "Models" }); @@ -1081,6 +1488,12 @@ function setupModelMenus() { } else { print("delete exists... don't add ours"); } + + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Paste Models", shortcutKey: "CTRL+META+V", afterItem: "Edit Properties..." }); + + Menu.addMenuItem({ menuName: "File", menuItemName: "Models", isSeparator: true, beforeItem: "Settings" }); + Menu.addMenuItem({ menuName: "File", menuItemName: "Export Models", shortcutKey: "CTRL+META+E", afterItem: "Models" }); + Menu.addMenuItem({ menuName: "File", menuItemName: "Import Models", shortcutKey: "CTRL+META+I", afterItem: "Export Models" }); } function cleanupModelMenus() { @@ -1090,6 +1503,12 @@ function cleanupModelMenus() { // delete our menuitems Menu.removeMenuItem("Edit", "Delete"); } + + Menu.removeMenuItem("Edit", "Paste Models"); + + Menu.removeSeparator("File", "Models"); + Menu.removeMenuItem("File", "Export Models"); + Menu.removeMenuItem("File", "Import Models"); } function scriptEnding() { @@ -1098,6 +1517,10 @@ function scriptEnding() { toolBar.cleanup(); cleanupModelMenus(); tooltip.cleanup(); + modelImporter.cleanup(); + if (exportMenu) { + exportMenu.close(); + } } Script.scriptEnding.connect(scriptEnding); @@ -1175,6 +1598,18 @@ function handeMenuEvent(menuItem){ Models.editModel(selectedModelID, selectedModelProperties); } + } else if (menuItem == "Paste Models") { + modelImporter.paste(); + } else if (menuItem == "Export Models") { + if (!exportMenu) { + exportMenu = new ExportMenu({ + onClose: function() { + exportMenu = null; + } + }); + } + } else if (menuItem == "Import Models") { + modelImporter.doImport(); } tooltip.show(false); } @@ -1221,4 +1656,4 @@ Controller.keyReleaseEvent.connect(function(event) { if (event.text == "BACKSPACE") { handeMenuEvent("Delete"); } -}); \ No newline at end of file +}); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3caf9b2f5f..e072ac7c44 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -141,6 +141,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _voxelImporter(), _importSucceded(false), _sharedVoxelSystem(TREE_SCALE, DEFAULT_MAX_VOXELS_PER_SYSTEM, &_clipboard), + _modelClipboardRenderer(), + _modelClipboard(), _wantToKillLocalVoxels(false), _viewFrustum(), _lastQueriedViewFrustum(), @@ -175,6 +177,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _lastNackTime(usecTimestampNow()), _lastSendDownstreamAudioStats(usecTimestampNow()) { + // read the ApplicationInfo.ini file for Name/Version/Domain information QSettings applicationInfo(Application::resourcesPath() + "info/ApplicationInfo.ini", QSettings::IniFormat); @@ -1510,6 +1513,33 @@ struct SendVoxelsOperationArgs { const unsigned char* newBaseOctCode; }; +bool Application::exportModels(const QString& filename, float x, float y, float z, float scale) { + QVector models; + _models.getTree()->findModelsInCube(AACube(glm::vec3(x / (float)TREE_SCALE, y / (float)TREE_SCALE, z / (float)TREE_SCALE), scale / (float)TREE_SCALE), models); + if (models.size() > 0) { + glm::vec3 root(x, y, z); + ModelTree exportTree; + + for (int i = 0; i < models.size(); i++) { + ModelItemProperties properties; + ModelItemID id = models.at(i)->getModelItemID(); + id.isKnownID = false; + properties.copyFromNewModelItem(*models.at(i)); + properties.setPosition(properties.getPosition() - root); + exportTree.addModel(id, properties); + } + + exportTree.writeToSVOFile(filename.toLocal8Bit().constData()); + } else { + qDebug() << "No models were selected"; + return false; + } + + // restore the main window's active state + _window->activateWindow(); + return true; +} + bool Application::sendVoxelsOperation(OctreeElement* element, void* extraData) { VoxelTreeElement* voxel = (VoxelTreeElement*)element; SendVoxelsOperationArgs* args = (SendVoxelsOperationArgs*)extraData; @@ -1591,6 +1621,19 @@ void Application::importVoxels() { emit importDone(); } +bool Application::importModels(const QString& filename) { + _modelClipboard.eraseAllOctreeElements(); + bool success = _modelClipboard.readFromSVOFile(filename.toLocal8Bit().constData()); + if (success) { + _modelClipboard.reaverageOctreeElements(); + } + return success; +} + +void Application::pasteModels(float x, float y, float z) { + _modelClipboard.sendModels(&_modelEditSender, x, y, z); +} + void Application::cutVoxels(const VoxelDetail& sourceVoxel) { copyVoxels(sourceVoxel); deleteVoxelAt(sourceVoxel); @@ -1754,6 +1797,10 @@ void Application::init() { _models.init(); _models.setViewFrustum(getViewFrustum()); + _modelClipboardRenderer.init(); + _modelClipboardRenderer.setViewFrustum(getViewFrustum()); + _modelClipboardRenderer.setTree(&_modelClipboard); + _metavoxels.init(); _particleCollisionSystem.init(&_particleEditSender, _particles.getTree(), _voxels.getTree(), &_audio, &_avatarManager); @@ -3675,6 +3722,8 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript connect(scriptEngine, SIGNAL(finished(const QString&)), this, SLOT(scriptFinished(const QString&))); + connect(scriptEngine, SIGNAL(loadScript(const QString&)), this, SLOT(loadScript(const QString&))); + scriptEngine->registerGlobalObject("Overlays", &_overlays); QScriptValue windowValue = scriptEngine->registerGlobalObject("Window", WindowScriptingInterface::getInstance()); diff --git a/interface/src/Application.h b/interface/src/Application.h index 8c0a999370..47ed702baf 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -203,6 +203,8 @@ public: bool getImportSucceded() { return _importSucceded; } VoxelSystem* getSharedVoxelSystem() { return &_sharedVoxelSystem; } VoxelTree* getClipboard() { return &_clipboard; } + ModelTree* getModelClipboard() { return &_modelClipboard; } + ModelTreeRenderer* getModelClipboardRenderer() { return &_modelClipboardRenderer; } Environment* getEnvironment() { return &_environment; } bool isMousePressed() const { return _mousePressed; } bool isMouseHidden() const { return _mouseHidden; } @@ -229,6 +231,7 @@ public: float getPacketsPerSecond() const { return _packetsPerSecond; } float getBytesPerSecond() const { return _bytesPerSecond; } const glm::vec3& getViewMatrixTranslation() const { return _viewMatrixTranslation; } + void setViewMatrixTranslation(const glm::vec3& translation) { _viewMatrixTranslation = translation; } /// if you need to access the application settings, use lockSettings()/unlockSettings() QSettings* lockSettings() { _settingsMutex.lock(); return _settings; } @@ -316,6 +319,10 @@ public slots: void nodeKilled(SharedNodePointer node); void packetSent(quint64 length); + void pasteModels(float x, float y, float z); + bool exportModels(const QString& filename, float x, float y, float z, float scale); + bool importModels(const QString& filename); + void importVoxels(); // doesn't include source voxel because it goes to clipboard void cutVoxels(const VoxelDetail& sourceVoxel); void copyVoxels(const VoxelDetail& sourceVoxel); @@ -465,6 +472,8 @@ private: ParticleCollisionSystem _particleCollisionSystem; ModelTreeRenderer _models; + ModelTreeRenderer _modelClipboardRenderer; + ModelTree _modelClipboard; QByteArray _voxelsFilename; bool _wantToKillLocalVoxels; diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 4ed1f7aeb3..db55cc3e94 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -49,6 +49,7 @@ static const float AUDIO_CALLBACK_MSECS = (float) NETWORK_BUFFER_LENGTH_SAMPLES_ static const int NUMBER_OF_NOISE_SAMPLE_FRAMES = 300; static const int FRAMES_AVAILABLE_STATS_WINDOW_SECONDS = 10; +static const int APPROXIMATELY_30_SECONDS_OF_AUDIO_PACKETS = (int)(30.0f * 1000.0f / AUDIO_CALLBACK_MSECS); // Mute icon configration static const int MUTE_ICON_SIZE = 24; @@ -112,7 +113,10 @@ Audio::Audio(QObject* parent) : _outgoingAvatarAudioSequenceNumber(0), _audioInputMsecsReadStats(MSECS_PER_SECOND / (float)AUDIO_CALLBACK_MSECS * CALLBACK_ACCELERATOR_RATIO, FRAMES_AVAILABLE_STATS_WINDOW_SECONDS), _inputRingBufferMsecsAvailableStats(1, FRAMES_AVAILABLE_STATS_WINDOW_SECONDS), - _audioOutputMsecsUnplayedStats(1, FRAMES_AVAILABLE_STATS_WINDOW_SECONDS) + _audioOutputMsecsUnplayedStats(1, FRAMES_AVAILABLE_STATS_WINDOW_SECONDS), + _lastSentAudioPacket(0), + _packetSentTimeGaps(1, APPROXIMATELY_30_SECONDS_OF_AUDIO_PACKETS) + { // clear the array of locally injected samples memset(_localProceduralSamples, 0, NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL); @@ -128,7 +132,6 @@ void Audio::init(QGLWidget *parent) { void Audio::reset() { _receivedAudioStream.reset(); - resetStats(); } @@ -142,6 +145,7 @@ void Audio::resetStats() { _inputRingBufferMsecsAvailableStats.reset(); _audioOutputMsecsUnplayedStats.reset(); + _packetSentTimeGaps.reset(); } void Audio::audioMixerKilled() { @@ -699,6 +703,17 @@ void Audio::handleAudioInput() { // memcpy our orientation memcpy(currentPacketPtr, &headOrientation, sizeof(headOrientation)); currentPacketPtr += sizeof(headOrientation); + + // first time this is 0 + if (_lastSentAudioPacket == 0) { + _lastSentAudioPacket = usecTimestampNow(); + } else { + quint64 now = usecTimestampNow(); + quint64 gap = now - _lastSentAudioPacket; + _packetSentTimeGaps.update(gap); + + _lastSentAudioPacket = now; + } nodeList->writeDatagram(audioDataPacket, numAudioBytes + leadingBytes, audioMixer); _outgoingAvatarAudioSequenceNumber++; @@ -1307,10 +1322,10 @@ void Audio::renderStats(const float* color, int width, int height) { return; } - const int linesWhenCentered = _statsShowInjectedStreams ? 30 : 23; + const int linesWhenCentered = _statsShowInjectedStreams ? 34 : 27; const int CENTERED_BACKGROUND_HEIGHT = STATS_HEIGHT_PER_LINE * linesWhenCentered; - int lines = _statsShowInjectedStreams ? _audioMixerInjectedStreamAudioStatsMap.size() * 7 + 23 : 23; + int lines = _statsShowInjectedStreams ? _audioMixerInjectedStreamAudioStatsMap.size() * 7 + 27 : 27; int statsHeight = STATS_HEIGHT_PER_LINE * lines; @@ -1384,7 +1399,28 @@ void Audio::renderStats(const float* color, int width, int height) { verticalOffset += STATS_HEIGHT_PER_LINE; // blank line - char upstreamMicLabelString[] = "Upstream mic audio stats:"; + char clientUpstreamMicLabelString[] = "Upstream Mic Audio Packets Sent Gaps (by client):"; + verticalOffset += STATS_HEIGHT_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, clientUpstreamMicLabelString, color); + + char stringBuffer[512]; + sprintf(stringBuffer, " Inter-packet timegaps (overall) | min: %9s, max: %9s, avg: %9s", + formatUsecTime(_packetSentTimeGaps.getMin()).toLatin1().data(), + formatUsecTime(_packetSentTimeGaps.getMax()).toLatin1().data(), + formatUsecTime(_packetSentTimeGaps.getAverage()).toLatin1().data()); + verticalOffset += STATS_HEIGHT_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, stringBuffer, color); + + sprintf(stringBuffer, " Inter-packet timegaps (last 30s) | min: %9s, max: %9s, avg: %9s", + formatUsecTime(_packetSentTimeGaps.getWindowMin()).toLatin1().data(), + formatUsecTime(_packetSentTimeGaps.getWindowMax()).toLatin1().data(), + formatUsecTime(_packetSentTimeGaps.getWindowAverage()).toLatin1().data()); + verticalOffset += STATS_HEIGHT_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, stringBuffer, color); + + verticalOffset += STATS_HEIGHT_PER_LINE; // blank line + + char upstreamMicLabelString[] = "Upstream mic audio stats (received and reported by audio-mixer):"; verticalOffset += STATS_HEIGHT_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, upstreamMicLabelString, color); diff --git a/interface/src/Audio.h b/interface/src/Audio.h index 3006446db1..9dad3b82d7 100644 --- a/interface/src/Audio.h +++ b/interface/src/Audio.h @@ -279,6 +279,9 @@ private: MovingMinMaxAvg _inputRingBufferMsecsAvailableStats; MovingMinMaxAvg _audioOutputMsecsUnplayedStats; + + quint64 _lastSentAudioPacket; + MovingMinMaxAvg _packetSentTimeGaps; }; diff --git a/interface/src/avatar/MuscleConstraint.cpp b/interface/src/avatar/MuscleConstraint.cpp index f4ba7975d0..31da56d3d3 100644 --- a/interface/src/avatar/MuscleConstraint.cpp +++ b/interface/src/avatar/MuscleConstraint.cpp @@ -16,8 +16,7 @@ const float DEFAULT_MUSCLE_STRENGTH = 0.5f * MAX_MUSCLE_STRENGTH; -MuscleConstraint::MuscleConstraint(VerletPoint* parent, VerletPoint* child) - : _rootPoint(parent), _childPoint(child), +MuscleConstraint::MuscleConstraint(VerletPoint* parent, VerletPoint* child) : _rootPoint(parent), _childPoint(child), _parentIndex(-1), _childndex(-1), _strength(DEFAULT_MUSCLE_STRENGTH) { _childOffset = child->_position - parent->_position; } diff --git a/interface/src/avatar/MuscleConstraint.h b/interface/src/avatar/MuscleConstraint.h index 882b351b80..b2387a33f0 100644 --- a/interface/src/avatar/MuscleConstraint.h +++ b/interface/src/avatar/MuscleConstraint.h @@ -17,7 +17,7 @@ // MuscleConstraint is a simple constraint that pushes the child toward an offset relative to the parent. // It does NOT push the parent. -const float MAX_MUSCLE_STRENGTH = 0.5f; +const float MAX_MUSCLE_STRENGTH = 0.75f; class MuscleConstraint : public Constraint { public: diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index dc6a309e70..c8e7451fc1 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -568,8 +568,8 @@ void SkeletonModel::buildRagdollConstraints() { ++itr; } - float MAX_STRENGTH = 0.3f; - float MIN_STRENGTH = 0.005f; + float MAX_STRENGTH = 0.6f; + float MIN_STRENGTH = 0.05f; // each joint gets a MuscleConstraint to its parent for (int i = 1; i < numPoints; ++i) { const JointState& state = _jointStates.at(i); @@ -578,7 +578,6 @@ void SkeletonModel::buildRagdollConstraints() { continue; } MuscleConstraint* constraint = new MuscleConstraint(&(_ragdollPoints[p]), &(_ragdollPoints[i])); - _ragdollConstraints.push_back(constraint); _muscleConstraints.push_back(constraint); // Short joints are more susceptible to wiggle so we modulate the strength based on the joint's length: @@ -644,6 +643,10 @@ void SkeletonModel::updateVisibleJointStates() { void SkeletonModel::stepRagdollForward(float deltaTime) { Ragdoll::stepRagdollForward(deltaTime); updateMuscles(); + int numConstraints = _muscleConstraints.size(); + for (int i = 0; i < numConstraints; ++i) { + _muscleConstraints[i]->enforce(); + } } float DENSITY_OF_WATER = 1000.0f; // kg/m^3 @@ -743,13 +746,8 @@ void SkeletonModel::updateMuscles() { for (int i = 0; i < numConstraints; ++i) { MuscleConstraint* constraint = _muscleConstraints[i]; int j = constraint->getParentIndex(); - if (j == -1) { - continue; - } int k = constraint->getChildIndex(); - if (k == -1) { - continue; - } + assert(j != -1 && k != -1); constraint->setChildOffset(_jointStates.at(k).getPosition() - _jointStates.at(j).getPosition()); } } diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index 0e63a386ed..084c7c7787 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -131,3 +131,16 @@ void ClipboardScriptingInterface::nudgeVoxel(float x, float y, float z, float s, Application::getInstance()->nudgeVoxelsByVector(sourceVoxel, nudgeVecInTreeSpace); } + + +bool ClipboardScriptingInterface::exportModels(const QString& filename, float x, float y, float z, float s) { + return Application::getInstance()->exportModels(filename, x, y, z, s); +} + +bool ClipboardScriptingInterface::importModels(const QString& filename) { + return Application::getInstance()->importModels(filename); +} + +void ClipboardScriptingInterface::pasteModels(float x, float y, float z, float s) { + Application::getInstance()->pasteModels(x, y, z); +} diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h index b7b1d85625..d322fea1f7 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.h +++ b/interface/src/scripting/ClipboardScriptingInterface.h @@ -45,6 +45,10 @@ public slots: void nudgeVoxel(const VoxelDetail& sourceVoxel, const glm::vec3& nudgeVec); void nudgeVoxel(float x, float y, float z, float s, const glm::vec3& nudgeVec); + + bool importModels(const QString& filename); + bool exportModels(const QString& filename, float x, float y, float z, float s); + void pasteModels(float x, float y, float z, float s); }; #endif // hifi_ClipboardScriptingInterface_h diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 827f66c8d5..ea0eeb0dd9 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -67,6 +67,15 @@ QScriptValue WindowScriptingInterface::browse(const QString& title, const QStrin return retVal; } +QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { + QScriptValue retVal; + QMetaObject::invokeMethod(this, "showBrowse", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QScriptValue, retVal), + Q_ARG(const QString&, title), Q_ARG(const QString&, directory), Q_ARG(const QString&, nameFilter), + Q_ARG(QFileDialog::AcceptMode, QFileDialog::AcceptSave)); + return retVal; +} + QScriptValue WindowScriptingInterface::s3Browse(const QString& nameFilter) { QScriptValue retVal; QMetaObject::invokeMethod(this, "showS3Browse", Qt::BlockingQueuedConnection, @@ -182,18 +191,26 @@ QScriptValue WindowScriptingInterface::showPrompt(const QString& message, const /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` -QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QString& directory, const QString& nameFilter) { +QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QString& directory, const QString& nameFilter, + QFileDialog::AcceptMode acceptMode) { // On OS X `directory` does not work as expected unless a file is included in the path, so we append a bogus // filename if the directory is valid. QString path = ""; QFileInfo fileInfo = QFileInfo(directory); + qDebug() << "File: " << directory << fileInfo.isFile(); if (fileInfo.isDir()) { fileInfo.setFile(directory, "__HIFI_INVALID_FILE__"); path = fileInfo.filePath(); } QFileDialog fileDialog(Application::getInstance()->getWindow(), title, path, nameFilter); - fileDialog.setFileMode(QFileDialog::ExistingFile); + fileDialog.setAcceptMode(acceptMode); + qDebug() << "Opening!"; + QUrl fileUrl(directory); + if (acceptMode == QFileDialog::AcceptSave) { + fileDialog.setFileMode(QFileDialog::Directory); + fileDialog.selectFile(fileUrl.fileName()); + } if (fileDialog.exec()) { return QScriptValue(fileDialog.selectedFiles().first()); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 654b048b24..b04c927427 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -31,6 +31,7 @@ public slots: QScriptValue form(const QString& title, QScriptValue array); QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue s3Browse(const QString& nameFilter = ""); private slots: @@ -38,7 +39,8 @@ private slots: QScriptValue showConfirm(const QString& message); QScriptValue showForm(const QString& title, QScriptValue form); QScriptValue showPrompt(const QString& message, const QString& defaultText); - QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter); + QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter, + QFileDialog::AcceptMode acceptMode = QFileDialog::AcceptOpen); QScriptValue showS3Browse(const QString& nameFilter); private: diff --git a/interface/src/ui/overlays/LocalModelsOverlay.cpp b/interface/src/ui/overlays/LocalModelsOverlay.cpp new file mode 100644 index 0000000000..6bb1d9ce88 --- /dev/null +++ b/interface/src/ui/overlays/LocalModelsOverlay.cpp @@ -0,0 +1,40 @@ +// +// LocalModelsOverlay.cpp +// interface/src/ui/overlays +// +// Created by Ryan Huffman on 07/08/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Application.h" + +#include "LocalModelsOverlay.h" + +LocalModelsOverlay::LocalModelsOverlay(ModelTreeRenderer* modelTreeRenderer) : + Volume3DOverlay(), + _modelTreeRenderer(modelTreeRenderer) { +} + +LocalModelsOverlay::~LocalModelsOverlay() { +} + +void LocalModelsOverlay::update(float deltatime) { + _modelTreeRenderer->update(); +} + +void LocalModelsOverlay::render() { + if (_visible) { + glPushMatrix(); { + Application* app = Application::getInstance(); + glm::vec3 oldTranslation = app->getViewMatrixTranslation(); + app->setViewMatrixTranslation(oldTranslation + _position); + + _modelTreeRenderer->render(); + + Application::getInstance()->setViewMatrixTranslation(oldTranslation); + } glPopMatrix(); + } +} diff --git a/interface/src/ui/overlays/LocalModelsOverlay.h b/interface/src/ui/overlays/LocalModelsOverlay.h new file mode 100644 index 0000000000..7c4bffa342 --- /dev/null +++ b/interface/src/ui/overlays/LocalModelsOverlay.h @@ -0,0 +1,32 @@ +// +// LocalModelsOverlay.h +// interface/src/ui/overlays +// +// Created by Ryan Huffman on 07/08/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_LocalModelsOverlay_h +#define hifi_LocalModelsOverlay_h + +#include "models/ModelTreeRenderer.h" + +#include "Volume3DOverlay.h" + +class LocalModelsOverlay : public Volume3DOverlay { + Q_OBJECT +public: + LocalModelsOverlay(ModelTreeRenderer* modelTreeRenderer); + ~LocalModelsOverlay(); + + virtual void update(float deltatime); + virtual void render(); + +private: + ModelTreeRenderer *_modelTreeRenderer; +}; + +#endif // hifi_LocalModelsOverlay_h diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 5d16bd78e5..91856d0722 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -14,6 +14,7 @@ #include "Cube3DOverlay.h" #include "ImageOverlay.h" #include "Line3DOverlay.h" +#include "LocalModelsOverlay.h" #include "LocalVoxelsOverlay.h" #include "ModelOverlay.h" #include "Overlays.h" @@ -158,6 +159,12 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope thisOverlay->setProperties(properties); created = true; is3D = true; + } else if (type == "localmodels") { + thisOverlay = new LocalModelsOverlay(Application::getInstance()->getModelClipboardRenderer()); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + is3D = true; } else if (type == "model") { thisOverlay = new ModelOverlay(); thisOverlay->init(_parent); diff --git a/libraries/models/src/ModelItem.cpp b/libraries/models/src/ModelItem.cpp index 750af8f1b6..2a3a88fb65 100644 --- a/libraries/models/src/ModelItem.cpp +++ b/libraries/models/src/ModelItem.cpp @@ -1157,6 +1157,38 @@ void ModelItemProperties::copyFromModelItem(const ModelItem& modelItem) { _defaultSettings = false; } +void ModelItemProperties::copyFromNewModelItem(const ModelItem& modelItem) { + _position = modelItem.getPosition() * (float) TREE_SCALE; + _color = modelItem.getXColor(); + _radius = modelItem.getRadius() * (float) TREE_SCALE; + _shouldDie = modelItem.getShouldDie(); + _modelURL = modelItem.getModelURL(); + _modelRotation = modelItem.getModelRotation(); + _animationURL = modelItem.getAnimationURL(); + _animationIsPlaying = modelItem.getAnimationIsPlaying(); + _animationFrameIndex = modelItem.getAnimationFrameIndex(); + _animationFPS = modelItem.getAnimationFPS(); + _glowLevel = modelItem.getGlowLevel(); + _sittingPoints = modelItem.getSittingPoints(); + + _id = modelItem.getID(); + _idSet = true; + + _positionChanged = true; + _colorChanged = true; + _radiusChanged = true; + + _shouldDieChanged = true; + _modelURLChanged = true; + _modelRotationChanged = true; + _animationURLChanged = true; + _animationIsPlayingChanged = true; + _animationFrameIndexChanged = true; + _animationFPSChanged = true; + _glowLevelChanged = true; + _defaultSettings = true; +} + QScriptValue ModelItemPropertiesToScriptValue(QScriptEngine* engine, const ModelItemProperties& properties) { return properties.copyToScriptValue(engine); } diff --git a/libraries/models/src/ModelItem.h b/libraries/models/src/ModelItem.h index 43aaca48a0..3e3c46a12d 100644 --- a/libraries/models/src/ModelItem.h +++ b/libraries/models/src/ModelItem.h @@ -74,6 +74,7 @@ public: void copyToModelItem(ModelItem& modelItem) const; void copyFromModelItem(const ModelItem& modelItem); + void copyFromNewModelItem(const ModelItem& modelItem); const glm::vec3& getPosition() const { return _position; } xColor getColor() const { return _color; } diff --git a/libraries/models/src/ModelTree.cpp b/libraries/models/src/ModelTree.cpp index 763f0a969e..206e67078c 100644 --- a/libraries/models/src/ModelTree.cpp +++ b/libraries/models/src/ModelTree.cpp @@ -9,6 +9,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "ModelEditPacketSender.h" +#include "ModelItem.h" + #include "ModelTree.h" ModelTree::ModelTree(bool shouldReaverage) : Octree(shouldReaverage) { @@ -108,6 +111,7 @@ bool FindAndUpdateModelOperator::PostRecursion(OctreeElement* element) { return !_found; // if we haven't yet found it, keep looking } + // TODO: improve this to not use multiple recursions void ModelTree::storeModel(const ModelItem& model, const SharedNodePointer& senderNode) { // First, look for the existing model in the tree.. @@ -199,6 +203,32 @@ void ModelTree::deleteModel(const ModelItemID& modelID) { } } +void ModelTree::sendModels(ModelEditPacketSender* packetSender, float x, float y, float z) { + SendModelsOperationArgs args; + args.packetSender = packetSender; + args.root = glm::vec3(x, y, z); + recurseTreeWithOperation(sendModelsOperation, &args); + packetSender->releaseQueuedMessages(); +} + +bool ModelTree::sendModelsOperation(OctreeElement* element, void* extraData) { + SendModelsOperationArgs* args = static_cast(extraData); + ModelTreeElement* modelTreeElement = static_cast(element); + + const QList& modelList = modelTreeElement->getModels(); + + for (int i = 0; i < modelList.size(); i++) { + uint32_t creatorTokenID = ModelItem::getNextCreatorTokenID(); + ModelItemID id(NEW_MODEL, creatorTokenID, false); + ModelItemProperties properties; + properties.copyFromNewModelItem(modelList.at(i)); + properties.setPosition(properties.getPosition() + args->root); + args->packetSender->queueModelEditMessage(PacketTypeModelAddOrEdit, id, properties); + } + + return true; +} + // scans the tree and handles mapping locally created models to know IDs. // in the event that this tree is also viewing the scene, then we need to also // search the tree to make sure we don't have a duplicate model from the viewing @@ -353,8 +383,28 @@ public: QVector _foundModels; }; +void ModelTree::findModelsInCube(const AACube& cube, QVector& foundModels) { + FindModelsInCubeArgs args(cube); + lockForRead(); + recurseTreeWithOperation(findInCubeOperation, &args); + unlock(); + // swap the two lists of model pointers instead of copy + foundModels.swap(args._foundModels); +} + +bool ModelTree::findInCubeOperation(OctreeElement* element, void* extraData) { + FindModelsInCubeArgs* args = static_cast(extraData); + const AACube& elementCube = element->getAACube(); + if (elementCube.touches(args->_cube)) { + ModelTreeElement* modelTreeElement = static_cast(element); + modelTreeElement->getModelsInside(args->_cube, args->_foundModels); + return true; + } + return false; +} + bool ModelTree::findInCubeForUpdateOperation(OctreeElement* element, void* extraData) { - FindModelsInCubeArgs* args = static_cast< FindModelsInCubeArgs*>(extraData); + FindModelsInCubeArgs* args = static_cast(extraData); const AACube& elementCube = element->getAACube(); if (elementCube.touches(args->_cube)) { ModelTreeElement* modelTreeElement = static_cast(element); @@ -364,7 +414,7 @@ bool ModelTree::findInCubeForUpdateOperation(OctreeElement* element, void* extra return false; } -void ModelTree::findModelsForUpdate(const AACube& cube, QVector foundModels) { +void ModelTree::findModelsForUpdate(const AACube& cube, QVector& foundModels) { FindModelsInCubeArgs args(cube); lockForRead(); recurseTreeWithOperation(findInCubeForUpdateOperation, &args); diff --git a/libraries/models/src/ModelTree.h b/libraries/models/src/ModelTree.h index a2a3c9cd28..e61cc6057e 100644 --- a/libraries/models/src/ModelTree.h +++ b/libraries/models/src/ModelTree.h @@ -36,7 +36,6 @@ public: /// Type safe version of getRoot() ModelTreeElement* getRoot() { return static_cast(_rootElement); } - // These methods will allow the OctreeServer to send your tree inbound edit packets of your // own definition. Implement these to allow your octree based server to support editing virtual bool getWantSVOfileVersions() const { return true; } @@ -62,12 +61,13 @@ public: /// \param foundModels[out] vector of const ModelItem* /// \remark Side effect: any initial contents in foundModels will be lost void findModels(const glm::vec3& center, float radius, QVector& foundModels); + void findModelsInCube(const AACube& cube, QVector& foundModels); /// finds all models that touch a cube /// \param cube the query cube /// \param foundModels[out] vector of non-const ModelItem* /// \remark Side effect: any initial contents in models will be lost - void findModelsForUpdate(const AACube& cube, QVector foundModels); + void findModelsForUpdate(const AACube& cube, QVector& foundModels); void addNewlyCreatedHook(NewlyCreatedModelHook* hook); void removeNewlyCreatedHook(NewlyCreatedModelHook* hook); @@ -83,11 +83,15 @@ public: void setFBXService(ModelItemFBXService* service) { _fbxService = service; } const FBXGeometry* getGeometryForModel(const ModelItem& modelItem) { return _fbxService ? _fbxService->getGeometryForModel(modelItem) : NULL; + } + void sendModels(ModelEditPacketSender* packetSender, float x, float y, float z); private: + static bool sendModelsOperation(OctreeElement* element, void* extraData); static bool updateOperation(OctreeElement* element, void* extraData); + static bool findInCubeOperation(OctreeElement* element, void* extraData); static bool findAndUpdateOperation(OctreeElement* element, void* extraData); static bool findAndUpdateWithIDandPropertiesOperation(OctreeElement* element, void* extraData); static bool findNearPointOperation(OctreeElement* element, void* extraData); diff --git a/libraries/models/src/ModelTreeElement.cpp b/libraries/models/src/ModelTreeElement.cpp index 960d1dd4cb..47ea8babac 100644 --- a/libraries/models/src/ModelTreeElement.cpp +++ b/libraries/models/src/ModelTreeElement.cpp @@ -399,6 +399,19 @@ void ModelTreeElement::getModels(const glm::vec3& searchPosition, float searchRa } } +void ModelTreeElement::getModelsInside(const AACube& box, QVector& foundModels) { + QList::iterator modelItr = _modelItems->begin(); + QList::iterator modelEnd = _modelItems->end(); + AACube modelCube; + while(modelItr != modelEnd) { + ModelItem* model = &(*modelItr); + if (box.contains(model->getPosition())) { + foundModels.push_back(model); + } + ++modelItr; + } +} + void ModelTreeElement::getModelsForUpdate(const AACube& box, QVector& foundModels) { QList::iterator modelItr = _modelItems->begin(); QList::iterator modelEnd = _modelItems->end(); diff --git a/libraries/models/src/ModelTreeElement.h b/libraries/models/src/ModelTreeElement.h index 8d2f5064bd..c0e2e36095 100644 --- a/libraries/models/src/ModelTreeElement.h +++ b/libraries/models/src/ModelTreeElement.h @@ -44,6 +44,12 @@ public: bool isViewing; }; +class SendModelsOperationArgs { +public: + glm::vec3 root; + ModelEditPacketSender* packetSender; +}; + class ModelTreeElement : public OctreeElement { @@ -132,6 +138,8 @@ public: /// \param models[out] vector of non-const ModelItem* void getModelsForUpdate(const AACube& box, QVector& foundModels); + void getModelsInside(const AACube& box, QVector& foundModels); + const ModelItem* getModelWithID(uint32_t id) const; bool removeModelWithID(uint32_t id); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index df66fa44d5..49cab1a1fb 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -156,7 +156,7 @@ ScriptEngine::ScriptEngine(const QUrl& scriptURL, } else { NetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkReply* reply = networkAccessManager.get(QNetworkRequest(url)); - qDebug() << "Downloading included script at" << url; + qDebug() << "Downloading script at" << url; QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); @@ -681,12 +681,12 @@ void ScriptEngine::include(const QString& includeFile) { #endif QFile scriptFile(fileName); if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { - qDebug() << "Loading file:" << fileName; + qDebug() << "Including file:" << fileName; QTextStream in(&scriptFile); includeContents = in.readAll(); } else { - qDebug() << "ERROR Loading file:" << fileName; - emit errorMessage("ERROR Loading file:" + fileName); + qDebug() << "ERROR Including file:" << fileName; + emit errorMessage("ERROR Including file:" + fileName); } } @@ -699,6 +699,11 @@ void ScriptEngine::include(const QString& includeFile) { } } +void ScriptEngine::load(const QString& loadFile) { + QUrl url = resolveInclude(loadFile); + emit loadScript(url.toString()); +} + void ScriptEngine::nodeKilled(SharedNodePointer node) { _outgoingScriptAudioSequenceNumbers.remove(node->getUUID()); } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index fe39f286be..17cda5e183 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -102,6 +102,7 @@ public slots: void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } void include(const QString& includeFile); + void load(const QString& loadfile); void print(const QString& message); void nodeKilled(SharedNodePointer node); @@ -115,6 +116,7 @@ signals: void errorMessage(const QString& message); void runningStateChanged(); void evaluationFinished(QScriptValue result, bool isException); + void loadScript(const QString& scriptName); protected: QString _scriptContents; diff --git a/libraries/shared/src/CapsuleShape.cpp b/libraries/shared/src/CapsuleShape.cpp index 12ab6ba479..03bc48bd94 100644 --- a/libraries/shared/src/CapsuleShape.cpp +++ b/libraries/shared/src/CapsuleShape.cpp @@ -73,7 +73,7 @@ void CapsuleShape::setEndPoints(const glm::vec3& startPoint, const glm::vec3& en if (height > EPSILON) { _halfHeight = 0.5f * height; axis /= height; - computeNewRotation(axis); + _rotation = computeNewRotation(axis); } updateBoundingRadius(); } diff --git a/libraries/shared/src/CollisionInfo.cpp b/libraries/shared/src/CollisionInfo.cpp index e862a22f4a..9dc321fa44 100644 --- a/libraries/shared/src/CollisionInfo.cpp +++ b/libraries/shared/src/CollisionInfo.cpp @@ -26,6 +26,16 @@ CollisionInfo::CollisionInfo() : _addedVelocity(0.f) { } +quint64 CollisionInfo::getShapePairKey() const { + if (_shapeB == NULL || _shapeA == NULL) { + // zero is an invalid key + return 0; + } + quint32 idA = _shapeA->getID(); + quint32 idB = _shapeB->getID(); + return idA < idB ? ((quint64)idA << 32) + (quint64)idB : ((quint64)idB << 32) + (quint64)idA; +} + CollisionList::CollisionList(int maxSize) : _maxSize(maxSize), _size(0) { diff --git a/libraries/shared/src/CollisionInfo.h b/libraries/shared/src/CollisionInfo.h index 1ab06e2ef5..6e70654d15 100644 --- a/libraries/shared/src/CollisionInfo.h +++ b/libraries/shared/src/CollisionInfo.h @@ -15,6 +15,7 @@ #include #include +#include #include class Shape; @@ -47,6 +48,9 @@ public: Shape* getShapeA() const { return const_cast(_shapeA); } Shape* getShapeB() const { return const_cast(_shapeB); } + /// \return unique key for shape pair + quint64 getShapePairKey() const; + const Shape* _shapeA; // pointer to shapeA in this collision const Shape* _shapeB; // pointer to shapeB in this collision diff --git a/libraries/shared/src/Constraint.h b/libraries/shared/src/Constraint.h index 422675b85d..9bbdc185e1 100644 --- a/libraries/shared/src/Constraint.h +++ b/libraries/shared/src/Constraint.h @@ -20,9 +20,6 @@ public: /// Enforce contraint by moving relevant points. /// \return max distance of point movement virtual float enforce() = 0; - -protected: - int _type; }; #endif // hifi_Constraint_h diff --git a/libraries/shared/src/ContactConstraint.cpp b/libraries/shared/src/ContactConstraint.cpp new file mode 100644 index 0000000000..d1d12fa771 --- /dev/null +++ b/libraries/shared/src/ContactConstraint.cpp @@ -0,0 +1,85 @@ +// +// ContactConstraint.cpp +// libraries/shared/src +// +// Created by Andrew Meadows 2014.07.30 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ContactConstraint.h" +#include "Shape.h" +#include "SharedUtil.h" + +ContactConstraint::ContactConstraint() : _lastFrame(0), _shapeA(NULL), _shapeB(NULL), + _offsetA(0.0f), _offsetB(0.0f), _normal(0.0f) { +} + +ContactConstraint::ContactConstraint(const CollisionInfo& collision, quint32 frame) : _lastFrame(frame), + _shapeA(collision.getShapeA()), _shapeB(collision.getShapeB()), _offsetA(0.0f), _offsetB(0.0f), _normal(0.0f) { + + _offsetA = collision._contactPoint - _shapeA->getTranslation(); + _offsetB = collision._contactPoint - collision._penetration - _shapeB->getTranslation(); + float pLength = glm::length(collision._penetration); + if (pLength > EPSILON) { + _normal = collision._penetration / pLength; + } + + if (_shapeA->getID() > _shapeB->getID()) { + // swap so that _shapeA always has lower ID + _shapeA = collision.getShapeB(); + _shapeB = collision.getShapeA(); + + glm::vec3 temp = _offsetA; + _offsetA = _offsetB; + _offsetB = temp; + _normal = - _normal; + } +} + +// virtual +float ContactConstraint::enforce() { + glm::vec3 pointA = _shapeA->getTranslation() + _offsetA; + glm::vec3 pointB = _shapeB->getTranslation() + _offsetB; + glm::vec3 penetration = pointA - pointB; + float pDotN = glm::dot(penetration, _normal); + if (pDotN > EPSILON) { + penetration = (0.99f * pDotN) * _normal; + // NOTE: Shape::computeEffectiveMass() has side effects: computes and caches partial Lagrangian coefficients + // which are then used in the accumulateDelta() calls below. + float massA = _shapeA->computeEffectiveMass(penetration, pointA); + float massB = _shapeB->computeEffectiveMass(-penetration, pointB); + float totalMass = massA + massB; + if (totalMass < EPSILON) { + massA = massB = 1.0f; + totalMass = 2.0f; + } + // NOTE: Shape::accumulateDelta() uses the coefficients from previous call to Shape::computeEffectiveMass() + // and remember that penetration points from A into B + _shapeA->accumulateDelta(massB / totalMass, -penetration); + _shapeB->accumulateDelta(massA / totalMass, penetration); + return pDotN; + } + return 0.0f; +} + +void ContactConstraint::updateContact(const CollisionInfo& collision, quint32 frame) { + _lastFrame = frame; + _offsetA = collision._contactPoint - collision._shapeA->getTranslation(); + _offsetB = collision._contactPoint - collision._penetration - collision._shapeB->getTranslation(); + float pLength = glm::length(collision._penetration); + if (pLength > EPSILON) { + _normal = collision._penetration / pLength; + } else { + _normal = glm::vec3(0.0f); + } + if (collision._shapeA->getID() > collision._shapeB->getID()) { + // our _shapeA always has lower ID + glm::vec3 temp = _offsetA; + _offsetA = _offsetB; + _offsetB = temp; + _normal = - _normal; + } +} diff --git a/libraries/shared/src/ContactConstraint.h b/libraries/shared/src/ContactConstraint.h new file mode 100644 index 0000000000..1c8b7d1b57 --- /dev/null +++ b/libraries/shared/src/ContactConstraint.h @@ -0,0 +1,44 @@ +// +// ContactConstraint.h +// libraries/shared/src +// +// Created by Andrew Meadows 2014.07.30 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ContactConstraint_h +#define hifi_ContactConstraint_h + +#include +#include + +#include "CollisionInfo.h" + +class Shape; + +class ContactConstraint { +public: + ContactConstraint(); + ContactConstraint(const CollisionInfo& collision, quint32 frame); + + virtual float enforce(); + + void updateContact(const CollisionInfo& collision, quint32 frame); + quint32 getLastFrame() const { return _lastFrame; } + + Shape* getShapeA() const { return _shapeA; } + Shape* getShapeB() const { return _shapeB; } + +protected: + quint32 _lastFrame; // frame count of last update + Shape* _shapeA; + Shape* _shapeB; + glm::vec3 _offsetA; // contact point relative to A's center + glm::vec3 _offsetB; // contact point relative to B's center + glm::vec3 _normal; // (points from A toward B) +}; + +#endif // hifi_ContactConstraint_h diff --git a/libraries/shared/src/PhysicsSimulation.cpp b/libraries/shared/src/PhysicsSimulation.cpp index ca9d3d303a..3e3529be10 100644 --- a/libraries/shared/src/PhysicsSimulation.cpp +++ b/libraries/shared/src/PhysicsSimulation.cpp @@ -14,6 +14,7 @@ #include "PhysicsSimulation.h" +#include "PerfStat.h" #include "PhysicsEntity.h" #include "Ragdoll.h" #include "SharedUtil.h" @@ -24,8 +25,7 @@ int MAX_ENTITIES_PER_SIMULATION = 64; int MAX_COLLISIONS_PER_SIMULATION = 256; -PhysicsSimulation::PhysicsSimulation() : _collisionList(MAX_COLLISIONS_PER_SIMULATION), - _numIterations(0), _numCollisions(0), _constraintError(0.0f), _stepTime(0) { +PhysicsSimulation::PhysicsSimulation() : _frame(0), _collisions(MAX_COLLISIONS_PER_SIMULATION) { } PhysicsSimulation::~PhysicsSimulation() { @@ -87,6 +87,15 @@ void PhysicsSimulation::removeEntity(PhysicsEntity* entity) { break; } } + // remove corresponding contacts + QMap::iterator itr = _contacts.begin(); + while (itr != _contacts.end()) { + if (entity == itr.value().getShapeA()->getEntity() || entity == itr.value().getShapeB()->getEntity()) { + itr = _contacts.erase(itr); + } else { + ++itr; + } + } } bool PhysicsSimulation::addRagdoll(Ragdoll* doll) { @@ -128,44 +137,51 @@ void PhysicsSimulation::removeRagdoll(Ragdoll* doll) { } void PhysicsSimulation::stepForward(float deltaTime, float minError, int maxIterations, quint64 maxUsec) { + ++_frame; quint64 now = usecTimestampNow(); quint64 startTime = now; quint64 expiry = startTime + maxUsec; moveRagdolls(deltaTime); - + computeCollisions(); + enforceContacts(); int numDolls = _dolls.size(); - _numCollisions = 0; + for (int i = 0; i < numDolls; ++i) { + _dolls[i]->enforceRagdollConstraints(); + } + int iterations = 0; float error = 0.0f; do { computeCollisions(); - processCollisions(); + updateContacts(); + resolveCollisions(); - // enforce constraints - error = 0.0f; - for (int i = 0; i < numDolls; ++i) { - error = glm::max(error, _dolls[i]->enforceRagdollConstraints()); + { // enforce constraints + PerformanceTimer perfTimer("5-enforce"); + error = 0.0f; + for (int i = 0; i < numDolls; ++i) { + error = glm::max(error, _dolls[i]->enforceRagdollConstraints()); + } } ++iterations; now = usecTimestampNow(); - } while (_numCollisions != 0 && (iterations < maxIterations) && (error > minError) && (now < expiry)); + } while (_collisions.size() != 0 && (iterations < maxIterations) && (error > minError) && (now < expiry)); - _numIterations = iterations; - _constraintError = error; - _stepTime = usecTimestampNow()- startTime; #ifdef ANDREW_DEBUG + quint64 stepTime = usecTimestampNow()- startTime; // temporary debug info for watching simulation performance - static int adebug = 0; ++adebug; - if (0 == (adebug % 100)) { - std::cout << "adebug Ni = " << _numIterations << " E = " << error << " t = " << _stepTime << std::endl; // adebug + if (0 == (_frame % 100)) { + std::cout << "Ni = " << iterations << " E = " << error << " t = " << stepTime << std::endl; } #endif // ANDREW_DEBUG + pruneContacts(); } void PhysicsSimulation::moveRagdolls(float deltaTime) { + PerformanceTimer perfTimer("1-integrate"); int numDolls = _dolls.size(); for (int i = 0; i < numDolls; ++i) { _dolls.at(i)->stepRagdollForward(deltaTime); @@ -173,7 +189,8 @@ void PhysicsSimulation::moveRagdolls(float deltaTime) { } void PhysicsSimulation::computeCollisions() { - _collisionList.clear(); + PerformanceTimer perfTimer("2-collide"); + _collisions.clear(); // TODO: keep track of QSet collidedEntities; int numEntities = _entities.size(); for (int i = 0; i < numEntities; ++i) { @@ -189,7 +206,7 @@ void PhysicsSimulation::computeCollisions() { for (int k = j+1; k < numShapes; ++k) { const Shape* otherShape = shapes.at(k); if (otherShape && entity->collisionsAreEnabled(j, k)) { - ShapeCollider::collideShapes(shape, otherShape, _collisionList); + ShapeCollider::collideShapes(shape, otherShape, _collisions); } } } @@ -197,18 +214,18 @@ void PhysicsSimulation::computeCollisions() { // collide with others for (int j = i+1; j < numEntities; ++j) { const QVector otherShapes = _entities.at(j)->getShapes(); - ShapeCollider::collideShapesWithShapes(shapes, otherShapes, _collisionList); + ShapeCollider::collideShapesWithShapes(shapes, otherShapes, _collisions); } } - _numCollisions = _collisionList.size(); } -void PhysicsSimulation::processCollisions() { +void PhysicsSimulation::resolveCollisions() { + PerformanceTimer perfTimer("4-resolve"); // walk all collisions, accumulate movement on shapes, and build a list of affected shapes QSet shapes; - int numCollisions = _collisionList.size(); + int numCollisions = _collisions.size(); for (int i = 0; i < numCollisions; ++i) { - CollisionInfo* collision = _collisionList.getCollision(i); + CollisionInfo* collision = _collisions.getCollision(i); collision->apply(); // there is always a shapeA shapes.insert(collision->getShapeA()); @@ -224,3 +241,59 @@ void PhysicsSimulation::processCollisions() { ++shapeItr; } } + +void PhysicsSimulation::enforceContacts() { + QSet shapes; + int numCollisions = _collisions.size(); + for (int i = 0; i < numCollisions; ++i) { + CollisionInfo* collision = _collisions.getCollision(i); + quint64 key = collision->getShapePairKey(); + if (key == 0) { + continue; + } + QMap::iterator itr = _contacts.find(key); + if (itr != _contacts.end()) { + if (itr.value().enforce() > 0.0f) { + shapes.insert(collision->getShapeA()); + shapes.insert(collision->getShapeB()); + } + } + } + // walk all affected shapes and apply accumulated movement + QSet::const_iterator shapeItr = shapes.constBegin(); + while (shapeItr != shapes.constEnd()) { + (*shapeItr)->applyAccumulatedDelta(); + ++shapeItr; + } +} + +void PhysicsSimulation::updateContacts() { + PerformanceTimer perfTimer("3-updateContacts"); + int numCollisions = _collisions.size(); + for (int i = 0; i < numCollisions; ++i) { + CollisionInfo* collision = _collisions.getCollision(i); + quint64 key = collision->getShapePairKey(); + if (key == 0) { + continue; + } + QMap::iterator itr = _contacts.find(key); + if (itr == _contacts.end()) { + _contacts.insert(key, ContactConstraint(*collision, _frame)); + } else { + itr.value().updateContact(*collision, _frame); + } + } +} + +const quint32 MAX_CONTACT_FRAME_LIFETIME = 2; + +void PhysicsSimulation::pruneContacts() { + QMap::iterator itr = _contacts.begin(); + while (itr != _contacts.end()) { + if (_frame - itr.value().getLastFrame() > MAX_CONTACT_FRAME_LIFETIME) { + itr = _contacts.erase(itr); + } else { + ++itr; + } + } +} diff --git a/libraries/shared/src/PhysicsSimulation.h b/libraries/shared/src/PhysicsSimulation.h index c611e06870..6e69e72219 100644 --- a/libraries/shared/src/PhysicsSimulation.h +++ b/libraries/shared/src/PhysicsSimulation.h @@ -12,9 +12,12 @@ #ifndef hifi_PhysicsSimulation #define hifi_PhysicsSimulation +#include +#include #include #include "CollisionInfo.h" +#include "ContactConstraint.h" class PhysicsEntity; class Ragdoll; @@ -41,20 +44,22 @@ public: /// \return distance of largest movement void stepForward(float deltaTime, float minError, int maxIterations, quint64 maxUsec); +protected: void moveRagdolls(float deltaTime); void computeCollisions(); - void processCollisions(); + void resolveCollisions(); + + void enforceContacts(); + void updateContacts(); + void pruneContacts(); private: - CollisionList _collisionList; - QVector _entities; - QVector _dolls; + quint32 _frame; - // some stats - int _numIterations; - int _numCollisions; - float _constraintError; - quint64 _stepTime; + QVector _dolls; + QVector _entities; + CollisionList _collisions; + QMap _contacts; }; #endif // hifi_PhysicsSimulation diff --git a/libraries/shared/src/Shape.h b/libraries/shared/src/Shape.h index 09ed30a116..b1efe6d9ce 100644 --- a/libraries/shared/src/Shape.h +++ b/libraries/shared/src/Shape.h @@ -14,6 +14,7 @@ #include #include +#include class PhysicsEntity; @@ -21,6 +22,7 @@ const float MAX_SHAPE_MASS = 1.0e18f; // something less than sqrt(FLT_MAX) class Shape { public: + static quint32 getNextID() { static quint32 nextID = 0; return ++nextID; } enum Type{ UNKNOWN_SHAPE = 0, @@ -30,10 +32,14 @@ public: LIST_SHAPE }; - Shape() : _type(UNKNOWN_SHAPE), _owningEntity(NULL), _boundingRadius(0.f), _translation(0.f), _rotation(), _mass(MAX_SHAPE_MASS) { } - virtual ~Shape() {} + Shape() : _type(UNKNOWN_SHAPE), _owningEntity(NULL), _boundingRadius(0.f), + _translation(0.f), _rotation(), _mass(MAX_SHAPE_MASS) { + _id = getNextID(); + } + virtual ~Shape() { } int getType() const { return _type; } + quint32 getID() const { return _id; } void setEntity(PhysicsEntity* entity) { _owningEntity = entity; } PhysicsEntity* getEntity() const { return _owningEntity; } @@ -69,17 +75,24 @@ public: protected: // these ctors are protected (used by derived classes only) - Shape(Type type) : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(0.f), _rotation() {} + Shape(Type type) : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(0.f), _rotation() { + _id = getNextID(); + } Shape(Type type, const glm::vec3& position) - : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation() {} + : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation() { + _id = getNextID(); + } Shape(Type type, const glm::vec3& position, const glm::quat& rotation) - : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation(rotation) {} + : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation(rotation) { + _id = getNextID(); + } void setBoundingRadius(float radius) { _boundingRadius = radius; } int _type; + unsigned int _id; PhysicsEntity* _owningEntity; float _boundingRadius; glm::vec3 _translation; diff --git a/libraries/shared/src/VerletCapsuleShape.cpp b/libraries/shared/src/VerletCapsuleShape.cpp index 3ac4899682..ab956264b5 100644 --- a/libraries/shared/src/VerletCapsuleShape.cpp +++ b/libraries/shared/src/VerletCapsuleShape.cpp @@ -90,11 +90,9 @@ float VerletCapsuleShape::computeEffectiveMass(const glm::vec3& penetration, con // one endpoint will move the full amount while the other will move less. _startLagrangeCoef = startCoef / maxCoef; _endLagrangeCoef = endCoef / maxCoef; - assert(!glm::isnan(_startLagrangeCoef)); - assert(!glm::isnan(_startLagrangeCoef)); } else { // The coefficients are the same --> the collision will move both equally - // as if the object were solid. + // as if the contact were at the center of mass. _startLagrangeCoef = 1.0f; _endLagrangeCoef = 1.0f; } @@ -104,8 +102,8 @@ float VerletCapsuleShape::computeEffectiveMass(const glm::vec3& penetration, con void VerletCapsuleShape::accumulateDelta(float relativeMassFactor, const glm::vec3& penetration) { assert(!glm::isnan(relativeMassFactor)); - _startPoint->accumulateDelta(relativeMassFactor * _startLagrangeCoef * penetration); - _endPoint->accumulateDelta(relativeMassFactor * _endLagrangeCoef * penetration); + _startPoint->accumulateDelta((relativeMassFactor * _startLagrangeCoef) * penetration); + _endPoint->accumulateDelta((relativeMassFactor * _endLagrangeCoef) * penetration); } void VerletCapsuleShape::applyAccumulatedDelta() { diff --git a/libraries/shared/src/VerletPoint.cpp b/libraries/shared/src/VerletPoint.cpp index 641ac39341..654a74d7ac 100644 --- a/libraries/shared/src/VerletPoint.cpp +++ b/libraries/shared/src/VerletPoint.cpp @@ -11,9 +11,11 @@ #include "VerletPoint.h" +const float INTEGRATION_FRICTION_FACTOR = 0.6f; + void VerletPoint::integrateForward() { glm::vec3 oldPosition = _position; - _position += 0.6f * (_position - _lastPosition); + _position += INTEGRATION_FRICTION_FACTOR * (_position - _lastPosition); _lastPosition = oldPosition; } diff --git a/libraries/shared/src/VerletPoint.h b/libraries/shared/src/VerletPoint.h index 076a624776..f59afb16c5 100644 --- a/libraries/shared/src/VerletPoint.h +++ b/libraries/shared/src/VerletPoint.h @@ -23,10 +23,6 @@ public: void accumulateDelta(const glm::vec3& delta); void applyAccumulatedDelta(); - glm::vec3 getAccumulatedDelta() const { - return (_numDeltas > 0) ? _accumulatedDelta / (float)_numDeltas : glm::vec3(0.0f); - } - glm::vec3 _position; glm::vec3 _lastPosition; float _mass; diff --git a/tests/jitter/CMakeLists.txt b/tests/jitter/CMakeLists.txt new file mode 100644 index 0000000000..8000e4af50 --- /dev/null +++ b/tests/jitter/CMakeLists.txt @@ -0,0 +1,33 @@ +set(TARGET_NAME jitter-tests) + +set(ROOT_DIR ../..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +#find_package(Qt5Network REQUIRED) +#find_package(Qt5Script REQUIRED) +#find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE) + +#include(${MACRO_DIR}/AutoMTC.cmake) +#auto_mtc(${TARGET_NAME} ${ROOT_DIR}) + +#qt5_use_modules(${TARGET_NAME} Network Script Widgets) + +#include glm - because it's a dependency of shared utils... +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} ${ROOT_DIR}) + +# link in the shared libraries +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) + +IF (WIN32) + target_link_libraries(${TARGET_NAME} Winmm Ws2_32) +ENDIF(WIN32) + diff --git a/tests/jitter/src/main.cpp b/tests/jitter/src/main.cpp new file mode 100644 index 0000000000..aefed73ccf --- /dev/null +++ b/tests/jitter/src/main.cpp @@ -0,0 +1,167 @@ +// +// main.cpp +// JitterTester +// +// Created by Philip on 8/1/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#include +#ifdef _WINDOWS +#include +#else +#include +#include +#endif +#include + +#include // for MovingMinMaxAvg +#include // for usecTimestampNow + +const quint64 MSEC_TO_USEC = 1000; + +void runSend(const char* addressOption, int port, int gap, int size, int report); +void runReceive(const char* addressOption, int port, int gap, int size, int report); + +int main(int argc, const char * argv[]) { + if (argc != 7) { + printf("usage: jitter-tests <--send|--receive>
\n"); + exit(1); + } + const char* typeOption = argv[1]; + const char* addressOption = argv[2]; + const char* portOption = argv[3]; + const char* gapOption = argv[4]; + const char* sizeOption = argv[5]; + const char* reportOption = argv[6]; + int port = atoi(portOption); + int gap = atoi(gapOption); + int size = atoi(sizeOption); + int report = atoi(reportOption); + + std::cout << "type:" << typeOption << "\n"; + std::cout << "address:" << addressOption << "\n"; + std::cout << "port:" << port << "\n"; + std::cout << "gap:" << gap << "\n"; + std::cout << "size:" << size << "\n"; + + if (strcmp(typeOption, "--send") == 0) { + runSend(addressOption, port, gap, size, report); + } else if (strcmp(typeOption, "--receive") == 0) { + runReceive(addressOption, port, gap, size, report); + } + exit(1); +} + +void runSend(const char* addressOption, int port, int gap, int size, int report) { + std::cout << "runSend...\n"; + + int sockfd; + struct sockaddr_in servaddr; + + char* outputBuffer = new char[size]; + memset(outputBuffer, 0, size); + + sockfd=socket(AF_INET,SOCK_DGRAM,0); + + memset(&servaddr, 0, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr=inet_addr(addressOption); + servaddr.sin_port=htons(port); + + const int SAMPLES_FOR_30_SECONDS = 30 * 1000000 / gap; + + std::cout << "SAMPLES_FOR_30_SECONDS:" << SAMPLES_FOR_30_SECONDS << "\n"; + + MovingMinMaxAvg timeGaps(1, SAMPLES_FOR_30_SECONDS); // stats + + quint64 last = usecTimestampNow(); + quint64 lastReport = 0; + + while (true) { + + quint64 now = usecTimestampNow(); + int actualGap = now - last; + + + if (actualGap >= gap) { + sendto(sockfd, outputBuffer, size, 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); + + int gapDifferece = actualGap - gap; + timeGaps.update(gapDifferece); + last = now; + + if (now - lastReport >= (report * MSEC_TO_USEC)) { + std::cout << "SEND gap Difference From Expected " + << "min: " << timeGaps.getMin() << " usecs, " + << "max: " << timeGaps.getMax() << " usecs, " + << "avg: " << timeGaps.getAverage() << " usecs, " + << "min last 30: " << timeGaps.getWindowMin() << " usecs, " + << "max last 30: " << timeGaps.getWindowMax() << " usecs, " + << "avg last 30: " << timeGaps.getWindowAverage() << " usecs " + << "\n"; + lastReport = now; + } + } + } +} + +void runReceive(const char* addressOption, int port, int gap, int size, int report) { + std::cout << "runReceive...\n"; + + + int sockfd,n; + struct sockaddr_in myaddr; + + char* inputBuffer = new char[size]; + memset(inputBuffer, 0, size); + + sockfd=socket(AF_INET, SOCK_DGRAM, 0); + + memset(&myaddr, 0, sizeof(myaddr)); + myaddr.sin_family = AF_INET; + myaddr.sin_addr.s_addr=htonl(INADDR_ANY); + myaddr.sin_port=htons(port); + + const int SAMPLES_FOR_30_SECONDS = 30 * 1000000 / gap; + + std::cout << "SAMPLES_FOR_30_SECONDS:" << SAMPLES_FOR_30_SECONDS << "\n"; + + MovingMinMaxAvg timeGaps(1, SAMPLES_FOR_30_SECONDS); // stats + + if (bind(sockfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0) { + std::cout << "bind failed\n"; + return; + } + + quint64 last = 0; // first case + quint64 lastReport = 0; + + while (true) { + n = recvfrom(sockfd, inputBuffer, size, 0, NULL, NULL); // we don't care about where it came from + + if (last == 0) { + last = usecTimestampNow(); + std::cout << "first packet received\n"; + } else { + quint64 now = usecTimestampNow(); + int actualGap = now - last; + int gapDifferece = actualGap - gap; + timeGaps.update(gapDifferece); + last = now; + + if (now - lastReport >= (report * MSEC_TO_USEC)) { + std::cout << "RECEIVE gap Difference From Expected " + << "min: " << timeGaps.getMin() << " usecs, " + << "max: " << timeGaps.getMax() << " usecs, " + << "avg: " << timeGaps.getAverage() << " usecs, " + << "min last 30: " << timeGaps.getWindowMin() << " usecs, " + << "max last 30: " << timeGaps.getWindowMax() << " usecs, " + << "avg last 30: " << timeGaps.getWindowAverage() << " usecs " + << "\n"; + lastReport = now; + } + } + } +} +