mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-08-04 23:34:39 +02:00
Merge branch 'master' into 19616
This commit is contained in:
commit
b634f8d8c0
37 changed files with 1267 additions and 81 deletions
|
@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ModelItem*> 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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -279,6 +279,9 @@ private:
|
|||
MovingMinMaxAvg<float> _inputRingBufferMsecsAvailableStats;
|
||||
|
||||
MovingMinMaxAvg<float> _audioOutputMsecsUnplayedStats;
|
||||
|
||||
quint64 _lastSentAudioPacket;
|
||||
MovingMinMaxAvg<quint64> _packetSentTimeGaps;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
40
interface/src/ui/overlays/LocalModelsOverlay.cpp
Normal file
40
interface/src/ui/overlays/LocalModelsOverlay.cpp
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
32
interface/src/ui/overlays/LocalModelsOverlay.h
Normal file
32
interface/src/ui/overlays/LocalModelsOverlay.h
Normal file
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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<SendModelsOperationArgs*>(extraData);
|
||||
ModelTreeElement* modelTreeElement = static_cast<ModelTreeElement*>(element);
|
||||
|
||||
const QList<ModelItem>& 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<ModelItem*> _foundModels;
|
||||
};
|
||||
|
||||
void ModelTree::findModelsInCube(const AACube& cube, QVector<ModelItem*>& 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<FindModelsInCubeArgs*>(extraData);
|
||||
const AACube& elementCube = element->getAACube();
|
||||
if (elementCube.touches(args->_cube)) {
|
||||
ModelTreeElement* modelTreeElement = static_cast<ModelTreeElement*>(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<FindModelsInCubeArgs*>(extraData);
|
||||
const AACube& elementCube = element->getAACube();
|
||||
if (elementCube.touches(args->_cube)) {
|
||||
ModelTreeElement* modelTreeElement = static_cast<ModelTreeElement*>(element);
|
||||
|
@ -364,7 +414,7 @@ bool ModelTree::findInCubeForUpdateOperation(OctreeElement* element, void* extra
|
|||
return false;
|
||||
}
|
||||
|
||||
void ModelTree::findModelsForUpdate(const AACube& cube, QVector<ModelItem*> foundModels) {
|
||||
void ModelTree::findModelsForUpdate(const AACube& cube, QVector<ModelItem*>& foundModels) {
|
||||
FindModelsInCubeArgs args(cube);
|
||||
lockForRead();
|
||||
recurseTreeWithOperation(findInCubeForUpdateOperation, &args);
|
||||
|
|
|
@ -36,7 +36,6 @@ public:
|
|||
/// Type safe version of getRoot()
|
||||
ModelTreeElement* getRoot() { return static_cast<ModelTreeElement*>(_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<const ModelItem*>& foundModels);
|
||||
void findModelsInCube(const AACube& cube, QVector<ModelItem*>& 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<ModelItem*> foundModels);
|
||||
void findModelsForUpdate(const AACube& cube, QVector<ModelItem*>& 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);
|
||||
|
|
|
@ -399,6 +399,19 @@ void ModelTreeElement::getModels(const glm::vec3& searchPosition, float searchRa
|
|||
}
|
||||
}
|
||||
|
||||
void ModelTreeElement::getModelsInside(const AACube& box, QVector<ModelItem*>& foundModels) {
|
||||
QList<ModelItem>::iterator modelItr = _modelItems->begin();
|
||||
QList<ModelItem>::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<ModelItem*>& foundModels) {
|
||||
QList<ModelItem>::iterator modelItr = _modelItems->begin();
|
||||
QList<ModelItem>::iterator modelEnd = _modelItems->end();
|
||||
|
|
|
@ -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<ModelItem*>& foundModels);
|
||||
|
||||
void getModelsInside(const AACube& box, QVector<ModelItem*>& foundModels);
|
||||
|
||||
const ModelItem* getModelWithID(uint32_t id) const;
|
||||
|
||||
bool removeModelWithID(uint32_t id);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ public slots:
|
|||
void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
|
||||
void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QVector>
|
||||
|
||||
class Shape;
|
||||
|
@ -47,6 +48,9 @@ public:
|
|||
Shape* getShapeA() const { return const_cast<Shape*>(_shapeA); }
|
||||
Shape* getShapeB() const { return const_cast<Shape*>(_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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
85
libraries/shared/src/ContactConstraint.cpp
Normal file
85
libraries/shared/src/ContactConstraint.cpp
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
44
libraries/shared/src/ContactConstraint.h
Normal file
44
libraries/shared/src/ContactConstraint.h
Normal file
|
@ -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 <QtGlobal>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#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
|
|
@ -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<quint64, ContactConstraint>::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<PhysicsEntity*> 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<Shape*> 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<Shape*> 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<Shape*> 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<quint64, ContactConstraint>::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<Shape*>::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<quint64, ContactConstraint>::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<quint64, ContactConstraint>::iterator itr = _contacts.begin();
|
||||
while (itr != _contacts.end()) {
|
||||
if (_frame - itr.value().getLastFrame() > MAX_CONTACT_FRAME_LIFETIME) {
|
||||
itr = _contacts.erase(itr);
|
||||
} else {
|
||||
++itr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,12 @@
|
|||
#ifndef hifi_PhysicsSimulation
|
||||
#define hifi_PhysicsSimulation
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QMap>
|
||||
#include <QVector>
|
||||
|
||||
#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<PhysicsEntity*> _entities;
|
||||
QVector<Ragdoll*> _dolls;
|
||||
quint32 _frame;
|
||||
|
||||
// some stats
|
||||
int _numIterations;
|
||||
int _numCollisions;
|
||||
float _constraintError;
|
||||
quint64 _stepTime;
|
||||
QVector<Ragdoll*> _dolls;
|
||||
QVector<PhysicsEntity*> _entities;
|
||||
CollisionList _collisions;
|
||||
QMap<quint64, ContactConstraint> _contacts;
|
||||
};
|
||||
|
||||
#endif // hifi_PhysicsSimulation
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <QtGlobal>
|
||||
|
||||
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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
33
tests/jitter/CMakeLists.txt
Normal file
33
tests/jitter/CMakeLists.txt
Normal file
|
@ -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)
|
||||
|
167
tests/jitter/src/main.cpp
Normal file
167
tests/jitter/src/main.cpp
Normal file
|
@ -0,0 +1,167 @@
|
|||
//
|
||||
// main.cpp
|
||||
// JitterTester
|
||||
//
|
||||
// Created by Philip on 8/1/14.
|
||||
// Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#include <iostream>
|
||||
#ifdef _WINDOWS
|
||||
#include <winsock2.h>
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
#include <stdio.h>
|
||||
|
||||
#include <MovingMinMaxAvg.h> // for MovingMinMaxAvg
|
||||
#include <SharedUtil.h> // 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> <address> <port> <gap in usecs> <packet size> <report interval in msecs>\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<int> 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<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue