mirror of
https://github.com/overte-org/overte.git
synced 2025-07-23 19:04:27 +02:00
Merge branch 'master' into bug-fix/tablet-ui-no-domain
This commit is contained in:
commit
6bef9e888b
70 changed files with 1852 additions and 703 deletions
|
@ -58,9 +58,9 @@ In a new Terminal window, run:
|
||||||
|
|
||||||
Any target can be terminated with Ctrl-C (SIGINT) in the associated Terminal window.
|
Any target can be terminated with Ctrl-C (SIGINT) in the associated Terminal window.
|
||||||
|
|
||||||
This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option.
|
This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option. The `-min` and `-max` options allow you to set a range of required assignment-clients, this allows you to have flexibility in the number of assignment-clients that are running. See `--help` for more options.
|
||||||
|
|
||||||
./assignment-client -n 4
|
./assignment-client --min 6 --max 20
|
||||||
|
|
||||||
To test things out you'll want to run the Interface client.
|
To test things out you'll want to run the Interface client.
|
||||||
|
|
||||||
|
|
|
@ -593,10 +593,15 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
|
||||||
// parse the identity packet and update the change timestamp if appropriate
|
// parse the identity packet and update the change timestamp if appropriate
|
||||||
AvatarData::Identity identity;
|
AvatarData::Identity identity;
|
||||||
AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity);
|
AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity);
|
||||||
if (avatar.processAvatarIdentity(identity)) {
|
bool identityChanged = false;
|
||||||
|
bool displayNameChanged = false;
|
||||||
|
avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged);
|
||||||
|
if (identityChanged) {
|
||||||
QMutexLocker nodeDataLocker(&nodeData->getMutex());
|
QMutexLocker nodeDataLocker(&nodeData->getMutex());
|
||||||
nodeData->flagIdentityChange();
|
nodeData->flagIdentityChange();
|
||||||
nodeData->setAvatarSessionDisplayNameMustChange(true);
|
if (displayNameChanged) {
|
||||||
|
nodeData->setAvatarSessionDisplayNameMustChange(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,7 +294,9 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) {
|
if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) {
|
||||||
// Fetch script from file synchronously. We don't want the server processing edits while a restarting entity server is fetching from a DOS'd source.
|
// Tell the tree that we have a filter, so that it doesn't accept edits until we have a filter function set up.
|
||||||
|
std::static_pointer_cast<EntityTree>(_tree)->setHasEntityFilter(true);
|
||||||
|
// Now fetch script from file asynchronously.
|
||||||
QUrl scriptURL(_entityEditFilter);
|
QUrl scriptURL(_entityEditFilter);
|
||||||
|
|
||||||
// The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp)
|
// The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp)
|
||||||
|
@ -315,8 +317,6 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
|
||||||
qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString());
|
qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString());
|
||||||
scriptRequest->send();
|
scriptRequest->send();
|
||||||
qDebug() << "script request sent";
|
qDebug() << "script request sent";
|
||||||
_scriptRequestLoop.exec(); // Block here, but allow the request to be processed and its signals to be handled.
|
|
||||||
qDebug() << "script request event loop complete";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,11 +367,7 @@ void EntityServer::scriptRequestFinished() {
|
||||||
return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter);
|
return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter);
|
||||||
});
|
});
|
||||||
scriptRequest->deleteLater();
|
scriptRequest->deleteLater();
|
||||||
qDebug() << "script request ending event loop. running:" << _scriptRequestLoop.isRunning();
|
qDebug() << "script request filter processed";
|
||||||
if (_scriptRequestLoop.isRunning()) {
|
|
||||||
_scriptRequestLoop.quit();
|
|
||||||
}
|
|
||||||
qDebug() << "script request event loop quit";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -386,11 +382,6 @@ void EntityServer::scriptRequestFinished() {
|
||||||
// Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities.
|
// Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities.
|
||||||
qDebug() << "script request failure causing stop";
|
qDebug() << "script request failure causing stop";
|
||||||
stop();
|
stop();
|
||||||
qDebug() << "script request ending event loop. running:" << _scriptRequestLoop.isRunning();
|
|
||||||
if (_scriptRequestLoop.isRunning()) {
|
|
||||||
_scriptRequestLoop.quit();
|
|
||||||
}
|
|
||||||
qDebug() << "script request event loop quit";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityServer::nodeAdded(SharedNodePointer node) {
|
void EntityServer::nodeAdded(SharedNodePointer node) {
|
||||||
|
|
|
@ -80,7 +80,6 @@ private:
|
||||||
|
|
||||||
QString _entityEditFilter{};
|
QString _entityEditFilter{};
|
||||||
QScriptEngine _entityEditFilterEngine{};
|
QScriptEngine _entityEditFilterEngine{};
|
||||||
QEventLoop _scriptRequestLoop{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // hifi_EntityServer_h
|
#endif // hifi_EntityServer_h
|
||||||
|
|
4
cmake/externals/wasapi/CMakeLists.txt
vendored
4
cmake/externals/wasapi/CMakeLists.txt
vendored
|
@ -6,8 +6,8 @@ if (WIN32)
|
||||||
include(ExternalProject)
|
include(ExternalProject)
|
||||||
ExternalProject_Add(
|
ExternalProject_Add(
|
||||||
${EXTERNAL_NAME}
|
${EXTERNAL_NAME}
|
||||||
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi6.zip
|
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip
|
||||||
URL_MD5 fcac808c1ba0b0f5b44ea06e2612ebab
|
URL_MD5 bc2861e50852dd590cdc773a14a041a7
|
||||||
CONFIGURE_COMMAND ""
|
CONFIGURE_COMMAND ""
|
||||||
BUILD_COMMAND ""
|
BUILD_COMMAND ""
|
||||||
INSTALL_COMMAND ""
|
INSTALL_COMMAND ""
|
||||||
|
|
|
@ -372,6 +372,13 @@
|
||||||
"help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.",
|
"help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.",
|
||||||
"value-hidden": true
|
"value-hidden": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "verify_http_password",
|
||||||
|
"label": "Verify HTTP Password",
|
||||||
|
"type": "password",
|
||||||
|
"help": "Must match the password entered above for change to be saved.",
|
||||||
|
"value-hidden": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "maximum_user_capacity",
|
"name": "maximum_user_capacity",
|
||||||
"label": "Maximum User Capacity",
|
"label": "Maximum User Capacity",
|
||||||
|
|
|
@ -125,6 +125,10 @@ tr.new-row {
|
||||||
background-color: #dff0d8;
|
background-color: #dff0d8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr.invalid-input {
|
||||||
|
background-color: #f2dede;
|
||||||
|
}
|
||||||
|
|
||||||
.graphable-stat {
|
.graphable-stat {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #5286BC;
|
color: #5286BC;
|
||||||
|
|
|
@ -38,7 +38,8 @@ var Settings = {
|
||||||
DOMAIN_ID_SELECTOR: '[name="metaverse.id"]',
|
DOMAIN_ID_SELECTOR: '[name="metaverse.id"]',
|
||||||
ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]',
|
ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]',
|
||||||
PLACES_TABLE_ID: 'places-table',
|
PLACES_TABLE_ID: 'places-table',
|
||||||
FORM_ID: 'settings-form'
|
FORM_ID: 'settings-form',
|
||||||
|
INVALID_ROW_CLASS: 'invalid-input'
|
||||||
};
|
};
|
||||||
|
|
||||||
var viewHelpers = {
|
var viewHelpers = {
|
||||||
|
@ -215,8 +216,8 @@ $(document).ready(function(){
|
||||||
sibling = sibling.next();
|
sibling = sibling.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
|
// for tables with categories we add the entry and setup the new row on enter
|
||||||
sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click();
|
if (sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).length) {
|
||||||
sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click();
|
sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click();
|
||||||
|
|
||||||
// set focus to the first input in the new row
|
// set focus to the first input in the new row
|
||||||
|
@ -891,39 +892,146 @@ function reloadSettings(callback) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateInputs() {
|
||||||
|
// check if any new values are bad
|
||||||
|
var tables = $('table');
|
||||||
|
|
||||||
|
var inputsValid = true;
|
||||||
|
|
||||||
|
var tables = $('table');
|
||||||
|
|
||||||
|
// clear any current invalid rows
|
||||||
|
$('tr.' + Settings.INVALID_ROW_CLASS).removeClass(Settings.INVALID_ROW_CLASS);
|
||||||
|
|
||||||
|
function markParentRowInvalid(rowChild) {
|
||||||
|
$(rowChild).closest('tr').addClass(Settings.INVALID_ROW_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(tables, function(table) {
|
||||||
|
// validate keys specificially for spaces and equality to an existing key
|
||||||
|
var newKeys = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' td.key');
|
||||||
|
|
||||||
|
var keyWithSpaces = false;
|
||||||
|
var empty = false;
|
||||||
|
var duplicateKey = false;
|
||||||
|
|
||||||
|
_.each(newKeys, function(keyCell) {
|
||||||
|
var keyVal = $(keyCell).children('input').val();
|
||||||
|
|
||||||
|
if (keyVal.indexOf(' ') !== -1) {
|
||||||
|
keyWithSpaces = true;
|
||||||
|
markParentRowInvalid(keyCell);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the key isn't empty
|
||||||
|
if (keyVal.length === 0) {
|
||||||
|
empty = true
|
||||||
|
|
||||||
|
markParentRowInvalid(input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we don't have duplicate keys in the table
|
||||||
|
var otherKeys = $(table).find('td.key').not(keyCell);
|
||||||
|
_.each(otherKeys, function(otherKeyCell) {
|
||||||
|
var keyInput = $(otherKeyCell).children('input');
|
||||||
|
|
||||||
|
if (keyInput.length) {
|
||||||
|
if ($(keyInput).val() == keyVal) {
|
||||||
|
duplicateKey = true;
|
||||||
|
}
|
||||||
|
} else if ($(otherKeyCell).html() == keyVal) {
|
||||||
|
duplicateKey = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateKey) {
|
||||||
|
markParentRowInvalid(keyCell);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if (keyWithSpaces) {
|
||||||
|
showErrorMessage("Error", "Key contains spaces");
|
||||||
|
inputsValid = false;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty) {
|
||||||
|
showErrorMessage("Error", "Empty field(s)");
|
||||||
|
inputsValid = false;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateKey) {
|
||||||
|
showErrorMessage("Error", "Two keys cannot be identical");
|
||||||
|
inputsValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return inputsValid;
|
||||||
|
}
|
||||||
|
|
||||||
var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!";
|
var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!";
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
// disable any inputs not changed
|
|
||||||
$("input:not([data-changed])").each(function(){
|
|
||||||
$(this).prop('disabled', true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// grab a JSON representation of the form via form2js
|
if (validateInputs()) {
|
||||||
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
|
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
|
||||||
|
|
||||||
// check if we've set the basic http password - if so convert it to base64
|
// disable any inputs not changed
|
||||||
if (formJSON["security"]) {
|
$("input:not([data-changed])").each(function(){
|
||||||
var password = formJSON["security"]["http_password"];
|
$(this).prop('disabled', true);
|
||||||
if (password && password.length > 0) {
|
});
|
||||||
formJSON["security"]["http_password"] = sha256_digest(password);
|
|
||||||
|
// grab a JSON representation of the form via form2js
|
||||||
|
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
|
||||||
|
|
||||||
|
// check if we've set the basic http password - if so convert it to base64
|
||||||
|
if (formJSON["security"]) {
|
||||||
|
var password = formJSON["security"]["http_password"];
|
||||||
|
if (password && password.length > 0) {
|
||||||
|
formJSON["security"]["http_password"] = sha256_digest(password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that the password and confirmation match before saving
|
||||||
|
var canPost = true;
|
||||||
|
|
||||||
|
if (formJSON["security"]) {
|
||||||
|
var password = formJSON["security"]["http_password"];
|
||||||
|
var verify_password = formJSON["security"]["verify_http_password"];
|
||||||
|
|
||||||
|
if (password && password.length > 0) {
|
||||||
|
if (password != verify_password) {
|
||||||
|
bootbox.alert({"message": "Passwords must match!", "title":"Password Error"});
|
||||||
|
canPost = false;
|
||||||
|
} else {
|
||||||
|
formJSON["security"]["http_password"] = sha256_digest(password);
|
||||||
|
delete formJSON["security"]["verify_http_password"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("----- SAVING ------");
|
||||||
|
console.log(formJSON);
|
||||||
|
|
||||||
|
// re-enable all inputs
|
||||||
|
$("input").each(function(){
|
||||||
|
$(this).prop('disabled', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove focus from the button
|
||||||
|
$(this).blur();
|
||||||
|
|
||||||
|
if (canPost) {
|
||||||
|
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
|
||||||
|
postSettings(formJSON);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("----- SAVING ------");
|
|
||||||
console.log(formJSON);
|
|
||||||
|
|
||||||
// re-enable all inputs
|
|
||||||
$("input").each(function(){
|
|
||||||
$(this).prop('disabled', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// remove focus from the button
|
|
||||||
$(this).blur();
|
|
||||||
|
|
||||||
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
|
|
||||||
postSettings(formJSON);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$('body').on('click', '.save-button', function(e){
|
$('body').on('click', '.save-button', function(e){
|
||||||
|
@ -1100,8 +1208,9 @@ function makeTable(setting, keypath, setting_value) {
|
||||||
if (setting.can_add_new_categories) {
|
if (setting.can_add_new_categories) {
|
||||||
html += makeTableCategoryInput(setting, numVisibleColumns);
|
html += makeTableCategoryInput(setting, numVisibleColumns);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setting.can_add_new_rows || setting.can_add_new_categories) {
|
if (setting.can_add_new_rows || setting.can_add_new_categories) {
|
||||||
html += makeTableInputs(setting, {}, "");
|
html += makeTableHiddenInputs(setting, {}, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
html += "</table>"
|
html += "</table>"
|
||||||
|
@ -1127,7 +1236,7 @@ function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns,
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeTableInputs(setting, initialValues, categoryValue) {
|
function makeTableHiddenInputs(setting, initialValues, categoryValue) {
|
||||||
var html = "<tr class='inputs'" + (setting.can_add_new_categories && !categoryValue ? " hidden" : "") + " " +
|
var html = "<tr class='inputs'" + (setting.can_add_new_categories && !categoryValue ? " hidden" : "") + " " +
|
||||||
(categoryValue ? ("data-category='" + categoryValue + "'") : "") + " " +
|
(categoryValue ? ("data-category='" + categoryValue + "'") : "") + " " +
|
||||||
(setting.categorize_by_key ? ("data-keep-field='" + setting.categorize_by_key + "'") : "") + ">";
|
(setting.categorize_by_key ? ("data-keep-field='" + setting.categorize_by_key + "'") : "") + ">";
|
||||||
|
@ -1138,7 +1247,7 @@ function makeTableInputs(setting, initialValues, categoryValue) {
|
||||||
|
|
||||||
if (setting.key) {
|
if (setting.key) {
|
||||||
html += "<td class='key' name='" + setting.key.name + "'>\
|
html += "<td class='key' name='" + setting.key.name + "'>\
|
||||||
<input type='text' class='form-control' placeholder='" + (_.has(setting.key, 'placeholder') ? setting.key.placeholder : "") + "' value=''>\
|
<input type='text' style='display: none;' class='form-control' placeholder='" + (_.has(setting.key, 'placeholder') ? setting.key.placeholder : "") + "' value=''>\
|
||||||
</td>"
|
</td>"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1147,14 +1256,14 @@ function makeTableInputs(setting, initialValues, categoryValue) {
|
||||||
if (col.type === "checkbox") {
|
if (col.type === "checkbox") {
|
||||||
html +=
|
html +=
|
||||||
"<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>" +
|
"<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>" +
|
||||||
"<input type='checkbox' class='form-control table-checkbox' " +
|
"<input type='checkbox' style='display: none;' class='form-control table-checkbox' " +
|
||||||
"name='" + col.name + "'" + (defaultValue ? " checked" : "") + "/>" +
|
"name='" + col.name + "'" + (defaultValue ? " checked" : "") + "/>" +
|
||||||
"</td>";
|
"</td>";
|
||||||
} else {
|
} else {
|
||||||
html +=
|
html +=
|
||||||
"<td " + (col.hidden ? "style='display: none;'" : "") + " class='" + Settings.DATA_COL_CLASS + "' " +
|
"<td " + (col.hidden ? "style='display: none;'" : "") + " class='" + Settings.DATA_COL_CLASS + "' " +
|
||||||
"name='" + col.name + "'>" +
|
"name='" + col.name + "'>" +
|
||||||
"<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "' " +
|
"<input type='text' style='display: none;' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "' " +
|
||||||
"value='" + (defaultValue || "") + "' data-default='" + (defaultValue || "") + "'" +
|
"value='" + (defaultValue || "") + "' data-default='" + (defaultValue || "") + "'" +
|
||||||
(col.readonly ? " readonly" : "") + ">" +
|
(col.readonly ? " readonly" : "") + ">" +
|
||||||
"</td>";
|
"</td>";
|
||||||
|
@ -1234,49 +1343,17 @@ function addTableRow(row) {
|
||||||
|
|
||||||
var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS);
|
var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS);
|
||||||
|
|
||||||
|
var input_clone = row.clone();
|
||||||
|
|
||||||
if (!isArray) {
|
if (!isArray) {
|
||||||
// Check key spaces
|
// show the key input
|
||||||
var key = row.children(".key").children("input").val()
|
var keyInput = row.children(".key").children("input");
|
||||||
if (key.indexOf(' ') !== -1) {
|
|
||||||
showErrorMessage("Error", "Key contains spaces")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check keys with the same name
|
|
||||||
var equals = false;
|
|
||||||
_.each(columns.children(".key"), function(element) {
|
|
||||||
if ($(element).text() === key) {
|
|
||||||
equals = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (equals) {
|
|
||||||
showErrorMessage("Error", "Two keys cannot be identical")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check empty fields
|
|
||||||
var empty = false;
|
|
||||||
_.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) {
|
|
||||||
if ($(element).val().length === 0) {
|
|
||||||
empty = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (empty) {
|
|
||||||
showErrorMessage("Error", "Empty field(s)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var input_clone = row.clone()
|
|
||||||
|
|
||||||
// Change input row to data row
|
// Change input row to data row
|
||||||
var table = row.parents("table")
|
var table = row.parents("table");
|
||||||
var setting_name = table.attr("name")
|
var setting_name = table.attr("name");
|
||||||
var full_name = setting_name + "." + key
|
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS);
|
||||||
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS)
|
|
||||||
row.removeClass("inputs")
|
|
||||||
|
|
||||||
_.each(row.children(), function(element) {
|
_.each(row.children(), function(element) {
|
||||||
if ($(element).hasClass("numbered")) {
|
if ($(element).hasClass("numbered")) {
|
||||||
|
@ -1298,56 +1375,43 @@ function addTableRow(row) {
|
||||||
anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES)
|
anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES)
|
||||||
} else if ($(element).hasClass("key")) {
|
} else if ($(element).hasClass("key")) {
|
||||||
var input = $(element).children("input")
|
var input = $(element).children("input")
|
||||||
$(element).html(input.val())
|
input.show();
|
||||||
input.remove()
|
|
||||||
} else if ($(element).hasClass(Settings.DATA_COL_CLASS)) {
|
} else if ($(element).hasClass(Settings.DATA_COL_CLASS)) {
|
||||||
// Hide inputs
|
// show inputs
|
||||||
var input = $(element).find("input")
|
var input = $(element).find("input");
|
||||||
var isCheckbox = false;
|
input.show();
|
||||||
var isTime = false;
|
|
||||||
if (input.hasClass("table-checkbox")) {
|
|
||||||
input = $(input).parent();
|
|
||||||
isCheckbox = true;
|
|
||||||
} else if (input.hasClass("table-time")) {
|
|
||||||
input = $(input).parent();
|
|
||||||
isTime = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var val = input.val();
|
var isCheckbox = input.hasClass("table-checkbox");
|
||||||
if (isCheckbox) {
|
|
||||||
// don't hide the checkbox
|
|
||||||
val = $(input).find("input").is(':checked');
|
|
||||||
} else if (isTime) {
|
|
||||||
// don't hide the time
|
|
||||||
} else {
|
|
||||||
input.attr("type", "hidden")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isArray) {
|
if (isArray) {
|
||||||
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
|
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
|
||||||
var key = $(element).attr('name')
|
var key = $(element).attr('name');
|
||||||
|
|
||||||
// are there multiple columns or just one?
|
// are there multiple columns or just one?
|
||||||
// with multiple we have an array of Objects, with one we have an array of whatever the value type is
|
// with multiple we have an array of Objects, with one we have an array of whatever the value type is
|
||||||
var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
|
var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
|
||||||
|
|
||||||
if (isCheckbox) {
|
if (isCheckbox) {
|
||||||
$(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
|
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
|
||||||
} else {
|
} else {
|
||||||
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
|
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
input.attr("name", full_name + "." + $(element).attr("name"))
|
// because the name of the setting in question requires the key
|
||||||
|
// setup a hook to change the HTML name of the element whenever the key changes
|
||||||
|
var colName = $(element).attr("name");
|
||||||
|
keyInput.on('change', function(){
|
||||||
|
input.attr("name", setting_name + "." + $(this).val() + "." + colName);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCheckbox) {
|
if (isCheckbox) {
|
||||||
$(input).find("input").attr("data-changed", "true");
|
$(input).find("input").attr("data-changed", "true");
|
||||||
} else {
|
} else {
|
||||||
input.attr("data-changed", "true");
|
input.attr("data-changed", "true");
|
||||||
$(element).append(val);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Unknown table element")
|
console.log("Unknown table element");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1377,7 +1441,12 @@ function deleteTableRow($row) {
|
||||||
$row.empty();
|
$row.empty();
|
||||||
|
|
||||||
if (!isArray) {
|
if (!isArray) {
|
||||||
$row.html("<input type='hidden' class='form-control' name='" + $row.attr('name') + "' data-changed='true' value=''>");
|
if ($row.attr('name')) {
|
||||||
|
$row.html("<input type='hidden' class='form-control' name='" + $row.attr('name') + "' data-changed='true' value=''>");
|
||||||
|
} else {
|
||||||
|
// for rows that didn't have a key, simply remove the row
|
||||||
|
$row.remove();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) {
|
if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) {
|
||||||
// This is the last row of the category, so delete the header
|
// This is the last row of the category, so delete the header
|
||||||
|
|
|
@ -33,6 +33,7 @@ Item {
|
||||||
propagateComposedEvents: true
|
propagateComposedEvents: true
|
||||||
acceptedButtons: "AllButtons"
|
acceptedButtons: "AllButtons"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
menu.visible = false;
|
||||||
menu.done();
|
menu.done();
|
||||||
mouse.accepted = false;
|
mouse.accepted = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ Item {
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
menu.visible = false;
|
||||||
root.triggered();
|
root.triggered();
|
||||||
menu.done();
|
menu.done();
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 124 KiB |
|
@ -34,6 +34,7 @@ Item {
|
||||||
property bool isMyCard: false
|
property bool isMyCard: false
|
||||||
property bool selected: false
|
property bool selected: false
|
||||||
property bool isAdmin: false
|
property bool isAdmin: false
|
||||||
|
property bool currentlyEditingDisplayName: false
|
||||||
|
|
||||||
/* User image commented out for now - will probably be re-introduced later.
|
/* User image commented out for now - will probably be re-introduced later.
|
||||||
Column {
|
Column {
|
||||||
|
@ -104,6 +105,7 @@ Item {
|
||||||
focus = false
|
focus = false
|
||||||
myDisplayName.border.width = 0
|
myDisplayName.border.width = 0
|
||||||
color = hifi.colors.darkGray
|
color = hifi.colors.darkGray
|
||||||
|
currentlyEditingDisplayName = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
@ -115,10 +117,12 @@ Item {
|
||||||
myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll();
|
myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll();
|
||||||
myDisplayNameText.focus = true
|
myDisplayNameText.focus = true
|
||||||
myDisplayNameText.color = "black"
|
myDisplayNameText.color = "black"
|
||||||
|
currentlyEditingDisplayName = true
|
||||||
}
|
}
|
||||||
onDoubleClicked: {
|
onDoubleClicked: {
|
||||||
myDisplayNameText.selectAll();
|
myDisplayNameText.selectAll();
|
||||||
myDisplayNameText.focus = true;
|
myDisplayNameText.focus = true;
|
||||||
|
currentlyEditingDisplayName = true
|
||||||
}
|
}
|
||||||
onEntered: myDisplayName.color = hifi.colors.lightGrayText
|
onEntered: myDisplayName.color = hifi.colors.lightGrayText
|
||||||
onExited: myDisplayName.color = hifi.colors.textFieldLightBackground
|
onExited: myDisplayName.color = hifi.colors.textFieldLightBackground
|
||||||
|
|
|
@ -51,6 +51,7 @@ Rectangle {
|
||||||
|
|
||||||
// This is the container for the PAL
|
// This is the container for the PAL
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
property bool punctuationMode: false
|
||||||
id: palContainer
|
id: palContainer
|
||||||
// Size
|
// Size
|
||||||
width: pal.width - 50
|
width: pal.width - 50
|
||||||
|
@ -421,6 +422,16 @@ Rectangle {
|
||||||
onExited: adminHelpText.color = hifi.colors.redHighlight
|
onExited: adminHelpText.color = hifi.colors.redHighlight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
HifiControls.Keyboard {
|
||||||
|
id: keyboard
|
||||||
|
raised: myCard.currentlyEditingDisplayName && HMD.active
|
||||||
|
numeric: parent.punctuationMode
|
||||||
|
anchors {
|
||||||
|
bottom: parent.bottom
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Timer used when selecting table rows that aren't yet present in the model
|
// Timer used when selecting table rows that aren't yet present in the model
|
||||||
// (i.e. when selecting avatars using edit.js or sphere overlays)
|
// (i.e. when selecting avatars using edit.js or sphere overlays)
|
||||||
|
|
|
@ -75,6 +75,14 @@ Item {
|
||||||
source: buttonOutline
|
source: buttonOutline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function urlHelper(src) {
|
||||||
|
if (src.match(/\bhttp/)) {
|
||||||
|
return src;
|
||||||
|
} else {
|
||||||
|
return "../../../" + src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: icon
|
id: icon
|
||||||
width: 50
|
width: 50
|
||||||
|
@ -84,7 +92,7 @@ Item {
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
fillMode: Image.Stretch
|
fillMode: Image.Stretch
|
||||||
source: "../../../" + tabletButton.icon
|
source: tabletButton.urlHelper(tabletButton.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
ColorOverlay {
|
ColorOverlay {
|
||||||
|
@ -185,7 +193,7 @@ Item {
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: icon
|
target: icon
|
||||||
source: "../../../" + tabletButton.activeIcon
|
source: tabletButton.urlHelper(tabletButton.activeIcon)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
|
|
|
@ -54,7 +54,10 @@ FocusScope {
|
||||||
onEntered: iconColorOverlay.color = "#1fc6a6";
|
onEntered: iconColorOverlay.color = "#1fc6a6";
|
||||||
onExited: iconColorOverlay.color = "#ffffff";
|
onExited: iconColorOverlay.color = "#ffffff";
|
||||||
// navigate back to root level menu
|
// navigate back to root level menu
|
||||||
onClicked: buildMenu();
|
onClicked: {
|
||||||
|
buildMenu();
|
||||||
|
tabletRoot.playButtonClickSound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,10 +82,12 @@ FocusScope {
|
||||||
onEntered: breadcrumbText.color = "#1fc6a6";
|
onEntered: breadcrumbText.color = "#1fc6a6";
|
||||||
onExited: breadcrumbText.color = "#34a2c7";
|
onExited: breadcrumbText.color = "#34a2c7";
|
||||||
// navigate back to parent level menu if there is one
|
// navigate back to parent level menu if there is one
|
||||||
onClicked:
|
onClicked: {
|
||||||
if (breadcrumbText.text !== "Menu") {
|
if (breadcrumbText.text !== "Menu") {
|
||||||
menuPopperUpper.closeLastMenu();
|
menuPopperUpper.closeLastMenu();
|
||||||
}
|
}
|
||||||
|
tabletRoot.playButtonClickSound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,10 @@ FocusScope {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onEntered: listView.currentIndex = index
|
onEntered: listView.currentIndex = index
|
||||||
onClicked: root.selected(item)
|
onClicked: {
|
||||||
|
root.selected(item)
|
||||||
|
tabletRoot.playButtonClickSound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ Item {
|
||||||
|
|
||||||
SoundEffect {
|
SoundEffect {
|
||||||
id: buttonClickSound
|
id: buttonClickSound
|
||||||
|
volume: 0.1
|
||||||
source: "../../../sounds/Gamemaster-Audio-button-click.wav"
|
source: "../../../sounds/Gamemaster-Audio-button-click.wav"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1202,8 +1202,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
||||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||||
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity,
|
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity,
|
||||||
[this](const EntityItemID& entityItemID, const PointerEvent& event) {
|
[this](const EntityItemID& entityItemID, const PointerEvent& event) {
|
||||||
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
|
auto entity = getEntities()->getTree()->findEntityByID(entityItemID);
|
||||||
setKeyboardFocusEntity(entityItemID);
|
if (entity && entity->wantsKeyboardFocus()) {
|
||||||
|
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
|
||||||
|
setKeyboardFocusEntity(entityItemID);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, [=](const EntityItemID& entityItemID) {
|
connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, [=](const EntityItemID& entityItemID) {
|
||||||
|
@ -4165,6 +4168,8 @@ void Application::setKeyboardFocusOverlay(unsigned int overlayID) {
|
||||||
auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR;
|
auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR;
|
||||||
const float OVERLAY_DEPTH = 0.0105f;
|
const float OVERLAY_DEPTH = 0.0105f;
|
||||||
setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH));
|
setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH));
|
||||||
|
} else if (_keyboardFocusHighlight) {
|
||||||
|
_keyboardFocusHighlight->setVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4379,6 +4384,10 @@ void Application::update(float deltaTime) {
|
||||||
PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount());
|
PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount());
|
||||||
PerformanceTimer perfTimer("harvestChanges");
|
PerformanceTimer perfTimer("harvestChanges");
|
||||||
if (_physicsEngine->hasOutgoingChanges()) {
|
if (_physicsEngine->hasOutgoingChanges()) {
|
||||||
|
// grab the collision events BEFORE handleOutgoingChanges() because at this point
|
||||||
|
// we have a better idea of which objects we own or should own.
|
||||||
|
auto& collisionEvents = _physicsEngine->getCollisionEvents();
|
||||||
|
|
||||||
getEntities()->getTree()->withWriteLock([&] {
|
getEntities()->getTree()->withWriteLock([&] {
|
||||||
PerformanceTimer perfTimer("handleOutgoingChanges");
|
PerformanceTimer perfTimer("handleOutgoingChanges");
|
||||||
const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges();
|
const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges();
|
||||||
|
@ -4386,11 +4395,10 @@ void Application::update(float deltaTime) {
|
||||||
avatarManager->handleOutgoingChanges(outgoingChanges);
|
avatarManager->handleOutgoingChanges(outgoingChanges);
|
||||||
});
|
});
|
||||||
|
|
||||||
auto collisionEvents = _physicsEngine->getCollisionEvents();
|
|
||||||
avatarManager->handleCollisionEvents(collisionEvents);
|
|
||||||
|
|
||||||
if (!_aboutToQuit) {
|
if (!_aboutToQuit) {
|
||||||
|
// handleCollisionEvents() AFTER handleOutgoinChanges()
|
||||||
PerformanceTimer perfTimer("entities");
|
PerformanceTimer perfTimer("entities");
|
||||||
|
avatarManager->handleCollisionEvents(collisionEvents);
|
||||||
// Collision events (and their scripts) must not be handled when we're locked, above. (That would risk
|
// Collision events (and their scripts) must not be handled when we're locked, above. (That would risk
|
||||||
// deadlock.)
|
// deadlock.)
|
||||||
_entitySimulation->handleCollisionEvents(collisionEvents);
|
_entitySimulation->handleCollisionEvents(collisionEvents);
|
||||||
|
|
|
@ -900,7 +900,7 @@ glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const {
|
||||||
_skeletonModel->getAbsoluteJointRotationInRigFrame(headJointIndex, rotation);
|
_skeletonModel->getAbsoluteJointRotationInRigFrame(headJointIndex, rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rotation;
|
return Quaternions::Y_180 * rotation * Quaternions::Y_180;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
glm::quat rotation;
|
glm::quat rotation;
|
||||||
|
@ -936,7 +936,7 @@ glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const {
|
||||||
_skeletonModel->getAbsoluteJointTranslationInRigFrame(headJointIndex, translation);
|
_skeletonModel->getAbsoluteJointTranslationInRigFrame(headJointIndex, translation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return translation;
|
return Quaternions::Y_180 * translation * Quaternions::Y_180;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
glm::vec3 translation;
|
glm::vec3 translation;
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
QString const ModelOverlay::TYPE = "model";
|
QString const ModelOverlay::TYPE = "model";
|
||||||
|
|
||||||
ModelOverlay::ModelOverlay()
|
ModelOverlay::ModelOverlay()
|
||||||
: _model(std::make_shared<Model>(std::make_shared<Rig>())),
|
: _model(std::make_shared<Model>(std::make_shared<Rig>(), nullptr, this)),
|
||||||
_modelTextures(QVariantMap())
|
_modelTextures(QVariantMap())
|
||||||
{
|
{
|
||||||
_model->init();
|
_model->init();
|
||||||
|
@ -27,7 +27,7 @@ ModelOverlay::ModelOverlay()
|
||||||
|
|
||||||
ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) :
|
ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) :
|
||||||
Volume3DOverlay(modelOverlay),
|
Volume3DOverlay(modelOverlay),
|
||||||
_model(std::make_shared<Model>(std::make_shared<Rig>())),
|
_model(std::make_shared<Model>(std::make_shared<Rig>(), nullptr, this)),
|
||||||
_modelTextures(QVariantMap()),
|
_modelTextures(QVariantMap()),
|
||||||
_url(modelOverlay->_url),
|
_url(modelOverlay->_url),
|
||||||
_updateModel(false)
|
_updateModel(false)
|
||||||
|
|
|
@ -112,6 +112,42 @@ private:
|
||||||
bool _quit { false };
|
bool _quit { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void AudioInjectorsThread::prepare() {
|
||||||
|
_audio->prepareLocalAudioInjectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) {
|
||||||
|
for (int i = 0; i < numSamples/2; i++) {
|
||||||
|
|
||||||
|
// read 2 samples
|
||||||
|
int16_t left = *source++;
|
||||||
|
int16_t right = *source++;
|
||||||
|
|
||||||
|
// write 2 + N samples
|
||||||
|
*dest++ = left;
|
||||||
|
*dest++ = right;
|
||||||
|
for (int n = 0; n < numExtraChannels; n++) {
|
||||||
|
*dest++ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) {
|
||||||
|
for (int i = 0; i < numSamples/2; i++) {
|
||||||
|
|
||||||
|
// read 2 samples
|
||||||
|
int16_t left = *source++;
|
||||||
|
int16_t right = *source++;
|
||||||
|
|
||||||
|
// write 1 sample
|
||||||
|
*dest++ = (int16_t)((left + right) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline float convertToFloat(int16_t sample) {
|
||||||
|
return (float)sample * (1 / 32768.0f);
|
||||||
|
}
|
||||||
|
|
||||||
AudioClient::AudioClient() :
|
AudioClient::AudioClient() :
|
||||||
AbstractAudioInterface(),
|
AbstractAudioInterface(),
|
||||||
_gate(this),
|
_gate(this),
|
||||||
|
@ -127,6 +163,7 @@ AudioClient::AudioClient() :
|
||||||
_loopbackAudioOutput(NULL),
|
_loopbackAudioOutput(NULL),
|
||||||
_loopbackOutputDevice(NULL),
|
_loopbackOutputDevice(NULL),
|
||||||
_inputRingBuffer(0),
|
_inputRingBuffer(0),
|
||||||
|
_localInjectorsStream(0),
|
||||||
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES),
|
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES),
|
||||||
_isStereoInput(false),
|
_isStereoInput(false),
|
||||||
_outputStarveDetectionStartTimeMsec(0),
|
_outputStarveDetectionStartTimeMsec(0),
|
||||||
|
@ -144,13 +181,18 @@ AudioClient::AudioClient() :
|
||||||
_reverbOptions(&_scriptReverbOptions),
|
_reverbOptions(&_scriptReverbOptions),
|
||||||
_inputToNetworkResampler(NULL),
|
_inputToNetworkResampler(NULL),
|
||||||
_networkToOutputResampler(NULL),
|
_networkToOutputResampler(NULL),
|
||||||
|
_localToOutputResampler(NULL),
|
||||||
|
_localAudioThread(this),
|
||||||
_audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT),
|
_audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT),
|
||||||
_outgoingAvatarAudioSequenceNumber(0),
|
_outgoingAvatarAudioSequenceNumber(0),
|
||||||
_audioOutputIODevice(_receivedAudioStream, this),
|
_audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this),
|
||||||
_stats(&_receivedAudioStream),
|
_stats(&_receivedAudioStream),
|
||||||
_inputGate(),
|
_inputGate(),
|
||||||
_positionGetter(DEFAULT_POSITION_GETTER),
|
_positionGetter(DEFAULT_POSITION_GETTER),
|
||||||
_orientationGetter(DEFAULT_ORIENTATION_GETTER) {
|
_orientationGetter(DEFAULT_ORIENTATION_GETTER) {
|
||||||
|
// avoid putting a lock in the device callback
|
||||||
|
assert(_localSamplesAvailable.is_lock_free());
|
||||||
|
|
||||||
// deprecate legacy settings
|
// deprecate legacy settings
|
||||||
{
|
{
|
||||||
Setting::Handle<int>::Deprecated("maxFramesOverDesired", InboundAudioStream::MAX_FRAMES_OVER_DESIRED);
|
Setting::Handle<int>::Deprecated("maxFramesOverDesired", InboundAudioStream::MAX_FRAMES_OVER_DESIRED);
|
||||||
|
@ -176,6 +218,10 @@ AudioClient::AudioClient() :
|
||||||
_checkDevicesThread->setPriority(QThread::LowPriority);
|
_checkDevicesThread->setPriority(QThread::LowPriority);
|
||||||
_checkDevicesThread->start();
|
_checkDevicesThread->start();
|
||||||
|
|
||||||
|
// start a thread to process local injectors
|
||||||
|
_localAudioThread.setObjectName("LocalAudio Thread");
|
||||||
|
_localAudioThread.start();
|
||||||
|
|
||||||
configureReverb();
|
configureReverb();
|
||||||
|
|
||||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||||
|
@ -213,6 +259,7 @@ void AudioClient::reset() {
|
||||||
_stats.reset();
|
_stats.reset();
|
||||||
_sourceReverb.reset();
|
_sourceReverb.reset();
|
||||||
_listenerReverb.reset();
|
_listenerReverb.reset();
|
||||||
|
_localReverb.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioClient::audioMixerKilled() {
|
void AudioClient::audioMixerKilled() {
|
||||||
|
@ -365,7 +412,7 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) {
|
||||||
CoUninitialize();
|
CoUninitialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
qCDebug(audioclient) << "DEBUG [" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]";
|
qCDebug(audioclient) << "[" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]";
|
||||||
|
|
||||||
return getNamedAudioDeviceForMode(mode, deviceName);
|
return getNamedAudioDeviceForMode(mode, deviceName);
|
||||||
#endif
|
#endif
|
||||||
|
@ -387,12 +434,12 @@ bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice,
|
||||||
audioFormat.setByteOrder(QAudioFormat::LittleEndian);
|
audioFormat.setByteOrder(QAudioFormat::LittleEndian);
|
||||||
|
|
||||||
if (!audioDevice.isFormatSupported(audioFormat)) {
|
if (!audioDevice.isFormatSupported(audioFormat)) {
|
||||||
qCDebug(audioclient) << "WARNING: The native format is" << audioFormat << "but isFormatSupported() failed.";
|
qCWarning(audioclient) << "The native format is" << audioFormat << "but isFormatSupported() failed.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// converting to/from this rate must produce an integral number of samples
|
// converting to/from this rate must produce an integral number of samples
|
||||||
if (audioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) {
|
if (audioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) {
|
||||||
qCDebug(audioclient) << "WARNING: The native sample rate [" << audioFormat.sampleRate() << "] is not supported.";
|
qCWarning(audioclient) << "The native sample rate [" << audioFormat.sampleRate() << "] is not supported.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -726,12 +773,12 @@ QVector<QString> AudioClient::getDeviceNames(QAudio::Mode mode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AudioClient::switchInputToAudioDevice(const QString& inputDeviceName) {
|
bool AudioClient::switchInputToAudioDevice(const QString& inputDeviceName) {
|
||||||
qCDebug(audioclient) << "DEBUG [" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]";
|
qCDebug(audioclient) << "[" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]";
|
||||||
return switchInputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName));
|
return switchInputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AudioClient::switchOutputToAudioDevice(const QString& outputDeviceName) {
|
bool AudioClient::switchOutputToAudioDevice(const QString& outputDeviceName) {
|
||||||
qCDebug(audioclient) << "DEBUG [" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]";
|
qCDebug(audioclient) << "[" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]";
|
||||||
return switchOutputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName));
|
return switchOutputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -762,6 +809,7 @@ void AudioClient::configureReverb() {
|
||||||
p.wetDryMix = _reverbOptions->getWetDryMix();
|
p.wetDryMix = _reverbOptions->getWetDryMix();
|
||||||
|
|
||||||
_listenerReverb.setParameters(&p);
|
_listenerReverb.setParameters(&p);
|
||||||
|
_localReverb.setParameters(&p);
|
||||||
|
|
||||||
// used only for adding self-reverb to loopback audio
|
// used only for adding self-reverb to loopback audio
|
||||||
p.sampleRate = _outputFormat.sampleRate();
|
p.sampleRate = _outputFormat.sampleRate();
|
||||||
|
@ -808,6 +856,7 @@ void AudioClient::setReverb(bool reverb) {
|
||||||
if (!_reverb) {
|
if (!_reverb) {
|
||||||
_sourceReverb.reset();
|
_sourceReverb.reset();
|
||||||
_listenerReverb.reset();
|
_listenerReverb.reset();
|
||||||
|
_localReverb.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -841,36 +890,6 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) {
|
|
||||||
|
|
||||||
for (int i = 0; i < numSamples/2; i++) {
|
|
||||||
|
|
||||||
// read 2 samples
|
|
||||||
int16_t left = *source++;
|
|
||||||
int16_t right = *source++;
|
|
||||||
|
|
||||||
// write 2 + N samples
|
|
||||||
*dest++ = left;
|
|
||||||
*dest++ = right;
|
|
||||||
for (int n = 0; n < numExtraChannels; n++) {
|
|
||||||
*dest++ = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) {
|
|
||||||
|
|
||||||
for (int i = 0; i < numSamples/2; i++) {
|
|
||||||
|
|
||||||
// read 2 samples
|
|
||||||
int16_t left = *source++;
|
|
||||||
int16_t right = *source++;
|
|
||||||
|
|
||||||
// write 1 sample
|
|
||||||
*dest++ = (int16_t)((left + right) / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) {
|
void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) {
|
||||||
// If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here.
|
// If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here.
|
||||||
bool hasReverb = _reverb || _receivedAudioStream.hasReverb();
|
bool hasReverb = _reverb || _receivedAudioStream.hasReverb();
|
||||||
|
@ -1082,14 +1101,78 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) {
|
||||||
PacketType::MicrophoneAudioWithEcho, _selectedCodecName);
|
PacketType::MicrophoneAudioWithEcho, _selectedCodecName);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
void AudioClient::prepareLocalAudioInjectors() {
|
||||||
|
if (_outputPeriod == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bufferCapacity = _localInjectorsStream.getSampleCapacity();
|
||||||
|
if (_localToOutputResampler) {
|
||||||
|
// avoid overwriting the buffer,
|
||||||
|
// instead of failing on writes because the buffer is used as a lock-free pipe
|
||||||
|
bufferCapacity -=
|
||||||
|
_localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) *
|
||||||
|
AudioConstants::STEREO;
|
||||||
|
bufferCapacity += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int samplesNeeded = std::numeric_limits<int>::max();
|
||||||
|
while (samplesNeeded > 0) {
|
||||||
|
// lock for every write to avoid locking out the device callback
|
||||||
|
// this lock is intentional - the buffer is only lock-free in its use in the device callback
|
||||||
|
Lock lock(_localAudioMutex);
|
||||||
|
|
||||||
|
samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed);
|
||||||
|
if (samplesNeeded <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a network frame of local injectors' audio
|
||||||
|
if (!mixLocalAudioInjectors(_localMixBuffer)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverb
|
||||||
|
if (_reverb) {
|
||||||
|
_localReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
int samples;
|
||||||
|
if (_localToOutputResampler) {
|
||||||
|
// resample to output sample rate
|
||||||
|
int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer,
|
||||||
|
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||||
|
|
||||||
|
// write to local injectors' ring buffer
|
||||||
|
samples = frames * AudioConstants::STEREO;
|
||||||
|
_localInjectorsStream.writeSamples(_localOutputMixBuffer, samples);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// write to local injectors' ring buffer
|
||||||
|
samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO;
|
||||||
|
_localInjectorsStream.writeSamples(_localMixBuffer,
|
||||||
|
AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
|
||||||
|
}
|
||||||
|
|
||||||
|
_localSamplesAvailable.fetch_add(samples, std::memory_order_release);
|
||||||
|
samplesNeeded -= samples;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
||||||
|
|
||||||
QVector<AudioInjector*> injectorsToRemove;
|
QVector<AudioInjector*> injectorsToRemove;
|
||||||
|
|
||||||
// lock the injector vector
|
// lock the injector vector
|
||||||
Lock lock(_injectorsMutex);
|
Lock lock(_injectorsMutex);
|
||||||
|
|
||||||
for (AudioInjector* injector : getActiveLocalAudioInjectors()) {
|
if (_activeLocalAudioInjectors.size() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float));
|
||||||
|
|
||||||
|
for (AudioInjector* injector : _activeLocalAudioInjectors) {
|
||||||
if (injector->getLocalBuffer()) {
|
if (injector->getLocalBuffer()) {
|
||||||
|
|
||||||
static const int HRTF_DATASET_INDEX = 1;
|
static const int HRTF_DATASET_INDEX = 1;
|
||||||
|
@ -1098,8 +1181,8 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
||||||
qint64 bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL;
|
qint64 bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL;
|
||||||
|
|
||||||
// get one frame from the injector
|
// get one frame from the injector
|
||||||
memset(_scratchBuffer, 0, bytesToRead);
|
memset(_localScratchBuffer, 0, bytesToRead);
|
||||||
if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, bytesToRead)) {
|
if (0 < injector->getLocalBuffer()->readData((char*)_localScratchBuffer, bytesToRead)) {
|
||||||
|
|
||||||
if (injector->isAmbisonic()) {
|
if (injector->isAmbisonic()) {
|
||||||
|
|
||||||
|
@ -1119,7 +1202,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
||||||
float qz = relativeOrientation.y;
|
float qz = relativeOrientation.y;
|
||||||
|
|
||||||
// Ambisonic gets spatialized into mixBuffer
|
// Ambisonic gets spatialized into mixBuffer
|
||||||
injector->getLocalFOA().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX,
|
injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX,
|
||||||
qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||||
|
|
||||||
} else if (injector->isStereo()) {
|
} else if (injector->isStereo()) {
|
||||||
|
@ -1127,7 +1210,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
||||||
// stereo gets directly mixed into mixBuffer
|
// stereo gets directly mixed into mixBuffer
|
||||||
float gain = injector->getVolume();
|
float gain = injector->getVolume();
|
||||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) {
|
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) {
|
||||||
mixBuffer[i] += (float)_scratchBuffer[i] * (1/32768.0f) * gain;
|
mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -1139,7 +1222,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
||||||
float azimuth = azimuthForSource(relativePosition);
|
float azimuth = azimuthForSource(relativePosition);
|
||||||
|
|
||||||
// mono gets spatialized into mixBuffer
|
// mono gets spatialized into mixBuffer
|
||||||
injector->getLocalHRTF().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX,
|
injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX,
|
||||||
azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1160,8 +1243,10 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
||||||
|
|
||||||
for (AudioInjector* injector : injectorsToRemove) {
|
for (AudioInjector* injector : injectorsToRemove) {
|
||||||
qCDebug(audioclient) << "removing injector";
|
qCDebug(audioclient) << "removing injector";
|
||||||
getActiveLocalAudioInjectors().removeOne(injector);
|
_activeLocalAudioInjectors.removeOne(injector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) {
|
void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) {
|
||||||
|
@ -1172,33 +1257,24 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA
|
||||||
outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE);
|
outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE);
|
||||||
int16_t* outputSamples = reinterpret_cast<int16_t*>(outputBuffer.data());
|
int16_t* outputSamples = reinterpret_cast<int16_t*>(outputBuffer.data());
|
||||||
|
|
||||||
// convert network audio to float
|
bool hasReverb = _reverb || _receivedAudioStream.hasReverb();
|
||||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) {
|
|
||||||
_mixBuffer[i] = (float)decodedSamples[i] * (1/32768.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// mix in active injectors
|
|
||||||
if (getActiveLocalAudioInjectors().size() > 0) {
|
|
||||||
mixLocalAudioInjectors(_mixBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply stereo reverb
|
// apply stereo reverb
|
||||||
bool hasReverb = _reverb || _receivedAudioStream.hasReverb();
|
|
||||||
if (hasReverb) {
|
if (hasReverb) {
|
||||||
updateReverbOptions();
|
updateReverbOptions();
|
||||||
_listenerReverb.render(_mixBuffer, _mixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
int16_t* reverbSamples = _networkToOutputResampler ? _networkScratchBuffer : outputSamples;
|
||||||
|
_listenerReverb.render(decodedSamples, reverbSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resample to output sample rate
|
||||||
if (_networkToOutputResampler) {
|
if (_networkToOutputResampler) {
|
||||||
|
const int16_t* inputSamples = hasReverb ? _networkScratchBuffer : decodedSamples;
|
||||||
|
_networkToOutputResampler->render(inputSamples, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
// resample to output sample rate
|
// if no transformations were applied, we still need to copy the buffer
|
||||||
_audioLimiter.render(_mixBuffer, _scratchBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
if (!hasReverb && !_networkToOutputResampler) {
|
||||||
_networkToOutputResampler->render(_scratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
memcpy(outputSamples, decodedSamples, decodedBuffer.size());
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// no resampling needed
|
|
||||||
_audioLimiter.render(_mixBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1381,6 +1457,9 @@ void AudioClient::outputNotify() {
|
||||||
bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) {
|
bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) {
|
||||||
bool supportedFormat = false;
|
bool supportedFormat = false;
|
||||||
|
|
||||||
|
Lock lock(_localAudioMutex);
|
||||||
|
_localSamplesAvailable.exchange(0, std::memory_order_release);
|
||||||
|
|
||||||
// cleanup any previously initialized device
|
// cleanup any previously initialized device
|
||||||
if (_audioOutput) {
|
if (_audioOutput) {
|
||||||
_audioOutput->stop();
|
_audioOutput->stop();
|
||||||
|
@ -1391,12 +1470,24 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
|
||||||
_loopbackOutputDevice = NULL;
|
_loopbackOutputDevice = NULL;
|
||||||
delete _loopbackAudioOutput;
|
delete _loopbackAudioOutput;
|
||||||
_loopbackAudioOutput = NULL;
|
_loopbackAudioOutput = NULL;
|
||||||
|
|
||||||
|
delete[] _outputMixBuffer;
|
||||||
|
_outputMixBuffer = NULL;
|
||||||
|
|
||||||
|
delete[] _outputScratchBuffer;
|
||||||
|
_outputScratchBuffer = NULL;
|
||||||
|
|
||||||
|
delete[] _localOutputMixBuffer;
|
||||||
|
_localOutputMixBuffer = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_networkToOutputResampler) {
|
if (_networkToOutputResampler) {
|
||||||
// if we were using an input to network resampler, delete it here
|
// if we were using an input to network resampler, delete it here
|
||||||
delete _networkToOutputResampler;
|
delete _networkToOutputResampler;
|
||||||
_networkToOutputResampler = NULL;
|
_networkToOutputResampler = NULL;
|
||||||
|
|
||||||
|
delete _localToOutputResampler;
|
||||||
|
_localToOutputResampler = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!outputDeviceInfo.isNull()) {
|
if (!outputDeviceInfo.isNull()) {
|
||||||
|
@ -1416,6 +1507,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
|
||||||
assert(_outputFormat.sampleSize() == 16);
|
assert(_outputFormat.sampleSize() == 16);
|
||||||
|
|
||||||
_networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT);
|
_networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT);
|
||||||
|
_localToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
qCDebug(audioclient) << "No resampling required for network output to match actual output format.";
|
qCDebug(audioclient) << "No resampling required for network output to match actual output format.";
|
||||||
|
@ -1441,6 +1533,14 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
|
||||||
_audioOutput->start(&_audioOutputIODevice);
|
_audioOutput->start(&_audioOutputIODevice);
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
|
||||||
|
int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE;
|
||||||
|
// device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes
|
||||||
|
_outputPeriod = periodSampleSize * 2;
|
||||||
|
_outputMixBuffer = new float[_outputPeriod];
|
||||||
|
_outputScratchBuffer = new int16_t[_outputPeriod];
|
||||||
|
_localOutputMixBuffer = new float[_outputPeriod];
|
||||||
|
_localInjectorsStream.resizeForFrameSize(_outputPeriod * 2);
|
||||||
|
|
||||||
qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize <<
|
qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize <<
|
||||||
"requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() <<
|
"requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() <<
|
||||||
"os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize();
|
"os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize();
|
||||||
|
@ -1550,26 +1650,61 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
|
||||||
// samples requested from OUTPUT_CHANNEL_COUNT
|
// samples requested from OUTPUT_CHANNEL_COUNT
|
||||||
int deviceChannelCount = _audio->_outputFormat.channelCount();
|
int deviceChannelCount = _audio->_outputFormat.channelCount();
|
||||||
int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount;
|
int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount;
|
||||||
|
// restrict samplesRequested to the size of our mix/scratch buffers
|
||||||
|
samplesRequested = std::min(samplesRequested, _audio->_outputPeriod);
|
||||||
|
|
||||||
int samplesPopped;
|
int16_t* scratchBuffer = _audio->_outputScratchBuffer;
|
||||||
int bytesWritten;
|
float* mixBuffer = _audio->_outputMixBuffer;
|
||||||
|
|
||||||
if ((samplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) {
|
int networkSamplesPopped;
|
||||||
qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable());
|
if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) {
|
||||||
|
qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested);
|
||||||
AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput();
|
AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput();
|
||||||
|
lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped);
|
||||||
|
|
||||||
// if required, upmix or downmix to deviceChannelCount
|
for (int i = 0; i < networkSamplesPopped; i++) {
|
||||||
if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) {
|
mixBuffer[i] = convertToFloat(scratchBuffer[i]);
|
||||||
lastPopOutput.readSamples((int16_t*)data, samplesPopped);
|
|
||||||
} else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) {
|
|
||||||
lastPopOutput.readSamplesWithUpmix((int16_t*)data, samplesPopped, deviceChannelCount - OUTPUT_CHANNEL_COUNT);
|
|
||||||
} else {
|
|
||||||
lastPopOutput.readSamplesWithDownmix((int16_t*)data, samplesPopped);
|
|
||||||
}
|
}
|
||||||
bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT;
|
|
||||||
|
samplesRequested = networkSamplesPopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
int injectorSamplesPopped = 0;
|
||||||
|
{
|
||||||
|
Lock lock(_audio->_localAudioMutex);
|
||||||
|
bool append = networkSamplesPopped > 0;
|
||||||
|
samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire));
|
||||||
|
if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) {
|
||||||
|
_audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release);
|
||||||
|
qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare injectors for the next callback
|
||||||
|
QMetaObject::invokeMethod(&_audio->_localAudioThread, "prepare", Qt::QueuedConnection);
|
||||||
|
|
||||||
|
int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped);
|
||||||
|
int framesPopped = samplesPopped / AudioConstants::STEREO;
|
||||||
|
int bytesWritten;
|
||||||
|
if (samplesPopped > 0) {
|
||||||
|
if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) {
|
||||||
|
// limit the audio
|
||||||
|
_audio->_audioLimiter.render(mixBuffer, (int16_t*)data, framesPopped);
|
||||||
|
} else {
|
||||||
|
_audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped);
|
||||||
|
|
||||||
|
// upmix or downmix to deviceChannelCount
|
||||||
|
if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) {
|
||||||
|
int extraChannels = deviceChannelCount - OUTPUT_CHANNEL_COUNT;
|
||||||
|
channelUpmix(scratchBuffer, (int16_t*)data, samplesPopped, extraChannels);
|
||||||
|
} else {
|
||||||
|
channelDownmix(scratchBuffer, (int16_t*)data, samplesPopped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount;
|
||||||
} else {
|
} else {
|
||||||
// nothing on network, don't grab anything from injectors, and just return 0s
|
// nothing on network, don't grab anything from injectors, and just return 0s
|
||||||
// this will flood the log: qCDebug(audioclient, "empty/partial network buffer");
|
|
||||||
memset(data, 0, maxSize);
|
memset(data, 0, maxSize);
|
||||||
bytesWritten = maxSize;
|
bytesWritten = maxSize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,9 +69,24 @@ class QIODevice;
|
||||||
class Transform;
|
class Transform;
|
||||||
class NLPacket;
|
class NLPacket;
|
||||||
|
|
||||||
|
class AudioInjectorsThread : public QThread {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
AudioInjectorsThread(AudioClient* audio) : _audio(audio) {}
|
||||||
|
|
||||||
|
public slots :
|
||||||
|
void prepare();
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioClient* _audio;
|
||||||
|
};
|
||||||
|
|
||||||
class AudioClient : public AbstractAudioInterface, public Dependency {
|
class AudioClient : public AbstractAudioInterface, public Dependency {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
SINGLETON_DEPENDENCY
|
SINGLETON_DEPENDENCY
|
||||||
|
|
||||||
|
using LocalInjectorsStream = AudioMixRingBuffer;
|
||||||
public:
|
public:
|
||||||
static const int MIN_BUFFER_FRAMES;
|
static const int MIN_BUFFER_FRAMES;
|
||||||
static const int MAX_BUFFER_FRAMES;
|
static const int MAX_BUFFER_FRAMES;
|
||||||
|
@ -84,8 +99,10 @@ public:
|
||||||
|
|
||||||
class AudioOutputIODevice : public QIODevice {
|
class AudioOutputIODevice : public QIODevice {
|
||||||
public:
|
public:
|
||||||
AudioOutputIODevice(MixedProcessedAudioStream& receivedAudioStream, AudioClient* audio) :
|
AudioOutputIODevice(LocalInjectorsStream& localInjectorsStream, MixedProcessedAudioStream& receivedAudioStream,
|
||||||
_receivedAudioStream(receivedAudioStream), _audio(audio), _unfulfilledReads(0) {};
|
AudioClient* audio) :
|
||||||
|
_localInjectorsStream(localInjectorsStream), _receivedAudioStream(receivedAudioStream),
|
||||||
|
_audio(audio), _unfulfilledReads(0) {}
|
||||||
|
|
||||||
void start() { open(QIODevice::ReadOnly | QIODevice::Unbuffered); }
|
void start() { open(QIODevice::ReadOnly | QIODevice::Unbuffered); }
|
||||||
void stop() { close(); }
|
void stop() { close(); }
|
||||||
|
@ -93,6 +110,7 @@ public:
|
||||||
qint64 writeData(const char * data, qint64 maxSize) override { return 0; }
|
qint64 writeData(const char * data, qint64 maxSize) override { return 0; }
|
||||||
int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; }
|
int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; }
|
||||||
private:
|
private:
|
||||||
|
LocalInjectorsStream& _localInjectorsStream;
|
||||||
MixedProcessedAudioStream& _receivedAudioStream;
|
MixedProcessedAudioStream& _receivedAudioStream;
|
||||||
AudioClient* _audio;
|
AudioClient* _audio;
|
||||||
int _unfulfilledReads;
|
int _unfulfilledReads;
|
||||||
|
@ -129,8 +147,6 @@ public:
|
||||||
|
|
||||||
Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale);
|
Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale);
|
||||||
|
|
||||||
QVector<AudioInjector*>& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; }
|
|
||||||
|
|
||||||
void checkDevices();
|
void checkDevices();
|
||||||
|
|
||||||
static const float CALLBACK_ACCELERATOR_RATIO;
|
static const float CALLBACK_ACCELERATOR_RATIO;
|
||||||
|
@ -171,6 +187,7 @@ public slots:
|
||||||
|
|
||||||
int setOutputBufferSize(int numFrames, bool persist = true);
|
int setOutputBufferSize(int numFrames, bool persist = true);
|
||||||
|
|
||||||
|
void prepareLocalAudioInjectors();
|
||||||
bool outputLocalInjector(AudioInjector* injector) override;
|
bool outputLocalInjector(AudioInjector* injector) override;
|
||||||
bool shouldLoopbackInjectors() override { return _shouldEchoToServer; }
|
bool shouldLoopbackInjectors() override { return _shouldEchoToServer; }
|
||||||
|
|
||||||
|
@ -218,7 +235,7 @@ protected:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void outputFormatChanged();
|
void outputFormatChanged();
|
||||||
void mixLocalAudioInjectors(float* mixBuffer);
|
bool mixLocalAudioInjectors(float* mixBuffer);
|
||||||
float azimuthForSource(const glm::vec3& relativePosition);
|
float azimuthForSource(const glm::vec3& relativePosition);
|
||||||
float gainForSource(float distance, float volume);
|
float gainForSource(float distance, float volume);
|
||||||
|
|
||||||
|
@ -262,6 +279,10 @@ private:
|
||||||
QAudioOutput* _loopbackAudioOutput;
|
QAudioOutput* _loopbackAudioOutput;
|
||||||
QIODevice* _loopbackOutputDevice;
|
QIODevice* _loopbackOutputDevice;
|
||||||
AudioRingBuffer _inputRingBuffer;
|
AudioRingBuffer _inputRingBuffer;
|
||||||
|
LocalInjectorsStream _localInjectorsStream;
|
||||||
|
// In order to use _localInjectorsStream as a lock-free pipe,
|
||||||
|
// use it with a single producer/consumer, and track available samples
|
||||||
|
std::atomic<int> _localSamplesAvailable { 0 };
|
||||||
MixedProcessedAudioStream _receivedAudioStream;
|
MixedProcessedAudioStream _receivedAudioStream;
|
||||||
bool _isStereoInput;
|
bool _isStereoInput;
|
||||||
|
|
||||||
|
@ -292,14 +313,28 @@ private:
|
||||||
AudioEffectOptions* _reverbOptions;
|
AudioEffectOptions* _reverbOptions;
|
||||||
AudioReverb _sourceReverb { AudioConstants::SAMPLE_RATE };
|
AudioReverb _sourceReverb { AudioConstants::SAMPLE_RATE };
|
||||||
AudioReverb _listenerReverb { AudioConstants::SAMPLE_RATE };
|
AudioReverb _listenerReverb { AudioConstants::SAMPLE_RATE };
|
||||||
|
AudioReverb _localReverb { AudioConstants::SAMPLE_RATE };
|
||||||
|
|
||||||
// possible streams needed for resample
|
// possible streams needed for resample
|
||||||
AudioSRC* _inputToNetworkResampler;
|
AudioSRC* _inputToNetworkResampler;
|
||||||
AudioSRC* _networkToOutputResampler;
|
AudioSRC* _networkToOutputResampler;
|
||||||
|
AudioSRC* _localToOutputResampler;
|
||||||
|
|
||||||
|
// for network audio (used by network audio thread)
|
||||||
|
int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
|
||||||
|
|
||||||
|
// for local audio (used by audio injectors thread)
|
||||||
|
float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
|
||||||
|
int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
|
||||||
|
float* _localOutputMixBuffer { NULL };
|
||||||
|
AudioInjectorsThread _localAudioThread;
|
||||||
|
Mutex _localAudioMutex;
|
||||||
|
|
||||||
|
// for output audio (used by this thread)
|
||||||
|
int _outputPeriod { 0 };
|
||||||
|
float* _outputMixBuffer { NULL };
|
||||||
|
int16_t* _outputScratchBuffer { NULL };
|
||||||
|
|
||||||
// for local hrtf-ing
|
|
||||||
float _mixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
|
|
||||||
int16_t _scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
|
|
||||||
AudioLimiter _audioLimiter;
|
AudioLimiter _audioLimiter;
|
||||||
|
|
||||||
// Adds Reverb
|
// Adds Reverb
|
||||||
|
|
|
@ -26,46 +26,51 @@
|
||||||
static const QString RING_BUFFER_OVERFLOW_DEBUG { "AudioRingBuffer::writeData has overflown the buffer. Overwriting old data." };
|
static const QString RING_BUFFER_OVERFLOW_DEBUG { "AudioRingBuffer::writeData has overflown the buffer. Overwriting old data." };
|
||||||
static const QString DROPPED_SILENT_DEBUG { "AudioRingBuffer::addSilentSamples dropping silent samples to prevent overflow." };
|
static const QString DROPPED_SILENT_DEBUG { "AudioRingBuffer::addSilentSamples dropping silent samples to prevent overflow." };
|
||||||
|
|
||||||
AudioRingBuffer::AudioRingBuffer(int numFrameSamples, int numFramesCapacity) :
|
template <class T>
|
||||||
|
AudioRingBufferTemplate<T>::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) :
|
||||||
_numFrameSamples(numFrameSamples),
|
_numFrameSamples(numFrameSamples),
|
||||||
_frameCapacity(numFramesCapacity),
|
_frameCapacity(numFramesCapacity),
|
||||||
_sampleCapacity(numFrameSamples * numFramesCapacity),
|
_sampleCapacity(numFrameSamples * numFramesCapacity),
|
||||||
_bufferLength(numFrameSamples * (numFramesCapacity + 1))
|
_bufferLength(numFrameSamples * (numFramesCapacity + 1))
|
||||||
{
|
{
|
||||||
if (numFrameSamples) {
|
if (numFrameSamples) {
|
||||||
_buffer = new int16_t[_bufferLength];
|
_buffer = new Sample[_bufferLength];
|
||||||
memset(_buffer, 0, _bufferLength * sizeof(int16_t));
|
memset(_buffer, 0, _bufferLength * SampleSize);
|
||||||
_nextOutput = _buffer;
|
_nextOutput = _buffer;
|
||||||
_endOfLastWrite = _buffer;
|
_endOfLastWrite = _buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
static QString repeatedOverflowMessage = LogHandler::getInstance().addRepeatedMessageRegex(RING_BUFFER_OVERFLOW_DEBUG);
|
static QString repeatedOverflowMessage = LogHandler::getInstance().addRepeatedMessageRegex(RING_BUFFER_OVERFLOW_DEBUG);
|
||||||
static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG);
|
static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG);
|
||||||
};
|
}
|
||||||
|
|
||||||
AudioRingBuffer::~AudioRingBuffer() {
|
template <class T>
|
||||||
|
AudioRingBufferTemplate<T>::~AudioRingBufferTemplate() {
|
||||||
delete[] _buffer;
|
delete[] _buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioRingBuffer::clear() {
|
template <class T>
|
||||||
|
void AudioRingBufferTemplate<T>::clear() {
|
||||||
_endOfLastWrite = _buffer;
|
_endOfLastWrite = _buffer;
|
||||||
_nextOutput = _buffer;
|
_nextOutput = _buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioRingBuffer::reset() {
|
template <class T>
|
||||||
|
void AudioRingBufferTemplate<T>::reset() {
|
||||||
clear();
|
clear();
|
||||||
_overflowCount = 0;
|
_overflowCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) {
|
template <class T>
|
||||||
|
void AudioRingBufferTemplate<T>::resizeForFrameSize(int numFrameSamples) {
|
||||||
delete[] _buffer;
|
delete[] _buffer;
|
||||||
_numFrameSamples = numFrameSamples;
|
_numFrameSamples = numFrameSamples;
|
||||||
_sampleCapacity = numFrameSamples * _frameCapacity;
|
_sampleCapacity = numFrameSamples * _frameCapacity;
|
||||||
_bufferLength = numFrameSamples * (_frameCapacity + 1);
|
_bufferLength = numFrameSamples * (_frameCapacity + 1);
|
||||||
|
|
||||||
if (numFrameSamples) {
|
if (numFrameSamples) {
|
||||||
_buffer = new int16_t[_bufferLength];
|
_buffer = new Sample[_bufferLength];
|
||||||
memset(_buffer, 0, _bufferLength * sizeof(int16_t));
|
memset(_buffer, 0, _bufferLength * SampleSize);
|
||||||
} else {
|
} else {
|
||||||
_buffer = nullptr;
|
_buffer = nullptr;
|
||||||
}
|
}
|
||||||
|
@ -73,17 +78,29 @@ void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioRingBuffer::readSamples(int16_t* destination, int maxSamples) {
|
template <class T>
|
||||||
return readData((char*)destination, maxSamples * sizeof(int16_t)) / sizeof(int16_t);
|
int AudioRingBufferTemplate<T>::readSamples(Sample* destination, int maxSamples) {
|
||||||
|
return readData((char*)destination, maxSamples * SampleSize) / SampleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioRingBuffer::writeSamples(const int16_t* source, int maxSamples) {
|
template <class T>
|
||||||
return writeData((char*)source, maxSamples * sizeof(int16_t)) / sizeof(int16_t);
|
int AudioRingBufferTemplate<T>::appendSamples(Sample* destination, int maxSamples, bool append) {
|
||||||
|
if (append) {
|
||||||
|
return appendData((char*)destination, maxSamples * SampleSize) / SampleSize;
|
||||||
|
} else {
|
||||||
|
return readData((char*)destination, maxSamples * SampleSize) / SampleSize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioRingBuffer::readData(char *data, int maxSize) {
|
template <class T>
|
||||||
|
int AudioRingBufferTemplate<T>::writeSamples(const Sample* source, int maxSamples) {
|
||||||
|
return writeData((char*)source, maxSamples * SampleSize) / SampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
int AudioRingBufferTemplate<T>::readData(char *data, int maxSize) {
|
||||||
// only copy up to the number of samples we have available
|
// only copy up to the number of samples we have available
|
||||||
int maxSamples = maxSize / sizeof(int16_t);
|
int maxSamples = maxSize / SampleSize;
|
||||||
int numReadSamples = std::min(maxSamples, samplesAvailable());
|
int numReadSamples = std::min(maxSamples, samplesAvailable());
|
||||||
|
|
||||||
if (_nextOutput + numReadSamples > _buffer + _bufferLength) {
|
if (_nextOutput + numReadSamples > _buffer + _bufferLength) {
|
||||||
|
@ -91,22 +108,56 @@ int AudioRingBuffer::readData(char *data, int maxSize) {
|
||||||
int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput;
|
int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput;
|
||||||
|
|
||||||
// read to the end of the buffer
|
// read to the end of the buffer
|
||||||
memcpy(data, _nextOutput, numSamplesToEnd * sizeof(int16_t));
|
memcpy(data, _nextOutput, numSamplesToEnd * SampleSize);
|
||||||
|
|
||||||
// read the rest from the beginning of the buffer
|
// read the rest from the beginning of the buffer
|
||||||
memcpy(data + (numSamplesToEnd * sizeof(int16_t)), _buffer, (numReadSamples - numSamplesToEnd) * sizeof(int16_t));
|
memcpy(data + (numSamplesToEnd * SampleSize), _buffer, (numReadSamples - numSamplesToEnd) * SampleSize);
|
||||||
} else {
|
} else {
|
||||||
memcpy(data, _nextOutput, numReadSamples * sizeof(int16_t));
|
memcpy(data, _nextOutput, numReadSamples * SampleSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
shiftReadPosition(numReadSamples);
|
shiftReadPosition(numReadSamples);
|
||||||
|
|
||||||
return numReadSamples * sizeof(int16_t);
|
return numReadSamples * SampleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioRingBuffer::writeData(const char* data, int maxSize) {
|
template <class T>
|
||||||
|
int AudioRingBufferTemplate<T>::appendData(char *data, int maxSize) {
|
||||||
|
// only copy up to the number of samples we have available
|
||||||
|
int maxSamples = maxSize / SampleSize;
|
||||||
|
int numReadSamples = std::min(maxSamples, samplesAvailable());
|
||||||
|
|
||||||
|
Sample* dest = reinterpret_cast<Sample*>(data);
|
||||||
|
Sample* output = _nextOutput;
|
||||||
|
if (_nextOutput + numReadSamples > _buffer + _bufferLength) {
|
||||||
|
// we're going to need to do two reads to get this data, it wraps around the edge
|
||||||
|
int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput;
|
||||||
|
|
||||||
|
// read to the end of the buffer
|
||||||
|
for (int i = 0; i < numSamplesToEnd; i++) {
|
||||||
|
*dest++ += *output++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the rest from the beginning of the buffer
|
||||||
|
output = _buffer;
|
||||||
|
for (int i = 0; i < (numReadSamples - numSamplesToEnd); i++) {
|
||||||
|
*dest++ += *output++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < numReadSamples; i++) {
|
||||||
|
*dest++ += *output++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shiftReadPosition(numReadSamples);
|
||||||
|
|
||||||
|
return numReadSamples * SampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
int AudioRingBufferTemplate<T>::writeData(const char* data, int maxSize) {
|
||||||
// only copy up to the number of samples we have capacity for
|
// only copy up to the number of samples we have capacity for
|
||||||
int maxSamples = maxSize / sizeof(int16_t);
|
int maxSamples = maxSize / SampleSize;
|
||||||
int numWriteSamples = std::min(maxSamples, _sampleCapacity);
|
int numWriteSamples = std::min(maxSamples, _sampleCapacity);
|
||||||
int samplesRoomFor = _sampleCapacity - samplesAvailable();
|
int samplesRoomFor = _sampleCapacity - samplesAvailable();
|
||||||
|
|
||||||
|
@ -124,20 +175,21 @@ int AudioRingBuffer::writeData(const char* data, int maxSize) {
|
||||||
int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite;
|
int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite;
|
||||||
|
|
||||||
// write to the end of the buffer
|
// write to the end of the buffer
|
||||||
memcpy(_endOfLastWrite, data, numSamplesToEnd * sizeof(int16_t));
|
memcpy(_endOfLastWrite, data, numSamplesToEnd * SampleSize);
|
||||||
|
|
||||||
// write the rest to the beginning of the buffer
|
// write the rest to the beginning of the buffer
|
||||||
memcpy(_buffer, data + (numSamplesToEnd * sizeof(int16_t)), (numWriteSamples - numSamplesToEnd) * sizeof(int16_t));
|
memcpy(_buffer, data + (numSamplesToEnd * SampleSize), (numWriteSamples - numSamplesToEnd) * SampleSize);
|
||||||
} else {
|
} else {
|
||||||
memcpy(_endOfLastWrite, data, numWriteSamples * sizeof(int16_t));
|
memcpy(_endOfLastWrite, data, numWriteSamples * SampleSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
_endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples);
|
_endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples);
|
||||||
|
|
||||||
return numWriteSamples * sizeof(int16_t);
|
return numWriteSamples * SampleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioRingBuffer::samplesAvailable() const {
|
template <class T>
|
||||||
|
int AudioRingBufferTemplate<T>::samplesAvailable() const {
|
||||||
if (!_endOfLastWrite) {
|
if (!_endOfLastWrite) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -149,31 +201,8 @@ int AudioRingBuffer::samplesAvailable() const {
|
||||||
return sampleDifference;
|
return sampleDifference;
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioRingBuffer::addSilentSamples(int silentSamples) {
|
template <class T>
|
||||||
// NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there
|
typename AudioRingBufferTemplate<T>::Sample* AudioRingBufferTemplate<T>::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const {
|
||||||
int numWriteSamples = std::min(silentSamples, _sampleCapacity);
|
|
||||||
int samplesRoomFor = _sampleCapacity - samplesAvailable();
|
|
||||||
|
|
||||||
if (numWriteSamples > samplesRoomFor) {
|
|
||||||
numWriteSamples = samplesRoomFor;
|
|
||||||
|
|
||||||
qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) {
|
|
||||||
int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite;
|
|
||||||
memset(_endOfLastWrite, 0, numSamplesToEnd * sizeof(int16_t));
|
|
||||||
memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * sizeof(int16_t));
|
|
||||||
} else {
|
|
||||||
memset(_endOfLastWrite, 0, numWriteSamples * sizeof(int16_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
_endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples);
|
|
||||||
|
|
||||||
return numWriteSamples;
|
|
||||||
}
|
|
||||||
|
|
||||||
int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const {
|
|
||||||
// NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur
|
// NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur
|
||||||
if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) {
|
if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) {
|
||||||
// this shift will wrap the position around to the beginning of the ring
|
// this shift will wrap the position around to the beginning of the ring
|
||||||
|
@ -186,11 +215,37 @@ int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const {
|
template <class T>
|
||||||
|
int AudioRingBufferTemplate<T>::addSilentSamples(int silentSamples) {
|
||||||
|
// NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there
|
||||||
|
int numWriteSamples = std::min(silentSamples, _sampleCapacity);
|
||||||
|
int samplesRoomFor = _sampleCapacity - samplesAvailable();
|
||||||
|
|
||||||
|
if (numWriteSamples > samplesRoomFor) {
|
||||||
|
numWriteSamples = samplesRoomFor;
|
||||||
|
|
||||||
|
qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) {
|
||||||
|
int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite;
|
||||||
|
memset(_endOfLastWrite, 0, numSamplesToEnd * SampleSize);
|
||||||
|
memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * SampleSize);
|
||||||
|
} else {
|
||||||
|
memset(_endOfLastWrite, 0, numWriteSamples * SampleSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
_endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples);
|
||||||
|
|
||||||
|
return numWriteSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
float AudioRingBufferTemplate<T>::getFrameLoudness(const Sample* frameStart) const {
|
||||||
// FIXME: This is a bad measure of loudness - normal estimation uses sqrt(sum(x*x))
|
// FIXME: This is a bad measure of loudness - normal estimation uses sqrt(sum(x*x))
|
||||||
float loudness = 0.0f;
|
float loudness = 0.0f;
|
||||||
const int16_t* sampleAt = frameStart;
|
const Sample* sampleAt = frameStart;
|
||||||
const int16_t* bufferLastAt = _buffer + _bufferLength - 1;
|
const Sample* bufferLastAt = _buffer + _bufferLength - 1;
|
||||||
|
|
||||||
for (int i = 0; i < _numFrameSamples; ++i) {
|
for (int i = 0; i < _numFrameSamples; ++i) {
|
||||||
loudness += (float) std::abs(*sampleAt);
|
loudness += (float) std::abs(*sampleAt);
|
||||||
|
@ -203,14 +258,16 @@ float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const {
|
||||||
return loudness;
|
return loudness;
|
||||||
}
|
}
|
||||||
|
|
||||||
float AudioRingBuffer::getFrameLoudness(ConstIterator frameStart) const {
|
template <class T>
|
||||||
|
float AudioRingBufferTemplate<T>::getFrameLoudness(ConstIterator frameStart) const {
|
||||||
if (frameStart.isNull()) {
|
if (frameStart.isNull()) {
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
return getFrameLoudness(&(*frameStart));
|
return getFrameLoudness(&(*frameStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) {
|
template <class T>
|
||||||
|
int AudioRingBufferTemplate<T>::writeSamples(ConstIterator source, int maxSamples) {
|
||||||
int samplesToCopy = std::min(maxSamples, _sampleCapacity);
|
int samplesToCopy = std::min(maxSamples, _sampleCapacity);
|
||||||
int samplesRoomFor = _sampleCapacity - samplesAvailable();
|
int samplesRoomFor = _sampleCapacity - samplesAvailable();
|
||||||
if (samplesToCopy > samplesRoomFor) {
|
if (samplesToCopy > samplesRoomFor) {
|
||||||
|
@ -221,7 +278,7 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) {
|
||||||
qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG);
|
qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG);
|
||||||
}
|
}
|
||||||
|
|
||||||
int16_t* bufferLast = _buffer + _bufferLength - 1;
|
Sample* bufferLast = _buffer + _bufferLength - 1;
|
||||||
for (int i = 0; i < samplesToCopy; i++) {
|
for (int i = 0; i < samplesToCopy; i++) {
|
||||||
*_endOfLastWrite = *source;
|
*_endOfLastWrite = *source;
|
||||||
_endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1;
|
_endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1;
|
||||||
|
@ -231,7 +288,8 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) {
|
||||||
return samplesToCopy;
|
return samplesToCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) {
|
template <class T>
|
||||||
|
int AudioRingBufferTemplate<T>::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) {
|
||||||
int samplesToCopy = std::min(maxSamples, _sampleCapacity);
|
int samplesToCopy = std::min(maxSamples, _sampleCapacity);
|
||||||
int samplesRoomFor = _sampleCapacity - samplesAvailable();
|
int samplesRoomFor = _sampleCapacity - samplesAvailable();
|
||||||
if (samplesToCopy > samplesRoomFor) {
|
if (samplesToCopy > samplesRoomFor) {
|
||||||
|
@ -242,12 +300,16 @@ int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples,
|
||||||
qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG);
|
qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG);
|
||||||
}
|
}
|
||||||
|
|
||||||
int16_t* bufferLast = _buffer + _bufferLength - 1;
|
Sample* bufferLast = _buffer + _bufferLength - 1;
|
||||||
for (int i = 0; i < samplesToCopy; i++) {
|
for (int i = 0; i < samplesToCopy; i++) {
|
||||||
*_endOfLastWrite = (int16_t)((float)(*source) * fade);
|
*_endOfLastWrite = (Sample)((float)(*source) * fade);
|
||||||
_endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1;
|
_endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1;
|
||||||
++source;
|
++source;
|
||||||
}
|
}
|
||||||
|
|
||||||
return samplesToCopy;
|
return samplesToCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// explicit instantiations for scratch/mix buffers
|
||||||
|
template class AudioRingBufferTemplate<int16_t>;
|
||||||
|
template class AudioRingBufferTemplate<float>;
|
||||||
|
|
|
@ -21,15 +21,19 @@
|
||||||
|
|
||||||
const int DEFAULT_RING_BUFFER_FRAME_CAPACITY = 10;
|
const int DEFAULT_RING_BUFFER_FRAME_CAPACITY = 10;
|
||||||
|
|
||||||
class AudioRingBuffer {
|
template <class T>
|
||||||
|
class AudioRingBufferTemplate {
|
||||||
|
using Sample = T;
|
||||||
|
static const int SampleSize = sizeof(Sample);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AudioRingBuffer(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY);
|
AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY);
|
||||||
~AudioRingBuffer();
|
~AudioRingBufferTemplate();
|
||||||
|
|
||||||
// disallow copying
|
// disallow copying
|
||||||
AudioRingBuffer(const AudioRingBuffer&) = delete;
|
AudioRingBufferTemplate(const AudioRingBufferTemplate&) = delete;
|
||||||
AudioRingBuffer(AudioRingBuffer&&) = delete;
|
AudioRingBufferTemplate(AudioRingBufferTemplate&&) = delete;
|
||||||
AudioRingBuffer& operator=(const AudioRingBuffer&) = delete;
|
AudioRingBufferTemplate& operator=(const AudioRingBufferTemplate&) = delete;
|
||||||
|
|
||||||
/// Invalidate any data in the buffer
|
/// Invalidate any data in the buffer
|
||||||
void clear();
|
void clear();
|
||||||
|
@ -41,13 +45,27 @@ public:
|
||||||
// FIXME: discards any data in the buffer
|
// FIXME: discards any data in the buffer
|
||||||
void resizeForFrameSize(int numFrameSamples);
|
void resizeForFrameSize(int numFrameSamples);
|
||||||
|
|
||||||
|
// Reading and writing to the buffer uses minimal shared data, such that
|
||||||
|
// in cases that avoid overwriting the buffer, a single producer/consumer
|
||||||
|
// may use this as a lock-free pipe (see audio-client/src/AudioClient.cpp).
|
||||||
|
// IMPORTANT: Avoid changes to the implementation that touch shared data unless you can
|
||||||
|
// maintain this behavior.
|
||||||
|
|
||||||
/// Read up to maxSamples into destination (will only read up to samplesAvailable())
|
/// Read up to maxSamples into destination (will only read up to samplesAvailable())
|
||||||
/// Returns number of read samples
|
/// Returns number of read samples
|
||||||
int readSamples(int16_t* destination, int maxSamples);
|
int readSamples(Sample* destination, int maxSamples);
|
||||||
|
|
||||||
|
/// Append up to maxSamples into destination (will only read up to samplesAvailable())
|
||||||
|
/// If append == false, behaves as readSamples
|
||||||
|
/// Returns number of appended samples
|
||||||
|
int appendSamples(Sample* destination, int maxSamples, bool append = true);
|
||||||
|
|
||||||
|
/// Skip up to maxSamples (will only skip up to samplesAvailable())
|
||||||
|
void skipSamples(int maxSamples) { shiftReadPosition(std::min(maxSamples, samplesAvailable())); }
|
||||||
|
|
||||||
/// Write up to maxSamples from source (will only write up to sample capacity)
|
/// Write up to maxSamples from source (will only write up to sample capacity)
|
||||||
/// Returns number of written samples
|
/// Returns number of written samples
|
||||||
int writeSamples(const int16_t* source, int maxSamples);
|
int writeSamples(const Sample* source, int maxSamples);
|
||||||
|
|
||||||
/// Write up to maxSamples silent samples (will only write until other data exists in the buffer)
|
/// Write up to maxSamples silent samples (will only write until other data exists in the buffer)
|
||||||
/// This method will not overwrite existing data in the buffer, instead dropping silent samples that would overflow
|
/// This method will not overwrite existing data in the buffer, instead dropping silent samples that would overflow
|
||||||
|
@ -58,13 +76,17 @@ public:
|
||||||
/// Returns number of read bytes
|
/// Returns number of read bytes
|
||||||
int readData(char* destination, int maxSize);
|
int readData(char* destination, int maxSize);
|
||||||
|
|
||||||
|
/// Append up to maxSize into destination
|
||||||
|
/// Returns number of read bytes
|
||||||
|
int appendData(char* destination, int maxSize);
|
||||||
|
|
||||||
/// Write up to maxSize from source
|
/// Write up to maxSize from source
|
||||||
/// Returns number of written bytes
|
/// Returns number of written bytes
|
||||||
int writeData(const char* source, int maxSize);
|
int writeData(const char* source, int maxSize);
|
||||||
|
|
||||||
/// Returns a reference to the index-th sample offset from the current read sample
|
/// Returns a reference to the index-th sample offset from the current read sample
|
||||||
int16_t& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); }
|
Sample& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); }
|
||||||
const int16_t& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); }
|
const Sample& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); }
|
||||||
|
|
||||||
/// Essentially discards the next numSamples from the ring buffer
|
/// Essentially discards the next numSamples from the ring buffer
|
||||||
/// NOTE: This is not checked - it is possible to shift past written data
|
/// NOTE: This is not checked - it is possible to shift past written data
|
||||||
|
@ -84,41 +106,104 @@ public:
|
||||||
|
|
||||||
class ConstIterator {
|
class ConstIterator {
|
||||||
public:
|
public:
|
||||||
ConstIterator();
|
ConstIterator() :
|
||||||
ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at);
|
_bufferLength(0),
|
||||||
|
_bufferFirst(NULL),
|
||||||
|
_bufferLast(NULL),
|
||||||
|
_at(NULL) {}
|
||||||
|
ConstIterator(Sample* bufferFirst, int capacity, Sample* at) :
|
||||||
|
_bufferLength(capacity),
|
||||||
|
_bufferFirst(bufferFirst),
|
||||||
|
_bufferLast(bufferFirst + capacity - 1),
|
||||||
|
_at(at) {}
|
||||||
ConstIterator(const ConstIterator& rhs) = default;
|
ConstIterator(const ConstIterator& rhs) = default;
|
||||||
|
|
||||||
bool isNull() const { return _at == NULL; }
|
bool isNull() const { return _at == NULL; }
|
||||||
|
|
||||||
bool operator==(const ConstIterator& rhs) { return _at == rhs._at; }
|
bool operator==(const ConstIterator& rhs) { return _at == rhs._at; }
|
||||||
bool operator!=(const ConstIterator& rhs) { return _at != rhs._at; }
|
bool operator!=(const ConstIterator& rhs) { return _at != rhs._at; }
|
||||||
const int16_t& operator*() { return *_at; }
|
const Sample& operator*() { return *_at; }
|
||||||
|
|
||||||
ConstIterator& operator=(const ConstIterator& rhs);
|
ConstIterator& operator=(const ConstIterator& rhs) {
|
||||||
ConstIterator& operator++();
|
_bufferLength = rhs._bufferLength;
|
||||||
ConstIterator operator++(int);
|
_bufferFirst = rhs._bufferFirst;
|
||||||
ConstIterator& operator--();
|
_bufferLast = rhs._bufferLast;
|
||||||
ConstIterator operator--(int);
|
_at = rhs._at;
|
||||||
const int16_t& operator[] (int i);
|
return *this;
|
||||||
ConstIterator operator+(int i);
|
}
|
||||||
ConstIterator operator-(int i);
|
ConstIterator& operator++() {
|
||||||
|
_at = (_at == _bufferLast) ? _bufferFirst : _at + 1;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
ConstIterator operator++(int) {
|
||||||
|
ConstIterator tmp(*this);
|
||||||
|
++(*this);
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
ConstIterator& operator--() {
|
||||||
|
_at = (_at == _bufferFirst) ? _bufferLast : _at - 1;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
ConstIterator operator--(int) {
|
||||||
|
ConstIterator tmp(*this);
|
||||||
|
--(*this);
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
const Sample& operator[] (int i) {
|
||||||
|
return *atShiftedBy(i);
|
||||||
|
}
|
||||||
|
ConstIterator operator+(int i) {
|
||||||
|
return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i));
|
||||||
|
}
|
||||||
|
ConstIterator operator-(int i) {
|
||||||
|
return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i));
|
||||||
|
}
|
||||||
|
|
||||||
|
void readSamples(Sample* dest, int numSamples) {
|
||||||
|
auto samplesToEnd = _bufferLast - _at + 1;
|
||||||
|
|
||||||
|
if (samplesToEnd >= numSamples) {
|
||||||
|
memcpy(dest, _at, numSamples * SampleSize);
|
||||||
|
_at += numSamples;
|
||||||
|
} else {
|
||||||
|
auto samplesFromStart = numSamples - samplesToEnd;
|
||||||
|
memcpy(dest, _at, samplesToEnd * SampleSize);
|
||||||
|
memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * SampleSize);
|
||||||
|
|
||||||
|
_at = _bufferFirst + samplesFromStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void readSamplesWithFade(Sample* dest, int numSamples, float fade) {
|
||||||
|
Sample* at = _at;
|
||||||
|
for (int i = 0; i < numSamples; i++) {
|
||||||
|
*dest = (float)*at * fade;
|
||||||
|
++dest;
|
||||||
|
at = (at == _bufferLast) ? _bufferFirst : at + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void readSamples(int16_t* dest, int numSamples);
|
|
||||||
void readSamplesWithFade(int16_t* dest, int numSamples, float fade);
|
|
||||||
void readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels);
|
|
||||||
void readSamplesWithDownmix(int16_t* dest, int numSamples);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int16_t* atShiftedBy(int i);
|
Sample* atShiftedBy(int i) {
|
||||||
|
i = (_at - _bufferFirst + i) % _bufferLength;
|
||||||
|
if (i < 0) {
|
||||||
|
i += _bufferLength;
|
||||||
|
}
|
||||||
|
return _bufferFirst + i;
|
||||||
|
}
|
||||||
|
|
||||||
int _bufferLength;
|
int _bufferLength;
|
||||||
int16_t* _bufferFirst;
|
Sample* _bufferFirst;
|
||||||
int16_t* _bufferLast;
|
Sample* _bufferLast;
|
||||||
int16_t* _at;
|
Sample* _at;
|
||||||
};
|
};
|
||||||
|
|
||||||
ConstIterator nextOutput() const;
|
ConstIterator nextOutput() const {
|
||||||
ConstIterator lastFrameWritten() const;
|
return ConstIterator(_buffer, _bufferLength, _nextOutput);
|
||||||
|
}
|
||||||
|
ConstIterator lastFrameWritten() const {
|
||||||
|
return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples;
|
||||||
|
}
|
||||||
|
|
||||||
int writeSamples(ConstIterator source, int maxSamples);
|
int writeSamples(ConstIterator source, int maxSamples);
|
||||||
int writeSamplesWithFade(ConstIterator source, int maxSamples, float fade);
|
int writeSamplesWithFade(ConstIterator source, int maxSamples, float fade);
|
||||||
|
@ -126,8 +211,8 @@ public:
|
||||||
float getFrameLoudness(ConstIterator frameStart) const;
|
float getFrameLoudness(ConstIterator frameStart) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int16_t* shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const;
|
Sample* shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const;
|
||||||
float getFrameLoudness(const int16_t* frameStart) const;
|
float getFrameLoudness(const Sample* frameStart) const;
|
||||||
|
|
||||||
int _numFrameSamples;
|
int _numFrameSamples;
|
||||||
int _frameCapacity;
|
int _frameCapacity;
|
||||||
|
@ -135,138 +220,13 @@ protected:
|
||||||
int _bufferLength; // actual _buffer length (_sampleCapacity + 1)
|
int _bufferLength; // actual _buffer length (_sampleCapacity + 1)
|
||||||
int _overflowCount{ 0 }; // times the ring buffer has overwritten data
|
int _overflowCount{ 0 }; // times the ring buffer has overwritten data
|
||||||
|
|
||||||
int16_t* _nextOutput{ nullptr };
|
Sample* _nextOutput{ nullptr };
|
||||||
int16_t* _endOfLastWrite{ nullptr };
|
Sample* _endOfLastWrite{ nullptr };
|
||||||
int16_t* _buffer{ nullptr };
|
Sample* _buffer{ nullptr };
|
||||||
};
|
};
|
||||||
|
|
||||||
// inline the iterator:
|
// expose explicit instantiations for scratch/mix buffers
|
||||||
inline AudioRingBuffer::ConstIterator::ConstIterator() :
|
using AudioRingBuffer = AudioRingBufferTemplate<int16_t>;
|
||||||
_bufferLength(0),
|
using AudioMixRingBuffer = AudioRingBufferTemplate<float>;
|
||||||
_bufferFirst(NULL),
|
|
||||||
_bufferLast(NULL),
|
|
||||||
_at(NULL) {}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator::ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at) :
|
|
||||||
_bufferLength(capacity),
|
|
||||||
_bufferFirst(bufferFirst),
|
|
||||||
_bufferLast(bufferFirst + capacity - 1),
|
|
||||||
_at(at) {}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator=(const ConstIterator& rhs) {
|
|
||||||
_bufferLength = rhs._bufferLength;
|
|
||||||
_bufferFirst = rhs._bufferFirst;
|
|
||||||
_bufferLast = rhs._bufferLast;
|
|
||||||
_at = rhs._at;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator++() {
|
|
||||||
_at = (_at == _bufferLast) ? _bufferFirst : _at + 1;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator++(int) {
|
|
||||||
ConstIterator tmp(*this);
|
|
||||||
++(*this);
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator--() {
|
|
||||||
_at = (_at == _bufferFirst) ? _bufferLast : _at - 1;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator--(int) {
|
|
||||||
ConstIterator tmp(*this);
|
|
||||||
--(*this);
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const int16_t& AudioRingBuffer::ConstIterator::operator[] (int i) {
|
|
||||||
return *atShiftedBy(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator+(int i) {
|
|
||||||
return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator-(int i) {
|
|
||||||
return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int16_t* AudioRingBuffer::ConstIterator::atShiftedBy(int i) {
|
|
||||||
i = (_at - _bufferFirst + i) % _bufferLength;
|
|
||||||
if (i < 0) {
|
|
||||||
i += _bufferLength;
|
|
||||||
}
|
|
||||||
return _bufferFirst + i;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AudioRingBuffer::ConstIterator::readSamples(int16_t* dest, int numSamples) {
|
|
||||||
auto samplesToEnd = _bufferLast - _at + 1;
|
|
||||||
|
|
||||||
if (samplesToEnd >= numSamples) {
|
|
||||||
memcpy(dest, _at, numSamples * sizeof(int16_t));
|
|
||||||
_at += numSamples;
|
|
||||||
} else {
|
|
||||||
auto samplesFromStart = numSamples - samplesToEnd;
|
|
||||||
memcpy(dest, _at, samplesToEnd * sizeof(int16_t));
|
|
||||||
memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * sizeof(int16_t));
|
|
||||||
|
|
||||||
_at = _bufferFirst + samplesFromStart;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, int numSamples, float fade) {
|
|
||||||
int16_t* at = _at;
|
|
||||||
for (int i = 0; i < numSamples; i++) {
|
|
||||||
*dest = (float)*at * fade;
|
|
||||||
++dest;
|
|
||||||
at = (at == _bufferLast) ? _bufferFirst : at + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels) {
|
|
||||||
int16_t* at = _at;
|
|
||||||
for (int i = 0; i < numSamples/2; i++) {
|
|
||||||
|
|
||||||
// read 2 samples
|
|
||||||
int16_t left = *at;
|
|
||||||
at = (at == _bufferLast) ? _bufferFirst : at + 1;
|
|
||||||
int16_t right = *at;
|
|
||||||
at = (at == _bufferLast) ? _bufferFirst : at + 1;
|
|
||||||
|
|
||||||
// write 2 + N samples
|
|
||||||
*dest++ = left;
|
|
||||||
*dest++ = right;
|
|
||||||
for (int n = 0; n < numExtraChannels; n++) {
|
|
||||||
*dest++ = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AudioRingBuffer::ConstIterator::readSamplesWithDownmix(int16_t* dest, int numSamples) {
|
|
||||||
int16_t* at = _at;
|
|
||||||
for (int i = 0; i < numSamples/2; i++) {
|
|
||||||
|
|
||||||
// read 2 samples
|
|
||||||
int16_t left = *at;
|
|
||||||
at = (at == _bufferLast) ? _bufferFirst : at + 1;
|
|
||||||
int16_t right = *at;
|
|
||||||
at = (at == _bufferLast) ? _bufferFirst : at + 1;
|
|
||||||
|
|
||||||
// write 1 sample
|
|
||||||
*dest++ = (int16_t)((left + right) / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator AudioRingBuffer::nextOutput() const {
|
|
||||||
return ConstIterator(_buffer, _bufferLength, _nextOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline AudioRingBuffer::ConstIterator AudioRingBuffer::lastFrameWritten() const {
|
|
||||||
return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // hifi_AudioRingBuffer_h
|
#endif // hifi_AudioRingBuffer_h
|
||||||
|
|
|
@ -1336,24 +1336,27 @@ const QUrl& AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) {
|
||||||
return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL;
|
return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AvatarData::processAvatarIdentity(const Identity& identity) {
|
void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged) {
|
||||||
bool hasIdentityChanged = false;
|
|
||||||
|
|
||||||
if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) {
|
if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) {
|
||||||
setSkeletonModelURL(identity.skeletonModelURL);
|
setSkeletonModelURL(identity.skeletonModelURL);
|
||||||
hasIdentityChanged = true;
|
identityChanged = true;
|
||||||
|
if (_firstSkeletonCheck) {
|
||||||
|
displayNameChanged = true;
|
||||||
|
}
|
||||||
_firstSkeletonCheck = false;
|
_firstSkeletonCheck = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (identity.displayName != _displayName) {
|
if (identity.displayName != _displayName) {
|
||||||
_displayName = identity.displayName;
|
_displayName = identity.displayName;
|
||||||
hasIdentityChanged = true;
|
identityChanged = true;
|
||||||
|
displayNameChanged = true;
|
||||||
}
|
}
|
||||||
maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName);
|
maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName);
|
||||||
|
|
||||||
if (identity.attachmentData != _attachmentData) {
|
if (identity.attachmentData != _attachmentData) {
|
||||||
setAttachmentData(identity.attachmentData);
|
setAttachmentData(identity.attachmentData);
|
||||||
hasIdentityChanged = true;
|
identityChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool avatarEntityDataChanged = false;
|
bool avatarEntityDataChanged = false;
|
||||||
|
@ -1362,10 +1365,8 @@ bool AvatarData::processAvatarIdentity(const Identity& identity) {
|
||||||
});
|
});
|
||||||
if (avatarEntityDataChanged) {
|
if (avatarEntityDataChanged) {
|
||||||
setAvatarEntityData(identity.avatarEntityData);
|
setAvatarEntityData(identity.avatarEntityData);
|
||||||
hasIdentityChanged = true;
|
identityChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasIdentityChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray AvatarData::identityByteArray() {
|
QByteArray AvatarData::identityByteArray() {
|
||||||
|
|
|
@ -471,8 +471,9 @@ public:
|
||||||
|
|
||||||
static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut);
|
static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut);
|
||||||
|
|
||||||
// returns true if identity has changed, false otherwise.
|
// identityChanged returns true if identity has changed, false otherwise.
|
||||||
bool processAvatarIdentity(const Identity& identity);
|
// displayNameChanged returns true if displayName has changed, false otherwise.
|
||||||
|
void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged);
|
||||||
|
|
||||||
QByteArray identityByteArray();
|
QByteArray identityByteArray();
|
||||||
|
|
||||||
|
|
|
@ -148,7 +148,9 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
|
||||||
if (!nodeList->isIgnoringNode(identity.uuid) || nodeList->getRequestsDomainListData()) {
|
if (!nodeList->isIgnoringNode(identity.uuid) || nodeList->getRequestsDomainListData()) {
|
||||||
// mesh URL for a UUID, find avatar in our list
|
// mesh URL for a UUID, find avatar in our list
|
||||||
auto avatar = newOrExistingAvatar(identity.uuid, sendingNode);
|
auto avatar = newOrExistingAvatar(identity.uuid, sendingNode);
|
||||||
avatar->processAvatarIdentity(identity);
|
bool identityChanged = false;
|
||||||
|
bool displayNameChanged = false;
|
||||||
|
avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -540,7 +540,7 @@ void EntityTreeRenderer::processEraseMessage(ReceivedMessage& message, const Sha
|
||||||
std::static_pointer_cast<EntityTree>(_tree)->processEraseMessage(message, sourceNode);
|
std::static_pointer_cast<EntityTree>(_tree)->processEraseMessage(message, sourceNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loadingPriority) {
|
ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loadingPriority, SpatiallyNestable* spatiallyNestableOverride) {
|
||||||
ModelPointer model = nullptr;
|
ModelPointer model = nullptr;
|
||||||
|
|
||||||
// Only create and delete models on the thread that owns the EntityTreeRenderer
|
// Only create and delete models on the thread that owns the EntityTreeRenderer
|
||||||
|
@ -552,7 +552,7 @@ ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loading
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
model = std::make_shared<Model>(std::make_shared<Rig>());
|
model = std::make_shared<Model>(std::make_shared<Rig>(), nullptr, spatiallyNestableOverride);
|
||||||
model->setLoadingPriority(loadingPriority);
|
model->setLoadingPriority(loadingPriority);
|
||||||
model->init();
|
model->init();
|
||||||
model->setURL(QUrl(url));
|
model->setURL(QUrl(url));
|
||||||
|
@ -956,68 +956,29 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree,
|
void EntityTreeRenderer::playEntityCollisionSound(EntityItemPointer entity, const Collision& collision) {
|
||||||
const EntityItemID& id, const Collision& collision) {
|
assert((bool)entity);
|
||||||
EntityItemPointer entity = entityTree->findEntityByEntityItemID(id);
|
SharedSoundPointer collisionSound = entity->getCollisionSound();
|
||||||
if (!entity) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
QUuid simulatorID = entity->getSimulatorID();
|
|
||||||
if (simulatorID.isNull()) {
|
|
||||||
// Can be null if it has never moved since being created or coming out of persistence.
|
|
||||||
// However, for there to be a collission, one of the two objects must be moving.
|
|
||||||
const EntityItemID& otherID = (id == collision.idA) ? collision.idB : collision.idA;
|
|
||||||
EntityItemPointer otherEntity = entityTree->findEntityByEntityItemID(otherID);
|
|
||||||
if (!otherEntity) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
simulatorID = otherEntity->getSimulatorID();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (simulatorID.isNull() || (simulatorID != myNodeID)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree,
|
|
||||||
const EntityItemID& id, const Collision& collision) {
|
|
||||||
|
|
||||||
if (!isCollisionOwner(myNodeID, entityTree, id, collision)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedSoundPointer collisionSound;
|
|
||||||
float mass = 1.0; // value doesn't get used, but set it so compiler is quiet
|
|
||||||
AACube minAACube;
|
|
||||||
bool success = false;
|
|
||||||
_tree->withReadLock([&] {
|
|
||||||
EntityItemPointer entity = entityTree->findEntityByEntityItemID(id);
|
|
||||||
if (entity) {
|
|
||||||
collisionSound = entity->getCollisionSound();
|
|
||||||
mass = entity->computeMass();
|
|
||||||
minAACube = entity->getMinimumAACube(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!collisionSound) {
|
if (!collisionSound) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
bool success = false;
|
||||||
|
AACube minAACube = entity->getMinimumAACube(success);
|
||||||
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float mass = entity->computeMass();
|
||||||
|
|
||||||
const float COLLISION_PENETRATION_TO_VELOCITY = 50; // as a subsitute for RELATIVE entity->getVelocity()
|
const float COLLISION_PENETRATION_TO_VELOCITY = 50.0f; // as a subsitute for RELATIVE entity->getVelocity()
|
||||||
// The collision.penetration is a pretty good indicator of changed velocity AFTER the initial contact,
|
// The collision.penetration is a pretty good indicator of changed velocity AFTER the initial contact,
|
||||||
// but that first contact depends on exactly where we hit in the physics step.
|
// but that first contact depends on exactly where we hit in the physics step.
|
||||||
// We can get a more consistent initial-contact energy reading by using the changed velocity.
|
// We can get a more consistent initial-contact energy reading by using the changed velocity.
|
||||||
// Note that velocityChange is not a good indicator for continuing collisions, because it does not distinguish
|
// Note that velocityChange is not a good indicator for continuing collisions, because it does not distinguish
|
||||||
// between bounce and sliding along a surface.
|
// between bounce and sliding along a surface.
|
||||||
const float linearVelocity = (collision.type == CONTACT_EVENT_TYPE_START) ?
|
const float speedSquared = (collision.type == CONTACT_EVENT_TYPE_START) ?
|
||||||
glm::length(collision.velocityChange) :
|
glm::length2(collision.velocityChange) :
|
||||||
glm::length(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY;
|
glm::length2(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY;
|
||||||
const float energy = mass * linearVelocity * linearVelocity / 2.0f;
|
const float energy = mass * speedSquared / 2.0f;
|
||||||
const glm::vec3 position = collision.contactPoint;
|
|
||||||
const float COLLISION_ENERGY_AT_FULL_VOLUME = (collision.type == CONTACT_EVENT_TYPE_START) ? 150.0f : 5.0f;
|
const float COLLISION_ENERGY_AT_FULL_VOLUME = (collision.type == CONTACT_EVENT_TYPE_START) ? 150.0f : 5.0f;
|
||||||
const float COLLISION_MINIMUM_VOLUME = 0.005f;
|
const float COLLISION_MINIMUM_VOLUME = 0.005f;
|
||||||
const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME);
|
const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME);
|
||||||
|
@ -1031,7 +992,7 @@ void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityT
|
||||||
// Shift the pitch down by ln(1 + (size / COLLISION_SIZE_FOR_STANDARD_PITCH)) / ln(2)
|
// Shift the pitch down by ln(1 + (size / COLLISION_SIZE_FOR_STANDARD_PITCH)) / ln(2)
|
||||||
const float COLLISION_SIZE_FOR_STANDARD_PITCH = 0.2f;
|
const float COLLISION_SIZE_FOR_STANDARD_PITCH = 0.2f;
|
||||||
const float stretchFactor = log(1.0f + (minAACube.getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / log(2);
|
const float stretchFactor = log(1.0f + (minAACube.getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / log(2);
|
||||||
AudioInjector::playSound(collisionSound, volume, stretchFactor, position);
|
AudioInjector::playSound(collisionSound, volume, stretchFactor, collision.contactPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB,
|
void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB,
|
||||||
|
@ -1041,30 +1002,28 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons
|
||||||
if (!_tree || _shuttingDown) {
|
if (!_tree || _shuttingDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Don't respond to small continuous contacts.
|
|
||||||
const float COLLISION_MINUMUM_PENETRATION = 0.002f;
|
|
||||||
if ((collision.type == CONTACT_EVENT_TYPE_CONTINUE) && (glm::length(collision.penetration) < COLLISION_MINUMUM_PENETRATION)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// See if we should play sounds
|
|
||||||
EntityTreePointer entityTree = std::static_pointer_cast<EntityTree>(_tree);
|
EntityTreePointer entityTree = std::static_pointer_cast<EntityTree>(_tree);
|
||||||
const QUuid& myNodeID = DependencyManager::get<NodeList>()->getSessionUUID();
|
const QUuid& myNodeID = DependencyManager::get<NodeList>()->getSessionUUID();
|
||||||
playEntityCollisionSound(myNodeID, entityTree, idA, collision);
|
|
||||||
playEntityCollisionSound(myNodeID, entityTree, idB, collision);
|
|
||||||
|
|
||||||
// And now the entity scripts
|
// trigger scripted collision sounds and events for locally owned objects
|
||||||
if (isCollisionOwner(myNodeID, entityTree, idA, collision)) {
|
EntityItemPointer entityA = entityTree->findEntityByEntityItemID(idA);
|
||||||
|
if ((bool)entityA && myNodeID == entityA->getSimulatorID()) {
|
||||||
|
playEntityCollisionSound(entityA, collision);
|
||||||
emit collisionWithEntity(idA, idB, collision);
|
emit collisionWithEntity(idA, idB, collision);
|
||||||
if (_entitiesScriptEngine) {
|
if (_entitiesScriptEngine) {
|
||||||
_entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision);
|
_entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EntityItemPointer entityB = entityTree->findEntityByEntityItemID(idB);
|
||||||
if (isCollisionOwner(myNodeID, entityTree, idB, collision)) {
|
if ((bool)entityB && myNodeID == entityB->getSimulatorID()) {
|
||||||
emit collisionWithEntity(idB, idA, collision);
|
playEntityCollisionSound(entityB, collision);
|
||||||
|
// since we're swapping A and B we need to send the inverted collision
|
||||||
|
Collision invertedCollision(collision);
|
||||||
|
invertedCollision.invert();
|
||||||
|
emit collisionWithEntity(idB, idA, invertedCollision);
|
||||||
if (_entitiesScriptEngine) {
|
if (_entitiesScriptEngine) {
|
||||||
_entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision);
|
_entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ public:
|
||||||
void reloadEntityScripts();
|
void reloadEntityScripts();
|
||||||
|
|
||||||
/// if a renderable entity item needs a model, we will allocate it for them
|
/// if a renderable entity item needs a model, we will allocate it for them
|
||||||
Q_INVOKABLE ModelPointer allocateModel(const QString& url, float loadingPriority = 0.0f);
|
Q_INVOKABLE ModelPointer allocateModel(const QString& url, float loadingPriority = 0.0f, SpatiallyNestable* spatiallyNestableOverride = nullptr);
|
||||||
|
|
||||||
/// if a renderable entity item needs to update the URL of a model, we will handle that for the entity
|
/// if a renderable entity item needs to update the URL of a model, we will handle that for the entity
|
||||||
Q_INVOKABLE ModelPointer updateModel(ModelPointer original, const QString& newUrl);
|
Q_INVOKABLE ModelPointer updateModel(ModelPointer original, const QString& newUrl);
|
||||||
|
@ -170,11 +170,7 @@ private:
|
||||||
bool _wantScripts;
|
bool _wantScripts;
|
||||||
QSharedPointer<ScriptEngine> _entitiesScriptEngine;
|
QSharedPointer<ScriptEngine> _entitiesScriptEngine;
|
||||||
|
|
||||||
bool isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree,
|
static void playEntityCollisionSound(EntityItemPointer entity, const Collision& collision);
|
||||||
const EntityItemID& id, const Collision& collision);
|
|
||||||
|
|
||||||
void playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree,
|
|
||||||
const EntityItemID& id, const Collision& collision);
|
|
||||||
|
|
||||||
bool _lastPointerEventValid;
|
bool _lastPointerEventValid;
|
||||||
PointerEvent _lastPointerEvent;
|
PointerEvent _lastPointerEvent;
|
||||||
|
|
|
@ -320,12 +320,9 @@ bool RenderableModelEntityItem::getAnimationFrame() {
|
||||||
glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform *
|
glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform *
|
||||||
rotationMat * fbxJoints[index].postTransform);
|
rotationMat * fbxJoints[index].postTransform);
|
||||||
_localJointTranslations[j] = extractTranslation(finalMat);
|
_localJointTranslations[j] = extractTranslation(finalMat);
|
||||||
_localJointTranslationsSet[j] = true;
|
|
||||||
_localJointTranslationsDirty[j] = true;
|
_localJointTranslationsDirty[j] = true;
|
||||||
|
|
||||||
_localJointRotations[j] = glmExtractRotation(finalMat);
|
_localJointRotations[j] = glmExtractRotation(finalMat);
|
||||||
|
|
||||||
_localJointRotationsSet[j] = true;
|
|
||||||
_localJointRotationsDirty[j] = true;
|
_localJointRotationsDirty[j] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -507,8 +504,7 @@ ModelPointer RenderableModelEntityItem::getModel(QSharedPointer<EntityTreeRender
|
||||||
if (!getModelURL().isEmpty()) {
|
if (!getModelURL().isEmpty()) {
|
||||||
// If we don't have a model, allocate one *immediately*
|
// If we don't have a model, allocate one *immediately*
|
||||||
if (!_model) {
|
if (!_model) {
|
||||||
_model = _myRenderer->allocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this));
|
_model = _myRenderer->allocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this), this);
|
||||||
_model->setSpatiallyNestableOverride(shared_from_this());
|
|
||||||
_needsInitialSimulation = true;
|
_needsInitialSimulation = true;
|
||||||
// If we need to change URLs, update it *after rendering* (to avoid access violations)
|
// If we need to change URLs, update it *after rendering* (to avoid access violations)
|
||||||
} else if (QUrl(getModelURL()) != _model->getURL()) {
|
} else if (QUrl(getModelURL()) != _model->getURL()) {
|
||||||
|
|
|
@ -659,6 +659,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
||||||
// NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data
|
// NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data
|
||||||
// even when we would otherwise ignore the rest of the packet.
|
// even when we would otherwise ignore the rest of the packet.
|
||||||
|
|
||||||
|
bool filterRejection = false;
|
||||||
if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) {
|
if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) {
|
||||||
|
|
||||||
QByteArray simOwnerData;
|
QByteArray simOwnerData;
|
||||||
|
@ -671,6 +672,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
||||||
if (wantTerseEditLogging() && _simulationOwner != newSimOwner) {
|
if (wantTerseEditLogging() && _simulationOwner != newSimOwner) {
|
||||||
qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << newSimOwner;
|
qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << newSimOwner;
|
||||||
}
|
}
|
||||||
|
// This is used in the custom physics setters, below. When an entity-server filter alters
|
||||||
|
// or rejects a set of properties, it clears this. In such cases, we don't want those custom
|
||||||
|
// setters to ignore what the server says.
|
||||||
|
filterRejection = newSimOwner.getID().isNull();
|
||||||
if (weOwnSimulation) {
|
if (weOwnSimulation) {
|
||||||
if (newSimOwner.getID().isNull() && !_simulationOwner.pendingRelease(lastEditedFromBufferAdjusted)) {
|
if (newSimOwner.getID().isNull() && !_simulationOwner.pendingRelease(lastEditedFromBufferAdjusted)) {
|
||||||
// entity-server is trying to clear our ownership (probably at our own request)
|
// entity-server is trying to clear our ownership (probably at our own request)
|
||||||
|
@ -696,6 +701,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
||||||
somethingChanged = true;
|
somethingChanged = true;
|
||||||
_simulationOwner.clearCurrentOwner();
|
_simulationOwner.clearCurrentOwner();
|
||||||
}
|
}
|
||||||
|
} else if (newSimOwner.matchesValidID(myNodeID) && !_hasBidOnSimulation) {
|
||||||
|
// entity-server tells us that we have simulation ownership while we never requested this for this EntityItem,
|
||||||
|
// this could happen when the user reloads the cache and entity tree.
|
||||||
|
_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID;
|
||||||
|
somethingChanged = true;
|
||||||
|
_simulationOwner.clearCurrentOwner();
|
||||||
|
weOwnSimulation = false;
|
||||||
} else if (_simulationOwner.set(newSimOwner)) {
|
} else if (_simulationOwner.set(newSimOwner)) {
|
||||||
_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID;
|
_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID;
|
||||||
somethingChanged = true;
|
somethingChanged = true;
|
||||||
|
@ -716,55 +728,45 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
||||||
// Note: duplicate packets are expected and not wrong. They may be sent for any number of
|
// Note: duplicate packets are expected and not wrong. They may be sent for any number of
|
||||||
// reasons and the contract is that the client handles them in an idempotent manner.
|
// reasons and the contract is that the client handles them in an idempotent manner.
|
||||||
auto lastEdited = lastEditedFromBufferAdjusted;
|
auto lastEdited = lastEditedFromBufferAdjusted;
|
||||||
auto customUpdatePositionFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){
|
bool otherOverwrites = overwriteLocalData && !weOwnSimulation;
|
||||||
bool simulationChanged = lastEdited > _lastUpdatedPositionTimestamp;
|
auto shouldUpdate = [lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) {
|
||||||
bool valueChanged = value != _lastUpdatedPositionValue;
|
bool simulationChanged = lastEdited > updatedTimestamp;
|
||||||
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
|
return otherOverwrites && simulationChanged && (valueChanged || filterRejection);
|
||||||
if (shouldUpdate) {
|
};
|
||||||
|
auto customUpdatePositionFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){
|
||||||
|
if (shouldUpdate(_lastUpdatedPositionTimestamp, value != _lastUpdatedPositionValue)) {
|
||||||
updatePositionFromNetwork(value);
|
updatePositionFromNetwork(value);
|
||||||
_lastUpdatedPositionTimestamp = lastEdited;
|
_lastUpdatedPositionTimestamp = lastEdited;
|
||||||
_lastUpdatedPositionValue = value;
|
_lastUpdatedPositionValue = value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto customUpdateRotationFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::quat value){
|
auto customUpdateRotationFromNetwork = [this, shouldUpdate, lastEdited](glm::quat value){
|
||||||
bool simulationChanged = lastEdited > _lastUpdatedRotationTimestamp;
|
if (shouldUpdate(_lastUpdatedRotationTimestamp, value != _lastUpdatedRotationValue)) {
|
||||||
bool valueChanged = value != _lastUpdatedRotationValue;
|
|
||||||
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
|
|
||||||
if (shouldUpdate) {
|
|
||||||
updateRotationFromNetwork(value);
|
updateRotationFromNetwork(value);
|
||||||
_lastUpdatedRotationTimestamp = lastEdited;
|
_lastUpdatedRotationTimestamp = lastEdited;
|
||||||
_lastUpdatedRotationValue = value;
|
_lastUpdatedRotationValue = value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto customUpdateVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){
|
auto customUpdateVelocityFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){
|
||||||
bool simulationChanged = lastEdited > _lastUpdatedVelocityTimestamp;
|
if (shouldUpdate(_lastUpdatedVelocityTimestamp, value != _lastUpdatedVelocityValue)) {
|
||||||
bool valueChanged = value != _lastUpdatedVelocityValue;
|
|
||||||
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
|
|
||||||
if (shouldUpdate) {
|
|
||||||
updateVelocityFromNetwork(value);
|
updateVelocityFromNetwork(value);
|
||||||
_lastUpdatedVelocityTimestamp = lastEdited;
|
_lastUpdatedVelocityTimestamp = lastEdited;
|
||||||
_lastUpdatedVelocityValue = value;
|
_lastUpdatedVelocityValue = value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto customUpdateAngularVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){
|
auto customUpdateAngularVelocityFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){
|
||||||
bool simulationChanged = lastEdited > _lastUpdatedAngularVelocityTimestamp;
|
if (shouldUpdate(_lastUpdatedAngularVelocityTimestamp, value != _lastUpdatedAngularVelocityValue)) {
|
||||||
bool valueChanged = value != _lastUpdatedAngularVelocityValue;
|
|
||||||
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
|
|
||||||
if (shouldUpdate) {
|
|
||||||
updateAngularVelocityFromNetwork(value);
|
updateAngularVelocityFromNetwork(value);
|
||||||
_lastUpdatedAngularVelocityTimestamp = lastEdited;
|
_lastUpdatedAngularVelocityTimestamp = lastEdited;
|
||||||
_lastUpdatedAngularVelocityValue = value;
|
_lastUpdatedAngularVelocityValue = value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto customSetAcceleration = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){
|
auto customSetAcceleration = [this, shouldUpdate, lastEdited](glm::vec3 value){
|
||||||
bool simulationChanged = lastEdited > _lastUpdatedAccelerationTimestamp;
|
if (shouldUpdate(_lastUpdatedAccelerationTimestamp, value != _lastUpdatedAccelerationValue)) {
|
||||||
bool valueChanged = value != _lastUpdatedAccelerationValue;
|
|
||||||
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
|
|
||||||
if (shouldUpdate) {
|
|
||||||
setAcceleration(value);
|
setAcceleration(value);
|
||||||
_lastUpdatedAccelerationTimestamp = lastEdited;
|
_lastUpdatedAccelerationTimestamp = lastEdited;
|
||||||
_lastUpdatedAccelerationValue = value;
|
_lastUpdatedAccelerationValue = value;
|
||||||
|
@ -1286,7 +1288,7 @@ void EntityItem::grabSimulationOwnership() {
|
||||||
auto nodeList = DependencyManager::get<NodeList>();
|
auto nodeList = DependencyManager::get<NodeList>();
|
||||||
if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) {
|
if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) {
|
||||||
// we already own it
|
// we already own it
|
||||||
_simulationOwner.promotePriority(SCRIPT_POKE_SIMULATION_PRIORITY);
|
_simulationOwner.promotePriority(SCRIPT_GRAB_SIMULATION_PRIORITY);
|
||||||
} else {
|
} else {
|
||||||
// we don't own it yet
|
// we don't own it yet
|
||||||
_simulationOwner.setPendingPriority(SCRIPT_GRAB_SIMULATION_PRIORITY, usecTimestampNow());
|
_simulationOwner.setPendingPriority(SCRIPT_GRAB_SIMULATION_PRIORITY, usecTimestampNow());
|
||||||
|
@ -1897,6 +1899,10 @@ void EntityItem::setPendingOwnershipPriority(quint8 priority, const quint64& tim
|
||||||
_simulationOwner.setPendingPriority(priority, timestamp);
|
_simulationOwner.setPendingPriority(priority, timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EntityItem::rememberHasSimulationOwnershipBid() const {
|
||||||
|
_hasBidOnSimulation = true;
|
||||||
|
}
|
||||||
|
|
||||||
QString EntityItem::actionsToDebugString() {
|
QString EntityItem::actionsToDebugString() {
|
||||||
QString result;
|
QString result;
|
||||||
QVector<QByteArray> serializedActions;
|
QVector<QByteArray> serializedActions;
|
||||||
|
|
|
@ -321,6 +321,7 @@ public:
|
||||||
void updateSimulationOwner(const SimulationOwner& owner);
|
void updateSimulationOwner(const SimulationOwner& owner);
|
||||||
void clearSimulationOwnership();
|
void clearSimulationOwnership();
|
||||||
void setPendingOwnershipPriority(quint8 priority, const quint64& timestamp);
|
void setPendingOwnershipPriority(quint8 priority, const quint64& timestamp);
|
||||||
|
void rememberHasSimulationOwnershipBid() const;
|
||||||
|
|
||||||
const QString& getMarketplaceID() const { return _marketplaceID; }
|
const QString& getMarketplaceID() const { return _marketplaceID; }
|
||||||
void setMarketplaceID(const QString& value) { _marketplaceID = value; }
|
void setMarketplaceID(const QString& value) { _marketplaceID = value; }
|
||||||
|
@ -497,16 +498,16 @@ protected:
|
||||||
mutable AABox _cachedAABox;
|
mutable AABox _cachedAABox;
|
||||||
mutable AACube _maxAACube;
|
mutable AACube _maxAACube;
|
||||||
mutable AACube _minAACube;
|
mutable AACube _minAACube;
|
||||||
mutable bool _recalcAABox = true;
|
mutable bool _recalcAABox { true };
|
||||||
mutable bool _recalcMinAACube = true;
|
mutable bool _recalcMinAACube { true };
|
||||||
mutable bool _recalcMaxAACube = true;
|
mutable bool _recalcMaxAACube { true };
|
||||||
|
|
||||||
float _localRenderAlpha;
|
float _localRenderAlpha;
|
||||||
float _density = ENTITY_ITEM_DEFAULT_DENSITY; // kg/m^3
|
float _density { ENTITY_ITEM_DEFAULT_DENSITY }; // kg/m^3
|
||||||
// NOTE: _volumeMultiplier is used to allow some mass properties code exist in the EntityItem base class
|
// NOTE: _volumeMultiplier is used to allow some mass properties code exist in the EntityItem base class
|
||||||
// rather than in all of the derived classes. If we ever collapse these classes to one we could do it a
|
// rather than in all of the derived classes. If we ever collapse these classes to one we could do it a
|
||||||
// different way.
|
// different way.
|
||||||
float _volumeMultiplier = 1.0f;
|
float _volumeMultiplier { 1.0f };
|
||||||
glm::vec3 _gravity;
|
glm::vec3 _gravity;
|
||||||
glm::vec3 _acceleration;
|
glm::vec3 _acceleration;
|
||||||
float _damping;
|
float _damping;
|
||||||
|
@ -516,7 +517,7 @@ protected:
|
||||||
|
|
||||||
QString _script; /// the value of the script property
|
QString _script; /// the value of the script property
|
||||||
QString _loadedScript; /// the value of _script when the last preload signal was sent
|
QString _loadedScript; /// the value of _script when the last preload signal was sent
|
||||||
quint64 _scriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload
|
quint64 _scriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload
|
||||||
|
|
||||||
QString _serverScripts;
|
QString _serverScripts;
|
||||||
/// keep track of time when _serverScripts property was last changed
|
/// keep track of time when _serverScripts property was last changed
|
||||||
|
@ -524,7 +525,7 @@ protected:
|
||||||
|
|
||||||
/// the value of _scriptTimestamp when the last preload signal was sent
|
/// the value of _scriptTimestamp when the last preload signal was sent
|
||||||
// NOTE: on construction we want this to be different from _scriptTimestamp so we intentionally bump it
|
// NOTE: on construction we want this to be different from _scriptTimestamp so we intentionally bump it
|
||||||
quint64 _loadedScriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 };
|
quint64 _loadedScriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 };
|
||||||
|
|
||||||
QString _collisionSoundURL;
|
QString _collisionSoundURL;
|
||||||
SharedSoundPointer _collisionSound;
|
SharedSoundPointer _collisionSound;
|
||||||
|
@ -562,8 +563,8 @@ protected:
|
||||||
uint32_t _dirtyFlags; // things that have changed from EXTERNAL changes (via script or packet) but NOT from simulation
|
uint32_t _dirtyFlags; // things that have changed from EXTERNAL changes (via script or packet) but NOT from simulation
|
||||||
|
|
||||||
// these backpointers are only ever set/cleared by friends:
|
// these backpointers are only ever set/cleared by friends:
|
||||||
EntityTreeElementPointer _element = nullptr; // set by EntityTreeElement
|
EntityTreeElementPointer _element { nullptr }; // set by EntityTreeElement
|
||||||
void* _physicsInfo = nullptr; // set by EntitySimulation
|
void* _physicsInfo { nullptr }; // set by EntitySimulation
|
||||||
bool _simulated; // set by EntitySimulation
|
bool _simulated; // set by EntitySimulation
|
||||||
|
|
||||||
bool addActionInternal(EntitySimulationPointer simulation, EntityActionPointer action);
|
bool addActionInternal(EntitySimulationPointer simulation, EntityActionPointer action);
|
||||||
|
@ -580,12 +581,15 @@ protected:
|
||||||
// are used to keep track of and work around this situation.
|
// are used to keep track of and work around this situation.
|
||||||
void checkWaitingToRemove(EntitySimulationPointer simulation = nullptr);
|
void checkWaitingToRemove(EntitySimulationPointer simulation = nullptr);
|
||||||
mutable QSet<QUuid> _actionsToRemove;
|
mutable QSet<QUuid> _actionsToRemove;
|
||||||
mutable bool _actionDataDirty = false;
|
mutable bool _actionDataDirty { false };
|
||||||
mutable bool _actionDataNeedsTransmit = false;
|
mutable bool _actionDataNeedsTransmit { false };
|
||||||
// _previouslyDeletedActions is used to avoid an action being re-added due to server round-trip lag
|
// _previouslyDeletedActions is used to avoid an action being re-added due to server round-trip lag
|
||||||
static quint64 _rememberDeletedActionTime;
|
static quint64 _rememberDeletedActionTime;
|
||||||
mutable QHash<QUuid, quint64> _previouslyDeletedActions;
|
mutable QHash<QUuid, quint64> _previouslyDeletedActions;
|
||||||
|
|
||||||
|
// per entity keep state if it ever bid on simulation, so that we can ignore false simulation ownership
|
||||||
|
mutable bool _hasBidOnSimulation { false };
|
||||||
|
|
||||||
QUuid _sourceUUID; /// the server node UUID we came from
|
QUuid _sourceUUID; /// the server node UUID we came from
|
||||||
|
|
||||||
bool _clientOnly { false };
|
bool _clientOnly { false };
|
||||||
|
@ -594,7 +598,7 @@ protected:
|
||||||
// physics related changes from the network to suppress any duplicates and make
|
// physics related changes from the network to suppress any duplicates and make
|
||||||
// sure redundant applications are idempotent
|
// sure redundant applications are idempotent
|
||||||
glm::vec3 _lastUpdatedPositionValue;
|
glm::vec3 _lastUpdatedPositionValue;
|
||||||
glm::quat _lastUpdatedRotationValue;
|
glm::quat _lastUpdatedRotationValue;
|
||||||
glm::vec3 _lastUpdatedVelocityValue;
|
glm::vec3 _lastUpdatedVelocityValue;
|
||||||
glm::vec3 _lastUpdatedAngularVelocityValue;
|
glm::vec3 _lastUpdatedAngularVelocityValue;
|
||||||
glm::vec3 _lastUpdatedAccelerationValue;
|
glm::vec3 _lastUpdatedAccelerationValue;
|
||||||
|
|
|
@ -226,6 +226,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
|
||||||
// and make note of it now, so we can act on it right away.
|
// and make note of it now, so we can act on it right away.
|
||||||
propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
|
propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
|
||||||
entity->setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
|
entity->setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
|
||||||
|
entity->rememberHasSimulationOwnershipBid();
|
||||||
}
|
}
|
||||||
|
|
||||||
entity->setLastBroadcast(usecTimestampNow());
|
entity->setLastBroadcast(usecTimestampNow());
|
||||||
|
@ -436,6 +437,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
|
||||||
// we make a bid for simulation ownership
|
// we make a bid for simulation ownership
|
||||||
properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
|
properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
|
||||||
entity->pokeSimulationOwnership();
|
entity->pokeSimulationOwnership();
|
||||||
|
entity->rememberHasSimulationOwnershipBid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (properties.parentRelatedPropertyChanged() && entity->computePuffedQueryAACube()) {
|
if (properties.parentRelatedPropertyChanged() && entity->computePuffedQueryAACube()) {
|
||||||
|
@ -1359,12 +1361,14 @@ bool EntityScriptingInterface::isChildOfParent(QUuid childID, QUuid parentID) {
|
||||||
|
|
||||||
_entityTree->withReadLock([&] {
|
_entityTree->withReadLock([&] {
|
||||||
EntityItemPointer parent = _entityTree->findEntityByEntityItemID(parentID);
|
EntityItemPointer parent = _entityTree->findEntityByEntityItemID(parentID);
|
||||||
parent->forEachDescendant([&](SpatiallyNestablePointer descendant) {
|
if (parent) {
|
||||||
if(descendant->getID() == childID) {
|
parent->forEachDescendant([&](SpatiallyNestablePointer descendant) {
|
||||||
isChild = true;
|
if (descendant->getID() == childID) {
|
||||||
return;
|
isChild = true;
|
||||||
}
|
return;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return isChild;
|
return isChild;
|
||||||
|
|
|
@ -64,7 +64,7 @@ EntityTree::~EntityTree() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityTree::setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist) {
|
void EntityTree::setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist) {
|
||||||
_entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(',');
|
_entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(',', QString::SkipEmptyParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -927,13 +927,21 @@ void EntityTree::initEntityEditFilterEngine(QScriptEngine* engine, std::function
|
||||||
_entityEditFilterHadUncaughtExceptions = entityEditFilterHadUncaughtExceptions;
|
_entityEditFilterHadUncaughtExceptions = entityEditFilterHadUncaughtExceptions;
|
||||||
auto global = _entityEditFilterEngine->globalObject();
|
auto global = _entityEditFilterEngine->globalObject();
|
||||||
_entityEditFilterFunction = global.property("filter");
|
_entityEditFilterFunction = global.property("filter");
|
||||||
_hasEntityEditFilter = _entityEditFilterFunction.isFunction();
|
if (!_entityEditFilterFunction.isFunction()) {
|
||||||
|
qCDebug(entities) << "Filter function specified but not found. Will reject all edits.";
|
||||||
|
_entityEditFilterEngine = nullptr; // So that we don't try to call it. See filterProperties.
|
||||||
|
}
|
||||||
|
_hasEntityEditFilter = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged) {
|
bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, bool isAdd) {
|
||||||
if (!_hasEntityEditFilter || !_entityEditFilterEngine) {
|
if (!_entityEditFilterEngine) {
|
||||||
propertiesOut = propertiesIn;
|
propertiesOut = propertiesIn;
|
||||||
wasChanged = false; // not changed
|
wasChanged = false; // not changed
|
||||||
|
if (_hasEntityEditFilter) {
|
||||||
|
qCDebug(entities) << "Rejecting properties because filter has not been set.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true; // allowed
|
return true; // allowed
|
||||||
}
|
}
|
||||||
auto oldProperties = propertiesIn.getDesiredProperties();
|
auto oldProperties = propertiesIn.getDesiredProperties();
|
||||||
|
@ -945,6 +953,7 @@ bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItem
|
||||||
auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter.
|
auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter.
|
||||||
QScriptValueList args;
|
QScriptValueList args;
|
||||||
args << inputValues;
|
args << inputValues;
|
||||||
|
args << isAdd;
|
||||||
|
|
||||||
QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args);
|
QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args);
|
||||||
if (_entityEditFilterHadUncaughtExceptions()) {
|
if (_entityEditFilterHadUncaughtExceptions()) {
|
||||||
|
@ -981,6 +990,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
||||||
}
|
}
|
||||||
|
|
||||||
int processedBytes = 0;
|
int processedBytes = 0;
|
||||||
|
bool isAdd = false;
|
||||||
// we handle these types of "edit" packets
|
// we handle these types of "edit" packets
|
||||||
switch (message.getType()) {
|
switch (message.getType()) {
|
||||||
case PacketType::EntityErase: {
|
case PacketType::EntityErase: {
|
||||||
|
@ -990,6 +1000,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
||||||
}
|
}
|
||||||
|
|
||||||
case PacketType::EntityAdd:
|
case PacketType::EntityAdd:
|
||||||
|
isAdd = true; // fall through to next case
|
||||||
case PacketType::EntityEdit: {
|
case PacketType::EntityEdit: {
|
||||||
quint64 startDecode = 0, endDecode = 0;
|
quint64 startDecode = 0, endDecode = 0;
|
||||||
quint64 startLookup = 0, endLookup = 0;
|
quint64 startLookup = 0, endLookup = 0;
|
||||||
|
@ -1032,7 +1043,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this was an add, we also want to tell the client that sent this edit that the entity was not added.
|
// If this was an add, we also want to tell the client that sent this edit that the entity was not added.
|
||||||
if (message.getType() == PacketType::EntityAdd) {
|
if (isAdd) {
|
||||||
QWriteLocker locker(&_recentlyDeletedEntitiesLock);
|
QWriteLocker locker(&_recentlyDeletedEntitiesLock);
|
||||||
_recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID);
|
_recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID);
|
||||||
validEditPacket = passedWhiteList;
|
validEditPacket = passedWhiteList;
|
||||||
|
@ -1042,7 +1053,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((message.getType() == PacketType::EntityAdd ||
|
if ((isAdd ||
|
||||||
(message.getType() == PacketType::EntityEdit && properties.lifetimeChanged())) &&
|
(message.getType() == PacketType::EntityEdit && properties.lifetimeChanged())) &&
|
||||||
!senderNode->getCanRez() && senderNode->getCanRezTmp()) {
|
!senderNode->getCanRez() && senderNode->getCanRezTmp()) {
|
||||||
// this node is only allowed to rez temporary entities. if need be, cap the lifetime.
|
// this node is only allowed to rez temporary entities. if need be, cap the lifetime.
|
||||||
|
@ -1060,9 +1071,11 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
||||||
startFilter = usecTimestampNow();
|
startFilter = usecTimestampNow();
|
||||||
bool wasChanged = false;
|
bool wasChanged = false;
|
||||||
// Having (un)lock rights bypasses the filter.
|
// Having (un)lock rights bypasses the filter.
|
||||||
bool allowed = senderNode->isAllowedEditor() || filterProperties(properties, properties, wasChanged);
|
bool allowed = senderNode->isAllowedEditor() || filterProperties(properties, properties, wasChanged, isAdd);
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
|
auto timestamp = properties.getLastEdited();
|
||||||
properties = EntityItemProperties();
|
properties = EntityItemProperties();
|
||||||
|
properties.setLastEdited(timestamp);
|
||||||
}
|
}
|
||||||
if (!allowed || wasChanged) {
|
if (!allowed || wasChanged) {
|
||||||
bumpTimestamp(properties);
|
bumpTimestamp(properties);
|
||||||
|
@ -1102,8 +1115,16 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
||||||
existingEntity->markAsChangedOnServer();
|
existingEntity->markAsChangedOnServer();
|
||||||
endUpdate = usecTimestampNow();
|
endUpdate = usecTimestampNow();
|
||||||
_totalUpdates++;
|
_totalUpdates++;
|
||||||
} else if (message.getType() == PacketType::EntityAdd) {
|
} else if (isAdd) {
|
||||||
if (senderNode->getCanRez() || senderNode->getCanRezTmp()) {
|
bool failedAdd = !allowed;
|
||||||
|
if (!allowed) {
|
||||||
|
qCDebug(entities) << "Filtered entity add. ID:" << entityItemID;
|
||||||
|
} else if (!senderNode->getCanRez() && !senderNode->getCanRezTmp()) {
|
||||||
|
failedAdd = true;
|
||||||
|
qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID()
|
||||||
|
<< "] attempted to add an entity ID:" << entityItemID;
|
||||||
|
|
||||||
|
} else {
|
||||||
// this is a new entity... assign a new entityID
|
// this is a new entity... assign a new entityID
|
||||||
properties.setCreated(properties.getLastEdited());
|
properties.setCreated(properties.getLastEdited());
|
||||||
properties.setLastEditedBy(senderNode->getUUID());
|
properties.setLastEditedBy(senderNode->getUUID());
|
||||||
|
@ -1118,7 +1139,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
||||||
startLogging = usecTimestampNow();
|
startLogging = usecTimestampNow();
|
||||||
if (wantEditLogging()) {
|
if (wantEditLogging()) {
|
||||||
qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:"
|
qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:"
|
||||||
<< newEntity->getEntityItemID();
|
<< newEntity->getEntityItemID();
|
||||||
qCDebug(entities) << " properties:" << properties;
|
qCDebug(entities) << " properties:" << properties;
|
||||||
}
|
}
|
||||||
if (wantTerseEditLogging()) {
|
if (wantTerseEditLogging()) {
|
||||||
|
@ -1128,10 +1149,14 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
||||||
}
|
}
|
||||||
endLogging = usecTimestampNow();
|
endLogging = usecTimestampNow();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
failedAdd = true;
|
||||||
|
qCDebug(entities) << "Add entity failed ID:" << entityItemID;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID()
|
if (failedAdd) { // Let client know it failed, so that they don't have an entity that no one else sees.
|
||||||
<< "] attempted to add an entity.";
|
QWriteLocker locker(&_recentlyDeletedEntitiesLock);
|
||||||
|
_recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
static QString repeatedMessage =
|
static QString repeatedMessage =
|
||||||
|
@ -1552,6 +1577,8 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra
|
||||||
return args->map->value(oldID);
|
return args->map->value(oldID);
|
||||||
}
|
}
|
||||||
EntityItemID newID = QUuid::createUuid();
|
EntityItemID newID = QUuid::createUuid();
|
||||||
|
args->map->insert(oldID, newID);
|
||||||
|
|
||||||
EntityItemProperties properties = item->getProperties();
|
EntityItemProperties properties = item->getProperties();
|
||||||
EntityItemID oldParentID = properties.getParentID();
|
EntityItemID oldParentID = properties.getParentID();
|
||||||
if (oldParentID.isInvalidID()) { // no parent
|
if (oldParentID.isInvalidID()) { // no parent
|
||||||
|
@ -1567,6 +1594,43 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!properties.getXNNeighborID().isInvalidID()) {
|
||||||
|
auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXNNeighborID());
|
||||||
|
if (neighborEntity) {
|
||||||
|
properties.setXNNeighborID(getMapped(neighborEntity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!properties.getXPNeighborID().isInvalidID()) {
|
||||||
|
auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXPNeighborID());
|
||||||
|
if (neighborEntity) {
|
||||||
|
properties.setXPNeighborID(getMapped(neighborEntity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!properties.getYNNeighborID().isInvalidID()) {
|
||||||
|
auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYNNeighborID());
|
||||||
|
if (neighborEntity) {
|
||||||
|
properties.setYNNeighborID(getMapped(neighborEntity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!properties.getYPNeighborID().isInvalidID()) {
|
||||||
|
auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYPNeighborID());
|
||||||
|
if (neighborEntity) {
|
||||||
|
properties.setYPNeighborID(getMapped(neighborEntity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!properties.getZNNeighborID().isInvalidID()) {
|
||||||
|
auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZNNeighborID());
|
||||||
|
if (neighborEntity) {
|
||||||
|
properties.setZNNeighborID(getMapped(neighborEntity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!properties.getZPNeighborID().isInvalidID()) {
|
||||||
|
auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZPNeighborID());
|
||||||
|
if (neighborEntity) {
|
||||||
|
properties.setZPNeighborID(getMapped(neighborEntity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// set creation time to "now" for imported entities
|
// set creation time to "now" for imported entities
|
||||||
properties.setCreated(usecTimestampNow());
|
properties.setCreated(usecTimestampNow());
|
||||||
|
|
||||||
|
@ -1584,7 +1648,6 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra
|
||||||
args->otherTree->addEntity(newID, properties);
|
args->otherTree->addEntity(newID, properties);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
args->map->insert(oldID, newID);
|
|
||||||
return newID;
|
return newID;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -267,6 +267,7 @@ public:
|
||||||
void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
|
void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
|
||||||
|
|
||||||
void initEntityEditFilterEngine(QScriptEngine* engine, std::function<bool()> entityEditFilterHadUncaughtExceptions);
|
void initEntityEditFilterEngine(QScriptEngine* engine, std::function<bool()> entityEditFilterHadUncaughtExceptions);
|
||||||
|
void setHasEntityFilter(bool hasFilter) { _hasEntityEditFilter = hasFilter; }
|
||||||
|
|
||||||
static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME;
|
static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME;
|
||||||
|
|
||||||
|
@ -356,7 +357,7 @@ protected:
|
||||||
|
|
||||||
float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
|
float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
|
||||||
|
|
||||||
bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged);
|
bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, bool isAdd);
|
||||||
bool _hasEntityEditFilter{ false };
|
bool _hasEntityEditFilter{ false };
|
||||||
QScriptEngine* _entityEditFilterEngine{};
|
QScriptEngine* _entityEditFilterEngine{};
|
||||||
QScriptValue _entityEditFilterFunction{};
|
QScriptValue _entityEditFilterFunction{};
|
||||||
|
|
|
@ -73,6 +73,9 @@ void KeyboardMouseDevice::mousePressEvent(QMouseEvent* event) {
|
||||||
_mousePressTime = usecTimestampNow();
|
_mousePressTime = usecTimestampNow();
|
||||||
_mouseMoved = false;
|
_mouseMoved = false;
|
||||||
|
|
||||||
|
_mousePressPos = event->pos();
|
||||||
|
_clickDeadspotActive = true;
|
||||||
|
|
||||||
eraseMouseClicked();
|
eraseMouseClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,9 +87,11 @@ void KeyboardMouseDevice::mouseReleaseEvent(QMouseEvent* event) {
|
||||||
// input for this button we might want to add some small tolerance to this so if you do a small drag it
|
// input for this button we might want to add some small tolerance to this so if you do a small drag it
|
||||||
// still counts as a click.
|
// still counts as a click.
|
||||||
static const int CLICK_TIME = USECS_PER_MSEC * 500; // 500 ms to click
|
static const int CLICK_TIME = USECS_PER_MSEC * 500; // 500 ms to click
|
||||||
if (!_mouseMoved && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) {
|
if (_clickDeadspotActive && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) {
|
||||||
_inputDevice->_buttonPressedMap.insert(_inputDevice->makeInput((Qt::MouseButton) event->button(), true).getChannel());
|
_inputDevice->_buttonPressedMap.insert(_inputDevice->makeInput((Qt::MouseButton) event->button(), true).getChannel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_clickDeadspotActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardMouseDevice::eraseMouseClicked() {
|
void KeyboardMouseDevice::eraseMouseClicked() {
|
||||||
|
@ -109,9 +114,14 @@ void KeyboardMouseDevice::mouseMoveEvent(QMouseEvent* event) {
|
||||||
// outside of the application window, because we don't get MouseEvents when the cursor is outside
|
// outside of the application window, because we don't get MouseEvents when the cursor is outside
|
||||||
// of the application window.
|
// of the application window.
|
||||||
_lastCursor = currentPos;
|
_lastCursor = currentPos;
|
||||||
|
|
||||||
_mouseMoved = true;
|
_mouseMoved = true;
|
||||||
|
|
||||||
eraseMouseClicked();
|
const int CLICK_EVENT_DEADSPOT = 6; // pixels
|
||||||
|
if (_clickDeadspotActive && (_mousePressPos - currentPos).manhattanLength() > CLICK_EVENT_DEADSPOT) {
|
||||||
|
eraseMouseClicked();
|
||||||
|
_clickDeadspotActive = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardMouseDevice::wheelEvent(QWheelEvent* event) {
|
void KeyboardMouseDevice::wheelEvent(QWheelEvent* event) {
|
||||||
|
|
|
@ -118,8 +118,10 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QPoint _lastCursor;
|
QPoint _lastCursor;
|
||||||
|
QPoint _mousePressPos;
|
||||||
quint64 _mousePressTime;
|
quint64 _mousePressTime;
|
||||||
bool _mouseMoved;
|
bool _mouseMoved;
|
||||||
|
bool _clickDeadspotActive;
|
||||||
glm::vec2 _lastTouch;
|
glm::vec2 _lastTouch;
|
||||||
std::shared_ptr<InputDevice> _inputDevice { std::make_shared<InputDevice>() };
|
std::shared_ptr<InputDevice> _inputDevice { std::make_shared<InputDevice>() };
|
||||||
|
|
||||||
|
|
|
@ -1084,17 +1084,17 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element,
|
||||||
params.stopReason = EncodeBitstreamParams::WAS_IN_VIEW;
|
params.stopReason = EncodeBitstreamParams::WAS_IN_VIEW;
|
||||||
return bytesAtThisLevel;
|
return bytesAtThisLevel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we're not in delta sending mode, and we weren't asked to do a force send, and the voxel hasn't changed,
|
// If we're not in delta sending mode, and we weren't asked to do a force send, and the octree element hasn't changed,
|
||||||
// then we can also bail early and save bits
|
// then we can also bail early and save bits
|
||||||
if (!params.forceSendScene && !params.deltaView &&
|
if (!params.forceSendScene && !params.deltaView &&
|
||||||
!element->hasChangedSince(params.lastQuerySent - CHANGE_FUDGE)) {
|
!element->hasChangedSince(params.lastQuerySent - CHANGE_FUDGE)) {
|
||||||
if (params.stats) {
|
if (params.stats) {
|
||||||
params.stats->skippedNoChange(element);
|
params.stats->skippedNoChange(element);
|
||||||
}
|
|
||||||
params.stopReason = EncodeBitstreamParams::NO_CHANGE;
|
|
||||||
return bytesAtThisLevel;
|
|
||||||
}
|
}
|
||||||
|
params.stopReason = EncodeBitstreamParams::NO_CHANGE;
|
||||||
|
return bytesAtThisLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool keepDiggingDeeper = true; // Assuming we're in view we have a great work ethic, we're always ready for more!
|
bool keepDiggingDeeper = true; // Assuming we're in view we have a great work ethic, we're always ready for more!
|
||||||
|
|
|
@ -13,15 +13,25 @@
|
||||||
|
|
||||||
void ContactInfo::update(uint32_t currentStep, const btManifoldPoint& p) {
|
void ContactInfo::update(uint32_t currentStep, const btManifoldPoint& p) {
|
||||||
_lastStep = currentStep;
|
_lastStep = currentStep;
|
||||||
++_numSteps;
|
|
||||||
positionWorldOnB = p.m_positionWorldOnB;
|
positionWorldOnB = p.m_positionWorldOnB;
|
||||||
normalWorldOnB = p.m_normalWorldOnB;
|
normalWorldOnB = p.m_normalWorldOnB;
|
||||||
distance = p.m_distance1;
|
distance = p.m_distance1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uint32_t STEPS_BETWEEN_CONTINUE_EVENTS = 9;
|
||||||
|
|
||||||
ContactEventType ContactInfo::computeType(uint32_t thisStep) {
|
ContactEventType ContactInfo::computeType(uint32_t thisStep) {
|
||||||
if (_lastStep != thisStep) {
|
if (_continueExpiry == 0) {
|
||||||
return CONTACT_EVENT_TYPE_END;
|
_continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS;
|
||||||
|
return CONTACT_EVENT_TYPE_START;
|
||||||
}
|
}
|
||||||
return (_numSteps == 1) ? CONTACT_EVENT_TYPE_START : CONTACT_EVENT_TYPE_CONTINUE;
|
return (_lastStep == thisStep) ? CONTACT_EVENT_TYPE_CONTINUE : CONTACT_EVENT_TYPE_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContactInfo::readyForContinue(uint32_t thisStep) {
|
||||||
|
if (thisStep > _continueExpiry) {
|
||||||
|
_continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,12 +26,14 @@ public:
|
||||||
const btVector3& getPositionWorldOnB() const { return positionWorldOnB; }
|
const btVector3& getPositionWorldOnB() const { return positionWorldOnB; }
|
||||||
btVector3 getPositionWorldOnA() const { return positionWorldOnB + normalWorldOnB * distance; }
|
btVector3 getPositionWorldOnA() const { return positionWorldOnB + normalWorldOnB * distance; }
|
||||||
|
|
||||||
|
bool readyForContinue(uint32_t thisStep);
|
||||||
|
|
||||||
btVector3 positionWorldOnB;
|
btVector3 positionWorldOnB;
|
||||||
btVector3 normalWorldOnB;
|
btVector3 normalWorldOnB;
|
||||||
btScalar distance;
|
btScalar distance;
|
||||||
private:
|
private:
|
||||||
uint32_t _lastStep = 0;
|
uint32_t _lastStep { 0 };
|
||||||
uint32_t _numSteps = 0;
|
uint32_t _continueExpiry { 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,11 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const {
|
||||||
BT_PROFILE("kinematicIntegration");
|
BT_PROFILE("kinematicIntegration");
|
||||||
// This is physical kinematic motion which steps strictly by the subframe count
|
// This is physical kinematic motion which steps strictly by the subframe count
|
||||||
// of the physics simulation and uses full gravity for acceleration.
|
// of the physics simulation and uses full gravity for acceleration.
|
||||||
_entity->setAcceleration(_entity->getGravity());
|
if (_entity->hasAncestorOfType(NestableType::Avatar)) {
|
||||||
|
_entity->setAcceleration(glm::vec3(0.0f));
|
||||||
|
} else {
|
||||||
|
_entity->setAcceleration(_entity->getGravity());
|
||||||
|
}
|
||||||
uint32_t thisStep = ObjectMotionState::getWorldSimulationStep();
|
uint32_t thisStep = ObjectMotionState::getWorldSimulationStep();
|
||||||
float dt = (thisStep - _lastKinematicStep) * PHYSICS_ENGINE_FIXED_SUBSTEP;
|
float dt = (thisStep - _lastKinematicStep) * PHYSICS_ENGINE_FIXED_SUBSTEP;
|
||||||
_entity->stepKinematicMotion(dt);
|
_entity->stepKinematicMotion(dt);
|
||||||
|
@ -582,6 +586,8 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_
|
||||||
_nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS;
|
_nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS;
|
||||||
// copy _outgoingPriority into pendingPriority...
|
// copy _outgoingPriority into pendingPriority...
|
||||||
_entity->setPendingOwnershipPriority(_outgoingPriority, now);
|
_entity->setPendingOwnershipPriority(_outgoingPriority, now);
|
||||||
|
// don't forget to remember that we have made a bid
|
||||||
|
_entity->rememberHasSimulationOwnershipBid();
|
||||||
// ...then reset _outgoingPriority in preparation for the next frame
|
// ...then reset _outgoingPriority in preparation for the next frame
|
||||||
_outgoingPriority = 0;
|
_outgoingPriority = 0;
|
||||||
} else if (_outgoingPriority != _entity->getSimulationPriority()) {
|
} else if (_outgoingPriority != _entity->getSimulationPriority()) {
|
||||||
|
@ -762,6 +768,11 @@ void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& ma
|
||||||
_entity->computeCollisionGroupAndFinalMask(group, mask);
|
_entity->computeCollisionGroupAndFinalMask(group, mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EntityMotionState::shouldBeLocallyOwned() const {
|
||||||
|
return (_outgoingPriority > VOLUNTEER_SIMULATION_PRIORITY && _outgoingPriority > _entity->getSimulationPriority()) ||
|
||||||
|
_entity->getSimulatorID() == Physics::getSessionUUID();
|
||||||
|
}
|
||||||
|
|
||||||
void EntityMotionState::upgradeOutgoingPriority(uint8_t priority) {
|
void EntityMotionState::upgradeOutgoingPriority(uint8_t priority) {
|
||||||
_outgoingPriority = glm::max<uint8_t>(_outgoingPriority, priority);
|
_outgoingPriority = glm::max<uint8_t>(_outgoingPriority, priority);
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,8 @@ public:
|
||||||
|
|
||||||
virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override;
|
virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override;
|
||||||
|
|
||||||
|
bool shouldBeLocallyOwned() const override;
|
||||||
|
|
||||||
friend class PhysicalEntitySimulation;
|
friend class PhysicalEntitySimulation;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
|
@ -146,6 +146,8 @@ public:
|
||||||
void dirtyInternalKinematicChanges() { _hasInternalKinematicChanges = true; }
|
void dirtyInternalKinematicChanges() { _hasInternalKinematicChanges = true; }
|
||||||
void clearInternalKinematicChanges() { _hasInternalKinematicChanges = false; }
|
void clearInternalKinematicChanges() { _hasInternalKinematicChanges = false; }
|
||||||
|
|
||||||
|
virtual bool shouldBeLocallyOwned() const { return false; }
|
||||||
|
|
||||||
friend class PhysicsEngine;
|
friend class PhysicsEngine;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
|
@ -270,7 +270,7 @@ void PhysicsEngine::stepSimulation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto onSubStep = [this]() {
|
auto onSubStep = [this]() {
|
||||||
updateContactMap();
|
this->updateContactMap();
|
||||||
};
|
};
|
||||||
|
|
||||||
int numSubsteps = _dynamicsWorld->stepSimulationWithSubstepCallback(timeStep, PHYSICS_ENGINE_MAX_NUM_SUBSTEPS,
|
int numSubsteps = _dynamicsWorld->stepSimulationWithSubstepCallback(timeStep, PHYSICS_ENGINE_MAX_NUM_SUBSTEPS,
|
||||||
|
@ -393,7 +393,6 @@ void PhysicsEngine::updateContactMap() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollisionEvents& PhysicsEngine::getCollisionEvents() {
|
const CollisionEvents& PhysicsEngine::getCollisionEvents() {
|
||||||
const uint32_t CONTINUE_EVENT_FILTER_FREQUENCY = 10;
|
|
||||||
_collisionEvents.clear();
|
_collisionEvents.clear();
|
||||||
|
|
||||||
// scan known contacts and trigger events
|
// scan known contacts and trigger events
|
||||||
|
@ -402,28 +401,42 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() {
|
||||||
while (contactItr != _contactMap.end()) {
|
while (contactItr != _contactMap.end()) {
|
||||||
ContactInfo& contact = contactItr->second;
|
ContactInfo& contact = contactItr->second;
|
||||||
ContactEventType type = contact.computeType(_numContactFrames);
|
ContactEventType type = contact.computeType(_numContactFrames);
|
||||||
if(type != CONTACT_EVENT_TYPE_CONTINUE || _numSubsteps % CONTINUE_EVENT_FILTER_FREQUENCY == 0) {
|
const btScalar SIGNIFICANT_DEPTH = -0.002f; // penetrations have negative distance
|
||||||
|
if (type != CONTACT_EVENT_TYPE_CONTINUE ||
|
||||||
|
(contact.distance < SIGNIFICANT_DEPTH &&
|
||||||
|
contact.readyForContinue(_numContactFrames))) {
|
||||||
ObjectMotionState* motionStateA = static_cast<ObjectMotionState*>(contactItr->first._a);
|
ObjectMotionState* motionStateA = static_cast<ObjectMotionState*>(contactItr->first._a);
|
||||||
ObjectMotionState* motionStateB = static_cast<ObjectMotionState*>(contactItr->first._b);
|
ObjectMotionState* motionStateB = static_cast<ObjectMotionState*>(contactItr->first._b);
|
||||||
glm::vec3 velocityChange = (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)) +
|
|
||||||
(motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f));
|
|
||||||
|
|
||||||
if (motionStateA) {
|
// NOTE: the MyAvatar RigidBody is the only object in the simulation that does NOT have a MotionState
|
||||||
|
// which means should we ever want to report ALL collision events against the avatar we can
|
||||||
|
// modify the logic below.
|
||||||
|
//
|
||||||
|
// We only create events when at least one of the objects is (or should be) owned in the local simulation.
|
||||||
|
if (motionStateA && (motionStateA->shouldBeLocallyOwned())) {
|
||||||
QUuid idA = motionStateA->getObjectID();
|
QUuid idA = motionStateA->getObjectID();
|
||||||
QUuid idB;
|
QUuid idB;
|
||||||
if (motionStateB) {
|
if (motionStateB) {
|
||||||
idB = motionStateB->getObjectID();
|
idB = motionStateB->getObjectID();
|
||||||
}
|
}
|
||||||
glm::vec3 position = bulletToGLM(contact.getPositionWorldOnB()) + _originOffset;
|
glm::vec3 position = bulletToGLM(contact.getPositionWorldOnB()) + _originOffset;
|
||||||
|
glm::vec3 velocityChange = motionStateA->getObjectLinearVelocityChange() +
|
||||||
|
(motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f));
|
||||||
glm::vec3 penetration = bulletToGLM(contact.distance * contact.normalWorldOnB);
|
glm::vec3 penetration = bulletToGLM(contact.distance * contact.normalWorldOnB);
|
||||||
_collisionEvents.push_back(Collision(type, idA, idB, position, penetration, velocityChange));
|
_collisionEvents.push_back(Collision(type, idA, idB, position, penetration, velocityChange));
|
||||||
} else if (motionStateB) {
|
} else if (motionStateB && (motionStateB->shouldBeLocallyOwned())) {
|
||||||
QUuid idB = motionStateB->getObjectID();
|
QUuid idB = motionStateB->getObjectID();
|
||||||
|
QUuid idA;
|
||||||
|
if (motionStateA) {
|
||||||
|
idA = motionStateA->getObjectID();
|
||||||
|
}
|
||||||
glm::vec3 position = bulletToGLM(contact.getPositionWorldOnA()) + _originOffset;
|
glm::vec3 position = bulletToGLM(contact.getPositionWorldOnA()) + _originOffset;
|
||||||
|
glm::vec3 velocityChange = motionStateB->getObjectLinearVelocityChange() +
|
||||||
|
(motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f));
|
||||||
// NOTE: we're flipping the order of A and B (so that the first objectID is never NULL)
|
// NOTE: we're flipping the order of A and B (so that the first objectID is never NULL)
|
||||||
// hence we must negate the penetration.
|
// hence we negate the penetration (because penetration always points from B to A).
|
||||||
glm::vec3 penetration = - bulletToGLM(contact.distance * contact.normalWorldOnB);
|
glm::vec3 penetration = - bulletToGLM(contact.distance * contact.normalWorldOnB);
|
||||||
_collisionEvents.push_back(Collision(type, idB, QUuid(), position, penetration, velocityChange));
|
_collisionEvents.push_back(Collision(type, idB, idA, position, penetration, velocityChange));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,15 @@ bool LightingModel::isAlbedoEnabled() const {
|
||||||
return (bool)_parametersBuffer.get<Parameters>().enableAlbedo;
|
return (bool)_parametersBuffer.get<Parameters>().enableAlbedo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LightingModel::setMaterialTexturing(bool enable) {
|
||||||
|
if (enable != isMaterialTexturingEnabled()) {
|
||||||
|
_parametersBuffer.edit<Parameters>().enableMaterialTexturing = (float)enable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool LightingModel::isMaterialTexturingEnabled() const {
|
||||||
|
return (bool)_parametersBuffer.get<Parameters>().enableMaterialTexturing;
|
||||||
|
}
|
||||||
|
|
||||||
void LightingModel::setAmbientLight(bool enable) {
|
void LightingModel::setAmbientLight(bool enable) {
|
||||||
if (enable != isAmbientLightEnabled()) {
|
if (enable != isAmbientLightEnabled()) {
|
||||||
_parametersBuffer.edit<Parameters>().enableAmbientLight = (float)enable;
|
_parametersBuffer.edit<Parameters>().enableAmbientLight = (float)enable;
|
||||||
|
@ -150,6 +159,8 @@ void MakeLightingModel::configure(const Config& config) {
|
||||||
_lightingModel->setSpecular(config.enableSpecular);
|
_lightingModel->setSpecular(config.enableSpecular);
|
||||||
_lightingModel->setAlbedo(config.enableAlbedo);
|
_lightingModel->setAlbedo(config.enableAlbedo);
|
||||||
|
|
||||||
|
_lightingModel->setMaterialTexturing(config.enableMaterialTexturing);
|
||||||
|
|
||||||
_lightingModel->setAmbientLight(config.enableAmbientLight);
|
_lightingModel->setAmbientLight(config.enableAmbientLight);
|
||||||
_lightingModel->setDirectionalLight(config.enableDirectionalLight);
|
_lightingModel->setDirectionalLight(config.enableDirectionalLight);
|
||||||
_lightingModel->setPointLight(config.enablePointLight);
|
_lightingModel->setPointLight(config.enablePointLight);
|
||||||
|
@ -161,4 +172,7 @@ void MakeLightingModel::configure(const Config& config) {
|
||||||
void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) {
|
void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) {
|
||||||
|
|
||||||
lightingModel = _lightingModel;
|
lightingModel = _lightingModel;
|
||||||
|
|
||||||
|
// make sure the enableTexturing flag of the render ARgs is in sync
|
||||||
|
renderContext->args->_enableTexturing = _lightingModel->isMaterialTexturingEnabled();
|
||||||
}
|
}
|
|
@ -49,6 +49,8 @@ public:
|
||||||
void setAlbedo(bool enable);
|
void setAlbedo(bool enable);
|
||||||
bool isAlbedoEnabled() const;
|
bool isAlbedoEnabled() const;
|
||||||
|
|
||||||
|
void setMaterialTexturing(bool enable);
|
||||||
|
bool isMaterialTexturingEnabled() const;
|
||||||
|
|
||||||
void setAmbientLight(bool enable);
|
void setAmbientLight(bool enable);
|
||||||
bool isAmbientLightEnabled() const;
|
bool isAmbientLightEnabled() const;
|
||||||
|
@ -88,9 +90,12 @@ protected:
|
||||||
float enableSpotLight{ 1.0f };
|
float enableSpotLight{ 1.0f };
|
||||||
|
|
||||||
float showLightContour{ 0.0f }; // false by default
|
float showLightContour{ 0.0f }; // false by default
|
||||||
|
|
||||||
float enableObscurance{ 1.0f };
|
float enableObscurance{ 1.0f };
|
||||||
|
|
||||||
glm::vec2 spares{ 0.0f };
|
float enableMaterialTexturing { 1.0f };
|
||||||
|
|
||||||
|
float spares{ 0.0f };
|
||||||
|
|
||||||
Parameters() {}
|
Parameters() {}
|
||||||
};
|
};
|
||||||
|
@ -117,6 +122,8 @@ class MakeLightingModelConfig : public render::Job::Config {
|
||||||
Q_PROPERTY(bool enableSpecular MEMBER enableSpecular NOTIFY dirty)
|
Q_PROPERTY(bool enableSpecular MEMBER enableSpecular NOTIFY dirty)
|
||||||
Q_PROPERTY(bool enableAlbedo MEMBER enableAlbedo NOTIFY dirty)
|
Q_PROPERTY(bool enableAlbedo MEMBER enableAlbedo NOTIFY dirty)
|
||||||
|
|
||||||
|
Q_PROPERTY(bool enableMaterialTexturing MEMBER enableMaterialTexturing NOTIFY dirty)
|
||||||
|
|
||||||
Q_PROPERTY(bool enableAmbientLight MEMBER enableAmbientLight NOTIFY dirty)
|
Q_PROPERTY(bool enableAmbientLight MEMBER enableAmbientLight NOTIFY dirty)
|
||||||
Q_PROPERTY(bool enableDirectionalLight MEMBER enableDirectionalLight NOTIFY dirty)
|
Q_PROPERTY(bool enableDirectionalLight MEMBER enableDirectionalLight NOTIFY dirty)
|
||||||
Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty)
|
Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty)
|
||||||
|
@ -136,13 +143,16 @@ public:
|
||||||
bool enableScattering{ true };
|
bool enableScattering{ true };
|
||||||
bool enableDiffuse{ true };
|
bool enableDiffuse{ true };
|
||||||
bool enableSpecular{ true };
|
bool enableSpecular{ true };
|
||||||
|
|
||||||
bool enableAlbedo{ true };
|
bool enableAlbedo{ true };
|
||||||
|
bool enableMaterialTexturing { true };
|
||||||
|
|
||||||
bool enableAmbientLight{ true };
|
bool enableAmbientLight{ true };
|
||||||
bool enableDirectionalLight{ true };
|
bool enableDirectionalLight{ true };
|
||||||
bool enablePointLight{ true };
|
bool enablePointLight{ true };
|
||||||
bool enableSpotLight{ true };
|
bool enableSpotLight{ true };
|
||||||
|
|
||||||
|
|
||||||
bool showLightContour { false }; // false by default
|
bool showLightContour { false }; // false by default
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
|
@ -129,7 +129,7 @@ void MeshPartPayload::bindMesh(gpu::Batch& batch) const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations) const {
|
void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations, bool enableTextures) const {
|
||||||
if (!_drawMaterial) {
|
if (!_drawMaterial) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,17 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat
|
||||||
numUnlit++;
|
numUnlit++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!enableTextures) {
|
||||||
|
batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, textureCache->getWhiteTexture());
|
||||||
|
batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, textureCache->getWhiteTexture());
|
||||||
|
batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, nullptr);
|
||||||
|
batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, nullptr);
|
||||||
|
batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, nullptr);
|
||||||
|
batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, nullptr);
|
||||||
|
batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Albedo
|
// Albedo
|
||||||
if (materialKey.isAlbedoMap()) {
|
if (materialKey.isAlbedoMap()) {
|
||||||
auto itr = textureMaps.find(model::MaterialKey::ALBEDO_MAP);
|
auto itr = textureMaps.find(model::MaterialKey::ALBEDO_MAP);
|
||||||
|
@ -271,7 +282,7 @@ void MeshPartPayload::render(RenderArgs* args) const {
|
||||||
bindMesh(batch);
|
bindMesh(batch);
|
||||||
|
|
||||||
// apply material properties
|
// apply material properties
|
||||||
bindMaterial(batch, locations);
|
bindMaterial(batch, locations, args->_enableTexturing);
|
||||||
|
|
||||||
if (args) {
|
if (args) {
|
||||||
args->_details._materialSwitches++;
|
args->_details._materialSwitches++;
|
||||||
|
@ -363,12 +374,7 @@ void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transf
|
||||||
_transform = transform;
|
_transform = transform;
|
||||||
|
|
||||||
if (clusterMatrices.size() > 0) {
|
if (clusterMatrices.size() > 0) {
|
||||||
_worldBound = AABox();
|
_worldBound = _adjustedLocalBound;
|
||||||
for (auto& clusterMatrix : clusterMatrices) {
|
|
||||||
AABox clusterBound = _localBound;
|
|
||||||
clusterBound.transform(clusterMatrix);
|
|
||||||
_worldBound += clusterBound;
|
|
||||||
}
|
|
||||||
_worldBound.transform(_transform);
|
_worldBound.transform(_transform);
|
||||||
if (clusterMatrices.size() == 1) {
|
if (clusterMatrices.size() == 1) {
|
||||||
_transform = _transform.worldTransform(Transform(clusterMatrices[0]));
|
_transform = _transform.worldTransform(Transform(clusterMatrices[0]));
|
||||||
|
@ -588,7 +594,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
|
||||||
bindMesh(batch);
|
bindMesh(batch);
|
||||||
|
|
||||||
// apply material properties
|
// apply material properties
|
||||||
bindMaterial(batch, locations);
|
bindMaterial(batch, locations, args->_enableTexturing);
|
||||||
|
|
||||||
args->_details._materialSwitches++;
|
args->_details._materialSwitches++;
|
||||||
|
|
||||||
|
@ -601,3 +607,15 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
|
||||||
const int INDICES_PER_TRIANGLE = 3;
|
const int INDICES_PER_TRIANGLE = 3;
|
||||||
args->_details._trianglesRendered += _drawPart._numIndices / INDICES_PER_TRIANGLE;
|
args->_details._trianglesRendered += _drawPart._numIndices / INDICES_PER_TRIANGLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ModelMeshPartPayload::computeAdjustedLocalBound(const QVector<glm::mat4>& clusterMatrices) {
|
||||||
|
_adjustedLocalBound = _localBound;
|
||||||
|
if (clusterMatrices.size() > 0) {
|
||||||
|
_adjustedLocalBound.transform(clusterMatrices[0]);
|
||||||
|
for (int i = 1; i < clusterMatrices.size(); ++i) {
|
||||||
|
AABox clusterBound = _localBound;
|
||||||
|
clusterBound.transform(clusterMatrices[i]);
|
||||||
|
_adjustedLocalBound += clusterBound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// ModelMeshPartPayload.h
|
// MeshPartPayload.h
|
||||||
// interface/src/renderer
|
// interface/src/renderer
|
||||||
//
|
//
|
||||||
// Created by Sam Gateau on 10/3/15.
|
// Created by Sam Gateau on 10/3/15.
|
||||||
|
@ -51,7 +51,7 @@ public:
|
||||||
// ModelMeshPartPayload functions to perform render
|
// ModelMeshPartPayload functions to perform render
|
||||||
void drawCall(gpu::Batch& batch) const;
|
void drawCall(gpu::Batch& batch) const;
|
||||||
virtual void bindMesh(gpu::Batch& batch) const;
|
virtual void bindMesh(gpu::Batch& batch) const;
|
||||||
virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations) const;
|
virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, bool enableTextures) const;
|
||||||
virtual void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const;
|
virtual void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const;
|
||||||
|
|
||||||
// Payload resource cached values
|
// Payload resource cached values
|
||||||
|
@ -61,6 +61,7 @@ public:
|
||||||
bool _hasColorAttrib { false };
|
bool _hasColorAttrib { false };
|
||||||
|
|
||||||
model::Box _localBound;
|
model::Box _localBound;
|
||||||
|
model::Box _adjustedLocalBound;
|
||||||
mutable model::Box _worldBound;
|
mutable model::Box _worldBound;
|
||||||
std::shared_ptr<const model::Mesh> _drawMesh;
|
std::shared_ptr<const model::Mesh> _drawMesh;
|
||||||
|
|
||||||
|
@ -105,6 +106,8 @@ public:
|
||||||
|
|
||||||
void initCache();
|
void initCache();
|
||||||
|
|
||||||
|
void computeAdjustedLocalBound(const QVector<glm::mat4>& clusterMatrices);
|
||||||
|
|
||||||
Model* _model;
|
Model* _model;
|
||||||
|
|
||||||
int _meshIndex;
|
int _meshIndex;
|
||||||
|
|
|
@ -78,11 +78,12 @@ void initCollisionMaterials() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Model::Model(RigPointer rig, QObject* parent) :
|
Model::Model(RigPointer rig, QObject* parent, SpatiallyNestable* spatiallyNestableOverride) :
|
||||||
QObject(parent),
|
QObject(parent),
|
||||||
_renderGeometry(),
|
_renderGeometry(),
|
||||||
_collisionGeometry(),
|
_collisionGeometry(),
|
||||||
_renderWatcher(_renderGeometry),
|
_renderWatcher(_renderGeometry),
|
||||||
|
_spatiallyNestableOverride(spatiallyNestableOverride),
|
||||||
_translation(0.0f),
|
_translation(0.0f),
|
||||||
_rotation(),
|
_rotation(),
|
||||||
_scale(1.0f, 1.0f, 1.0f),
|
_scale(1.0f, 1.0f, 1.0f),
|
||||||
|
@ -133,16 +134,10 @@ void Model::setRotation(const glm::quat& rotation) {
|
||||||
updateRenderItems();
|
updateRenderItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Model::setSpatiallyNestableOverride(SpatiallyNestablePointer override) {
|
|
||||||
_spatiallyNestableOverride = override;
|
|
||||||
updateRenderItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
Transform Model::getTransform() const {
|
Transform Model::getTransform() const {
|
||||||
SpatiallyNestablePointer spatiallyNestableOverride = _spatiallyNestableOverride.lock();
|
if (_spatiallyNestableOverride) {
|
||||||
if (spatiallyNestableOverride) {
|
|
||||||
bool success;
|
bool success;
|
||||||
Transform transform = spatiallyNestableOverride->getTransform(success);
|
Transform transform = _spatiallyNestableOverride->getTransform(success);
|
||||||
if (success) {
|
if (success) {
|
||||||
transform.setScale(getScale());
|
transform.setScale(getScale());
|
||||||
return transform;
|
return transform;
|
||||||
|
@ -1149,6 +1144,8 @@ void Model::simulate(float deltaTime, bool fullUpdate) {
|
||||||
// update the world space transforms for all joints
|
// update the world space transforms for all joints
|
||||||
glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset);
|
glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset);
|
||||||
updateRig(deltaTime, parentTransform);
|
updateRig(deltaTime, parentTransform);
|
||||||
|
|
||||||
|
computeMeshPartLocalBounds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1158,6 +1155,14 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||||
_rig->updateAnimations(deltaTime, parentTransform);
|
_rig->updateAnimations(deltaTime, parentTransform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Model::computeMeshPartLocalBounds() {
|
||||||
|
for (auto& part : _modelMeshRenderItemsSet) {
|
||||||
|
assert(part->_meshIndex < _modelMeshRenderItemsSet.size());
|
||||||
|
const Model::MeshState& state = _meshStates.at(part->_meshIndex);
|
||||||
|
part->computeAdjustedLocalBound(state.clusterMatrices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// virtual
|
// virtual
|
||||||
void Model::updateClusterMatrices() {
|
void Model::updateClusterMatrices() {
|
||||||
PerformanceTimer perfTimer("Model::updateClusterMatrices");
|
PerformanceTimer perfTimer("Model::updateClusterMatrices");
|
||||||
|
@ -1334,6 +1339,7 @@ void Model::createVisibleRenderItemSet() {
|
||||||
shapeID++;
|
shapeID++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
computeMeshPartLocalBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Model::createCollisionRenderItemSet() {
|
void Model::createCollisionRenderItemSet() {
|
||||||
|
|
|
@ -67,7 +67,7 @@ public:
|
||||||
|
|
||||||
static void setAbstractViewStateInterface(AbstractViewStateInterface* viewState) { _viewState = viewState; }
|
static void setAbstractViewStateInterface(AbstractViewStateInterface* viewState) { _viewState = viewState; }
|
||||||
|
|
||||||
Model(RigPointer rig, QObject* parent = nullptr);
|
Model(RigPointer rig, QObject* parent = nullptr, SpatiallyNestable* spatiallyNestableOverride = nullptr);
|
||||||
virtual ~Model();
|
virtual ~Model();
|
||||||
|
|
||||||
inline ModelPointer getThisPointer() const {
|
inline ModelPointer getThisPointer() const {
|
||||||
|
@ -205,7 +205,6 @@ public:
|
||||||
|
|
||||||
void setTranslation(const glm::vec3& translation);
|
void setTranslation(const glm::vec3& translation);
|
||||||
void setRotation(const glm::quat& rotation);
|
void setRotation(const glm::quat& rotation);
|
||||||
void setSpatiallyNestableOverride(SpatiallyNestablePointer ptr);
|
|
||||||
|
|
||||||
const glm::vec3& getTranslation() const { return _translation; }
|
const glm::vec3& getTranslation() const { return _translation; }
|
||||||
const glm::quat& getRotation() const { return _rotation; }
|
const glm::quat& getRotation() const { return _rotation; }
|
||||||
|
@ -244,7 +243,6 @@ public:
|
||||||
public:
|
public:
|
||||||
QVector<glm::mat4> clusterMatrices;
|
QVector<glm::mat4> clusterMatrices;
|
||||||
gpu::BufferPointer clusterBuffer;
|
gpu::BufferPointer clusterBuffer;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MeshState& getMeshState(int index) { return _meshStates.at(index); }
|
const MeshState& getMeshState(int index) { return _meshStates.at(index); }
|
||||||
|
@ -293,12 +291,11 @@ protected:
|
||||||
|
|
||||||
GeometryResourceWatcher _renderWatcher;
|
GeometryResourceWatcher _renderWatcher;
|
||||||
|
|
||||||
|
SpatiallyNestable* _spatiallyNestableOverride;
|
||||||
|
|
||||||
glm::vec3 _translation;
|
glm::vec3 _translation;
|
||||||
glm::quat _rotation;
|
glm::quat _rotation;
|
||||||
glm::vec3 _scale;
|
glm::vec3 _scale;
|
||||||
|
|
||||||
SpatiallyNestableWeakPointer _spatiallyNestableOverride;
|
|
||||||
|
|
||||||
glm::vec3 _offset;
|
glm::vec3 _offset;
|
||||||
|
|
||||||
static float FAKE_DIMENSION_PLACEHOLDER;
|
static float FAKE_DIMENSION_PLACEHOLDER;
|
||||||
|
@ -319,6 +316,7 @@ protected:
|
||||||
void scaleToFit();
|
void scaleToFit();
|
||||||
void snapToRegistrationPoint();
|
void snapToRegistrationPoint();
|
||||||
|
|
||||||
|
void computeMeshPartLocalBounds();
|
||||||
virtual void updateRig(float deltaTime, glm::mat4 parentTransform);
|
virtual void updateRig(float deltaTime, glm::mat4 parentTransform);
|
||||||
|
|
||||||
/// Restores the indexed joint to its default position.
|
/// Restores the indexed joint to its default position.
|
||||||
|
|
|
@ -23,10 +23,19 @@ void SoundEffect::setSource(QUrl url) {
|
||||||
_sound = DependencyManager::get<SoundCache>()->getSound(_url);
|
_sound = DependencyManager::get<SoundCache>()->getSound(_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float SoundEffect::getVolume() const {
|
||||||
|
return _volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SoundEffect::setVolume(float volume) {
|
||||||
|
_volume = volume;
|
||||||
|
}
|
||||||
|
|
||||||
void SoundEffect::play(QVariant position) {
|
void SoundEffect::play(QVariant position) {
|
||||||
AudioInjectorOptions options;
|
AudioInjectorOptions options;
|
||||||
options.position = vec3FromVariant(position);
|
options.position = vec3FromVariant(position);
|
||||||
options.localOnly = true;
|
options.localOnly = true;
|
||||||
|
options.volume = _volume;
|
||||||
if (_injector) {
|
if (_injector) {
|
||||||
_injector->setOptions(options);
|
_injector->setOptions(options);
|
||||||
_injector->restart();
|
_injector->restart();
|
||||||
|
|
|
@ -22,6 +22,7 @@ class AudioInjector;
|
||||||
class SoundEffect : public QQuickItem {
|
class SoundEffect : public QQuickItem {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QUrl source READ getSource WRITE setSource)
|
Q_PROPERTY(QUrl source READ getSource WRITE setSource)
|
||||||
|
Q_PROPERTY(float volume READ getVolume WRITE setVolume)
|
||||||
public:
|
public:
|
||||||
|
|
||||||
virtual ~SoundEffect();
|
virtual ~SoundEffect();
|
||||||
|
@ -29,9 +30,13 @@ public:
|
||||||
QUrl getSource() const;
|
QUrl getSource() const;
|
||||||
void setSource(QUrl url);
|
void setSource(QUrl url);
|
||||||
|
|
||||||
|
float getVolume() const;
|
||||||
|
void setVolume(float volume);
|
||||||
|
|
||||||
Q_INVOKABLE void play(QVariant position);
|
Q_INVOKABLE void play(QVariant position);
|
||||||
protected:
|
protected:
|
||||||
QUrl _url;
|
QUrl _url;
|
||||||
|
float _volume { 1.0f };
|
||||||
SharedSoundPointer _sound;
|
SharedSoundPointer _sound;
|
||||||
AudioInjector* _injector { nullptr };
|
AudioInjector* _injector { nullptr };
|
||||||
};
|
};
|
||||||
|
|
|
@ -170,14 +170,14 @@ public:
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* Returns the current value of this button's properties
|
* Returns the current value of this button's properties
|
||||||
* @function TabletButtonProxy#getProperties
|
* @function TabletButtonProxy#getProperties
|
||||||
* @returns {object}
|
* @returns {ButtonProperties}
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE QVariantMap getProperties() const;
|
Q_INVOKABLE QVariantMap getProperties() const;
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* Replace the values of some of this button's properties
|
* Replace the values of some of this button's properties
|
||||||
* @function TabletButtonProxy#editProperties
|
* @function TabletButtonProxy#editProperties
|
||||||
* @param properties {object} set of properties to change
|
* @param {ButtonProperties} properties - set of properties to change
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void editProperties(QVariantMap properties);
|
Q_INVOKABLE void editProperties(QVariantMap properties);
|
||||||
|
|
||||||
|
@ -199,4 +199,13 @@ protected:
|
||||||
QVariantMap _properties;
|
QVariantMap _properties;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**jsdoc
|
||||||
|
* @typedef TabletButtonProxy.ButtonProperties
|
||||||
|
* @property {string} text - button caption
|
||||||
|
* @property {string} icon - url to button icon. (50 x 50)
|
||||||
|
* @property {string} activeText - button caption when button is active
|
||||||
|
* @property {string} activeIcon - url to button icon used when button is active. (50 x 50)
|
||||||
|
* @property {string} isActive - true when button is active.
|
||||||
|
*/
|
||||||
|
|
||||||
#endif // hifi_TabletScriptingInterface_h
|
#endif // hifi_TabletScriptingInterface_h
|
||||||
|
|
|
@ -143,8 +143,15 @@ static inline bool cpuSupportsAVX2() {
|
||||||
bool result = false;
|
bool result = false;
|
||||||
if (cpuSupportsAVX()) {
|
if (cpuSupportsAVX()) {
|
||||||
|
|
||||||
if (__get_cpuid(0x7, &eax, &ebx, &ecx, &edx) && ((ebx & MASK_AVX2) == MASK_AVX2)) {
|
// Work around a bug where __get_cpuid(0x7) returns wrong values on older GCC
|
||||||
result = true;
|
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77756
|
||||||
|
if (__get_cpuid(0x0, &eax, &ebx, &ecx, &edx) && (eax >= 0x7)) {
|
||||||
|
|
||||||
|
__cpuid_count(0x7, 0x0, eax, ebx, ecx, edx);
|
||||||
|
|
||||||
|
if ((ebx & MASK_AVX2) == MASK_AVX2) {
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -742,6 +742,12 @@ void collisionFromScriptValue(const QScriptValue &object, Collision& collision)
|
||||||
// TODO: implement this when we know what it means to accept collision events from JS
|
// TODO: implement this when we know what it means to accept collision events from JS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Collision::invert() {
|
||||||
|
std::swap(idA, idB);
|
||||||
|
contactPoint += penetration;
|
||||||
|
penetration *= -1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
QScriptValue quuidToScriptValue(QScriptEngine* engine, const QUuid& uuid) {
|
QScriptValue quuidToScriptValue(QScriptEngine* engine, const QUuid& uuid) {
|
||||||
if (uuid.isNull()) {
|
if (uuid.isNull()) {
|
||||||
return QScriptValue::NullValue;
|
return QScriptValue::NullValue;
|
||||||
|
|
|
@ -142,11 +142,13 @@ public:
|
||||||
const glm::vec3& cPenetration, const glm::vec3& velocityChange)
|
const glm::vec3& cPenetration, const glm::vec3& velocityChange)
|
||||||
: type(cType), idA(cIdA), idB(cIdB), contactPoint(cPoint), penetration(cPenetration), velocityChange(velocityChange) { }
|
: type(cType), idA(cIdA), idB(cIdB), contactPoint(cPoint), penetration(cPenetration), velocityChange(velocityChange) { }
|
||||||
|
|
||||||
|
void invert(); // swap A and B
|
||||||
|
|
||||||
ContactEventType type;
|
ContactEventType type;
|
||||||
QUuid idA;
|
QUuid idA;
|
||||||
QUuid idB;
|
QUuid idB;
|
||||||
glm::vec3 contactPoint;
|
glm::vec3 contactPoint; // on B in world-frame
|
||||||
glm::vec3 penetration;
|
glm::vec3 penetration; // from B towards A in world-frame
|
||||||
glm::vec3 velocityChange;
|
glm::vec3 velocityChange;
|
||||||
};
|
};
|
||||||
Q_DECLARE_METATYPE(Collision)
|
Q_DECLARE_METATYPE(Collision)
|
||||||
|
|
|
@ -122,6 +122,7 @@ public:
|
||||||
gpu::Batch* _batch = nullptr;
|
gpu::Batch* _batch = nullptr;
|
||||||
|
|
||||||
std::shared_ptr<gpu::Texture> _whiteTexture;
|
std::shared_ptr<gpu::Texture> _whiteTexture;
|
||||||
|
bool _enableTexturing { true };
|
||||||
|
|
||||||
RenderDetails _details;
|
RenderDetails _details;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,54 +1,106 @@
|
||||||
function filter(p) {
|
function filter(p) {
|
||||||
/* block comments are ok, but not double-slash end-of-line-comments */
|
/******************************************************/
|
||||||
|
/* General Filter Comments
|
||||||
|
/*
|
||||||
|
- Custom filters must be named "filter" and must be global
|
||||||
|
- Block comments are ok, but not double-slash end-of-line-comments
|
||||||
|
- Certain JavaScript functions are not available, like Math.sign(), as they are undefined in QT's non-conforming JS
|
||||||
|
- HiFi's scripting interface is unavailable here. That means you can't call, for example, Users.*()
|
||||||
|
*/
|
||||||
|
/******************************************************/
|
||||||
|
|
||||||
|
/******************************************************/
|
||||||
|
/* Simple Filter Examples
|
||||||
|
/******************************************************/
|
||||||
/* Simple example: if someone specifies name, add an 'x' to it. Note that print is ok to use. */
|
/* Simple example: if someone specifies name, add an 'x' to it. Note that print is ok to use. */
|
||||||
if (p.name) {p.name += 'x'; print('fixme name', p. name);}
|
if (p.name) {p.name += 'x'; print('fixme name', p. name);}
|
||||||
|
|
||||||
|
|
||||||
/* This example clamps y. A better filter would probably zero y component of velocity and acceleration. */
|
/* This example clamps y. A better filter would probably zero y component of velocity and acceleration. */
|
||||||
if (p.position) {p.position.y = Math.min(1, p.position.y); print('fixme p.y', p.position.y);}
|
if (p.position) {p.position.y = Math.min(1, p.position.y); print('fixme p.y', p.position.y);}
|
||||||
|
|
||||||
/* Can also reject altogether */
|
|
||||||
|
/* Can also reject new properties altogether by returning false */
|
||||||
if (p.userData) { return false; }
|
if (p.userData) { return false; }
|
||||||
|
|
||||||
|
|
||||||
/* Reject if modifications made to Model properties */
|
/* Reject if modifications made to Model properties */
|
||||||
if (p.modelURL || p.compoundShapeURL || p.shape || p.shapeType || p.url || p.fps || p.currentFrame || p.running || p.loop || p.firstFrame || p.lastFrame || p.hold || p.textures || p.xTextureURL || p.yTextureURL || p.zTextureURL) { return false; }
|
if (p.modelURL || p.compoundShapeURL || p.shape || p.shapeType || p.url || p.fps || p.currentFrame || p.running || p.loop || p.firstFrame || p.lastFrame || p.hold || p.textures || p.xTextureURL || p.yTextureURL || p.zTextureURL) { return false; }
|
||||||
|
|
||||||
|
|
||||||
|
/******************************************************/
|
||||||
|
/* Physical Property Filter Examples
|
||||||
|
/*
|
||||||
|
NOTES about filtering physical properties:
|
||||||
|
- For now, ensure you always supply a new value for the filtered physical property
|
||||||
|
(instead of simply removing the property)
|
||||||
|
- Ensure you always specify a slightly different value for physical properties every
|
||||||
|
time your filter returns. Look to "var nearZero" below for an example).
|
||||||
|
This is necessary because Interface checks if a physical property has changed
|
||||||
|
when deciding whether to apply or reject the server's physical properties.
|
||||||
|
If a physical property's value doesn't change, Interface will reject the server's property value,
|
||||||
|
and Bullet will continue simulating the entity with stale physical properties.
|
||||||
|
Ensure that this value is not changed by such a small amount such that new values
|
||||||
|
fall within floating point precision boundaries. If you accidentally do this, prepare for many
|
||||||
|
hours of frustrating debugging :).
|
||||||
|
*/
|
||||||
|
/******************************************************/
|
||||||
/* Clamp velocity to maxVelocity units/second. Zeroing each component of acceleration keeps us from slamming.*/
|
/* Clamp velocity to maxVelocity units/second. Zeroing each component of acceleration keeps us from slamming.*/
|
||||||
var maxVelocity = 5;
|
|
||||||
if (p.velocity) {
|
if (p.velocity) {
|
||||||
|
var maxVelocity = 5;
|
||||||
|
/* Random near-zero value used as "zero" to prevent two sequential updates from being
|
||||||
|
exactly the same (which would cause them to be ignored) */
|
||||||
|
var nearZero = 0.0001 * Math.random() + 0.001;
|
||||||
|
function sign(val) {
|
||||||
|
if (val > 0) {
|
||||||
|
return 1;
|
||||||
|
} else if (val < 0) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (Math.abs(p.velocity.x) > maxVelocity) {
|
if (Math.abs(p.velocity.x) > maxVelocity) {
|
||||||
p.velocity.x = Math.sign(p.velocity.x) * maxVelocity;
|
p.velocity.x = sign(p.velocity.x) * (maxVelocity + nearZero);
|
||||||
p.acceleration.x = 0;
|
p.acceleration.x = nearZero;
|
||||||
}
|
}
|
||||||
if (Math.abs(p.velocity.y) > maxVelocity) {
|
if (Math.abs(p.velocity.y) > maxVelocity) {
|
||||||
p.velocity.y = Math.sign(p.velocity.y) * maxVelocity;
|
p.velocity.y = sign(p.velocity.y) * (maxVelocity + nearZero);
|
||||||
p.acceleration.y = 0;
|
p.acceleration.y = nearZero;
|
||||||
}
|
}
|
||||||
if (Math.abs(p.velocity.z) > maxVelocity) {
|
if (Math.abs(p.velocity.z) > maxVelocity) {
|
||||||
p.velocity.z = Math.sign(p.velocity.z) * maxVelocity;
|
p.velocity.z = sign(p.velocity.z) * (maxVelocity + nearZero);
|
||||||
p.acceleration.z = 0;
|
p.acceleration.z = nearZero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Define an axis-aligned zone in which entities are not allowed to enter. */
|
/* Define an axis-aligned zone in which entities are not allowed to enter. */
|
||||||
/* This example zone corresponds to an area to the right of the spawnpoint
|
/* This example zone corresponds to an area to the right of the spawnpoint
|
||||||
in your Sandbox. It's an area near the big rock to the right. If an entity
|
in your Sandbox. It's an area near the big rock to the right. If an entity
|
||||||
enters the zone, it'll move behind the rock.*/
|
enters the zone, it'll move behind the rock.*/
|
||||||
var boxMin = {x: 25.5, y: -0.48, z: -9.9};
|
|
||||||
var boxMax = {x: 31.1, y: 4, z: -3.79};
|
|
||||||
var zero = {x: 0.0, y: 0.0, z: 0.0};
|
|
||||||
|
|
||||||
if (p.position) {
|
if (p.position) {
|
||||||
|
/* Random near-zero value used as "zero" to prevent two sequential updates from being
|
||||||
|
exactly the same (which would cause them to be ignored) */
|
||||||
|
var nearZero = 0.0001 * Math.random() + 0.001;
|
||||||
|
/* Define the points that create the "NO ENTITIES ALLOWED" box */
|
||||||
|
var boxMin = {x: 25.5, y: -0.48, z: -9.9};
|
||||||
|
var boxMax = {x: 31.1, y: 4, z: -3.79};
|
||||||
|
/* Define the point that you want entites that enter the box to appear */
|
||||||
|
var resetPoint = {x: 29.5, y: 0.37 + nearZero, z: -2};
|
||||||
var x = p.position.x;
|
var x = p.position.x;
|
||||||
var y = p.position.y;
|
var y = p.position.y;
|
||||||
var z = p.position.z;
|
var z = p.position.z;
|
||||||
if ((x > boxMin.x && x < boxMax.x) &&
|
if ((x > boxMin.x && x < boxMax.x) &&
|
||||||
(y > boxMin.y && y < boxMax.y) &&
|
(y > boxMin.y && y < boxMax.y) &&
|
||||||
(z > boxMin.z && z < boxMax.z)) {
|
(z > boxMin.z && z < boxMax.z)) {
|
||||||
/* Move it to the origin of the zone */
|
p.position = resetPoint;
|
||||||
p.position = boxMin;
|
if (p.velocity) {
|
||||||
p.velocity = zero;
|
p.velocity = {x: 0, y: nearZero, z: 0};
|
||||||
p.acceleration = zero;
|
}
|
||||||
|
if (p.acceleration) {
|
||||||
|
p.acceleration = {x: 0, y: nearZero, z: 0};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ var DEFAULT_SCRIPTS = [
|
||||||
"system/goto.js",
|
"system/goto.js",
|
||||||
"system/marketplaces/marketplaces.js",
|
"system/marketplaces/marketplaces.js",
|
||||||
"system/edit.js",
|
"system/edit.js",
|
||||||
"system/users.js",
|
"system/tablet-users.js",
|
||||||
"system/selectAudioDevice.js",
|
"system/selectAudioDevice.js",
|
||||||
"system/notifications.js",
|
"system/notifications.js",
|
||||||
"system/controllers/controllerDisplayManager.js",
|
"system/controllers/controllerDisplayManager.js",
|
||||||
|
|
|
@ -25,6 +25,7 @@ Column {
|
||||||
"Lightmap:LightingModel:enableLightmap",
|
"Lightmap:LightingModel:enableLightmap",
|
||||||
"Background:LightingModel:enableBackground",
|
"Background:LightingModel:enableBackground",
|
||||||
"ssao:AmbientOcclusion:enabled",
|
"ssao:AmbientOcclusion:enabled",
|
||||||
|
"Textures:LightingModel:enableMaterialTexturing",
|
||||||
]
|
]
|
||||||
CheckBox {
|
CheckBox {
|
||||||
text: modelData.split(":")[0]
|
text: modelData.split(":")[0]
|
||||||
|
|
|
@ -854,7 +854,7 @@ function MyController(hand) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState = function(newState, reason) {
|
this.setState = function(newState, reason) {
|
||||||
if (isInEditMode() && (newState !== STATE_OFF &&
|
if ((isInEditMode() && this.grabbedEntity !== HMD.tabletID )&& (newState !== STATE_OFF &&
|
||||||
newState !== STATE_SEARCHING &&
|
newState !== STATE_SEARCHING &&
|
||||||
newState !== STATE_OVERLAY_STYLUS_TOUCHING)) {
|
newState !== STATE_OVERLAY_STYLUS_TOUCHING)) {
|
||||||
return;
|
return;
|
||||||
|
@ -1699,7 +1699,7 @@ function MyController(hand) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isTablet = function (entityID) {
|
this.isTablet = function (entityID) {
|
||||||
if (entityID === HMD.tabletID) { // XXX what's a better way to know this?
|
if (entityID === HMD.tabletID) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -2907,7 +2907,7 @@ function MyController(hand) {
|
||||||
var pos3D = intersectInfo.point;
|
var pos3D = intersectInfo.point;
|
||||||
|
|
||||||
if (this.state == STATE_OVERLAY_STYLUS_TOUCHING &&
|
if (this.state == STATE_OVERLAY_STYLUS_TOUCHING &&
|
||||||
!this.deadspotExpired &&
|
!this.tabletStabbed &&
|
||||||
intersectInfo.distance < WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE) {
|
intersectInfo.distance < WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE) {
|
||||||
// they've stabbed the tablet, don't send events until they pull back
|
// they've stabbed the tablet, don't send events until they pull back
|
||||||
this.tabletStabbed = true;
|
this.tabletStabbed = true;
|
||||||
|
@ -2915,8 +2915,15 @@ function MyController(hand) {
|
||||||
this.tabletStabbedPos3D = pos3D;
|
this.tabletStabbedPos3D = pos3D;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.tabletStabbed) {
|
if (this.tabletStabbed) {
|
||||||
return;
|
var origin = {x: this.tabletStabbedPos2D.x, y: this.tabletStabbedPos2D.y, z: 0};
|
||||||
|
var point = {x: pos2D.x, y: pos2D.y, z: 0};
|
||||||
|
var offset = Vec3.distance(origin, point);
|
||||||
|
var radius = 0.05;
|
||||||
|
if (offset < radius) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Overlays.keyboardFocusOverlay != this.grabbedOverlay) {
|
if (Overlays.keyboardFocusOverlay != this.grabbedOverlay) {
|
||||||
|
|
|
@ -210,7 +210,10 @@ var toolBar = (function () {
|
||||||
var button = toolBar.addButton({
|
var button = toolBar.addButton({
|
||||||
objectName: name,
|
objectName: name,
|
||||||
imageURL: imageUrl,
|
imageURL: imageUrl,
|
||||||
buttonState: 1,
|
imageOffOut: 1,
|
||||||
|
imageOffIn: 2,
|
||||||
|
imageOnOut: 0,
|
||||||
|
imageOnIn: 2,
|
||||||
alpha: 0.9,
|
alpha: 0.9,
|
||||||
visible: true
|
visible: true
|
||||||
});
|
});
|
||||||
|
|
465
scripts/system/html/users.html
Normal file
465
scripts/system/html/users.html
Normal file
|
@ -0,0 +1,465 @@
|
||||||
|
<!--
|
||||||
|
// users.html
|
||||||
|
//
|
||||||
|
// Created by Faye Li on 18 Jan 2017
|
||||||
|
// Copyright 2017 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
-->
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Users Online</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Raleway:300,400,600,700"" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
font-family: 'Raleway', sans-serif;
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(#2b2b2b, #0f212e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 90px;
|
||||||
|
background: linear-gradient(#2b2b2b, #1e1e1e);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar .myContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 30px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#refresh-button {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-info-div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#visibility-toggle {
|
||||||
|
font-family: 'Raleway';
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 13px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
vertical-align: top;
|
||||||
|
height: 28px;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 0px 18px;
|
||||||
|
margin-right: 0px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
color: #121212;
|
||||||
|
background-color: #afafaf;
|
||||||
|
background: linear-gradient(#fff 20%, #afafaf 100%);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#visibility-toggle:enabled:hover {
|
||||||
|
background: linear-gradient(#fff, #fff);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#visibility-toggle:active {
|
||||||
|
background: linear-gradient(#afafaf, #afafaf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#visibility-toggle span {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs li {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs li.current {
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.current {
|
||||||
|
display: inherit;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 15px 0px 15px 15px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content ul li {
|
||||||
|
padding: 2px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button] {
|
||||||
|
font-family: 'Raleway';
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 13px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
vertical-align: top;
|
||||||
|
height: 28px;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 0px 18px;
|
||||||
|
margin-right: 6px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #000;
|
||||||
|
background: linear-gradient(#343434 20%, #000 100%);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button].red {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #94132e;
|
||||||
|
background: linear-gradient(#d42043 20%, #94132e 100%);
|
||||||
|
}
|
||||||
|
input[type=button].blue {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1080b8;
|
||||||
|
background: linear-gradient(#00b4ef 20%, #1080b8 100%);
|
||||||
|
}
|
||||||
|
input[type=button].white {
|
||||||
|
color: #121212;
|
||||||
|
background-color: #afafaf;
|
||||||
|
background: linear-gradient(#fff 20%, #afafaf 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button]:enabled:hover {
|
||||||
|
background: linear-gradient(#000, #000);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
input[type=button].red:enabled:hover {
|
||||||
|
background: linear-gradient(#d42043, #d42043);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
input[type=button].blue:enabled:hover {
|
||||||
|
background: linear-gradient(#00b4ef, #00b4ef);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
input[type=button].white:enabled:hover {
|
||||||
|
background: linear-gradient(#fff, #fff);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button]:active {
|
||||||
|
background: linear-gradient(#343434, #343434);
|
||||||
|
}
|
||||||
|
input[type=button].red:active {
|
||||||
|
background: linear-gradient(#94132e, #94132e);
|
||||||
|
}
|
||||||
|
input[type=button].blue:active {
|
||||||
|
background: linear-gradient(#1080b8, #1080b8);
|
||||||
|
}
|
||||||
|
input[type=button].white:active {
|
||||||
|
background: linear-gradient(#afafaf, #afafaf);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button]:disabled {
|
||||||
|
color: #252525;
|
||||||
|
background: linear-gradient(#575757 20%, #252525 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button][pressed=pressed] {
|
||||||
|
color: #00b4ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
#friends-button {
|
||||||
|
margin: 0px 0px 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Vertically Center Modal*/
|
||||||
|
.modal {
|
||||||
|
color: black;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal:before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu li {
|
||||||
|
color: #333;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu .divider {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu li:hover {
|
||||||
|
background: #dcdcdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu li h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu li p {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="myContainer">
|
||||||
|
<div>Users Online</div>
|
||||||
|
<img id="refresh-button" onclick="pollUsers()" src="https://hifi-content.s3.amazonaws.com/faye/tablet-dev/refresh-icon.svg"></img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div id="user-info-div">
|
||||||
|
<h4></h4>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button id="visibility-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
Online
|
||||||
|
<span class="glyphicon glyphicon-menu-down"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="visibility-toggle">
|
||||||
|
<li class="visibility-option" data-visibility="all">
|
||||||
|
<h6>Online</h6>
|
||||||
|
<p>You will be shown online to everyone else. Anybody will be able to find you from the users online list and jump to your current location.</p>
|
||||||
|
</li>
|
||||||
|
<li role="separator" class="divider"></li>
|
||||||
|
<li class="visibility-option" data-visibility="friends">
|
||||||
|
<h6>Available to Friends Only</h6>
|
||||||
|
<p>You will be shown online only to users you have added as friends. Other users may still interact with you in the same domain, but they won't be able to find you from the users online list.</p>
|
||||||
|
</li>
|
||||||
|
<li role="separator" class="divider"></li>
|
||||||
|
<li class="visibility-option" data-visibility="none">
|
||||||
|
<h6>Appear Offline</h6>
|
||||||
|
<p>No one will be able to find you from the users online list. However, this does not prevent other users in the same domain from interacting with you. For a complete "Do not disturb" mode, you may want to go to your own private domain and set allow entering to no one.</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="tabs">
|
||||||
|
<li tab-id="tab-1" class="current">Everyone (0)</li>
|
||||||
|
<li tab-id="tab-2">Friends (0)</li>
|
||||||
|
</ul>
|
||||||
|
<div id="tab-1" class="tab-content current">
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
<div id="tab-2" class="tab-content">
|
||||||
|
<ul></ul>
|
||||||
|
<input type="button" class="blue" id="friends-button" value="Add/Remove Friends">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
<h4 class="modal-title" id="myModalLabel">Jump to username @ Placename</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
This will teleport you to a new location and possibly another domain. Are you sure?
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="button" data-dismiss="modal" value="Cancel">
|
||||||
|
<input type="button" data-dismiss="modal" id="jump-to-confirm-button" value="OK">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
var METAVERSE_API_URL = "https://metaverse.highfidelity.com/api/v1/users?status=online";
|
||||||
|
var FRIENDS_FILTER = "&filter=friends";
|
||||||
|
var myUsername = null;
|
||||||
|
var myVisibility = null;
|
||||||
|
|
||||||
|
function displayUsers(data, element) {
|
||||||
|
element.empty();
|
||||||
|
for (var i = 0; i < data.users.length; i++) {
|
||||||
|
// Don't display users who aren't in a domain
|
||||||
|
if (typeof data.users[i].location.root.name === "undefined") {
|
||||||
|
console.log(data.users[i].username + "is online but not in a domain");
|
||||||
|
$("#dev-div").append("<p>" + data.users[i].username + "is online but not in a domain</p>");
|
||||||
|
} else {
|
||||||
|
$("#dev-div").append("<li>" + data.users[i].username + " @ " + data.users[i].location.root.name + "</li>");
|
||||||
|
if (data.users[i].username === myUsername) {
|
||||||
|
$("#user-info-div h4").text(data.users[i].username + " @ " + data.users[i].location.root.name);
|
||||||
|
} else {
|
||||||
|
console.log(data.users[i].username + " @ " + data.users[i].location.root.name);
|
||||||
|
// Create a list item and put user info in data-* attributes, also make it trigger the jump to confirmation modal
|
||||||
|
$("<li></li>", {
|
||||||
|
"data-toggle": "modal",
|
||||||
|
"data-target": "#myModal",
|
||||||
|
"data-username": data.users[i].username,
|
||||||
|
"data-placename": data.users[i].location.root.name,
|
||||||
|
text: data.users[i].username + " @ " + data.users[i].location.root.name
|
||||||
|
}).appendTo(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processData(data, type) {
|
||||||
|
var num = data.users.length;
|
||||||
|
if (type === "everyone") {
|
||||||
|
$(".tabs li:nth-child(1)").text("Everyone (" + num + ")");
|
||||||
|
displayUsers(data, $("#tab-1 ul"));
|
||||||
|
} else if (type === "friends") {
|
||||||
|
$(".tabs li:nth-child(2)").text("Friends (" + num + ")");
|
||||||
|
displayUsers(data, $("#tab-2 ul"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollUsers() {
|
||||||
|
$("#dev-div").append("<p>polling users..</p>");
|
||||||
|
$.ajax({
|
||||||
|
url: METAVERSE_API_URL,
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
$("#dev-div").append("<p>polling everyone sucess</p>");
|
||||||
|
processData(response.data, "everyone");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$.ajax({
|
||||||
|
url: METAVERSE_API_URL + FRIENDS_FILTER,
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
$("#dev-div").append("<p>polling friends sucess</p>");
|
||||||
|
processData(response.data, "friends");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScriptEventReceived(event) {
|
||||||
|
$("#dev-div").append("<p>Received a script event, its type is " + typeof event + "</p>");
|
||||||
|
if (typeof event === "string") {
|
||||||
|
// Parse the string into an object
|
||||||
|
event = JSON.parse(event);
|
||||||
|
}
|
||||||
|
if (event.type === "user-info") {
|
||||||
|
myUsername = event.data.username;
|
||||||
|
myVisibility = event.data.visibility;
|
||||||
|
$("#user-info-div h4").text(myUsername);
|
||||||
|
var buttonText = "Online";
|
||||||
|
if (myVisibility === "none") {
|
||||||
|
buttonText = "Appear Offline";
|
||||||
|
} else if (myVisibility === "friends") {
|
||||||
|
buttonText = "Available To Friends Only";
|
||||||
|
}
|
||||||
|
$("#visibility-toggle").html(buttonText + "<span class='glyphicon glyphicon-menu-down'></span>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
$("#dev-div").append("<p>ready</p>");
|
||||||
|
// Auto-load user lists when page loads
|
||||||
|
pollUsers();
|
||||||
|
|
||||||
|
// Click listener for tabs
|
||||||
|
$(".tabs li").click(function() {
|
||||||
|
var tabID = $(this).attr("tab-id");
|
||||||
|
$(".tab-content").removeClass("current");
|
||||||
|
$("#" + tabID).addClass("current");
|
||||||
|
$(".tabs li").removeClass("current");
|
||||||
|
$(this).addClass("current");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jump to user: Fill confirmation modal with data of the selected user
|
||||||
|
$("#myModal").on("show.bs.modal", function (event) {
|
||||||
|
// Get the element that triggered the modal
|
||||||
|
var li = $(event.relatedTarget);
|
||||||
|
// Extract info from data-* attributes
|
||||||
|
var username = li.data("username");
|
||||||
|
var placename = li.data("placename");
|
||||||
|
// Write info to the modal
|
||||||
|
var modal = $(this);
|
||||||
|
modal.find(".modal-title").text("Jump to " + username + " @ " + placename);
|
||||||
|
$("#jump-to-confirm-button").data("username", username);
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#jump-to-confirm-button").click(function() {
|
||||||
|
var jumpToObject = {
|
||||||
|
"type": "jump-to",
|
||||||
|
"data": {
|
||||||
|
"username": $(this).data("username")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify(jumpToObject));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click listener for toggling who can see me
|
||||||
|
$(".visibility-option").click(function() {
|
||||||
|
myVisibility = $(this).data("visibility");
|
||||||
|
var newButtonText = $(this).find("h6").text();
|
||||||
|
$("#visibility-toggle").html(newButtonText + "<span class='glyphicon glyphicon-menu-down'></span>");
|
||||||
|
var visibilityObject = {
|
||||||
|
"type": "toggle-visibility",
|
||||||
|
"data": {
|
||||||
|
"visibility": myVisibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify(visibilityObject));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for events from hifi
|
||||||
|
EventBridge.scriptEventReceived.connect(onScriptEventReceived);
|
||||||
|
|
||||||
|
// Send a ready event to hifi
|
||||||
|
var eventObject = {"type": "ready"};
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify(eventObject));
|
||||||
|
|
||||||
|
// Click listener for add/remove friends button
|
||||||
|
$("#friends-button").click(function() {
|
||||||
|
// Send a manage friends event to hifi
|
||||||
|
eventObject = {"type": "manage-friends"};
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify(eventObject));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -24,31 +24,33 @@ var CAMERA_MATRIX = -7;
|
||||||
var ROT_Y_180 = {x: 0, y: 1, z: 0, w: 0};
|
var ROT_Y_180 = {x: 0, y: 1, z: 0, w: 0};
|
||||||
var TABLET_TEXTURE_RESOLUTION = { x: 480, y: 706 };
|
var TABLET_TEXTURE_RESOLUTION = { x: 480, y: 706 };
|
||||||
var INCHES_TO_METERS = 1 / 39.3701;
|
var INCHES_TO_METERS = 1 / 39.3701;
|
||||||
|
|
||||||
var AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}";
|
var AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}";
|
||||||
|
|
||||||
var TABLET_URL = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx";
|
var TABLET_URL = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx";
|
||||||
|
var NO_HANDS = -1;
|
||||||
|
|
||||||
// will need to be recaclulated if dimensions of fbx model change.
|
// will need to be recaclulated if dimensions of fbx model change.
|
||||||
var TABLET_NATURAL_DIMENSIONS = {x: 33.797, y: 50.129, z: 2.269};
|
var TABLET_NATURAL_DIMENSIONS = {x: 33.797, y: 50.129, z: 2.269};
|
||||||
var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png";
|
var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png";
|
||||||
var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx";
|
var TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx";
|
||||||
// returns object with two fields:
|
// returns object with two fields:
|
||||||
// * position - position in front of the user
|
// * position - position in front of the user
|
||||||
// * rotation - rotation of entity so it faces the user.
|
// * rotation - rotation of entity so it faces the user.
|
||||||
function calcSpawnInfo(hand, height) {
|
function calcSpawnInfo(hand, height) {
|
||||||
var noHands = -1;
|
|
||||||
var finalPosition;
|
var finalPosition;
|
||||||
if (HMD.active && hand !== noHands) {
|
|
||||||
|
var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position;
|
||||||
|
var headRot = (HMD.active && Camera.mode === "first person") ? HMD.orientation : Camera.orientation;
|
||||||
|
|
||||||
|
if (HMD.active && hand !== NO_HANDS) {
|
||||||
var handController = getControllerWorldLocation(hand, true);
|
var handController = getControllerWorldLocation(hand, true);
|
||||||
var controllerPosition = handController.position;
|
var controllerPosition = handController.position;
|
||||||
|
|
||||||
// compute the angle of the chord with length (height / 2)
|
// compute the angle of the chord with length (height / 2)
|
||||||
var theta = Math.asin(height / (2 * Vec3.distance(HMD.position, controllerPosition)));
|
var theta = Math.asin(height / (2 * Vec3.distance(headPos, controllerPosition)));
|
||||||
|
|
||||||
// then we can use this angle to rotate the vector between the HMD position and the center of the tablet.
|
// then we can use this angle to rotate the vector between the HMD position and the center of the tablet.
|
||||||
// this vector, u, will become our new look at direction.
|
// this vector, u, will become our new look at direction.
|
||||||
var d = Vec3.normalize(Vec3.subtract(HMD.position, controllerPosition));
|
var d = Vec3.normalize(Vec3.subtract(headPos, controllerPosition));
|
||||||
var w = Vec3.normalize(Vec3.cross(Y_AXIS, d));
|
var w = Vec3.normalize(Vec3.cross(Y_AXIS, d));
|
||||||
var q = Quat.angleAxis(theta * (180 / Math.PI), w);
|
var q = Quat.angleAxis(theta * (180 / Math.PI), w);
|
||||||
var u = Vec3.multiplyQbyV(q, d);
|
var u = Vec3.multiplyQbyV(q, d);
|
||||||
|
@ -66,8 +68,8 @@ function calcSpawnInfo(hand, height) {
|
||||||
rotation: lookAtRot
|
rotation: lookAtRot
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
var front = Quat.getFront(Camera.orientation);
|
var front = Quat.getFront(headRot);
|
||||||
finalPosition = Vec3.sum(Camera.position, Vec3.multiply(0.6, front));
|
finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, front));
|
||||||
var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, front, {x: 0, y: 1, z: 0});
|
var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, front, {x: 0, y: 1, z: 0});
|
||||||
return {
|
return {
|
||||||
position: finalPosition,
|
position: finalPosition,
|
||||||
|
@ -200,6 +202,11 @@ WebTablet = function (url, width, dpi, hand, clientOnly) {
|
||||||
_this.geometryChanged(geometry);
|
_this.geometryChanged(geometry);
|
||||||
};
|
};
|
||||||
Window.geometryChanged.connect(this.myGeometryChanged);
|
Window.geometryChanged.connect(this.myGeometryChanged);
|
||||||
|
|
||||||
|
this.myCameraModeChanged = function(newMode) {
|
||||||
|
_this.cameraModeChanged(newMode);
|
||||||
|
};
|
||||||
|
Camera.modeUpdated.connect(this.myCameraModeChanged);
|
||||||
};
|
};
|
||||||
|
|
||||||
WebTablet.prototype.setHomeButtonTexture = function() {
|
WebTablet.prototype.setHomeButtonTexture = function() {
|
||||||
|
@ -230,11 +237,11 @@ WebTablet.prototype.destroy = function () {
|
||||||
Controller.mouseReleaseEvent.disconnect(this.myMouseReleaseEvent);
|
Controller.mouseReleaseEvent.disconnect(this.myMouseReleaseEvent);
|
||||||
|
|
||||||
Window.geometryChanged.disconnect(this.myGeometryChanged);
|
Window.geometryChanged.disconnect(this.myGeometryChanged);
|
||||||
|
Camera.modeUpdated.disconnect(this.myCameraModeChanged);
|
||||||
};
|
};
|
||||||
|
|
||||||
WebTablet.prototype.geometryChanged = function (geometry) {
|
WebTablet.prototype.geometryChanged = function (geometry) {
|
||||||
if (!HMD.active) {
|
if (!HMD.active) {
|
||||||
var NO_HANDS = -1;
|
|
||||||
var tabletProperties = {};
|
var tabletProperties = {};
|
||||||
// compute position, rotation & parentJointIndex of the tablet
|
// compute position, rotation & parentJointIndex of the tablet
|
||||||
this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties);
|
this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties);
|
||||||
|
@ -290,7 +297,6 @@ WebTablet.prototype.onHmdChanged = function () {
|
||||||
Controller.mouseReleaseEvent.connect(this.myMouseReleaseEvent);
|
Controller.mouseReleaseEvent.connect(this.myMouseReleaseEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
var NO_HANDS = -1;
|
|
||||||
var tabletProperties = {};
|
var tabletProperties = {};
|
||||||
// compute position, rotation & parentJointIndex of the tablet
|
// compute position, rotation & parentJointIndex of the tablet
|
||||||
this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties);
|
this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties);
|
||||||
|
@ -330,6 +336,7 @@ WebTablet.prototype.cleanUpOldTablets = function() {
|
||||||
this.cleanUpOldTabletsOnJoint(SENSOR_TO_ROOM_MATRIX);
|
this.cleanUpOldTabletsOnJoint(SENSOR_TO_ROOM_MATRIX);
|
||||||
this.cleanUpOldTabletsOnJoint(CAMERA_MATRIX);
|
this.cleanUpOldTabletsOnJoint(CAMERA_MATRIX);
|
||||||
this.cleanUpOldTabletsOnJoint(65529);
|
this.cleanUpOldTabletsOnJoint(65529);
|
||||||
|
this.cleanUpOldTabletsOnJoint(65534);
|
||||||
};
|
};
|
||||||
|
|
||||||
WebTablet.prototype.unregister = function() {
|
WebTablet.prototype.unregister = function() {
|
||||||
|
@ -373,6 +380,18 @@ WebTablet.prototype.mousePressEvent = function (event) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
WebTablet.prototype.cameraModeChanged = function (newMode) {
|
||||||
|
// reposition the tablet.
|
||||||
|
// This allows HMD.position to reflect the new camera mode.
|
||||||
|
if (HMD.active) {
|
||||||
|
var self = this;
|
||||||
|
var tabletProperties = {};
|
||||||
|
// compute position, rotation & parentJointIndex of the tablet
|
||||||
|
self.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties);
|
||||||
|
Entities.editEntity(self.tabletEntityID, tabletProperties);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) {
|
function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) {
|
||||||
var rayDirectionDotPlaneNormal = Vec3.dot(rayDirection, planeNormal);
|
var rayDirectionDotPlaneNormal = Vec3.dot(rayDirection, planeNormal);
|
||||||
if (rayDirectionDotPlaneNormal > 0.00001 || rayDirectionDotPlaneNormal < -0.00001) {
|
if (rayDirectionDotPlaneNormal > 0.00001 || rayDirectionDotPlaneNormal < -0.00001) {
|
||||||
|
|
|
@ -1038,7 +1038,7 @@ SelectionDisplay = (function() {
|
||||||
if (entityIntersection.intersects &&
|
if (entityIntersection.intersects &&
|
||||||
(!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) {
|
(!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) {
|
||||||
|
|
||||||
if (HMD.tabletID == entityIntersection.entityID) {
|
if (HMD.tabletID === entityIntersection.entityID) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace";
|
||||||
var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page.
|
var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page.
|
||||||
var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html");
|
var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html");
|
||||||
var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js");
|
var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js");
|
||||||
var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png";
|
var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png";
|
||||||
|
|
||||||
// Event bridge messages.
|
// Event bridge messages.
|
||||||
var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD";
|
var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD";
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
//
|
//
|
||||||
|
|
||||||
var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png";
|
var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png";
|
||||||
(function() {
|
(function() {
|
||||||
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
||||||
var button = tablet.addButton({
|
var button = tablet.addButton({
|
||||||
|
|
|
@ -520,6 +520,7 @@ function onClicked() {
|
||||||
Controller.mouseMoveEvent.connect(handleMouseMoveEvent);
|
Controller.mouseMoveEvent.connect(handleMouseMoveEvent);
|
||||||
triggerMapping.enable();
|
triggerMapping.enable();
|
||||||
triggerPressMapping.enable();
|
triggerPressMapping.enable();
|
||||||
|
createAudioInterval();
|
||||||
} else {
|
} else {
|
||||||
off();
|
off();
|
||||||
}
|
}
|
||||||
|
|
98
scripts/system/tablet-users.js
Normal file
98
scripts/system/tablet-users.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
//
|
||||||
|
// users.js
|
||||||
|
//
|
||||||
|
// Created by Faye Li on 18 Jan 2017.
|
||||||
|
// Copyright 2017 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
(function() { // BEGIN LOCAL_SCOPE
|
||||||
|
var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html";
|
||||||
|
|
||||||
|
var FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends";
|
||||||
|
var FRIENDS_WINDOW_WIDTH = 290;
|
||||||
|
var FRIENDS_WINDOW_HEIGHT = 500;
|
||||||
|
var FRIENDS_WINDOW_TITLE = "Add/Remove Friends";
|
||||||
|
|
||||||
|
// Initialise visibility based on global service
|
||||||
|
var VISIBILITY_VALUES_SET = {};
|
||||||
|
VISIBILITY_VALUES_SET["all"] = true;
|
||||||
|
VISIBILITY_VALUES_SET["friends"] = true;
|
||||||
|
VISIBILITY_VALUES_SET["none"] = true;
|
||||||
|
var myVisibility;
|
||||||
|
if (GlobalServices.findableBy in VISIBILITY_VALUES_SET) {
|
||||||
|
myVisibility = GlobalServices.findableBy;
|
||||||
|
} else {
|
||||||
|
// default to friends if it can't be determined
|
||||||
|
myVisibility = "friends";
|
||||||
|
GlobalServices.findableBy = myVisibilty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
||||||
|
var button = tablet.addButton({
|
||||||
|
icon: "icons/tablet-icons/people-i.svg",
|
||||||
|
text: "Users"
|
||||||
|
});
|
||||||
|
|
||||||
|
function onClicked() {
|
||||||
|
tablet.gotoWebScreen(USERS_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWebEventReceived(event) {
|
||||||
|
print("Script received a web event, its type is " + typeof event);
|
||||||
|
if (typeof event === "string") {
|
||||||
|
event = JSON.parse(event);
|
||||||
|
}
|
||||||
|
if (event.type === "ready") {
|
||||||
|
// send username to html
|
||||||
|
var myUsername = GlobalServices.username;
|
||||||
|
var object = {
|
||||||
|
"type": "user-info",
|
||||||
|
"data": {
|
||||||
|
"username": myUsername,
|
||||||
|
"visibility": myVisibility
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tablet.emitScriptEvent(JSON.stringify(object));
|
||||||
|
}
|
||||||
|
if (event.type === "manage-friends") {
|
||||||
|
// open a web overlay to metaverse friends page
|
||||||
|
var friendsWindow = new OverlayWebWindow({
|
||||||
|
title: FRIENDS_WINDOW_TITLE,
|
||||||
|
width: FRIENDS_WINDOW_WIDTH,
|
||||||
|
height: FRIENDS_WINDOW_HEIGHT,
|
||||||
|
visible: false
|
||||||
|
});
|
||||||
|
friendsWindow.setURL(FRIENDS_WINDOW_URL);
|
||||||
|
friendsWindow.setVisible(true);
|
||||||
|
friendsWindow.raise();
|
||||||
|
}
|
||||||
|
if (event.type === "jump-to") {
|
||||||
|
if (typeof event.data.username !== undefined) {
|
||||||
|
// teleport to selected user from the online users list
|
||||||
|
location.goToUser(event.data.username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.type === "toggle-visibility") {
|
||||||
|
if (typeof event.data.visibility !== undefined) {
|
||||||
|
// update your visibility (all, friends, or none)
|
||||||
|
myVisibility = event.data.visibility;
|
||||||
|
GlobalServices.findableBy = myVisibility;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.clicked.connect(onClicked);
|
||||||
|
tablet.webEventReceived.connect(onWebEventReceived);
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
button.clicked.disconnect(onClicked);
|
||||||
|
tablet.removeButton(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
Script.scriptEnding.connect(cleanup);
|
||||||
|
}()); // END LOCAL_SCOPE
|
21
unpublishedScripts/marketplace/teaLight/teaLight.js
Normal file
21
unpublishedScripts/marketplace/teaLight/teaLight.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
(function() {
|
||||||
|
var MINIMUM_LIGHT_INTENSITY = 100.0;
|
||||||
|
var MAXIMUM_LIGHT_INTENSITY = 125.0;
|
||||||
|
|
||||||
|
// Return a random number between `low` (inclusive) and `high` (exclusive)
|
||||||
|
function randFloat(low, high) {
|
||||||
|
return low + Math.random() * (high - low);
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
this.preload = function(entityID) {
|
||||||
|
self.intervalID = Script.setInterval(function() {
|
||||||
|
Entities.editEntity(entityID, {
|
||||||
|
intensity: randFloat(MINIMUM_LIGHT_INTENSITY, MAXIMUM_LIGHT_INTENSITY)
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
this.unload = function() {
|
||||||
|
Script.clearInterval(self.intervalID);
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in a new issue