diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d02ad73b47..7af9ffd85c 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -267,10 +267,6 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, &DomainServer::nodeKilled); - QTimer* silentNodeTimer = new QTimer(this); - connect(silentNodeTimer, SIGNAL(timeout()), nodeList.data(), SLOT(removeSilentNodes())); - silentNodeTimer->start(NODE_SILENCE_THRESHOLD_MSECS); - connect(&nodeList->getNodeSocket(), SIGNAL(readyRead()), SLOT(readAvailableDatagrams())); // add whatever static assignments that have been parsed to the queue diff --git a/examples/defaultScripts.js b/examples/defaultScripts.js index 6b57bf18dd..05ffb0bd3f 100644 --- a/examples/defaultScripts.js +++ b/examples/defaultScripts.js @@ -9,7 +9,7 @@ // Script.load("progress.js"); -Script.load("editEntities.js"); +Script.load("edit.js"); Script.load("selectAudioDevice.js"); Script.load("controllers/hydra/hydraMove.js"); Script.load("headMove.js"); diff --git a/examples/editEntities.js b/examples/edit.js similarity index 94% rename from examples/editEntities.js rename to examples/edit.js index c236336266..7519f56c46 100644 --- a/examples/editEntities.js +++ b/examples/edit.js @@ -29,12 +29,15 @@ Script.include([ "libraries/entityCameraTool.js", "libraries/gridTool.js", "libraries/entityList.js", + "libraries/lightOverlayManager.js", ]); var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; var entityPropertyDialogBox = EntityPropertyDialogBox; +var lightOverlayManager = new LightOverlayManager(); + var cameraManager = new CameraManager(); var grid = Grid(); @@ -45,6 +48,7 @@ var entityListTool = EntityListTool(); selectionManager.addEventListener(function() { selectionDisplay.updateHandles(); + lightOverlayManager.updatePositions(); }); var windowDimensions = Controller.getViewportDimensions(); @@ -70,13 +74,17 @@ var DEFAULT_DIMENSIONS = { z: DEFAULT_DIMENSION }; +var DEFAULT_LIGHT_DIMENSIONS = Vec3.multiply(20, DEFAULT_DIMENSIONS); + var MENU_INSPECT_TOOL_ENABLED = "Inspect Tool"; var MENU_AUTO_FOCUS_ON_SELECT = "Auto Focus on Select"; var MENU_EASE_ON_FOCUS = "Ease Orientation on Focus"; +var MENU_SHOW_LIGHTS_IN_EDIT_MODE = "Show Lights in Edit Mode"; var SETTING_INSPECT_TOOL_ENABLED = "inspectToolEnabled"; var SETTING_AUTO_FOCUS_ON_SELECT = "autoFocusOnSelect"; var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; +var SETTING_SHOW_LIGHTS_IN_EDIT_MODE = "showLightsInEditMode"; var INSUFFICIENT_PERMISSIONS_ERROR_MSG = "You do not have the necessary permissions to edit on this domain." @@ -110,6 +118,10 @@ var importingSVOOverlay = Overlays.addOverlay("text", { visible: false, }); +var MARKETPLACE_URL = "https://metaverse.highfidelity.io/marketplace"; +var marketplaceWindow = new WebWindow('Marketplace', MARKETPLACE_URL, 900, 700, false); +marketplaceWindow.setVisible(false); + var toolBar = (function () { var that = {}, toolBar, @@ -126,7 +138,7 @@ var toolBar = (function () { // Hide active button for now - this may come back, so not deleting yet. activeButton = toolBar.addTool({ - imageURL: toolIconUrl + "models-tool.svg", + imageURL: toolIconUrl + "edit-status.svg", subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, width: toolWidth, height: toolHeight, @@ -135,7 +147,7 @@ var toolBar = (function () { }, true, false); newModelButton = toolBar.addTool({ - imageURL: toolIconUrl + "add-model-tool.svg", + imageURL: toolIconUrl + "upload.svg", subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, width: toolWidth, height: toolHeight, @@ -144,7 +156,7 @@ var toolBar = (function () { }); browseModelsButton = toolBar.addTool({ - imageURL: toolIconUrl + "list-icon.svg", + imageURL: toolIconUrl + "marketplace.svg", width: toolWidth, height: toolHeight, alpha: 0.9, @@ -186,6 +198,8 @@ var toolBar = (function () { alpha: 0.9, visible: false }); + + that.setActive(false); } that.setActive = function(active) { @@ -214,6 +228,7 @@ var toolBar = (function () { } } toolBar.selectTool(activeButton, isActive); + lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); }; // Sets visibility of tool buttons, excluding the power button @@ -311,7 +326,7 @@ var toolBar = (function () { return true; } if (browseModelsButton === toolBar.clicked(clickedOverlay)) { - browseModelsButtonDown = true; + marketplaceWindow.setVisible(true); return true; } @@ -354,8 +369,8 @@ var toolBar = (function () { if (position.x > 0 && position.y > 0 && position.z > 0) { placingEntityID = Entities.addEntity({ type: "Light", - position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), DEFAULT_DIMENSIONS), - dimensions: DEFAULT_DIMENSIONS, + position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_LIGHT_DIMENSIONS), DEFAULT_LIGHT_DIMENSIONS), + dimensions: DEFAULT_LIGHT_DIMENSIONS, isSpotlight: false, diffuseColor: { red: 255, green: 255, blue: 255 }, ambientColor: { red: 255, green: 255, blue: 255 }, @@ -469,12 +484,31 @@ function rayPlaneIntersection(pickRay, point, normal) { function findClickedEntity(event) { var pickRay = Camera.computePickRay(event.x, event.y); - var foundIntersection = Entities.findRayIntersection(pickRay, true); // want precision picking + var entityResult = Entities.findRayIntersection(pickRay, true); // want precision picking + var lightResult = lightOverlayManager.findRayIntersection(pickRay); + lightResult.accurate = true; - if (!foundIntersection.accurate) { + var result; + + if (!entityResult.intersects && !lightResult.intersects) { + return null; + } else if (entityResult.intersects && !lightResult.intersects) { + result = entityResult; + } else if (!entityResult.intersects && lightResult.intersects) { + result = lightResult; + } else { + if (entityResult.distance < lightResult.distance) { + result = entityResult; + } else { + result = lightResult; + } + } + + if (!result.accurate) { return null; } - var foundEntity = foundIntersection.entityID; + + var foundEntity = result.entityID; if (!foundEntity.isKnownID) { var identify = Entities.identifyEntity(foundEntity); @@ -730,6 +764,8 @@ function setupModelMenus() { isCheckable: true, isChecked: Settings.getValue(SETTING_AUTO_FOCUS_ON_SELECT) == "true" }); Menu.addMenuItem({ menuName: "View", menuItemName: MENU_EASE_ON_FOCUS, afterItem: MENU_AUTO_FOCUS_ON_SELECT, isCheckable: true, isChecked: Settings.getValue(SETTING_EASE_ON_FOCUS) == "true" }); + Menu.addMenuItem({ menuName: "View", menuItemName: MENU_SHOW_LIGHTS_IN_EDIT_MODE, afterItem: MENU_EASE_ON_FOCUS, + isCheckable: true, isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE) == "true" }); Entities.setLightsArePickable(false); } @@ -756,11 +792,13 @@ function cleanupModelMenus() { Menu.removeMenuItem("View", MENU_INSPECT_TOOL_ENABLED); Menu.removeMenuItem("View", MENU_AUTO_FOCUS_ON_SELECT); Menu.removeMenuItem("View", MENU_EASE_ON_FOCUS); + Menu.removeMenuItem("View", MENU_SHOW_LIGHTS_IN_EDIT_MODE); } Script.scriptEnding.connect(function() { Settings.setValue(SETTING_AUTO_FOCUS_ON_SELECT, Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)); Settings.setValue(SETTING_EASE_ON_FOCUS, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); + Settings.setValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); progressDialog.cleanup(); toolBar.cleanup(); @@ -837,6 +875,8 @@ function handeMenuEvent(menuItem) { } } else if (menuItem == "Entity List...") { entityListTool.toggleVisible(); + } else if (menuItem == MENU_SHOW_LIGHTS_IN_EDIT_MODE) { + lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); } tooltip.show(false); } @@ -861,6 +901,8 @@ function importSVO(importURL) { if (isActive) { selectionManager.setSelections(pastedEntityIDs); } + + Window.raiseMainWindow(); } else { Window.alert("There was an error importing the entity file."); } @@ -989,7 +1031,7 @@ PropertiesTool = function(opts) { var that = {}; var url = Script.resolvePath('html/entityProperties.html'); - var webView = new WebWindow('Entity Properties', url, 200, 280); + var webView = new WebWindow('Entity Properties', url, 200, 280, true); var visible = false; diff --git a/examples/libraries/entityList.js b/examples/libraries/entityList.js index 942edf18b6..d0b8ddac7f 100644 --- a/examples/libraries/entityList.js +++ b/examples/libraries/entityList.js @@ -2,7 +2,7 @@ EntityListTool = function(opts) { var that = {}; var url = Script.resolvePath('html/entityList.html'); - var webView = new WebWindow('Entities', url, 200, 280); + var webView = new WebWindow('Entities', url, 200, 280, true); var visible = false; diff --git a/examples/libraries/gridTool.js b/examples/libraries/gridTool.js index 6e16186abc..ed4e999be8 100644 --- a/examples/libraries/gridTool.js +++ b/examples/libraries/gridTool.js @@ -229,7 +229,7 @@ GridTool = function(opts) { var listeners = []; var url = Script.resolvePath('html/gridControls.html'); - var webView = new WebWindow('Grid', url, 200, 280); + var webView = new WebWindow('Grid', url, 200, 280, true); horizontalGrid.addListener(function(data) { webView.eventBridge.emitScriptEvent(JSON.stringify(data)); diff --git a/examples/libraries/lightOverlayManager.js b/examples/libraries/lightOverlayManager.js new file mode 100644 index 0000000000..8032d77c49 --- /dev/null +++ b/examples/libraries/lightOverlayManager.js @@ -0,0 +1,135 @@ +var POINT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/point-light.svg"; +var SPOT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/spot-light.svg"; + +LightOverlayManager = function() { + var self = this; + + var visible = false; + + // List of all created overlays + var allOverlays = []; + + // List of overlays not currently being used + var unusedOverlays = []; + + // Map from EntityItemID.id to overlay id + var entityOverlays = {}; + + // Map from EntityItemID.id to EntityItemID object + var entityIDs = {}; + + this.updatePositions = function(ids) { + for (var id in entityIDs) { + var entityID = entityIDs[id]; + var properties = Entities.getEntityProperties(entityID); + Overlays.editOverlay(entityOverlays[entityID.id], { + position: properties.position + }); + } + }; + + this.findRayIntersection = function(pickRay) { + var result = Overlays.findRayIntersection(pickRay); + var found = false; + + if (result.intersects) { + for (var id in entityOverlays) { + if (result.overlayID == entityOverlays[id]) { + result.entityID = entityIDs[id]; + found = true; + break; + } + } + + if (!found) { + result.intersects = false; + } + } + + return result; + }; + + this.setVisible = function(isVisible) { + if (visible != isVisible) { + visible = isVisible; + for (var id in entityOverlays) { + Overlays.editOverlay(entityOverlays[id], { visible: visible }); + } + } + }; + + // Allocate or get an unused overlay + function getOverlay() { + if (unusedOverlays.length == 0) { + var overlay = Overlays.addOverlay("billboard", { + }); + allOverlays.push(overlay); + } else { + var overlay = unusedOverlays.pop(); + }; + return overlay; + } + + function releaseOverlay(overlay) { + unusedOverlays.push(overlay); + Overlays.editOverlay(overlay, { visible: false }); + } + + function addEntity(entityID) { + var properties = Entities.getEntityProperties(entityID); + if (properties.type == "Light" && !(entityID.id in entityOverlays)) { + var overlay = getOverlay(); + entityOverlays[entityID.id] = overlay; + entityIDs[entityID.id] = entityID; + Overlays.editOverlay(overlay, { + position: properties.position, + url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, + rotation: Quat.fromPitchYawRollDegrees(0, 0, 270), + visible: visible, + alpha: 0.9, + scale: 0.5, + color: { red: 255, green: 255, blue: 255 } + }); + } + } + + function deleteEntity(entityID) { + if (entityID.id in entityOverlays) { + releaseOverlay(entityOverlays[entityID.id]); + delete entityOverlays[entityID.id]; + } + } + + function changeEntityID(oldEntityID, newEntityID) { + entityOverlays[newEntityID.id] = entityOverlays[oldEntityID.id]; + entityIDs[newEntityID.id] = newEntityID; + + delete entityOverlays[oldEntityID.id]; + delete entityIDs[oldEntityID.id]; + } + + function clearEntities() { + for (var id in entityOverlays) { + releaseOverlay(entityOverlays[id]); + } + entityOverlays = {}; + entityIDs = {}; + } + + Entities.addingEntity.connect(addEntity); + Entities.changingEntityID.connect(changeEntityID); + Entities.deletingEntity.connect(deleteEntity); + Entities.clearingEntities.connect(clearEntities); + + // Add existing entities + var ids = Entities.findEntities(MyAvatar.position, 100); + for (var i = 0; i < ids.length; i++) { + addEntity(ids[i]); + } + + Script.scriptEnding.connect(function() { + for (var i = 0; i < allOverlays.length; i++) { + Overlays.deleteOverlay(allOverlays[i]); + } + }); +}; diff --git a/examples/libraries/toolBars.js b/examples/libraries/toolBars.js index 951b6704ec..670a69dec7 100644 --- a/examples/libraries/toolBars.js +++ b/examples/libraries/toolBars.js @@ -94,7 +94,7 @@ Tool = function(properties, selectable, selected) { // selectable and selected a } selected = doSelect; - properties.subImage.y = (selected ? 2 : 1) * properties.subImage.height; + properties.subImage.y = (selected ? 0 : 1) * properties.subImage.height; Overlays.editOverlay(this.overlay(), { subImage: properties.subImage }); } this.toggle = function() { @@ -102,7 +102,7 @@ Tool = function(properties, selectable, selected) { // selectable and selected a return; } selected = !selected; - properties.subImage.y = (selected ? 2 : 1) * properties.subImage.height; + properties.subImage.y = (selected ? 0 : 1) * properties.subImage.height; Overlays.editOverlay(this.overlay(), { subImage: properties.subImage }); return selected; diff --git a/examples/users.js b/examples/users.js index 0274cd7321..d9da6c89ae 100644 --- a/examples/users.js +++ b/examples/users.js @@ -13,29 +13,39 @@ var usersWindow = (function () { var WINDOW_WIDTH_2D = 150, WINDOW_MARGIN_2D = 12, + WINDOW_FONT_2D = { size: 12 }, WINDOW_FOREGROUND_COLOR_2D = { red: 240, green: 240, blue: 240 }, WINDOW_FOREGROUND_ALPHA_2D = 0.9, WINDOW_HEADING_COLOR_2D = { red: 180, green: 180, blue: 180 }, WINDOW_HEADING_ALPHA_2D = 0.9, WINDOW_BACKGROUND_COLOR_2D = { red: 80, green: 80, blue: 80 }, WINDOW_BACKGROUND_ALPHA_2D = 0.7, - windowHeight = 0, - usersPane2D, - usersHeading2D, - USERS_FONT_2D = { size: 14 }, - usersLineHeight, - usersLineSpacing, + windowPane2D, + windowHeading2D, + VISIBILITY_SPACER_2D = 12, // Space between list of users and visibility controls + visibilityHeading2D, + VISIBILITY_RADIO_SPACE = 16, + visibilityControls2D, + windowHeight, + windowTextHeight, + windowLineSpacing, + windowLineHeight, // = windowTextHeight + windowLineSpacing - usersOnline, // Raw data - linesOfUsers, // Array of indexes pointing into usersOnline + usersOnline, // Raw users data + linesOfUsers = [], // Array of indexes pointing into usersOnline API_URL = "https://metaverse.highfidelity.io/api/v1/users?status=online", - HTTP_GET_TIMEOUT = 60000, // ms = 1 minute + HTTP_GET_TIMEOUT = 60000, // ms = 1 minute usersRequest, processUsers, - usersTimedOut, + pollUsersTimedOut, usersTimer = null, - UPDATE_TIMEOUT = 5000, // ms = 5s + USERS_UPDATE_TIMEOUT = 5000, // ms = 5s + + myVisibility, + VISIBILITY_VALUES = ["all", "friends", "none"], + visibilityInterval, + VISIBILITY_POLL_INTERVAL = 5000, // ms = 5s MENU_NAME = "Tools", MENU_ITEM = "Users Online", @@ -43,45 +53,56 @@ var usersWindow = (function () { isVisible = true, - viewportHeight; + viewportHeight, - function onMousePressEvent(event) { - var clickedOverlay, - numLinesBefore, - overlayX, - overlayY, - minY, - maxY, - lineClicked; + HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/", + RADIO_BUTTON_SVG = HIFI_PUBLIC_BUCKET + "images/radio-button.svg", + RADIO_BUTTON_SVG_DIAMETER = 14, + RADIO_BUTTON_DISPLAY_SCALE = 0.7, // 1.0 = windowTextHeight + radioButtonDiameter; - if (!isVisible) { - return; - } + function calculateWindowHeight() { + // Reserve 5 lines for window heading plus visibility heading and controls + // Subtract windowLineSpacing for both end of user list and end of controls + windowHeight = (linesOfUsers.length > 0 ? linesOfUsers.length + 5 : 5) * windowLineHeight + - 2 * windowLineSpacing + VISIBILITY_SPACER_2D + 2 * WINDOW_MARGIN_2D; + } - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); - if (clickedOverlay === usersPane2D) { + function updateOverlayPositions() { + var i, + y; - overlayX = event.x - WINDOW_MARGIN_2D; - overlayY = event.y - viewportHeight + windowHeight - WINDOW_MARGIN_2D - (usersLineHeight + usersLineSpacing); - - numLinesBefore = Math.floor(overlayY / (usersLineHeight + usersLineSpacing)); - minY = numLinesBefore * (usersLineHeight + usersLineSpacing); - maxY = minY + usersLineHeight; - - lineClicked = -1; - if (minY <= overlayY && overlayY <= maxY) { - lineClicked = numLinesBefore; - } - - if (0 <= lineClicked && lineClicked < linesOfUsers.length - && overlayX <= usersOnline[linesOfUsers[lineClicked]].usernameWidth) { - //print("Go to " + usersOnline[linesOfUsers[lineClicked]].username); - location.goToUser(usersOnline[linesOfUsers[lineClicked]].username); - } + Overlays.editOverlay(windowPane2D, { + y: viewportHeight - windowHeight + }); + Overlays.editOverlay(windowHeading2D, { + y: viewportHeight - windowHeight + WINDOW_MARGIN_2D + }); + Overlays.editOverlay(visibilityHeading2D, { + y: viewportHeight - 4 * windowLineHeight + windowLineSpacing - WINDOW_MARGIN_2D + }); + for (i = 0; i < visibilityControls2D.length; i += 1) { + y = viewportHeight - (3 - i) * windowLineHeight + windowLineSpacing - WINDOW_MARGIN_2D; + Overlays.editOverlay(visibilityControls2D[i].radioOverlay, { y: y }); + Overlays.editOverlay(visibilityControls2D[i].textOverlay, { y: y }); } } - function updateWindow() { + function updateVisibilityControls() { + var i, + color; + + for (i = 0; i < visibilityControls2D.length; i += 1) { + color = visibilityControls2D[i].selected ? WINDOW_FOREGROUND_COLOR_2D : WINDOW_HEADING_COLOR_2D; + Overlays.editOverlay(visibilityControls2D[i].radioOverlay, { + color: color, + subImage: { y: visibilityControls2D[i].selected ? 0 : RADIO_BUTTON_SVG_DIAMETER } + }); + Overlays.editOverlay(visibilityControls2D[i].textOverlay, { color: color }); + } + } + + function updateUsersDisplay() { var displayText = "", myUsername, user, @@ -92,32 +113,38 @@ var usersWindow = (function () { for (i = 0; i < usersOnline.length; i += 1) { user = usersOnline[i]; if (user.username !== myUsername && user.online) { - usersOnline[i].usernameWidth = Overlays.textSize(usersPane2D, user.username).width; + usersOnline[i].usernameWidth = Overlays.textSize(windowPane2D, user.username).width; linesOfUsers.push(i); displayText += "\n" + user.username; + if (user.location.root) { + displayText += " @ " + user.location.root.name; + } } } - windowHeight = (linesOfUsers.length > 0 ? linesOfUsers.length + 1 : 1) * (usersLineHeight + usersLineSpacing) - - usersLineSpacing + 2 * WINDOW_MARGIN_2D; // First or only line is for heading + displayText = displayText.slice(1); // Remove leading "\n". - Overlays.editOverlay(usersPane2D, { + calculateWindowHeight(); + + Overlays.editOverlay(windowPane2D, { y: viewportHeight - windowHeight, height: windowHeight, text: displayText }); - Overlays.editOverlay(usersHeading2D, { + Overlays.editOverlay(windowHeading2D, { y: viewportHeight - windowHeight + WINDOW_MARGIN_2D, - text: linesOfUsers.length > 0 ? "Online" : "No users online" + text: linesOfUsers.length > 0 ? "Users online" : "No users online" }); + + updateOverlayPositions(); } - function requestUsers() { + function pollUsers() { usersRequest = new XMLHttpRequest(); usersRequest.open("GET", API_URL, true); usersRequest.timeout = HTTP_GET_TIMEOUT; - usersRequest.ontimeout = usersTimedOut; + usersRequest.ontimeout = pollUsersTimedOut; usersRequest.onreadystatechange = processUsers; usersRequest.send(); } @@ -130,46 +157,62 @@ var usersWindow = (function () { response = JSON.parse(usersRequest.responseText); if (response.status !== "success") { print("Error: Request for users status returned status = " + response.status); - usersTimer = Script.setTimeout(requestUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. + usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. return; } if (!response.hasOwnProperty("data") || !response.data.hasOwnProperty("users")) { print("Error: Request for users status returned invalid data"); - usersTimer = Script.setTimeout(requestUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. + usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. return; } usersOnline = response.data.users; - updateWindow(); + updateUsersDisplay(); } else { print("Error: Request for users status returned " + usersRequest.status + " " + usersRequest.statusText); - usersTimer = Script.setTimeout(requestUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. + usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. return; } - usersTimer = Script.setTimeout(requestUsers, UPDATE_TIMEOUT); // Update after finished processing. + usersTimer = Script.setTimeout(pollUsers, USERS_UPDATE_TIMEOUT); // Update after finished processing. } }; - usersTimedOut = function () { + pollUsersTimedOut = function () { print("Error: Request for users status timed out"); - usersTimer = Script.setTimeout(requestUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. + usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. }; + function pollVisibility() { + var currentVisibility = myVisibility; + + myVisibility = GlobalServices.findableBy; + if (myVisibility !== currentVisibility) { + updateVisibilityControls(); + } + } + function setVisible(visible) { + var i; + isVisible = visible; if (isVisible) { if (usersTimer === null) { - requestUsers(); + pollUsers(); } } else { Script.clearTimeout(usersTimer); usersTimer = null; } - Overlays.editOverlay(usersPane2D, { visible: isVisible }); - Overlays.editOverlay(usersHeading2D, { visible: isVisible }); + Overlays.editOverlay(windowPane2D, { visible: isVisible }); + Overlays.editOverlay(windowHeading2D, { visible: isVisible }); + Overlays.editOverlay(visibilityHeading2D, { visible: isVisible }); + for (i = 0; i < visibilityControls2D.length; i += 1) { + Overlays.editOverlay(visibilityControls2D[i].radioOverlay, { visible: isVisible }); + Overlays.editOverlay(visibilityControls2D[i].textOverlay, { visible: isVisible }); + } } function onMenuItemEvent(event) { @@ -178,48 +221,182 @@ var usersWindow = (function () { } } - function update() { + function onMousePressEvent(event) { + var clickedOverlay, + numLinesBefore, + overlayX, + overlayY, + minY, + maxY, + lineClicked, + i, + visibilityChanged; + + if (!isVisible) { + return; + } + + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + + if (clickedOverlay === windowPane2D) { + + overlayX = event.x - WINDOW_MARGIN_2D; + overlayY = event.y - viewportHeight + windowHeight - WINDOW_MARGIN_2D - windowLineHeight; + + numLinesBefore = Math.floor(overlayY / windowLineHeight); + minY = numLinesBefore * windowLineHeight; + maxY = minY + windowTextHeight; + + lineClicked = -1; + if (minY <= overlayY && overlayY <= maxY) { + lineClicked = numLinesBefore; + } + + if (0 <= lineClicked && lineClicked < linesOfUsers.length + && overlayX <= usersOnline[linesOfUsers[lineClicked]].usernameWidth) { + //print("Go to " + usersOnline[linesOfUsers[lineClicked]].username); + location.goToUser(usersOnline[linesOfUsers[lineClicked]].username); + } + } + + visibilityChanged = false; + for (i = 0; i < visibilityControls2D.length; i += 1) { + // Don't need to test radioOverlay if it us under textOverlay. + if (clickedOverlay === visibilityControls2D[i].textOverlay) { + GlobalServices.findableBy = VISIBILITY_VALUES[i]; + visibilityChanged = true; + } + } + if (visibilityChanged) { + for (i = 0; i < visibilityControls2D.length; i += 1) { + // Don't need to handle radioOverlay if it us under textOverlay. + visibilityControls2D[i].selected = clickedOverlay === visibilityControls2D[i].textOverlay; + } + updateVisibilityControls(); + } + } + + function onScriptUpdate() { + var oldViewportHeight = viewportHeight; + viewportHeight = Controller.getViewportDimensions().y; - Overlays.editOverlay(usersPane2D, { - y: viewportHeight - windowHeight - }); - Overlays.editOverlay(usersHeading2D, { - y: viewportHeight - windowHeight + WINDOW_MARGIN_2D - }); + if (viewportHeight !== oldViewportHeight) { + updateOverlayPositions(); + } } function setUp() { - usersPane2D = Overlays.addOverlay("text", { - bounds: { x: 0, y: 0, width: WINDOW_WIDTH_2D, height: 0 }, - topMargin: WINDOW_MARGIN_2D, + var textSizeOverlay; + + textSizeOverlay = Overlays.addOverlay("text", { font: WINDOW_FONT_2D, visible: false }); + windowTextHeight = Math.floor(Overlays.textSize(textSizeOverlay, "1").height); + windowLineSpacing = Math.floor(Overlays.textSize(textSizeOverlay, "1\n2").height - 2 * windowTextHeight); + windowLineHeight = windowTextHeight + windowLineSpacing; + radioButtonDiameter = RADIO_BUTTON_DISPLAY_SCALE * windowTextHeight; + Overlays.deleteOverlay(textSizeOverlay); + + calculateWindowHeight(); + + viewportHeight = Controller.getViewportDimensions().y; + + windowPane2D = Overlays.addOverlay("text", { + x: 0, + y: viewportHeight, // Start up off-screen + width: WINDOW_WIDTH_2D, + height: windowHeight, + topMargin: WINDOW_MARGIN_2D + windowLineHeight, leftMargin: WINDOW_MARGIN_2D, color: WINDOW_FOREGROUND_COLOR_2D, alpha: WINDOW_FOREGROUND_ALPHA_2D, backgroundColor: WINDOW_BACKGROUND_COLOR_2D, backgroundAlpha: WINDOW_BACKGROUND_ALPHA_2D, text: "", - font: USERS_FONT_2D, + font: WINDOW_FONT_2D, visible: isVisible }); - usersHeading2D = Overlays.addOverlay("text", { + windowHeading2D = Overlays.addOverlay("text", { x: WINDOW_MARGIN_2D, + y: viewportHeight, width: WINDOW_WIDTH_2D - 2 * WINDOW_MARGIN_2D, - height: USERS_FONT_2D.size, + height: windowTextHeight, topMargin: 0, leftMargin: 0, color: WINDOW_HEADING_COLOR_2D, alpha: WINDOW_HEADING_ALPHA_2D, backgroundAlpha: 0.0, text: "No users online", - font: USERS_FONT_2D, + font: WINDOW_FONT_2D, visible: isVisible }); - viewportHeight = Controller.getViewportDimensions().y; + visibilityHeading2D = Overlays.addOverlay("text", { + x: WINDOW_MARGIN_2D, + y: viewportHeight, + width: WINDOW_WIDTH_2D - 2 * WINDOW_MARGIN_2D, + height: windowTextHeight, + topMargin: 0, + leftMargin: 0, + color: WINDOW_HEADING_COLOR_2D, + alpha: WINDOW_HEADING_ALPHA_2D, + backgroundAlpha: 0.0, + text: "I am visible to:", + font: WINDOW_FONT_2D, + visible: isVisible + }); - usersLineHeight = Math.floor(Overlays.textSize(usersPane2D, "1").height); - usersLineSpacing = Math.floor(Overlays.textSize(usersPane2D, "1\n2").height - 2 * usersLineHeight); + myVisibility = GlobalServices.findableBy; + if (!/^(all|friends|none)$/.test(myVisibility)) { + print("Error: Unrecognized findableBy value"); + myVisibility = ""; + } + + visibilityControls2D = [{ + radioOverlay: Overlays.addOverlay("image", { // Create first so that it is under textOverlay. + x: WINDOW_MARGIN_2D, + y: viewportHeight, + width: radioButtonDiameter, + height: radioButtonDiameter, + imageURL: RADIO_BUTTON_SVG, + subImage: { + x: 0, + y: RADIO_BUTTON_SVG_DIAMETER, // Off + width: RADIO_BUTTON_SVG_DIAMETER, + height: RADIO_BUTTON_SVG_DIAMETER + }, + color: WINDOW_HEADING_COLOR_2D, + alpha: WINDOW_FOREGROUND_ALPHA_2D + }), + textOverlay: Overlays.addOverlay("text", { + x: WINDOW_MARGIN_2D, + y: viewportHeight, + width: WINDOW_WIDTH_2D - 2 * WINDOW_MARGIN_2D, + height: windowTextHeight, + topMargin: 0, + leftMargin: VISIBILITY_RADIO_SPACE, + color: WINDOW_HEADING_COLOR_2D, + alpha: WINDOW_FOREGROUND_ALPHA_2D, + backgroundAlpha: 0.0, + text: "everyone", + font: WINDOW_FONT_2D, + visible: isVisible + }), + selected: myVisibility === VISIBILITY_VALUES[0] + } ]; + visibilityControls2D[1] = { + radioOverlay: Overlays.cloneOverlay(visibilityControls2D[0].radioOverlay), + textOverlay: Overlays.cloneOverlay(visibilityControls2D[0].textOverlay), + selected: myVisibility === VISIBILITY_VALUES[1] + }; + Overlays.editOverlay(visibilityControls2D[1].textOverlay, { text: "my friends" }); + visibilityControls2D[2] = { + radioOverlay: Overlays.cloneOverlay(visibilityControls2D[0].radioOverlay), + textOverlay: Overlays.cloneOverlay(visibilityControls2D[0].textOverlay), + selected: myVisibility === VISIBILITY_VALUES[2] + }; + Overlays.editOverlay(visibilityControls2D[2].textOverlay, { text: "no one" }); + + updateVisibilityControls(); Controller.mousePressEvent.connect(onMousePressEvent); @@ -232,17 +409,28 @@ var usersWindow = (function () { }); Menu.menuItemEvent.connect(onMenuItemEvent); - Script.update.connect(update); + Script.update.connect(onScriptUpdate); + + visibilityInterval = Script.setInterval(pollVisibility, VISIBILITY_POLL_INTERVAL); + + pollUsers(); - requestUsers(); } function tearDown() { + var i; + Menu.removeMenuItem(MENU_NAME, MENU_ITEM); + Script.clearInterval(visibilityInterval); Script.clearTimeout(usersTimer); - Overlays.deleteOverlay(usersHeading2D); - Overlays.deleteOverlay(usersPane2D); + Overlays.deleteOverlay(windowPane2D); + Overlays.deleteOverlay(windowHeading2D); + Overlays.deleteOverlay(visibilityHeading2D); + for (i = 0; i <= visibilityControls2D.length; i += 1) { + Overlays.deleteOverlay(visibilityControls2D[i].textOverlay); + Overlays.deleteOverlay(visibilityControls2D[i].radioOverlay); + } } setUp(); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f04a0c28c6..fb97936526 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -146,7 +146,6 @@ const qint64 MAXIMUM_CACHE_SIZE = 10 * BYTES_PER_GIGABYTES; // 10GB static QTimer* locationUpdateTimer = NULL; static QTimer* balanceUpdateTimer = NULL; -static QTimer* silentNodeTimer = NULL; static QTimer* identityPacketTimer = NULL; static QTimer* billboardPacketTimer = NULL; static QTimer* checkFPStimer = NULL; @@ -258,7 +257,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _dependencyManagerIsSetup(setupEssentials(argc, argv)), _window(new MainWindow(desktop())), _toolWindow(NULL), - _nodeThread(new QThread(this)), _datagramProcessor(), _undoStack(), _undoStackScriptingInterface(&_undoStack), @@ -329,18 +327,25 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _runningScriptsWidget = new RunningScriptsWidget(_window); // start the nodeThread so its event loop is running - _nodeThread->setObjectName("Datagram Processor Thread"); - _nodeThread->start(); + QThread* nodeThread = new QThread(this); + nodeThread->setObjectName("Datagram Processor Thread"); + nodeThread->start(); // make sure the node thread is given highest priority - _nodeThread->setPriority(QThread::TimeCriticalPriority); + nodeThread->setPriority(QThread::TimeCriticalPriority); + + _datagramProcessor = new DatagramProcessor(nodeList.data()); + + // have the NodeList use deleteLater from DM customDeleter + nodeList->setCustomDeleter([](Dependency* dependency) { + static_cast(dependency)->deleteLater(); + }); // put the NodeList and datagram processing on the node thread - nodeList->moveToThread(_nodeThread); - _datagramProcessor.moveToThread(_nodeThread); + nodeList->moveToThread(nodeThread); // connect the DataProcessor processDatagrams slot to the QUDPSocket readyRead() signal - connect(&nodeList->getNodeSocket(), SIGNAL(readyRead()), &_datagramProcessor, SLOT(processDatagrams())); + connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, _datagramProcessor, &DatagramProcessor::processDatagrams); // put the audio processing on a separate thread QThread* audioThread = new QThread(); @@ -427,12 +432,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // connect to the packet sent signal of the _entityEditSender connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); - // move the silentNodeTimer to the _nodeThread - silentNodeTimer = new QTimer(); - connect(silentNodeTimer, SIGNAL(timeout()), nodeList.data(), SLOT(removeSilentNodes())); - silentNodeTimer->start(NODE_SILENCE_THRESHOLD_MSECS); - silentNodeTimer->moveToThread(_nodeThread); - // send the identity packet for our avatar each second to our avatar mixer identityPacketTimer = new QTimer(); connect(identityPacketTimer, &QTimer::timeout, _myAvatar, &MyAvatar::sendIdentityPacket); @@ -547,7 +546,7 @@ void Application::aboutToQuit() { } void Application::cleanupBeforeQuit() { - _datagramProcessor.shutdown(); // tell the datagram processor we're shutting down, so it can short circuit + _datagramProcessor->shutdown(); // tell the datagram processor we're shutting down, so it can short circuit _entities.shutdown(); // tell the entities system we're shutting down, so it will stop running scripts ScriptEngine::stopAllScripts(this); // stop all currently running global scripts @@ -555,7 +554,6 @@ void Application::cleanupBeforeQuit() { // depending on what thread they run in locationUpdateTimer->stop(); balanceUpdateTimer->stop(); - QMetaObject::invokeMethod(silentNodeTimer, "stop", Qt::BlockingQueuedConnection); identityPacketTimer->stop(); billboardPacketTimer->stop(); checkFPStimer->stop(); @@ -565,7 +563,6 @@ void Application::cleanupBeforeQuit() { // and then delete those that got created by "new" delete locationUpdateTimer; delete balanceUpdateTimer; - delete silentNodeTimer; delete identityPacketTimer; delete billboardPacketTimer; delete checkFPStimer; @@ -576,10 +573,6 @@ void Application::cleanupBeforeQuit() { _settingsThread.quit(); saveSettings(); _window->saveGeometry(); - - // TODO: now that this is in cleanupBeforeQuit do we really need it to stop and force - // an event loop to send the packet? - UserActivityLogger::getInstance().close(); // let the avatar mixer know we're out MyAvatar::sendKillAvatar(); @@ -597,10 +590,6 @@ Application::~Application() { tree->lockForWrite(); _entities.getTree()->setSimulation(NULL); tree->unlock(); - - // ask the datagram processing thread to quit and wait until it is done - _nodeThread->quit(); - _nodeThread->wait(); _octreeProcessor.terminate(); _entityEditSender.terminate(); @@ -620,6 +609,13 @@ Application::~Application() { DependencyManager::destroy(); //DependencyManager::destroy(); DependencyManager::destroy(); + + QThread* nodeThread = DependencyManager::get()->thread(); + DependencyManager::destroy(); + + // ask the node thread to quit and wait until it is done + nodeThread->quit(); + nodeThread->wait(); qInstallMessageHandler(NULL); // NOTE: Do this as late as possible so we continue to get our log messages } @@ -877,6 +873,10 @@ void Application::controlledBroadcastToNodes(const QByteArray& packet, const Nod } } +void Application::importSVOFromURL(QUrl url) { + emit svoImportRequested(url.url()); +} + bool Application::event(QEvent* event) { // handle custom URL @@ -889,7 +889,7 @@ bool Application::event(QEvent* event) { if (!url.isEmpty()) { if (url.scheme() == HIFI_URL_SCHEME) { DependencyManager::get()->handleLookupString(fileEvent->url().toString()); - } else if (url.url().toLower().endsWith(SVO_EXTENSION)) { + } else if (url.path().toLower().endsWith(SVO_EXTENSION)) { emit svoImportRequested(url.url()); } } @@ -1455,10 +1455,11 @@ void Application::dropEvent(QDropEvent *event) { QString snapshotPath; const QMimeData *mimeData = event->mimeData(); foreach (QUrl url, mimeData->urls()) { - if (url.url().toLower().endsWith(SNAPSHOT_EXTENSION)) { + auto lower = url.path().toLower(); + if (lower.endsWith(SNAPSHOT_EXTENSION)) { snapshotPath = url.toLocalFile(); break; - } else if (url.url().toLower().endsWith(SVO_EXTENSION)) { + } else if (lower.endsWith(SVO_EXTENSION)) { emit svoImportRequested(url.url()); event->acceptProposedAction(); return; @@ -1498,7 +1499,7 @@ void Application::checkFPS() { _fps = (float)_frameCount / diffTime; _frameCount = 0; - _datagramProcessor.resetCounters(); + _datagramProcessor->resetCounters(); _timerStart.start(); // ask the node list to check in with the domain server @@ -2739,7 +2740,6 @@ const GLfloat WORLD_DIFFUSE_COLOR[] = { 0.6f, 0.525f, 0.525f }; const GLfloat WORLD_SPECULAR_COLOR[] = { 0.94f, 0.94f, 0.737f, 1.0f }; const glm::vec3 GLOBAL_LIGHT_COLOR = { 0.6f, 0.525f, 0.525f }; -const float GLOBAL_LIGHT_INTENSITY = 1.0f; void Application::setupWorldLight() { @@ -4094,5 +4094,8 @@ void Application::checkSkeleton() { _myAvatar->setSkeletonModelURL(DEFAULT_BODY_MODEL_URL); _myAvatar->sendIdentityPacket(); + } else { + _myAvatar->updateLocalAABox(); + _physicsEngine.setAvatarData(_myAvatar); } } diff --git a/interface/src/Application.h b/interface/src/Application.h index bcd31fcd51..b013692393 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -216,6 +216,8 @@ public: float getFieldOfView() { return _fieldOfView.get(); } void setFieldOfView(float fov) { _fieldOfView.set(fov); } + void importSVOFromURL(QUrl url); + NodeToOctreeSceneStats* getOcteeSceneStats() { return &_octreeServerSceneStats; } void lockOctreeSceneStats() { _octreeSceneStatsLock.lockForRead(); } void unlockOctreeSceneStats() { _octreeSceneStatsLock.unlock(); } @@ -445,10 +447,8 @@ private: MainWindow* _window; ToolWindow* _toolWindow; - - - QThread* _nodeThread; - DatagramProcessor _datagramProcessor; + + DatagramProcessor* _datagramProcessor; QUndoStack _undoStack; UndoStackScriptingInterface _undoStackScriptingInterface; diff --git a/interface/src/GLCanvas.cpp b/interface/src/GLCanvas.cpp index 4ece8f0857..4587fca0f4 100644 --- a/interface/src/GLCanvas.cpp +++ b/interface/src/GLCanvas.cpp @@ -170,7 +170,7 @@ void GLCanvas::wheelEvent(QWheelEvent* event) { void GLCanvas::dragEnterEvent(QDragEnterEvent* event) { const QMimeData *mimeData = event->mimeData(); foreach (QUrl url, mimeData->urls()) { - auto lower = url.url().toLower(); + auto lower = url.path().toLower(); if (lower.endsWith(SNAPSHOT_EXTENSION) || lower.endsWith(SVO_EXTENSION)) { event->acceptProposedAction(); break; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index cdd0d9bf90..2823e8eb23 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -697,6 +697,12 @@ void Menu::removeAction(QMenu* menu, const QString& actionName) { } void Menu::setIsOptionChecked(const QString& menuOption, bool isChecked) { + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(Menu::getInstance(), "setIsOptionChecked", Qt::BlockingQueuedConnection, + Q_ARG(const QString&, menuOption), + Q_ARG(bool, isChecked)); + return; + } QAction* menu = _actionHash.value(menuOption); if (menu) { menu->setChecked(isChecked); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index a0fa21c674..75d77b780a 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1026,11 +1026,6 @@ float Avatar::getHeadHeight() const { return DEFAULT_HEAD_HEIGHT; } -float Avatar::getBoundingRadius() const { - // TODO: also use head model when computing the avatar's bounding radius - return _skeletonModel.getBoundingRadius(); -} - float Avatar::getPelvisFloatingHeight() const { return -_skeletonModel.getBindExtents().minimum.y; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index e9a21af98e..0cde800be0 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -133,9 +133,6 @@ public: virtual void applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration) { } - /// \return bounding radius of avatar - virtual float getBoundingRadius() const; - Q_INVOKABLE void setSkeletonOffset(const glm::vec3& offset); Q_INVOKABLE glm::vec3 getSkeletonOffset() { return _skeletonOffset; } virtual glm::vec3 getSkeletonPosition() const; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9ecc0a3798..bdb0877cda 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -49,8 +49,6 @@ using namespace std; const glm::vec3 DEFAULT_UP_DIRECTION(0.0f, 1.0f, 0.0f); const float YAW_SPEED = 500.0f; // degrees/sec const float PITCH_SPEED = 100.0f; // degrees/sec -const float COLLISION_RADIUS_SCALAR = 1.2f; // pertains to avatar-to-avatar collisions -const float COLLISION_RADIUS_SCALE = 0.125f; const float DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES = 30.0f; const float MAX_WALKING_SPEED = 2.5f; // human walking speed @@ -956,6 +954,17 @@ glm::vec3 MyAvatar::getSkeletonPosition() const { return Avatar::getPosition(); } +void MyAvatar::updateLocalAABox() { + const CapsuleShape& capsule = _skeletonModel.getBoundingShape(); + float radius = capsule.getRadius(); + float height = 2.0f * (capsule.getHalfHeight() + radius); + glm::vec3 offset = _skeletonModel.getBoundingShapeOffset(); + glm::vec3 corner(-radius, -0.5f * height, -radius); + corner += offset; + glm::vec3 scale(2.0f * radius, height, 2.0f * radius); + _localAABox.setBox(corner, scale); +} + QString MyAvatar::getScriptedMotorFrame() const { QString frame = "avatar"; if (_scriptedMotorFrame == SCRIPTED_MOTOR_CAMERA_FRAME) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 7376acfdcb..08c0228f1e 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -121,6 +121,7 @@ public: virtual void setAttachmentData(const QVector& attachmentData); virtual glm::vec3 getSkeletonPosition() const; + void updateLocalAABox(); void clearJointAnimationPriorities(); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index d083116ecd..4fdebd5f6f 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -659,57 +659,56 @@ void SkeletonModel::buildShapes() { void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { // compute default joint transforms int numStates = _jointStates.size(); + assert(numStates == _shapes.size()); QVector transforms; transforms.fill(glm::mat4(), numStates); - // compute the default transforms - for (int i = 0; i < numStates; i++) { - JointState& state = _jointStates[i]; - const FBXJoint& joint = state.getFBXJoint(); - int parentIndex = joint.parentIndex; - if (parentIndex == -1) { - transforms[i] = _jointStates[i].getTransform(); - continue; - } - - glm::quat modifiedRotation = joint.preRotation * joint.rotation * joint.postRotation; - transforms[i] = transforms[parentIndex] * glm::translate(joint.translation) - * joint.preTransform * glm::mat4_cast(modifiedRotation) * joint.postTransform; - // TODO: Andrew to harvest transforms here to move shapes to correct positions so that - // bounding capsule calculations below are correct. - } - // compute bounding box that encloses all shapes Extents totalExtents; totalExtents.reset(); totalExtents.addPoint(glm::vec3(0.0f)); - for (int i = 0; i < _shapes.size(); i++) { + for (int i = 0; i < numStates; i++) { + // compute the default transform of this joint + JointState& state = _jointStates[i]; + const FBXJoint& joint = state.getFBXJoint(); + int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + transforms[i] = _jointStates[i].getTransform(); + } else { + glm::quat modifiedRotation = joint.preRotation * joint.rotation * joint.postRotation; + transforms[i] = transforms[parentIndex] * glm::translate(joint.translation) + * joint.preTransform * glm::mat4_cast(modifiedRotation) * joint.postTransform; + } + Shape* shape = _shapes[i]; if (!shape) { continue; } + + // Each joint with a shape contributes to the totalExtents: a box + // that contains the sphere centered at the end of the joint with radius of the bone. + // TODO: skip hand and arm shapes for bounding box calculation - Extents shapeExtents; - shapeExtents.reset(); - glm::vec3 localPosition = shape->getTranslation(); + glm::vec3 jointPosition = extractTranslation(transforms[i]); + int type = shape->getType(); if (type == CAPSULE_SHAPE) { // add the two furthest surface points of the capsule CapsuleShape* capsule = static_cast(shape); - glm::vec3 axis; - capsule->computeNormalizedAxis(axis); float radius = capsule->getRadius(); - float halfHeight = capsule->getHalfHeight(); - axis = halfHeight * axis + glm::vec3(radius); - - shapeExtents.addPoint(localPosition + axis); - shapeExtents.addPoint(localPosition - axis); + glm::vec3 axis(radius); + Extents shapeExtents; + shapeExtents.reset(); + shapeExtents.addPoint(jointPosition + axis); + shapeExtents.addPoint(jointPosition - axis); totalExtents.addExtents(shapeExtents); } else if (type == SPHERE_SHAPE) { float radius = shape->getBoundingRadius(); - glm::vec3 axis = glm::vec3(radius); - shapeExtents.addPoint(localPosition + axis); - shapeExtents.addPoint(localPosition - axis); + glm::vec3 axis(radius); + Extents shapeExtents; + shapeExtents.reset(); + shapeExtents.addPoint(jointPosition + axis); + shapeExtents.addPoint(jointPosition - axis); totalExtents.addExtents(shapeExtents); } } @@ -791,8 +790,6 @@ void SkeletonModel::renderBoundingCollisionShapes(float alpha) { glPopMatrix(); } -const int BALL_SUBDIVISIONS = 10; - bool SkeletonModel::hasSkeleton() { return isActive() ? _geometry->getFBXGeometry().rootJointIndex != -1 : false; } diff --git a/interface/src/devices/TV3DManager.cpp b/interface/src/devices/TV3DManager.cpp index 205ebfd29d..b5f57301f1 100644 --- a/interface/src/devices/TV3DManager.cpp +++ b/interface/src/devices/TV3DManager.cpp @@ -107,13 +107,16 @@ void TV3DManager::display(Camera& whichCamera) { const bool displayOverlays = Menu::getInstance()->isOptionChecked(MenuOption::UserInterface); DependencyManager::get()->prepare(); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_SCISSOR_TEST); // render left side view glViewport(portalX, portalY, portalW, portalH); glScissor(portalX, portalY, portalW, portalH); + + Camera eyeCamera; + eyeCamera.setRotation(whichCamera.getRotation()); + eyeCamera.setPosition(whichCamera.getPosition()); glPushMatrix(); { @@ -129,7 +132,8 @@ void TV3DManager::display(Camera& whichCamera) { glMatrixMode(GL_MODELVIEW); glLoadIdentity(); - Application::getInstance()->displaySide(whichCamera, false, RenderArgs::STEREO_LEFT); + eyeCamera.setEyeOffsetPosition(glm::vec3(-_activeEye->modelTranslation,0,0)); + Application::getInstance()->displaySide(eyeCamera, false, RenderArgs::MONO); if (displayOverlays) { applicationOverlay.displayOverlayTexture3DTV(whichCamera, _aspect, fov); @@ -150,7 +154,7 @@ void TV3DManager::display(Camera& whichCamera) { _activeEye = &_rightEye; glMatrixMode(GL_PROJECTION); glLoadIdentity(); // reset projection matrix - glFrustum(_rightEye.left, _rightEye.right, _rightEye.bottom, _rightEye.top, nearZ, farZ); // set left view frustum + glFrustum(_rightEye.left, _rightEye.right, _rightEye.bottom, _rightEye.top, nearZ, farZ); // set right view frustum GLfloat p[4][4]; glGetFloatv(GL_PROJECTION_MATRIX, &(p[0][0])); GLfloat cotangent = p[1][1]; @@ -159,7 +163,8 @@ void TV3DManager::display(Camera& whichCamera) { glMatrixMode(GL_MODELVIEW); glLoadIdentity(); - Application::getInstance()->displaySide(whichCamera, false, RenderArgs::STEREO_RIGHT); + eyeCamera.setEyeOffsetPosition(glm::vec3(-_activeEye->modelTranslation,0,0)); + Application::getInstance()->displaySide(eyeCamera, false, RenderArgs::MONO); if (displayOverlays) { applicationOverlay.displayOverlayTexture3DTV(whichCamera, _aspect, fov); diff --git a/interface/src/scripting/WebWindowClass.cpp b/interface/src/scripting/WebWindowClass.cpp index 2e0f88c776..ae0e327cb1 100644 --- a/interface/src/scripting/WebWindowClass.cpp +++ b/interface/src/scripting/WebWindowClass.cpp @@ -18,8 +18,10 @@ #include #include "Application.h" -#include "WindowScriptingInterface.h" +#include "ui/DataWebPage.h" +#include "MainWindow.h" #include "WebWindowClass.h" +#include "WindowScriptingInterface.h" ScriptEventBridge::ScriptEventBridge(QObject* parent) : QObject(parent) { } @@ -32,27 +34,47 @@ void ScriptEventBridge::emitScriptEvent(const QString& data) { emit scriptEventReceived(data); } - -WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height) +WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow) : QObject(NULL), - _eventBridge(new ScriptEventBridge(this)) { + _eventBridge(new ScriptEventBridge(this)), + _isToolWindow(isToolWindow) { - ToolWindow* toolWindow = Application::getInstance()->getToolWindow(); + if (_isToolWindow) { + ToolWindow* toolWindow = Application::getInstance()->getToolWindow(); - _dockWidget = new QDockWidget(title, toolWindow); - _dockWidget->setFeatures(QDockWidget::DockWidgetMovable); + auto dockWidget = new QDockWidget(title, toolWindow); + dockWidget->setFeatures(QDockWidget::DockWidgetMovable); - _webView = new QWebView(_dockWidget); + _webView = new QWebView(dockWidget); + addEventBridgeToWindowObject(); + + dockWidget->setWidget(_webView); + + toolWindow->addDockWidget(Qt::RightDockWidgetArea, dockWidget); + + _windowWidget = dockWidget; + } else { + _windowWidget = new QWidget(Application::getInstance()->getWindow(), Qt::Window); + _windowWidget->setMinimumSize(width, height); + + auto layout = new QVBoxLayout(_windowWidget); + layout->setContentsMargins(0, 0, 0, 0); + _windowWidget->setLayout(layout); + + _webView = new QWebView(_windowWidget); + + layout->addWidget(_webView); + + addEventBridgeToWindowObject(); + } + + _webView->setPage(new DataWebPage()); _webView->setUrl(url); - addEventBridgeToWindowObject(); - _dockWidget->setWidget(_webView); - - toolWindow->addDockWidget(Qt::RightDockWidgetArea, _dockWidget); + connect(this, &WebWindowClass::destroyed, _windowWidget, &QWidget::deleteLater); connect(_webView->page()->mainFrame(), &QWebFrame::javaScriptWindowObjectCleared, this, &WebWindowClass::addEventBridgeToWindowObject); - connect(this, &WebWindowClass::destroyed, _dockWidget, &QWidget::deleteLater); } WebWindowClass::~WebWindowClass() { @@ -64,10 +86,14 @@ void WebWindowClass::addEventBridgeToWindowObject() { void WebWindowClass::setVisible(bool visible) { if (visible) { - QMetaObject::invokeMethod( - Application::getInstance()->getToolWindow(), "setVisible", Qt::BlockingQueuedConnection, Q_ARG(bool, visible)); + if (_isToolWindow) { + QMetaObject::invokeMethod( + Application::getInstance()->getToolWindow(), "setVisible", Qt::BlockingQueuedConnection, Q_ARG(bool, visible)); + } else { + QMetaObject::invokeMethod(_windowWidget, "raise", Qt::BlockingQueuedConnection); + } } - QMetaObject::invokeMethod(_dockWidget, "setVisible", Qt::BlockingQueuedConnection, Q_ARG(bool, visible)); + QMetaObject::invokeMethod(_windowWidget, "setVisible", Qt::BlockingQueuedConnection, Q_ARG(bool, visible)); } QScriptValue WebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { @@ -78,7 +104,8 @@ QScriptValue WebWindowClass::constructor(QScriptContext* context, QScriptEngine* Q_ARG(const QString&, file), Q_ARG(QString, context->argument(1).toString()), Q_ARG(int, context->argument(2).toInteger()), - Q_ARG(int, context->argument(3).toInteger())); + Q_ARG(int, context->argument(3).toInteger()), + Q_ARG(bool, context->argument(4).toBool())); connect(engine, &QScriptEngine::destroyed, retVal, &WebWindowClass::deleteLater); diff --git a/interface/src/scripting/WebWindowClass.h b/interface/src/scripting/WebWindowClass.h index 0fa88804f2..c923fdd748 100644 --- a/interface/src/scripting/WebWindowClass.h +++ b/interface/src/scripting/WebWindowClass.h @@ -35,7 +35,7 @@ class WebWindowClass : public QObject { Q_OBJECT Q_PROPERTY(QObject* eventBridge READ getEventBridge) public: - WebWindowClass(const QString& title, const QString& url, int width, int height); + WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow = false); ~WebWindowClass(); static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); @@ -46,9 +46,10 @@ public slots: void addEventBridgeToWindowObject(); private: - QDockWidget* _dockWidget; + QWidget* _windowWidget; QWebView* _webView; ScriptEventBridge* _eventBridge; + bool _isToolWindow; }; #endif diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 52de31df3c..7f4b5ddf45 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -35,8 +35,8 @@ WindowScriptingInterface::WindowScriptingInterface() : connect(Application::getInstance(), &Application::svoImportRequested, this, &WindowScriptingInterface::svoImportRequested); } -WebWindowClass* WindowScriptingInterface::doCreateWebWindow(const QString& title, const QString& url, int width, int height) { - return new WebWindowClass(title, url, width, height); +WebWindowClass* WindowScriptingInterface::doCreateWebWindow(const QString& title, const QString& url, int width, int height, bool isToolWindow) { + return new WebWindowClass(title, url, width, height, isToolWindow); } QScriptValue WindowScriptingInterface::hasFocus() { @@ -44,8 +44,13 @@ QScriptValue WindowScriptingInterface::hasFocus() { } void WindowScriptingInterface::setFocus() { - Application::getInstance()->getWindow()->activateWindow(); - Application::getInstance()->getWindow()->setFocus(); + auto window = Application::getInstance()->getWindow(); + window->activateWindow(); + window->setFocus(); +} + +void WindowScriptingInterface::raiseMainWindow() { + Application::getInstance()->getWindow()->raise(); } void WindowScriptingInterface::setCursorVisible(bool visible) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 34942366eb..6a812f14e3 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -43,6 +43,7 @@ public slots: void setCursorVisible(bool visible); QScriptValue hasFocus(); void setFocus(); + void raiseMainWindow(); QScriptValue alert(const QString& message = ""); QScriptValue confirm(const QString& message = ""); QScriptValue form(const QString& title, QScriptValue array); @@ -83,7 +84,7 @@ private slots: void nonBlockingFormAccepted() { _nonBlockingFormActive = false; _formResult = QDialog::Accepted; emit nonBlockingFormClosed(); } void nonBlockingFormRejected() { _nonBlockingFormActive = false; _formResult = QDialog::Rejected; emit nonBlockingFormClosed(); } - WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); + WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height, bool isToolWindow); private: QString jsRegExp2QtRegExp(QString string); diff --git a/interface/src/ui/DataWebPage.cpp b/interface/src/ui/DataWebPage.cpp index 299ef86c51..bec2c98f55 100644 --- a/interface/src/ui/DataWebPage.cpp +++ b/interface/src/ui/DataWebPage.cpp @@ -11,6 +11,7 @@ #include +#include "Application.h" #include #include @@ -21,7 +22,7 @@ DataWebPage::DataWebPage(QObject* parent) : { // use an OAuthNetworkAccessManager instead of regular QNetworkAccessManager so our requests are authed setNetworkAccessManager(OAuthNetworkAccessManager::getInstance()); - + // give the page an empty stylesheet settings()->setUserStyleSheetUrl(QUrl()); } @@ -31,8 +32,12 @@ void DataWebPage::javaScriptConsoleMessage(const QString& message, int lineNumbe } bool DataWebPage::acceptNavigationRequest(QWebFrame* frame, const QNetworkRequest& request, QWebPage::NavigationType type) { - + if (!request.url().toString().startsWith(HIFI_URL_SCHEME)) { + if (request.url().path().toLower().endsWith(SVO_EXTENSION)) { + Application::getInstance()->importSVOFromURL(request.url()); + return false; + } return true; } else { // this is a hifi URL - have the AddressManager handle it @@ -40,4 +45,8 @@ bool DataWebPage::acceptNavigationRequest(QWebFrame* frame, const QNetworkReques Qt::AutoConnection, Q_ARG(const QString&, request.url().toString())); return false; } -} \ No newline at end of file +} + +QString DataWebPage::userAgentForUrl(const QUrl& url) const { + return HIGH_FIDELITY_USER_AGENT; +} diff --git a/interface/src/ui/DataWebPage.h b/interface/src/ui/DataWebPage.h index 6d89077a33..c1c343a216 100644 --- a/interface/src/ui/DataWebPage.h +++ b/interface/src/ui/DataWebPage.h @@ -20,6 +20,7 @@ public: protected: void javaScriptConsoleMessage(const QString & message, int lineNumber, const QString & sourceID); bool acceptNavigationRequest(QWebFrame* frame, const QNetworkRequest& request, QWebPage::NavigationType type); + virtual QString userAgentForUrl(const QUrl& url) const; }; -#endif // hifi_DataWebPage_h \ No newline at end of file +#endif // hifi_DataWebPage_h diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index 88c097575b..3e3e823737 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -58,6 +58,7 @@ void BillboardOverlay::render(RenderArgs* args) { // rotate about vertical to face the camera rotation = Application::getInstance()->getCamera()->getRotation(); rotation *= glm::angleAxis(glm::pi(), glm::vec3(0.0f, 1.0f, 0.0f)); + rotation *= getRotation(); } else { rotation = getRotation(); } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 3ab8f38fe6..6d7395754c 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -32,6 +32,9 @@ quint64 DEFAULT_FILTERED_LOG_EXPIRY = 2 * USECS_PER_SECOND; using namespace std; +const glm::vec3 DEFAULT_LOCAL_AABOX_CORNER(-0.5f); +const glm::vec3 DEFAULT_LOCAL_AABOX_SCALE(1.0f); + AvatarData::AvatarData() : _sessionUUID(), _position(0.0f), @@ -55,9 +58,9 @@ AvatarData::AvatarData() : _errorLogExpiry(0), _owningAvatarMixer(), _lastUpdateTimer(), - _velocity(0.0f) + _velocity(0.0f), + _localAABox(DEFAULT_LOCAL_AABOX_CORNER, DEFAULT_LOCAL_AABOX_SCALE) { - } AvatarData::~AvatarData() { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 02b2c364c6..28123124a0 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -50,6 +50,7 @@ typedef unsigned long long quint64; #include +#include "AABox.h" #include "HandData.h" #include "HeadData.h" #include "Player.h" @@ -296,8 +297,7 @@ public: QElapsedTimer& getLastUpdateTimer() { return _lastUpdateTimer; } - virtual float getBoundingRadius() const { return 1.0f; } - + const AABox& getLocalAABox() const { return _localAABox; } const Referential* getReferential() const { return _referential; } void togglePhysicsEnabled() { _enablePhysics = !_enablePhysics; } @@ -403,6 +403,8 @@ protected: glm::vec3 _velocity; + AABox _localAABox; + private: // privatize the copy constructor and assignment operator so they cannot be called AvatarData(const AvatarData&); diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 5ef0db57ec..639798527a 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -35,6 +35,25 @@ bool EntityScriptingInterface::canAdjustLocks() { } +void EntityScriptingInterface::setEntityTree(EntityTree* modelTree) { + if (_entityTree) { + disconnect(_entityTree, &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity); + disconnect(_entityTree, &EntityTree::deletingEntity, this, &EntityScriptingInterface::deletingEntity); + disconnect(_entityTree, &EntityTree::changingEntityID, this, &EntityScriptingInterface::changingEntityID); + disconnect(_entityTree, &EntityTree::clearingEntities, this, &EntityScriptingInterface::clearingEntities); + } + + _entityTree = modelTree; + + if (_entityTree) { + connect(_entityTree, &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity); + connect(_entityTree, &EntityTree::deletingEntity, this, &EntityScriptingInterface::deletingEntity); + connect(_entityTree, &EntityTree::changingEntityID, this, &EntityScriptingInterface::changingEntityID); + connect(_entityTree, &EntityTree::clearingEntities, this, &EntityScriptingInterface::clearingEntities); + } +} + + EntityItemID EntityScriptingInterface::addEntity(const EntityItemProperties& properties) { // The application will keep track of creatorTokenID diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index bac018f2ae..9300149a98 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -58,7 +58,7 @@ public: virtual NodeType_t getServerNodeType() const { return NodeType::EntityServer; } virtual OctreeEditPacketSender* createPacketSender() { return new EntityEditPacketSender(); } - void setEntityTree(EntityTree* modelTree) { _entityTree = modelTree; } + void setEntityTree(EntityTree* modelTree); EntityTree* getEntityTree(EntityTree*) { return _entityTree; } public slots: @@ -129,6 +129,11 @@ signals: void enterEntity(const EntityItemID& entityItemID); void leaveEntity(const EntityItemID& entityItemID); + void deletingEntity(const EntityItemID& entityID); + void addingEntity(const EntityItemID& entityID); + void changingEntityID(const EntityItemID& oldEntityID, const EntityItemID& newEntityID); + void clearingEntities(); + private: void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 9cbd1ce070..aee34e12c7 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -39,6 +39,8 @@ EntityTreeElement* EntityTree::createNewElement(unsigned char * octalCode) { } void EntityTree::eraseAllOctreeElements(bool createNewRoot) { + emit clearingEntities(); + // this would be a good place to clean up our entities... if (_simulation) { _simulation->lock(); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 5f7b89950b..1edb16e31c 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -166,6 +166,7 @@ signals: void addingEntity(const EntityItemID& entityID); void entityScriptChanging(const EntityItemID& entityItemID); void changingEntityID(const EntityItemID& oldEntityID, const EntityItemID& newEntityID); + void clearingEntities(); private: diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp index 0812066918..4568949f0e 100644 --- a/libraries/entities/src/LightEntityItem.cpp +++ b/libraries/entities/src/LightEntityItem.cpp @@ -33,7 +33,6 @@ LightEntityItem::LightEntityItem(const EntityItemID& entityItemID, const EntityI _type = EntityTypes::Light; // default property values - const quint8 MAX_COLOR = 255; _color[RED_INDEX] = _color[GREEN_INDEX] = _color[BLUE_INDEX] = 0; _intensity = 1.0f; _exponent = 0.0f; diff --git a/libraries/model/src/model/Stage.cpp b/libraries/model/src/model/Stage.cpp index 970539a908..1c171eee76 100644 --- a/libraries/model/src/model/Stage.cpp +++ b/libraries/model/src/model/Stage.cpp @@ -134,55 +134,55 @@ void EarthSunModel::setSunLongitude(float lon) { _sunLongitude = validateLongitude(lon); invalidate(); } - -Atmosphere::Atmosphere() { - // only if created from nothing shall we create the Buffer to store the properties - Data data; - _dataBuffer = gpu::BufferView(new gpu::Buffer(sizeof(Data), (const gpu::Buffer::Byte*) &data)); - + +Atmosphere::Atmosphere() { + // only if created from nothing shall we create the Buffer to store the properties + Data data; + _dataBuffer = gpu::BufferView(new gpu::Buffer(sizeof(Data), (const gpu::Buffer::Byte*) &data)); + setScatteringWavelength(_scatteringWavelength); setRayleighScattering(_rayleighScattering); setInnerOuterRadiuses(getInnerRadius(), getOuterRadius()); } -void Atmosphere::setScatteringWavelength(Vec3 wavelength) { - _scatteringWavelength = wavelength; - Data& data = editData(); - data._invWaveLength = Vec4(1.0f / powf(wavelength.x, 4.0f), 1.0f / powf(wavelength.y, 4.0f), 1.0f / powf(wavelength.z, 4.0f), 0.0f); -} - -void Atmosphere::setRayleighScattering(float scattering) { - _rayleighScattering = scattering; - updateScattering(); -} - -void Atmosphere::setMieScattering(float scattering) { - _mieScattering = scattering; - updateScattering(); -} - -void Atmosphere::setSunBrightness(float brightness) { - _sunBrightness = brightness; - updateScattering(); -} +void Atmosphere::setScatteringWavelength(Vec3 wavelength) { + _scatteringWavelength = wavelength; + Data& data = editData(); + data._invWaveLength = Vec4(1.0f / powf(wavelength.x, 4.0f), 1.0f / powf(wavelength.y, 4.0f), 1.0f / powf(wavelength.z, 4.0f), 0.0f); +} -void Atmosphere::updateScattering() { - Data& data = editData(); - - data._scatterings.x = getRayleighScattering() * getSunBrightness(); - data._scatterings.y = getMieScattering() * getSunBrightness(); - - data._scatterings.z = getRayleighScattering() * 4.0f * glm::pi(); - data._scatterings.w = getMieScattering() * 4.0f * glm::pi(); -} +void Atmosphere::setRayleighScattering(float scattering) { + _rayleighScattering = scattering; + updateScattering(); +} -void Atmosphere::setInnerOuterRadiuses(float inner, float outer) { - Data& data = editData(); - data._radiuses.x = inner; - data._radiuses.y = outer; - data._scales.x = 1.0f / (outer - inner); - data._scales.z = data._scales.x / data._scales.y; -} +void Atmosphere::setMieScattering(float scattering) { + _mieScattering = scattering; + updateScattering(); +} + +void Atmosphere::setSunBrightness(float brightness) { + _sunBrightness = brightness; + updateScattering(); +} + +void Atmosphere::updateScattering() { + Data& data = editData(); + + data._scatterings.x = getRayleighScattering() * getSunBrightness(); + data._scatterings.y = getMieScattering() * getSunBrightness(); + + data._scatterings.z = getRayleighScattering() * 4.0f * glm::pi(); + data._scatterings.w = getMieScattering() * 4.0f * glm::pi(); +} + +void Atmosphere::setInnerOuterRadiuses(float inner, float outer) { + Data& data = editData(); + data._radiuses.x = inner; + data._radiuses.y = outer; + data._scales.x = 1.0f / (outer - inner); + data._scales.z = data._scales.x / data._scales.y; +} const int NUM_DAYS_PER_YEAR = 365; @@ -267,7 +267,7 @@ void SunSkyStage::updateGraphicsObject() const { static int firstTime = 0; if (firstTime == 0) { firstTime++; - bool result = gpu::Shader::makeProgram(*(_skyPipeline->getProgram())); + gpu::Shader::makeProgram(*(_skyPipeline->getProgram())); } diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index e71c80efc8..2a809f2a7c 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include @@ -300,8 +299,6 @@ void AccountManager::processReply() { passErrorToCallback(requestReply); } delete requestReply; - - emit replyFinished(); } void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) { @@ -342,15 +339,6 @@ void AccountManager::passErrorToCallback(QNetworkReply* requestReply) { } } -void AccountManager::waitForAllPendingReplies() { - while (_pendingCallbackMap.size() > 0) { - QEventLoop loop; - QObject::connect(this, &AccountManager::replyFinished, &loop, &QEventLoop::quit); - loop.exec(); - } -} - - void AccountManager::persistAccountToSettings() { if (_shouldPersistToSettingsFile) { // store this access token into the local settings diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 22d070fbe6..2c9a441db1 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -72,8 +72,6 @@ public: void requestProfile(); DataServerAccountInfo& getAccountInfo() { return _accountInfo; } - - void waitForAllPendingReplies(); public slots: void requestAccessToken(const QString& login, const QString& password); @@ -95,8 +93,6 @@ signals: void loginFailed(); void logoutComplete(); void balanceChanged(qint64 newBalance); - void replyFinished(); - private slots: void processReply(); void handleKeypairGenerationError(); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 279d958082..520dc650ed 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -80,6 +80,10 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short connect(localSocketUpdate, &QTimer::timeout, this, &LimitedNodeList::updateLocalSockAddr); localSocketUpdate->start(LOCAL_SOCKET_UPDATE_INTERVAL_MSECS); + QTimer* silentNodeTimer = new QTimer(this); + connect(silentNodeTimer, &QTimer::timeout, this, &LimitedNodeList::removeSilentNodes); + silentNodeTimer->start(NODE_SILENCE_THRESHOLD_MSECS); + // check the local socket right now updateLocalSockAddr(); @@ -500,6 +504,7 @@ void LimitedNodeList::resetPacketStats() { } void LimitedNodeList::removeSilentNodes() { + QSet killedNodes; eachNodeHashIterator([&](NodeHash::iterator& it){ diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index afbdf23fba..a071eced31 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -223,6 +223,8 @@ protected: HifiSockAddr _localSockAddr; HifiSockAddr _publicSockAddr; HifiSockAddr _stunSockAddr; + + QTimer* _silentNodeTimer; // XXX can BandwidthRecorder be used for this? int _numCollectedPackets; diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 1c6de4bb6c..ccfaa7a4cf 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -37,6 +37,7 @@ const quint64 DOMAIN_SERVER_CHECK_IN_MSECS = 1 * 1000; const int MAX_SILENT_DOMAIN_SERVER_CHECK_INS = 5; +class Application; class Assignment; class NodeList : public LimitedNodeList { @@ -95,6 +96,8 @@ private: HifiSockAddr _assignmentServerSocket; bool _hasCompletedInitialSTUNFailure; unsigned int _stunRequestsSinceSuccess; + + friend class Application; }; #endif // hifi_NodeList_h diff --git a/libraries/networking/src/OAuthNetworkAccessManager.cpp b/libraries/networking/src/OAuthNetworkAccessManager.cpp index 1a52dc4ce7..89a73d5984 100644 --- a/libraries/networking/src/OAuthNetworkAccessManager.cpp +++ b/libraries/networking/src/OAuthNetworkAccessManager.cpp @@ -14,6 +14,7 @@ #include #include "AccountManager.h" +#include "LimitedNodeList.h" #include "SharedUtil.h" #include "OAuthNetworkAccessManager.h" @@ -32,7 +33,7 @@ QNetworkReply* OAuthNetworkAccessManager::createRequest(QNetworkAccessManager::O QIODevice* outgoingData) { AccountManager& accountManager = AccountManager::getInstance(); - if (accountManager.hasValidAccessToken()) { + if (accountManager.hasValidAccessToken() && req.url().host() == DEFAULT_NODE_AUTH_URL.host()) { QNetworkRequest authenticatedRequest(req); authenticatedRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); authenticatedRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 658f32aba1..dfb0d6c150 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -224,8 +224,12 @@ void Resource::refresh() { } if (_reply) { ResourceCache::requestCompleted(this); - delete _reply; + _reply->disconnect(this); + _replyTimer->disconnect(this); + _reply->deleteLater(); _reply = nullptr; + _replyTimer->deleteLater(); + _replyTimer = nullptr; } init(); _request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); @@ -296,9 +300,9 @@ void Resource::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { return; } _reply->disconnect(this); + _replyTimer->disconnect(this); QNetworkReply* reply = _reply; _reply = nullptr; - _replyTimer->disconnect(this); _replyTimer->deleteLater(); _replyTimer = nullptr; ResourceCache::requestCompleted(this); @@ -328,6 +332,10 @@ void Resource::maybeRefresh() { // We don't need to update, return return; } + } else if (!variant.isValid() || !variant.canConvert() || + !variant.value().isValid() || variant.value().isNull()) { + qDebug() << "Cannot determine when" << _url.fileName() << "was modified last, cached version might be outdated"; + return; } qDebug() << "Loaded" << _url.fileName() << "from the disk cache but the network version is newer, refreshing."; refresh(); @@ -351,10 +359,19 @@ void Resource::makeRequest() { } else { if (Q_LIKELY(NetworkAccessManager::getInstance().cache())) { QNetworkCacheMetaData metaData = NetworkAccessManager::getInstance().cache()->metaData(_url); + bool needUpdate = false; if (metaData.expirationDate().isNull() || metaData.expirationDate() <= QDateTime::currentDateTime()) { // If the expiration date is NULL or in the past, // put one far enough away that it won't be an issue. metaData.setExpirationDate(QDateTime::currentDateTime().addYears(100)); + needUpdate = true; + } + if (metaData.lastModified().isNull()) { + // If the lastModified date is NULL, set it to now. + metaData.setLastModified(QDateTime::currentDateTime()); + needUpdate = true; + } + if (needUpdate) { NetworkAccessManager::getInstance().cache()->updateMetaData(metaData); } } @@ -369,9 +386,9 @@ void Resource::makeRequest() { void Resource::handleReplyError(QNetworkReply::NetworkError error, QDebug debug) { _reply->disconnect(this); + _replyTimer->disconnect(this); _reply->deleteLater(); _reply = nullptr; - _replyTimer->disconnect(this); _replyTimer->deleteLater(); _replyTimer = nullptr; ResourceCache::requestCompleted(this); diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index ea94a8e22c..79b4e7f437 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -67,10 +67,6 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy connect(domainServerTimer, SIGNAL(timeout()), this, SLOT(checkInWithDomainServerOrExit())); domainServerTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - QTimer* silentNodeRemovalTimer = new QTimer(this); - connect(silentNodeRemovalTimer, SIGNAL(timeout()), nodeList.data(), SLOT(removeSilentNodes())); - silentNodeRemovalTimer->start(NODE_SILENCE_THRESHOLD_MSECS); - if (shouldSendStats) { // send a stats packet every 1 second QTimer* statsTimer = new QTimer(this); diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index f74ea99c1e..64828708b2 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -61,7 +61,7 @@ void UserActivityLogger::logAction(QString action, QJsonObject details, JSONCall params.errorCallbackReceiver = this; params.errorCallbackMethod = "requestError"; } - + accountManager.authenticatedRequest(USER_ACTIVITY_URL, QNetworkAccessManager::PostOperation, params, @@ -86,13 +86,6 @@ void UserActivityLogger::launch(QString applicationVersion) { logAction(ACTION_NAME, actionDetails); } -void UserActivityLogger::close() { - const QString ACTION_NAME = "close"; - logAction(ACTION_NAME, QJsonObject()); - - AccountManager::getInstance().waitForAllPendingReplies(); -} - void UserActivityLogger::changedDisplayName(QString displayName) { const QString ACTION_NAME = "changed_display_name"; QJsonObject actionDetails; diff --git a/libraries/networking/src/UserActivityLogger.h b/libraries/networking/src/UserActivityLogger.h index 295ad5ee8d..2811be86a8 100644 --- a/libraries/networking/src/UserActivityLogger.h +++ b/libraries/networking/src/UserActivityLogger.h @@ -30,7 +30,7 @@ public slots: void logAction(QString action, QJsonObject details = QJsonObject(), JSONCallbackParameters params = JSONCallbackParameters()); void launch(QString applicationVersion); - void close(); + void changedDisplayName(QString displayName); void changedModel(QString typeOfModel, QString modelURL); void changedDomain(QString domainURL); diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp new file mode 100644 index 0000000000..5173b368c1 --- /dev/null +++ b/libraries/physics/src/CharacterController.cpp @@ -0,0 +1,706 @@ +/* +Bullet Continuous Collision Detection and Physics Library +Copyright (c) 2003-2008 Erwin Coumans http://bulletphysics.com + +This software is provided 'as-is', without any express or implied warranty. +In no event will the authors be held liable for any damages arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it freely, +subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. + If you use this software in a product, an acknowledgment in the product documentation would be appreciated + but is not required. +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + + +//#include + +#include "BulletCollision/CollisionDispatch/btGhostObject.h" + +#include "CharacterController.h" + + +// static helper method +static btVector3 getNormalizedVector(const btVector3& v) { + // NOTE: check the length first, then normalize + // --> avoids assert when trying to normalize zero-length vectors + btScalar vLength = v.length(); + if (vLength < FLT_EPSILON) { + return btVector3(0.0f, 0.0f, 0.0f); + } + btVector3 n = v; + n /= vLength; + return n; +} + +///@todo Interact with dynamic objects, +///Ride kinematicly animated platforms properly +///More realistic (or maybe just a config option) falling +/// -> Should integrate falling velocity manually and use that in stepDown() +///Support jumping +///Support ducking + +/* This callback is unused, but we're keeping it around just in case we figure out how to use it. +class btKinematicClosestNotMeRayResultCallback : public btCollisionWorld::ClosestRayResultCallback +{ +public: +btKinematicClosestNotMeRayResultCallback(btCollisionObject* me) : btCollisionWorld::ClosestRayResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)) +{ +m_me = me; +} + +virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) +{ +if(rayResult.m_collisionObject == m_me) +return 1.0; + +return ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); +} +protected: +btCollisionObject* m_me; +}; +*/ + +class btKinematicClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback +{ + public: + btKinematicClosestNotMeConvexResultCallback(btCollisionObject* me, const btVector3& up, btScalar minSlopeDot) + : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)) + , m_me(me) + , m_up(up) + , m_minSlopeDot(minSlopeDot) + { + } + + virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) { + if (convexResult.m_hitCollisionObject == m_me) { + return btScalar(1.0); + } + + if (!convexResult.m_hitCollisionObject->hasContactResponse()) { + return btScalar(1.0); + } + + btVector3 hitNormalWorld; + if (normalInWorldSpace) { + hitNormalWorld = convexResult.m_hitNormalLocal; + } else { + ///need to transform normal into worldspace + hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; + } + + btScalar dotUp = m_up.dot(hitNormalWorld); + if (dotUp < m_minSlopeDot) { + return btScalar(1.0); + } + + return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace); + } + +protected: + btCollisionObject* m_me; + const btVector3 m_up; + btScalar m_minSlopeDot; +}; + +/* + * Returns the reflection direction of a ray going 'direction' hitting a surface with normal 'normal' + * + * from: http://www-cs-students.stanford.edu/~adityagp/final/node3.html + */ +btVector3 CharacterController::computeReflectionDirection(const btVector3& direction, const btVector3& normal) +{ + return direction - (btScalar(2.0) * direction.dot(normal)) * normal; +} + +/* + * Returns the portion of 'direction' that is parallel to 'normal' + */ +btVector3 CharacterController::parallelComponent(const btVector3& direction, const btVector3& normal) +{ + btScalar magnitude = direction.dot(normal); + return normal * magnitude; +} + +/* + * Returns the portion of 'direction' that is perpindicular to 'normal' + */ +btVector3 CharacterController::perpindicularComponent(const btVector3& direction, const btVector3& normal) +{ + return direction - parallelComponent(direction, normal); +} + +CharacterController::CharacterController( + btPairCachingGhostObject* ghostObject, + btConvexShape* convexShape, + btScalar stepHeight, + int upAxis) { + m_upAxis = upAxis; + m_addedMargin = 0.02; + m_walkDirection.setValue(0,0,0); + m_useGhostObjectSweepTest = true; + m_ghostObject = ghostObject; + m_stepHeight = stepHeight; + m_turnAngle = btScalar(0.0); + m_convexShape = convexShape; + m_useWalkDirection = true; // use walk direction by default, legacy behavior + m_velocityTimeInterval = 0.0; + m_verticalVelocity = 0.0; + m_verticalOffset = 0.0; + m_gravity = 9.8 * 3 ; // 3G acceleration. + m_fallSpeed = 55.0; // Terminal velocity of a sky diver in m/s. + m_jumpSpeed = 10.0; // ? + m_wasOnGround = false; + m_wasJumping = false; + m_interpolateUp = true; + setMaxSlope(btRadians(45.0)); + m_currentStepOffset = 0; + + // internal state data members + full_drop = false; + bounce_fix = false; +} + +CharacterController::~CharacterController() { +} + +btPairCachingGhostObject* CharacterController::getGhostObject() { + return m_ghostObject; +} + +bool CharacterController::recoverFromPenetration(btCollisionWorld* collisionWorld) { + // Here we must refresh the overlapping paircache as the penetrating movement itself or the + // previous recovery iteration might have used setWorldTransform and pushed us into an object + // that is not in the previous cache contents from the last timestep, as will happen if we + // are pushed into a new AABB overlap. Unhandled this means the next convex sweep gets stuck. + // + // Do this by calling the broadphase's setAabb with the moved AABB, this will update the broadphase + // paircache and the ghostobject's internal paircache at the same time. /BW + + btVector3 minAabb, maxAabb; + m_convexShape->getAabb(m_ghostObject->getWorldTransform(), minAabb, maxAabb); + collisionWorld->getBroadphase()->setAabb(m_ghostObject->getBroadphaseHandle(), + minAabb, + maxAabb, + collisionWorld->getDispatcher()); + + bool penetration = false; + + collisionWorld->getDispatcher()->dispatchAllCollisionPairs(m_ghostObject->getOverlappingPairCache(), collisionWorld->getDispatchInfo(), collisionWorld->getDispatcher()); + + m_currentPosition = m_ghostObject->getWorldTransform().getOrigin(); + + btScalar maxPen = btScalar(0.0); + for (int i = 0; i < m_ghostObject->getOverlappingPairCache()->getNumOverlappingPairs(); i++) { + m_manifoldArray.resize(0); + + btBroadphasePair* collisionPair = &m_ghostObject->getOverlappingPairCache()->getOverlappingPairArray()[i]; + + btCollisionObject* obj0 = static_cast(collisionPair->m_pProxy0->m_clientObject); + btCollisionObject* obj1 = static_cast(collisionPair->m_pProxy1->m_clientObject); + + if ((obj0 && !obj0->hasContactResponse()) || (obj1 && !obj1->hasContactResponse())) { + continue; + } + + if (collisionPair->m_algorithm) { + collisionPair->m_algorithm->getAllContactManifolds(m_manifoldArray); + } + + for (int j=0;jgetBody0() == m_ghostObject ? btScalar(-1.0) : btScalar(1.0); + for (int p=0;pgetNumContacts();p++) { + const btManifoldPoint&pt = manifold->getContactPoint(p); + + btScalar dist = pt.getDistance(); + + if (dist < 0.0) { + if (dist < maxPen) { + maxPen = dist; + m_touchingNormal = pt.m_normalWorldOnB * directionSign;//?? + + } + m_currentPosition += pt.m_normalWorldOnB * directionSign * dist * btScalar(0.2); + penetration = true; + } else { + //printf("touching %f\n", dist); + } + } + + //manifold->clearManifold(); + } + } + btTransform newTrans = m_ghostObject->getWorldTransform(); + newTrans.setOrigin(m_currentPosition); + m_ghostObject->setWorldTransform(newTrans); + //printf("m_touchingNormal = %f,%f,%f\n", m_touchingNormal[0], m_touchingNormal[1], m_touchingNormal[2]); + return penetration; +} + +void CharacterController::stepUp( btCollisionWorld* world) { + // phase 1: up + btTransform start, end; + m_targetPosition = m_currentPosition + getUpAxisDirections()[m_upAxis] * (m_stepHeight + (m_verticalOffset > 0.f?m_verticalOffset:0.f)); + + start.setIdentity(); + end.setIdentity(); + + /* FIXME: Handle penetration properly */ + start.setOrigin(m_currentPosition + getUpAxisDirections()[m_upAxis] * (m_convexShape->getMargin() + m_addedMargin)); + end.setOrigin(m_targetPosition); + + btKinematicClosestNotMeConvexResultCallback callback(m_ghostObject, -getUpAxisDirections()[m_upAxis], btScalar(0.7071)); + callback.m_collisionFilterGroup = getGhostObject()->getBroadphaseHandle()->m_collisionFilterGroup; + callback.m_collisionFilterMask = getGhostObject()->getBroadphaseHandle()->m_collisionFilterMask; + + if (m_useGhostObjectSweepTest) { + m_ghostObject->convexSweepTest(m_convexShape, start, end, callback, world->getDispatchInfo().m_allowedCcdPenetration); + } + else { + world->convexSweepTest(m_convexShape, start, end, callback); + } + + if (callback.hasHit()) { + // Only modify the position if the hit was a slope and not a wall or ceiling. + if (callback.m_hitNormalWorld.dot(getUpAxisDirections()[m_upAxis]) > 0.0) { + // we moved up only a fraction of the step height + m_currentStepOffset = m_stepHeight * callback.m_closestHitFraction; + if (m_interpolateUp == true) { + m_currentPosition.setInterpolate3 (m_currentPosition, m_targetPosition, callback.m_closestHitFraction); + } else { + m_currentPosition = m_targetPosition; + } + } + m_verticalVelocity = 0.0; + m_verticalOffset = 0.0; + } else { + m_currentStepOffset = m_stepHeight; + m_currentPosition = m_targetPosition; + } +} + +void CharacterController::updateTargetPositionBasedOnCollision(const btVector3& hitNormal, btScalar tangentMag, btScalar normalMag) { + btVector3 movementDirection = m_targetPosition - m_currentPosition; + btScalar movementLength = movementDirection.length(); + if (movementLength>SIMD_EPSILON) { + movementDirection.normalize(); + + btVector3 reflectDir = computeReflectionDirection(movementDirection, hitNormal); + reflectDir.normalize(); + + btVector3 parallelDir, perpindicularDir; + + parallelDir = parallelComponent(reflectDir, hitNormal); + perpindicularDir = perpindicularComponent(reflectDir, hitNormal); + + m_targetPosition = m_currentPosition; + //if (tangentMag != 0.0) { + if (0) { + btVector3 parComponent = parallelDir * btScalar(tangentMag*movementLength); + //printf("parComponent=%f,%f,%f\n", parComponent[0], parComponent[1], parComponent[2]); + m_targetPosition += parComponent; + } + + if (normalMag != 0.0) { + btVector3 perpComponent = perpindicularDir * btScalar(normalMag*movementLength); + //printf("perpComponent=%f,%f,%f\n", perpComponent[0], perpComponent[1], perpComponent[2]); + m_targetPosition += perpComponent; + } + } else { + //printf("movementLength don't normalize a zero vector\n"); + } +} + +void CharacterController::stepForwardAndStrafe( btCollisionWorld* collisionWorld, const btVector3& walkMove) { + //printf("m_normalizedDirection=%f,%f,%f\n", + // m_normalizedDirection[0], m_normalizedDirection[1], m_normalizedDirection[2]); + // phase 2: forward and strafe + btTransform start, end; + m_targetPosition = m_currentPosition + walkMove; + + start.setIdentity(); + end.setIdentity(); + + btScalar fraction = 1.0; + btScalar distance2 = (m_currentPosition-m_targetPosition).length2(); + //printf("distance2=%f\n", distance2); + + if (m_touchingContact) { + if (m_normalizedDirection.dot(m_touchingNormal) > btScalar(0.0)) { + //interferes with step movement + //updateTargetPositionBasedOnCollision(m_touchingNormal); + } + } + + int maxIter = 10; + + while (fraction > btScalar(0.01) && maxIter-- > 0) { + start.setOrigin(m_currentPosition); + end.setOrigin(m_targetPosition); + btVector3 sweepDirNegative(m_currentPosition - m_targetPosition); + + btKinematicClosestNotMeConvexResultCallback callback(m_ghostObject, sweepDirNegative, btScalar(0.0)); + callback.m_collisionFilterGroup = getGhostObject()->getBroadphaseHandle()->m_collisionFilterGroup; + callback.m_collisionFilterMask = getGhostObject()->getBroadphaseHandle()->m_collisionFilterMask; + + + btScalar margin = m_convexShape->getMargin(); + m_convexShape->setMargin(margin + m_addedMargin); + + + if (m_useGhostObjectSweepTest) { + m_ghostObject->convexSweepTest(m_convexShape, start, end, callback, collisionWorld->getDispatchInfo().m_allowedCcdPenetration); + } else { + collisionWorld->convexSweepTest(m_convexShape, start, end, callback, collisionWorld->getDispatchInfo().m_allowedCcdPenetration); + } + + m_convexShape->setMargin(margin); + + + fraction -= callback.m_closestHitFraction; + + if (callback.hasHit()) { + // we moved only a fraction + //btScalar hitDistance; + //hitDistance = (callback.m_hitPointWorld - m_currentPosition).length(); + + //m_currentPosition.setInterpolate3 (m_currentPosition, m_targetPosition, callback.m_closestHitFraction); + + updateTargetPositionBasedOnCollision(callback.m_hitNormalWorld); + btVector3 currentDir = m_targetPosition - m_currentPosition; + distance2 = currentDir.length2(); + if (distance2 > SIMD_EPSILON) { + currentDir.normalize(); + /* See Quake2: "If velocity is against original velocity, stop ead to avoid tiny oscilations in sloping corners." */ + if (currentDir.dot(m_normalizedDirection) <= btScalar(0.0)) { + break; + } + } else { + //printf("currentDir: don't normalize a zero vector\n"); + break; + } + } else { + // we moved whole way + m_currentPosition = m_targetPosition; + } + + //if (callback.m_closestHitFraction == 0.f) { + // break; + //} + + } +} + +void CharacterController::stepDown( btCollisionWorld* collisionWorld, btScalar dt) { + btTransform start, end, end_double; + bool runonce = false; + + // phase 3: down + /*btScalar additionalDownStep = (m_wasOnGround && !onGround()) ? m_stepHeight : 0.0; + btVector3 step_drop = getUpAxisDirections()[m_upAxis] * (m_currentStepOffset + additionalDownStep); + btScalar downVelocity = (additionalDownStep == 0.0 && m_verticalVelocity<0.0?-m_verticalVelocity:0.0) * dt; + btVector3 gravity_drop = getUpAxisDirections()[m_upAxis] * downVelocity; + m_targetPosition -= (step_drop + gravity_drop);*/ + + btVector3 orig_position = m_targetPosition; + + btScalar downVelocity = (m_verticalVelocity<0.f?-m_verticalVelocity:0.f) * dt; + + if (downVelocity > 0.0 && downVelocity > m_fallSpeed && (m_wasOnGround || !m_wasJumping)) { + downVelocity = m_fallSpeed; + } + + btVector3 step_drop = getUpAxisDirections()[m_upAxis] * (m_currentStepOffset + downVelocity); + m_targetPosition -= step_drop; + + btKinematicClosestNotMeConvexResultCallback callback(m_ghostObject, getUpAxisDirections()[m_upAxis], m_maxSlopeCosine); + callback.m_collisionFilterGroup = getGhostObject()->getBroadphaseHandle()->m_collisionFilterGroup; + callback.m_collisionFilterMask = getGhostObject()->getBroadphaseHandle()->m_collisionFilterMask; + + btKinematicClosestNotMeConvexResultCallback callback2 (m_ghostObject, getUpAxisDirections()[m_upAxis], m_maxSlopeCosine); + callback2.m_collisionFilterGroup = getGhostObject()->getBroadphaseHandle()->m_collisionFilterGroup; + callback2.m_collisionFilterMask = getGhostObject()->getBroadphaseHandle()->m_collisionFilterMask; + + while (1) { + start.setIdentity(); + end.setIdentity(); + + end_double.setIdentity(); + + start.setOrigin(m_currentPosition); + end.setOrigin(m_targetPosition); + + //set double test for 2x the step drop, to check for a large drop vs small drop + end_double.setOrigin(m_targetPosition - step_drop); + + if (m_useGhostObjectSweepTest) { + m_ghostObject->convexSweepTest(m_convexShape, start, end, callback, collisionWorld->getDispatchInfo().m_allowedCcdPenetration); + + if (!callback.hasHit()) { + //test a double fall height, to see if the character should interpolate it's fall (full) or not (partial) + m_ghostObject->convexSweepTest(m_convexShape, start, end_double, callback2, collisionWorld->getDispatchInfo().m_allowedCcdPenetration); + } + } else { + collisionWorld->convexSweepTest(m_convexShape, start, end, callback, collisionWorld->getDispatchInfo().m_allowedCcdPenetration); + + if (!callback.hasHit()) { + //test a double fall height, to see if the character should interpolate it's fall (large) or not (small) + collisionWorld->convexSweepTest(m_convexShape, start, end_double, callback2, collisionWorld->getDispatchInfo().m_allowedCcdPenetration); + } + } + + btScalar downVelocity2 = (m_verticalVelocity<0.f?-m_verticalVelocity:0.f) * dt; + bool has_hit = false; + if(bounce_fix == true) { + has_hit = callback.hasHit() || callback2.hasHit(); + } else { + has_hit = callback2.hasHit(); + } + + if(downVelocity2 > 0.0 && downVelocity2 < m_stepHeight && has_hit == true && runonce == false + && (m_wasOnGround || !m_wasJumping)) { + //redo the velocity calculation when falling a small amount, for fast stairs motion + //for larger falls, use the smoother/slower interpolated movement by not touching the target position + + m_targetPosition = orig_position; + downVelocity = m_stepHeight; + + btVector3 step_drop = getUpAxisDirections()[m_upAxis] * (m_currentStepOffset + downVelocity); + m_targetPosition -= step_drop; + runonce = true; + continue; //re-run previous tests + } + break; + } + + if (callback.hasHit() || runonce == true) { + // we dropped a fraction of the height -> hit floor + + btScalar fraction = (m_currentPosition.getY() - callback.m_hitPointWorld.getY()) / 2; + + //printf("hitpoint: %g - pos %g\n", callback.m_hitPointWorld.getY(), m_currentPosition.getY()); + + if (bounce_fix == true) { + if (full_drop == true) { + m_currentPosition.setInterpolate3 (m_currentPosition, m_targetPosition, callback.m_closestHitFraction); + } else { + //due to errors in the closestHitFraction variable when used with large polygons, calculate the hit fraction manually + m_currentPosition.setInterpolate3 (m_currentPosition, m_targetPosition, fraction); + } + } + else + m_currentPosition.setInterpolate3 (m_currentPosition, m_targetPosition, callback.m_closestHitFraction); + + full_drop = false; + + m_verticalVelocity = 0.0; + m_verticalOffset = 0.0; + m_wasJumping = false; + } else { + // we dropped the full height + + full_drop = true; + + if (bounce_fix == true) { + downVelocity = (m_verticalVelocity<0.f?-m_verticalVelocity:0.f) * dt; + if (downVelocity > m_fallSpeed && (m_wasOnGround || !m_wasJumping)) { + m_targetPosition += step_drop; //undo previous target change + downVelocity = m_fallSpeed; + step_drop = getUpAxisDirections()[m_upAxis] * (m_currentStepOffset + downVelocity); + m_targetPosition -= step_drop; + } + } + //printf("full drop - %g, %g\n", m_currentPosition.getY(), m_targetPosition.getY()); + + m_currentPosition = m_targetPosition; + } +} + + + +void CharacterController::setWalkDirection(const btVector3& walkDirection) { + m_useWalkDirection = true; + m_walkDirection = walkDirection; + m_normalizedDirection = getNormalizedVector(m_walkDirection); +} + + + +void CharacterController::setVelocityForTimeInterval(const btVector3& velocity, btScalar timeInterval) { + //printf("setVelocity!\n"); + //printf(" interval: %f\n", timeInterval); + //printf(" velocity: (%f, %f, %f)\n", velocity.x(), velocity.y(), velocity.z()); + + m_useWalkDirection = false; + m_walkDirection = velocity; + m_normalizedDirection = getNormalizedVector(m_walkDirection); + m_velocityTimeInterval += timeInterval; +} + +void CharacterController::reset( btCollisionWorld* collisionWorld ) { + m_verticalVelocity = 0.0; + m_verticalOffset = 0.0; + m_wasOnGround = false; + m_wasJumping = false; + m_walkDirection.setValue(0,0,0); + m_velocityTimeInterval = 0.0; + + //clear pair cache + btHashedOverlappingPairCache *cache = m_ghostObject->getOverlappingPairCache(); + while (cache->getOverlappingPairArray().size() > 0) { + cache->removeOverlappingPair(cache->getOverlappingPairArray()[0].m_pProxy0, cache->getOverlappingPairArray()[0].m_pProxy1, collisionWorld->getDispatcher()); + } +} + +void CharacterController::warp(const btVector3& origin) { + btTransform xform; + xform.setIdentity(); + xform.setOrigin(origin); + m_ghostObject->setWorldTransform(xform); +} + + +void CharacterController::preStep( btCollisionWorld* collisionWorld) { + int numPenetrationLoops = 0; + m_touchingContact = false; + while (recoverFromPenetration(collisionWorld)) { + numPenetrationLoops++; + m_touchingContact = true; + if (numPenetrationLoops > 4) { + //printf("character could not recover from penetration = %d\n", numPenetrationLoops); + break; + } + } + + m_currentPosition = m_ghostObject->getWorldTransform().getOrigin(); + m_targetPosition = m_currentPosition; + //printf("m_targetPosition=%f,%f,%f\n", m_targetPosition[0], m_targetPosition[1], m_targetPosition[2]); +} + +void CharacterController::playerStep( btCollisionWorld* collisionWorld, btScalar dt) { + //printf("playerStep(): "); + //printf(" dt = %f", dt); + + // quick check... + if (!m_useWalkDirection && m_velocityTimeInterval <= 0.0) { + //printf("\n"); + return; // no motion + } + + m_wasOnGround = onGround(); + + // Update fall velocity. + m_verticalVelocity -= m_gravity * dt; + if (m_verticalVelocity > 0.0 && m_verticalVelocity > m_jumpSpeed) { + m_verticalVelocity = m_jumpSpeed; + } + if (m_verticalVelocity < 0.0 && btFabs(m_verticalVelocity) > btFabs(m_fallSpeed)) { + m_verticalVelocity = -btFabs(m_fallSpeed); + } + m_verticalOffset = m_verticalVelocity * dt; + + + btTransform xform; + xform = m_ghostObject->getWorldTransform(); + + //printf("walkDirection(%f,%f,%f)\n", walkDirection[0], walkDirection[1], walkDirection[2]); + //printf("walkSpeed=%f\n", walkSpeed); + + stepUp (collisionWorld); + if (m_useWalkDirection) { + stepForwardAndStrafe(collisionWorld, m_walkDirection); + } else { + //printf(" time: %f", m_velocityTimeInterval); + // still have some time left for moving! + btScalar dtMoving = + (dt < m_velocityTimeInterval) ? dt : m_velocityTimeInterval; + m_velocityTimeInterval -= dt; + + // how far will we move while we are moving? + btVector3 move = m_walkDirection * dtMoving; + + //printf(" dtMoving: %f", dtMoving); + + // okay, step + stepForwardAndStrafe(collisionWorld, move); + } + stepDown(collisionWorld, dt); + + //printf("\n"); + + xform.setOrigin(m_currentPosition); + m_ghostObject->setWorldTransform(xform); +} + +void CharacterController::setFallSpeed(btScalar fallSpeed) { + m_fallSpeed = fallSpeed; +} + +void CharacterController::setJumpSpeed(btScalar jumpSpeed) { + m_jumpSpeed = jumpSpeed; +} + +void CharacterController::setMaxJumpHeight(btScalar maxJumpHeight) { + m_maxJumpHeight = maxJumpHeight; +} + +bool CharacterController::canJump() const { + return onGround(); +} + +void CharacterController::jump() { + if (!canJump()) { + return; + } + + m_verticalVelocity = m_jumpSpeed; + m_wasJumping = true; + +#if 0 + currently no jumping. + btTransform xform; + m_rigidBody->getMotionState()->getWorldTransform(xform); + btVector3 up = xform.getBasis()[1]; + up.normalize(); + btScalar magnitude = (btScalar(1.0)/m_rigidBody->getInvMass()) * btScalar(8.0); + m_rigidBody->applyCentralImpulse (up * magnitude); +#endif +} + +void CharacterController::setGravity(btScalar gravity) { + m_gravity = gravity; +} + +btScalar CharacterController::getGravity() const { + return m_gravity; +} + +void CharacterController::setMaxSlope(btScalar slopeRadians) { + m_maxSlopeRadians = slopeRadians; + m_maxSlopeCosine = btCos(slopeRadians); +} + +btScalar CharacterController::getMaxSlope() const { + return m_maxSlopeRadians; +} + +bool CharacterController::onGround() const { + return m_verticalVelocity == 0.0 && m_verticalOffset == 0.0; +} + +btVector3* CharacterController::getUpAxisDirections() { + static btVector3 sUpAxisDirection[3] = { btVector3(1.0f, 0.0f, 0.0f), btVector3(0.0f, 1.0f, 0.0f), btVector3(0.0f, 0.0f, 1.0f) }; + + return sUpAxisDirection; +} + +void CharacterController::debugDraw(btIDebugDraw* debugDrawer) { +} + +void CharacterController::setUpInterpolate(bool value) { + m_interpolateUp = value; +} diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h new file mode 100644 index 0000000000..301253b2bd --- /dev/null +++ b/libraries/physics/src/CharacterController.h @@ -0,0 +1,172 @@ +/* +Bullet Continuous Collision Detection and Physics Library +Copyright (c) 2003-2008 Erwin Coumans http://bulletphysics.com + +This software is provided 'as-is', without any express or implied warranty. +In no event will the authors be held liable for any damages arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it freely, +subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. + If you use this software in a product, an acknowledgment in the product documentation would be appreciated but + is not required. +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + + +#ifndef hifi_CharacterController_h +#define hifi_CharacterController_h + +#include +#include +#include + + +class btConvexShape; +class btCollisionWorld; +class btCollisionDispatcher; +class btPairCachingGhostObject; + +///CharacterController is a custom version of btKinematicCharacterController + +///btKinematicCharacterController is an object that supports a sliding motion in a world. +///It uses a ghost object and convex sweep test to test for upcoming collisions. This is combined with discrete collision detection to recover from penetrations. +///Interaction between btKinematicCharacterController and dynamic rigid bodies needs to be explicity implemented by the user. + +ATTRIBUTE_ALIGNED16(class) CharacterController : public btCharacterControllerInterface +{ +protected: + + btScalar m_halfHeight; + + btPairCachingGhostObject* m_ghostObject; + btConvexShape* m_convexShape;//is also in m_ghostObject, but it needs to be convex, so we store it here to avoid upcast + + btScalar m_verticalVelocity; + btScalar m_verticalOffset; + btScalar m_fallSpeed; + btScalar m_jumpSpeed; + btScalar m_maxJumpHeight; + btScalar m_maxSlopeRadians; // Slope angle that is set (used for returning the exact value) + btScalar m_maxSlopeCosine; // Cosine equivalent of m_maxSlopeRadians (calculated once when set, for optimization) + btScalar m_gravity; + + btScalar m_turnAngle; + + btScalar m_stepHeight; + + btScalar m_addedMargin;//@todo: remove this and fix the code + + ///this is the desired walk direction, set by the user + btVector3 m_walkDirection; + btVector3 m_normalizedDirection; + + //some internal variables + btVector3 m_currentPosition; + btScalar m_currentStepOffset; + btVector3 m_targetPosition; + + ///keep track of the contact manifolds + btManifoldArray m_manifoldArray; + + bool m_touchingContact; + btVector3 m_touchingNormal; + + bool m_wasOnGround; + bool m_wasJumping; + bool m_useGhostObjectSweepTest; + bool m_useWalkDirection; + btScalar m_velocityTimeInterval; + int m_upAxis; + + static btVector3* getUpAxisDirections(); + bool m_interpolateUp; + bool full_drop; + bool bounce_fix; + + btVector3 computeReflectionDirection(const btVector3& direction, const btVector3& normal); + btVector3 parallelComponent(const btVector3& direction, const btVector3& normal); + btVector3 perpindicularComponent(const btVector3& direction, const btVector3& normal); + + bool recoverFromPenetration(btCollisionWorld* collisionWorld); + void stepUp(btCollisionWorld* collisionWorld); + void updateTargetPositionBasedOnCollision(const btVector3& hit_normal, btScalar tangentMag = btScalar(0.0), btScalar normalMag = btScalar(1.0)); + void stepForwardAndStrafe(btCollisionWorld* collisionWorld, const btVector3& walkMove); + void stepDown(btCollisionWorld* collisionWorld, btScalar dt); +public: + + BT_DECLARE_ALIGNED_ALLOCATOR(); + + CharacterController( + btPairCachingGhostObject* ghostObject, + btConvexShape* convexShape, + btScalar stepHeight, + int upAxis = 1); + ~CharacterController(); + + + ///btActionInterface interface + virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) { + preStep(collisionWorld); + playerStep(collisionWorld, deltaTime); + } + + ///btActionInterface interface + void debugDraw(btIDebugDraw* debugDrawer); + + void setUpAxis(int axis) { + if (axis < 0) + axis = 0; + if (axis > 2) + axis = 2; + m_upAxis = axis; + } + + /// This should probably be called setPositionIncrementPerSimulatorStep. + /// This is neither a direction nor a velocity, but the amount to + /// increment the position each simulation iteration, regardless + /// of dt. + /// This call will reset any velocity set by setVelocityForTimeInterval(). + virtual void setWalkDirection(const btVector3& walkDirection); + + /// Caller provides a velocity with which the character should move for + /// the given time period. After the time period, velocity is reset + /// to zero. + /// This call will reset any walk direction set by setWalkDirection(). + /// Negative time intervals will result in no motion. + virtual void setVelocityForTimeInterval(const btVector3& velocity, + btScalar timeInterval); + + void reset(btCollisionWorld* collisionWorld ); + void warp(const btVector3& origin); + + void preStep(btCollisionWorld* collisionWorld); + void playerStep(btCollisionWorld* collisionWorld, btScalar dt); + + void setFallSpeed(btScalar fallSpeed); + void setJumpSpeed(btScalar jumpSpeed); + void setMaxJumpHeight(btScalar maxJumpHeight); + bool canJump() const; + + void jump(); + + void setGravity(btScalar gravity); + btScalar getGravity() const; + + /// The max slope determines the maximum angle that the controller can walk up. + /// The slope angle is measured in radians. + void setMaxSlope(btScalar slopeRadians); + btScalar getMaxSlope() const; + + btPairCachingGhostObject* getGhostObject(); + void setUseGhostSweepTest(bool useGhostObjectSweepTest) { + m_useGhostObjectSweepTest = useGhostObjectSweepTest; + } + + bool onGround() const; + void setUpInterpolate(bool value); +}; + +#endif // hifi_CharacterController_h diff --git a/libraries/physics/src/MeshInfo.cpp b/libraries/physics/src/MeshInfo.cpp index 8df5ff914d..29ddc74a98 100644 --- a/libraries/physics/src/MeshInfo.cpp +++ b/libraries/physics/src/MeshInfo.cpp @@ -17,9 +17,11 @@ using namespace meshinfo; //origin is the default reference point for generating the tetrahedron from each triangle of the mesh. MeshInfo::MeshInfo(vector *vertices, vector *triangles) :\ -_vertices(vertices), -_triangles(triangles), -_centerOfMass(Vertex(0.0, 0.0, 0.0)){ + _vertices(vertices), + _centerOfMass(Vertex(0.0, 0.0, 0.0)), + _triangles(triangles) +{ + } MeshInfo::~MeshInfo(){ diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 634c3707a8..3e2fabfd89 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -9,11 +9,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "PhysicsEngine.h" #include "ShapeInfoUtil.h" #include "PhysicsHelpers.h" #include "ThreadSafeDynamicsWorld.h" -#include "AvatarData.h" static uint32_t _numSubsteps; @@ -23,10 +24,12 @@ uint32_t PhysicsEngine::getNumSubsteps() { } PhysicsEngine::PhysicsEngine(const glm::vec3& offset) - : _originOffset(offset) { + : _originOffset(offset), + _avatarShapeLocalOffset(0.0f) { } PhysicsEngine::~PhysicsEngine() { + // TODO: delete engine components... if we ever plan to create more than one instance } // begin EntitySimulation overrides @@ -260,6 +263,9 @@ void PhysicsEngine::init(EntityEditPacketSender* packetSender) { _constraintSolver = new btSequentialImpulseConstraintSolver; _dynamicsWorld = new ThreadSafeDynamicsWorld(_collisionDispatcher, _broadphaseFilter, _constraintSolver, _collisionConfig); + _ghostPairCallback = new btGhostPairCallback(); + _dynamicsWorld->getPairCache()->setInternalGhostPairCallback(_ghostPairCallback); + // default gravity of the world is zero, so each object must specify its own gravity // TODO: set up gravity zones _dynamicsWorld->setGravity(btVector3(0.0f, 0.0f, 0.0f)); @@ -271,6 +277,9 @@ void PhysicsEngine::init(EntityEditPacketSender* packetSender) { } void PhysicsEngine::stepSimulation() { + // expect the engine to have an avatar (and hence: a character controller) + assert(_avatarData); + lock(); // NOTE: the grand order of operations is: // (1) relay incoming changes @@ -288,17 +297,13 @@ void PhysicsEngine::stepSimulation() { float timeStep = btMin(dt, MAX_TIMESTEP); if (_avatarData->isPhysicsEnabled()) { - _avatarGhostObject->setWorldTransform(btTransform(glmToBullet(_avatarData->getOrientation()), - glmToBullet(_avatarData->getPosition()))); - // WORKAROUND: there is a bug in the debug Bullet-2.82 libs where a zero length walk velocity will trigger - // an assert when the getNormalizedVector() helper function in btKinematicCharacterController.cpp tries to - // first normalize a vector before checking its length. Here we workaround the problem by checking the - // length first. NOTE: the character's velocity is reset to zero after each step, so when we DON'T set - // the velocity for this time interval it is the same thing as setting its velocity to zero. + // update character controller + glm::quat rotation = _avatarData->getOrientation(); + glm::vec3 position = _avatarData->getPosition() + rotation * _avatarShapeLocalOffset; + _avatarGhostObject->setWorldTransform(btTransform(glmToBullet(rotation), glmToBullet(position))); + btVector3 walkVelocity = glmToBullet(_avatarData->getVelocity()); - if (walkVelocity.length2() > FLT_EPSILON * FLT_EPSILON) { - _characterController->setVelocityForTimeInterval(walkVelocity, timeStep); - } + _characterController->setVelocityForTimeInterval(walkVelocity, timeStep); } // This is step (2). @@ -323,8 +328,10 @@ void PhysicsEngine::stepSimulation() { if (_avatarData->isPhysicsEnabled()) { const btTransform& avatarTransform = _avatarGhostObject->getWorldTransform(); - _avatarData->setOrientation(bulletToGLM(avatarTransform.getRotation())); - _avatarData->setPosition(bulletToGLM(avatarTransform.getOrigin())); + glm::quat rotation = bulletToGLM(avatarTransform.getRotation()); + glm::vec3 offset = rotation * _avatarShapeLocalOffset; + _avatarData->setOrientation(rotation); + _avatarData->setPosition(bulletToGLM(avatarTransform.getOrigin()) - offset); } unlock(); @@ -610,28 +617,73 @@ bool PhysicsEngine::updateObjectHard(btRigidBody* body, ObjectMotionState* motio void PhysicsEngine::setAvatarData(AvatarData *avatarData) { - _avatarData = avatarData; + assert(avatarData); // don't pass NULL argument + + // compute capsule dimensions + AABox box = avatarData->getLocalAABox(); + const glm::vec3& diagonal = box.getScale(); + float radius = 0.5f * sqrtf(0.5f * (diagonal.x * diagonal.x + diagonal.z * diagonal.z)); + float halfHeight = 0.5f * diagonal.y - radius; + float MIN_HALF_HEIGHT = 0.1f; + if (halfHeight < MIN_HALF_HEIGHT) { + halfHeight = MIN_HALF_HEIGHT; + } + glm::vec3 offset = box.getCorner() + 0.5f * diagonal; + + if (!_avatarData) { + // _avatarData is being initialized + _avatarData = avatarData; + } else { + // _avatarData is being updated + assert(_avatarData == avatarData); + + // get old dimensions from shape + btCapsuleShape* capsule = static_cast(_avatarGhostObject->getCollisionShape()); + btScalar oldRadius = capsule->getRadius(); + btScalar oldHalfHeight = capsule->getHalfHeight(); + + // compare dimensions (and offset) + float radiusDelta = glm::abs(radius - oldRadius); + float heightDelta = glm::abs(halfHeight - oldHalfHeight); + if (radiusDelta < FLT_EPSILON && heightDelta < FLT_EPSILON) { + // shape hasn't changed --> nothing to do + float offsetDelta = glm::distance(offset, _avatarShapeLocalOffset); + if (offsetDelta > FLT_EPSILON) { + // if only the offset changes then we can update it --> no need to rebuild shape + _avatarShapeLocalOffset = offset; + } + return; + } + + // delete old controller and friends + _dynamicsWorld->removeCollisionObject(_avatarGhostObject); + _dynamicsWorld->removeAction(_characterController); + delete _characterController; + _characterController = NULL; + delete _avatarGhostObject; + _avatarGhostObject = NULL; + delete capsule; + } + + // set offset + _avatarShapeLocalOffset = offset; + + // build ghost, shape, and controller _avatarGhostObject = new btPairCachingGhostObject(); _avatarGhostObject->setWorldTransform(btTransform(glmToBullet(_avatarData->getOrientation()), - glmToBullet(_avatarData->getPosition()))); + glmToBullet(_avatarData->getPosition()))); + // ?TODO: use ShapeManager to get avatar's shape? + btCapsuleShape* capsule = new btCapsuleShape(radius, 2.0f * halfHeight); - // XXX these values should be computed from the character model. - btScalar characterRadius = 0.3f; - btScalar characterHeight = 1.75 - 2.0f * characterRadius; - btScalar stepHeight = btScalar(0.35); - - btConvexShape* capsule = new btCapsuleShape(characterRadius, characterHeight); _avatarGhostObject->setCollisionShape(capsule); _avatarGhostObject->setCollisionFlags(btCollisionObject::CF_CHARACTER_OBJECT); - _characterController = new btKinematicCharacterController(_avatarGhostObject, capsule, stepHeight); + const float MIN_STEP_HEIGHT = 0.35f; + btScalar stepHeight = glm::max(MIN_STEP_HEIGHT, radius + 0.5f * halfHeight); + _characterController = new CharacterController(_avatarGhostObject, capsule, stepHeight); _dynamicsWorld->addCollisionObject(_avatarGhostObject, btBroadphaseProxy::CharacterFilter, - btBroadphaseProxy::StaticFilter | btBroadphaseProxy::DefaultFilter); + btBroadphaseProxy::StaticFilter | btBroadphaseProxy::DefaultFilter); _dynamicsWorld->addAction(_characterController); - _characterController->reset (_dynamicsWorld); - // _characterController->warp (btVector3(10.210001,-2.0306311,16.576973)); - - btGhostPairCallback* ghostPairCallback = new btGhostPairCallback(); - _dynamicsWorld->getPairCache()->setInternalGhostPairCallback(ghostPairCallback); + _characterController->reset(_dynamicsWorld); } diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index 649aec2755..acf1617b16 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -16,21 +16,19 @@ #include #include -#include #include -#include -#include -#include +//#include +#include #include #include #include "BulletUtil.h" +#include "CharacterController.h" #include "ContactInfo.h" #include "EntityMotionState.h" #include "ShapeManager.h" #include "ThreadSafeDynamicsWorld.h" -#include "AvatarData.h" const float HALF_SIMULATION_EXTENT = 512.0f; // meters @@ -106,6 +104,7 @@ private: btBroadphaseInterface* _broadphaseFilter = NULL; btSequentialImpulseConstraintSolver* _constraintSolver = NULL; ThreadSafeDynamicsWorld* _dynamicsWorld = NULL; + btGhostPairCallback* _ghostPairCallback = NULL; ShapeManager _shapeManager; glm::vec3 _originOffset; @@ -123,9 +122,10 @@ private: uint32_t _lastNumSubstepsAtUpdateInternal = 0; /// character collisions - btCharacterControllerInterface* _characterController = 0; - class btPairCachingGhostObject* _avatarGhostObject = 0; - AvatarData *_avatarData = 0; + CharacterController* _characterController = NULL; + class btPairCachingGhostObject* _avatarGhostObject = NULL; + AvatarData* _avatarData = NULL; + glm::vec3 _avatarShapeLocalOffset; }; #endif // hifi_PhysicsEngine_h diff --git a/libraries/physics/src/ShapeManager.cpp b/libraries/physics/src/ShapeManager.cpp index 02228bf7aa..513fbfa7a5 100644 --- a/libraries/physics/src/ShapeManager.cpp +++ b/libraries/physics/src/ShapeManager.cpp @@ -21,7 +21,7 @@ ShapeManager::~ShapeManager() { int numShapes = _shapeMap.size(); for (int i = 0; i < numShapes; ++i) { ShapeReference* shapeRef = _shapeMap.getAtIndex(i); - delete shapeRef->_shape; + delete shapeRef->shape; } _shapeMap.clear(); } @@ -40,26 +40,27 @@ btCollisionShape* ShapeManager::getShape(const ShapeInfo& info) { DoubleHashKey key = info.getHash(); ShapeReference* shapeRef = _shapeMap.find(key); if (shapeRef) { - shapeRef->_refCount++; - return shapeRef->_shape; + shapeRef->refCount++; + return shapeRef->shape; } btCollisionShape* shape = ShapeInfoUtil::createShapeFromInfo(info); if (shape) { ShapeReference newRef; - newRef._refCount = 1; - newRef._shape = shape; + newRef.refCount = 1; + newRef.shape = shape; + newRef.key = key; _shapeMap.insert(key, newRef); } return shape; } -bool ShapeManager::releaseShape(const ShapeInfo& info) { - DoubleHashKey key = info.getHash(); +// private helper method +bool ShapeManager::releaseShape(const DoubleHashKey& key) { ShapeReference* shapeRef = _shapeMap.find(key); if (shapeRef) { - if (shapeRef->_refCount > 0) { - shapeRef->_refCount--; - if (shapeRef->_refCount == 0) { + if (shapeRef->refCount > 0) { + shapeRef->refCount--; + if (shapeRef->refCount == 0) { _pendingGarbage.push_back(key); const int MAX_GARBAGE_CAPACITY = 127; if (_pendingGarbage.size() > MAX_GARBAGE_CAPACITY) { @@ -78,10 +79,19 @@ bool ShapeManager::releaseShape(const ShapeInfo& info) { return false; } +bool ShapeManager::releaseShape(const ShapeInfo& info) { + return releaseShape(info.getHash()); +} + bool ShapeManager::releaseShape(const btCollisionShape* shape) { - ShapeInfo info; - ShapeInfoUtil::collectInfoFromShape(shape, info); - return releaseShape(info); + int numShapes = _shapeMap.size(); + for (int i = 0; i < numShapes; ++i) { + ShapeReference* shapeRef = _shapeMap.getAtIndex(i); + if (shape == shapeRef->shape) { + return releaseShape(shapeRef->key); + } + } + return false; } void ShapeManager::collectGarbage() { @@ -89,8 +99,8 @@ void ShapeManager::collectGarbage() { for (int i = 0; i < numShapes; ++i) { DoubleHashKey& key = _pendingGarbage[i]; ShapeReference* shapeRef = _shapeMap.find(key); - if (shapeRef && shapeRef->_refCount == 0) { - delete shapeRef->_shape; + if (shapeRef && shapeRef->refCount == 0) { + delete shapeRef->shape; _shapeMap.remove(key); } } @@ -101,7 +111,29 @@ int ShapeManager::getNumReferences(const ShapeInfo& info) const { DoubleHashKey key = info.getHash(); const ShapeReference* shapeRef = _shapeMap.find(key); if (shapeRef) { - return shapeRef->_refCount; + return shapeRef->refCount; } - return -1; + return 0; +} + +int ShapeManager::getNumReferences(const btCollisionShape* shape) const { + int numShapes = _shapeMap.size(); + for (int i = 0; i < numShapes; ++i) { + const ShapeReference* shapeRef = _shapeMap.getAtIndex(i); + if (shape == shapeRef->shape) { + return shapeRef->refCount; + } + } + return 0; +} + +bool ShapeManager::hasShape(const btCollisionShape* shape) const { + int numShapes = _shapeMap.size(); + for (int i = 0; i < numShapes; ++i) { + const ShapeReference* shapeRef = _shapeMap.getAtIndex(i); + if (shape == shapeRef->shape) { + return true; + } + } + return false; } diff --git a/libraries/physics/src/ShapeManager.h b/libraries/physics/src/ShapeManager.h index 3fe80c7b61..e04fa1280c 100644 --- a/libraries/physics/src/ShapeManager.h +++ b/libraries/physics/src/ShapeManager.h @@ -38,12 +38,17 @@ public: // validation methods int getNumShapes() const { return _shapeMap.size(); } int getNumReferences(const ShapeInfo& info) const; + int getNumReferences(const btCollisionShape* shape) const; + bool hasShape(const btCollisionShape* shape) const; private: + bool releaseShape(const DoubleHashKey& key); + struct ShapeReference { - int _refCount; - btCollisionShape* _shape; - ShapeReference() : _refCount(0), _shape(NULL) {} + int refCount; + btCollisionShape* shape; + DoubleHashKey key; + ShapeReference() : refCount(0), shape(NULL) {} }; btHashMap _shapeMap; diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index f09ff9bd72..9cf76dd1dc 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -68,7 +68,7 @@ static const quint64 USECS_PER_SECOND = USECS_PER_MSEC * MSECS_PER_SECOND; const int BITS_IN_BYTE = 8; // Use a custom User-Agent to avoid ModSecurity filtering, e.g. by hosting providers. -const QByteArray HIGH_FIDELITY_USER_AGENT = "Mozilla/5.0 (HighFidelity)"; +const QByteArray HIGH_FIDELITY_USER_AGENT = "Mozilla/5.0 (HighFidelityInterface)"; quint64 usecTimestampNow(bool wantDebug = false); void usecTimestampNowForceClockSkew(int clockSkew); diff --git a/tests/physics/src/MeshInfoTests.cpp b/tests/physics/src/MeshInfoTests.cpp index fa841f3930..221ffa117c 100644 --- a/tests/physics/src/MeshInfoTests.cpp +++ b/tests/physics/src/MeshInfoTests.cpp @@ -104,15 +104,16 @@ void MeshInfoTests::testWithTetrahedronAsMesh(){ glm::vec3 p2(52.61236, 5.00000, -5.38580); glm::vec3 p3(2.00000, 5.00000, 3.00000); glm::vec3 centroid(15.92492, 0.782813, 3.72962); - float volume = 1873.233236f; - float inertia_a = 43520.33257f; - //actual should be 194711.28938f. But for some reason it becomes 194711.296875 during + /* TODO: actually test inertia/volume calculations here + //float volume = 1873.233236f; //runtime due to how floating points are stored. + float inertia_a = 43520.33257f; float inertia_b = 194711.289f; float inertia_c = 191168.76173f; float inertia_aa = 4417.66150f; float inertia_bb = -46343.16662f; float inertia_cc = 11996.20119f; + */ std::cout << std::setprecision(12); vector vertices = { p0, p1, p2, p3 }; vector triangles = { 0, 2, 1, 0, 3, 2, 0, 1, 3, 1, 2, 3 }; @@ -129,6 +130,7 @@ void MeshInfoTests::testWithTetrahedronAsMesh(){ p2 -= centroid; p3 -= centroid; } + void MeshInfoTests::testWithCube(){ glm::vec3 p0(1.0, -1.0, -1.0); glm::vec3 p1(1.0, -1.0, 1.0); @@ -232,4 +234,4 @@ void MeshInfoTests::runAllTests(){ testWithTetrahedronAsMesh(); testWithUnitCube(); testWithCube(); -} \ No newline at end of file +} diff --git a/tests/physics/src/ShapeManagerTests.cpp b/tests/physics/src/ShapeManagerTests.cpp index 6dfae15e78..d86f296d0e 100644 --- a/tests/physics/src/ShapeManagerTests.cpp +++ b/tests/physics/src/ShapeManagerTests.cpp @@ -21,10 +21,8 @@ void ShapeManagerTests::testShapeAccounting() { ShapeInfo info; info.setBox(glm::vec3(1.0f, 1.0f, 1.0f)); - // NOTE: ShapeManager returns -1 as refcount when the shape is unknown, - // which is distinct from "known but with zero references" int numReferences = shapeManager.getNumReferences(info); - if (numReferences != -1) { + if (numReferences != 0) { std::cout << __FILE__ << ":" << __LINE__ << " ERROR: expected ignorant ShapeManager after initialization" << std::endl; } @@ -104,8 +102,7 @@ void ShapeManagerTests::testShapeAccounting() { if (shapeManager.getNumShapes() != 0) { std::cout << __FILE__ << ":" << __LINE__ << " ERROR: expected zero shapes after release" << std::endl; } - numReferences = shapeManager.getNumReferences(info); - if (numReferences != -1) { + if (shapeManager.hasShape(shape)) { std::cout << __FILE__ << ":" << __LINE__ << " ERROR: expected ignorant ShapeManager after garbage collection" << std::endl; } @@ -122,33 +119,64 @@ void ShapeManagerTests::testShapeAccounting() { void ShapeManagerTests::addManyShapes() { ShapeManager shapeManager; + QVector shapes; + int numSizes = 100; float startSize = 1.0f; float endSize = 99.0f; float deltaSize = (endSize - startSize) / (float)numSizes; ShapeInfo info; for (int i = 0; i < numSizes; ++i) { + // make a sphere float s = startSize + (float)i * deltaSize; glm::vec3 scale(s, 1.23f + s, s - 0.573f); info.setBox(0.5f * scale); btCollisionShape* shape = shapeManager.getShape(info); + shapes.push_back(shape); if (!shape) { std::cout << __FILE__ << ":" << __LINE__ << " ERROR: i = " << i << " null box shape for scale = " << scale << std::endl; } + + // make a box float radius = 0.5f * s; info.setSphere(radius); shape = shapeManager.getShape(info); + shapes.push_back(shape); if (!shape) { std::cout << __FILE__ << ":" << __LINE__ << " ERROR: i = " << i << " null sphere shape for radius = " << radius << std::endl; } } + + // verify shape count int numShapes = shapeManager.getNumShapes(); if (numShapes != 2 * numSizes) { std::cout << __FILE__ << ":" << __LINE__ << " ERROR: expected numShapes = " << numSizes << " but found numShapes = " << numShapes << std::endl; } + + // release each shape by pointer + for (int i = 0; i < numShapes; ++i) { + btCollisionShape* shape = shapes[i]; + bool success = shapeManager.releaseShape(shape); + if (!success) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: failed to release shape index " << i << std::endl; + break; + } + } + + // verify zero references + for (int i = 0; i < numShapes; ++i) { + btCollisionShape* shape = shapes[i]; + int numReferences = shapeManager.getNumReferences(shape); + if (numReferences != 0) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected zero references for shape " << i + << " but refCount = " << numReferences << std::endl; + } + } } void ShapeManagerTests::addBoxShape() {