Merge pull request #9320 from sethalves/tablet-ui

Tablet ui
This commit is contained in:
Seth Alves 2017-01-05 15:43:00 -08:00 committed by GitHub
commit ce4dabfcf4
60 changed files with 1506 additions and 431 deletions

View file

@ -32,6 +32,7 @@
#include "AudioMixerClientData.h"
#include "AvatarAudioStream.h"
#include "InjectedAudioStream.h"
#include "AudioHelpers.h"
#include "AudioMixerSlave.h"
@ -406,63 +407,6 @@ void AudioMixerSlave::addStreamToMix(AudioMixerClientData& listenerNodeData, con
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
}
const int IEEE754_MANT_BITS = 23;
const int IEEE754_EXPN_BIAS = 127;
//
// for x > 0.0f, returns log2(x)
// for x <= 0.0f, returns large negative value
//
// abs |error| < 8e-3, smooth (exact for x=2^N) for NPOLY=3
// abs |error| < 2e-4, smooth (exact for x=2^N) for NPOLY=5
// rel |error| < 0.4 from precision loss very close to 1.0f
//
static inline float fastlog2(float x) {
union { float f; int32_t i; } mant, bits = { x };
// split into mantissa and exponent
mant.i = (bits.i & ((1 << IEEE754_MANT_BITS) - 1)) | (IEEE754_EXPN_BIAS << IEEE754_MANT_BITS);
int32_t expn = (bits.i >> IEEE754_MANT_BITS) - IEEE754_EXPN_BIAS;
mant.f -= 1.0f;
// polynomial for log2(1+x) over x=[0,1]
//x = (-0.346555386f * mant.f + 1.346555386f) * mant.f;
x = (((-0.0821307180f * mant.f + 0.321188984f) * mant.f - 0.677784014f) * mant.f + 1.43872575f) * mant.f;
return x + expn;
}
//
// for -126 <= x < 128, returns exp2(x)
//
// rel |error| < 3e-3, smooth (exact for x=N) for NPOLY=3
// rel |error| < 9e-6, smooth (exact for x=N) for NPOLY=5
//
static inline float fastexp2(float x) {
union { float f; int32_t i; } xi;
// bias such that x > 0
x += IEEE754_EXPN_BIAS;
//x = MAX(x, 1.0f);
//x = MIN(x, 254.9999f);
// split into integer and fraction
xi.i = (int32_t)x;
x -= xi.i;
// construct exp2(xi) as a float
xi.i <<= IEEE754_MANT_BITS;
// polynomial for exp2(x) over x=[0,1]
//x = (0.339766028f * x + 0.660233972f) * x + 1.0f;
x = (((0.0135557472f * x + 0.0520323690f) * x + 0.241379763f) * x + 0.693032121f) * x + 1.0f;
return x * xi.f;
}
float AudioMixerSlave::gainForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
const glm::vec3& relativePosition, bool isEcho) {
float gain = 1.0f;
@ -514,7 +458,7 @@ float AudioMixerSlave::gainForSource(const AvatarAudioStream& listeningNodeStrea
g = (g > 1.0f) ? 1.0f : g;
// calculate the distance coefficient using the distance to this node
float distanceCoefficient = fastexp2(fastlog2(g) * fastlog2(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE));
float distanceCoefficient = fastExp2f(fastLog2f(g) * fastLog2f(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE));
// multiply the current attenuation coefficient by the distance coefficient
gain *= distanceCoefficient;

View file

@ -50,6 +50,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch);
@ -108,6 +109,13 @@ void AvatarMixer::broadcastAvatarData() {
const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES;
const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO;
// only send extra avatar data (avatars out of view, ignored) every Nth AvatarData frame
// Extra avatar data will be sent (AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND/EXTRA_AVATAR_DATA_FRAME_RATIO) times
// per second.
// This value should be a power of two for performance purposes, as the mixer performs a modulo operation every frame
// to determine whether the extra data should be sent.
const int EXTRA_AVATAR_DATA_FRAME_RATIO = 16;
// NOTE: The following code calculates the _performanceThrottlingRatio based on how much the avatar-mixer was
// able to sleep. This will eventually be used to ask for an additional avatar-mixer to help out. Currently the value
// is unused as it is assumed this should not be hit before the avatar-mixer hits the desired bandwidth limit per client.
@ -205,6 +213,15 @@ void AvatarMixer::broadcastAvatarData() {
// use the data rate specifically for avatar data for FRD adjustment checks
float avatarDataRateLastSecond = nodeData->getOutboundAvatarDataKbps();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that are not in the view frustrum
bool getsOutOfView = nodeData->getRequestsDomainListData();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that they've ignored
bool getsIgnoredByMe = getsOutOfView;
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them
bool getsAnyIgnored = getsIgnoredByMe && node->getCanKick();
// Check if it is time to adjust what we send this client based on the observed
// bandwidth to this node. We do this once a second, which is also the window for
// the bandwidth reported by node->getOutboundBandwidth();
@ -275,14 +292,14 @@ void AvatarMixer::broadcastAvatarData() {
// or that has ignored the viewing node
if (!otherNode->getLinkedData()
|| otherNode->getUUID() == node->getUUID()
|| node->isIgnoringNodeWithID(otherNode->getUUID())
|| otherNode->isIgnoringNodeWithID(node->getUUID())) {
|| (node->isIgnoringNodeWithID(otherNode->getUUID()) && !getsIgnoredByMe)
|| (otherNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) {
return false;
} else {
AvatarMixerClientData* otherData = reinterpret_cast<AvatarMixerClientData*>(otherNode->getLinkedData());
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
// Check to see if the space bubble is enabled
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) {
if ((node->isIgnoreRadiusEnabled() && !getsIgnoredByMe) || (otherNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
// Define the minimum bubble size
static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
// Define the scale of the box for the current node
@ -333,7 +350,6 @@ void AvatarMixer::broadcastAvatarData() {
&& (forceSend
|| otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp
|| distribution(generator) < IDENTITY_SEND_PROBABILITY)) {
sendIdentityPacket(otherNodeData, node);
}
@ -349,6 +365,7 @@ void AvatarMixer::broadcastAvatarData() {
maxAvatarDistanceThisFrame = std::max(maxAvatarDistanceThisFrame, distanceToAvatar);
if (distanceToAvatar != 0.0f
&& !getsOutOfView
&& distribution(generator) > (nodeData->getFullRateDistance() / distanceToAvatar)) {
return;
}
@ -380,15 +397,21 @@ void AvatarMixer::broadcastAvatarData() {
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNodeData->getLastReceivedSequenceNumber());
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
// determine if avatar is in view, to determine how much data to include...
glm::vec3 otherNodeBoxScale = (otherNodeData->getPosition() - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
bool isInView = nodeData->otherAvatarInView(otherNodeBox);
// this throttles the extra data to only be sent every Nth message
if (!isInView && getsOutOfView && (lastSeqToReceiver % EXTRA_AVATAR_DATA_FRAME_RATIO > 0)) {
return;
}
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
AvatarData::AvatarDataDetail detail;
if (!nodeData->otherAvatarInView(otherNodeBox)) {
if (!isInView && !getsOutOfView) {
detail = AvatarData::MinimumData;
nodeData->incrementAvatarOutOfView();
} else {
@ -527,6 +550,20 @@ void AvatarMixer::handleViewFrustumPacket(QSharedPointer<ReceivedMessage> messag
}
}
void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->getOrCreateLinkedData(senderNode);
if (senderNode->getLinkedData()) {
AvatarMixerClientData* nodeData = dynamic_cast<AvatarMixerClientData*>(senderNode->getLinkedData());
if (nodeData != nullptr) {
bool isRequesting;
message->readPrimitive(&isRequesting);
nodeData->setRequestsDomainListData(isRequesting);
}
}
}
void AvatarMixer::handleAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->updateNodeWithDataFromPacket(message, senderNode);

View file

@ -42,6 +42,7 @@ private slots:
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);

View file

@ -101,6 +101,8 @@ public:
void incrementAvatarOutOfView() { _recentOtherAvatarsOutOfView++; }
const QString& getBaseDisplayName() { return _baseDisplayName; }
void setBaseDisplayName(const QString& baseDisplayName) { _baseDisplayName = baseDisplayName; }
bool getRequestsDomainListData() { return _requestsDomainListData; }
void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; }
private:
AvatarSharedPointer _avatar { new AvatarData() };
@ -129,6 +131,7 @@ private:
int _recentOtherAvatarsInView { 0 };
int _recentOtherAvatarsOutOfView { 0 };
QString _baseDisplayName{}; // The santized key used in determinging unique sessionDisplayName, so that we can remove from dictionary.
bool _requestsDomainListData { false };
};
#endif // hifi_AvatarMixerClientData_h

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View file

@ -79,6 +79,10 @@ Item {
StatText {
text: "Avatar Simrate: " + root.avatarSimrate
}
StatText {
text: "Missed Frame Count: " + root.appdropped;
visible: root.appdropped > 0;
}
StatText {
text: "Packets In/Out: " + root.packetInCount + "/" + root.packetOutCount
}

View file

@ -72,6 +72,18 @@ Original.CheckBox {
border.color: hifi.colors.checkboxCheckedBorder
visible: checked && !pressed || !checked && pressed
}
Rectangle {
id: disabledOverlay
visible: !enabled
width: boxSize
height: boxSize
radius: boxRadius
border.width: 1
border.color: hifi.colors.baseGrayHighlight
color: hifi.colors.baseGrayHighlight
opacity: 0.5
}
}
label: Label {

View file

@ -35,6 +35,7 @@ Row {
property int usernameTextHeight: 12
property real audioLevel: 0.0
/* User image commented out for now - will probably be re-introduced later.
Column {
id: avatarImage
// Size
@ -48,10 +49,11 @@ Row {
height: parent.height
}
}
*/
Column {
id: textContainer
// Size
width: parent.width - avatarImage.width - parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing
width: parent.width - /*avatarImage.width - */parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing
height: contentHeight
// DisplayName Text

View file

@ -40,7 +40,11 @@ Item {
property int myCardHeight: 70
property int rowHeight: 70
property int actionButtonWidth: 75
property int nameCardWidth: width - actionButtonWidth*(iAmAdmin ? 4 : 2)
property int nameCardWidth: width - actionButtonWidth*(iAmAdmin ? 4 : 2) - 4
property var myData: ({displayName: "", userName: "", audioLevel: 0.0}) // valid dummy until set
property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring.
property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities.
property bool iAmAdmin: false
// This contains the current user's NameCard and will contain other information in the future
Rectangle {
@ -85,7 +89,7 @@ Item {
Rectangle {
id: adminTab
// Size
width: actionButtonWidth * 2 - 2
width: actionButtonWidth * 2 + 2
height: 40
// Anchors
anchors.bottom: myInfo.bottom
@ -100,6 +104,7 @@ Item {
border.width: 2
// "ADMIN" text
RalewaySemiBold {
id: adminTabText
text: "ADMIN"
// Text size
size: hifi.fontSizes.tableHeading + 2
@ -169,7 +174,9 @@ Item {
movable: false
resizable: false
}
model: userModel
model: ListModel {
id: userModel
}
// This Rectangle refers to each Row in the table.
rowDelegate: Rectangle { // The only way I know to specify a row height.
@ -183,16 +190,17 @@ Item {
// This Item refers to the contents of each Cell
itemDelegate: Item {
id: itemCell
property bool isCheckBox: typeof(styleData.value) === 'boolean'
property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"
property bool isButton: styleData.role === "mute" || styleData.role === "kick"
// This NameCard refers to the cell that contains an avatar's
// DisplayName and UserName
NameCard {
id: nameCard
// Properties
displayName: styleData.value
userName: model.userName
audioLevel: model.audioLevel
visible: !isCheckBox
userName: model && model.userName
audioLevel: model && model.audioLevel
visible: !isCheckBox && !isButton
// Size
width: nameCardWidth
height: parent.height
@ -200,19 +208,69 @@ Item {
anchors.left: parent.left
}
// This CheckBox belongs in the columns that contain the action buttons ("Mute", "Ban", etc)
// This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now)
// KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox
// will appear in the "hovered" state. Hovering over the checkbox will fix it.
// Clicking on the sides of the sorting header doesn't cause this problem.
// I'm guessing this is a QT bug and not anything I can fix. I spent too long trying to work around it...
// I'm just going to leave the minor visual bug in.
HifiControls.CheckBox {
id: actionCheckBox
visible: isCheckBox
anchors.centerIn: parent
checked: model[styleData.role]
// If this is a "Personal Mute" checkbox, disable the checkbox if the "Ignore" checkbox is checked.
enabled: !(styleData.role === "personalMute" && model["ignore"])
boxSize: 24
onClicked: {
var newValue = !model[styleData.role]
var datum = userData[model.userIndex]
datum[styleData.role] = model[styleData.role] = newValue
userModel.setProperty(model.userIndex, styleData.role, newValue)
userModelData[model.userIndex][styleData.role] = newValue // Defensive programming
Users[styleData.role](model.sessionId, newValue)
if (styleData.role === "ignore") {
userModel.setProperty(model.userIndex, "personalMute", newValue)
userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming
if (newValue) {
ignored[model.sessionId] = userModelData[model.userIndex]
} else {
delete ignored[model.sessionId]
}
}
// http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript
// I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by
// "checked:" statement above.
checked = Qt.binding(function() { return (model[styleData.role])})
}
}
// This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now)
HifiControls.Button {
id: actionButton
color: 2 // Red
visible: isButton
anchors.centerIn: parent
width: 32
height: 24
onClicked: {
Users[styleData.role](model.sessionId)
// Just for now, while we cannot undo things:
userData.splice(model.userIndex, 1)
sortModel()
if (styleData.role === "kick") {
// Just for now, while we cannot undo "Ban":
userModel.remove(model.userIndex)
delete userModelData[model.userIndex] // Defensive programming
sortModel()
}
}
// muted/error glyphs
HiFiGlyphs {
text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted
// Size
size: parent.height*1.3
// Anchors
anchors.fill: parent
// Style
horizontalAlignment: Text.AlignHCenter
color: enabled ? hifi.buttons.textColor[actionButton.color]
: hifi.buttons.disabledTextColor[actionButton.colorScheme]
}
}
}
@ -256,7 +314,6 @@ Item {
onExited: reloadButton.color = (pressed ? hifi.colors.lightGrayText: hifi.colors.darkGray)
}
}
// Separator between user and admin functions
Rectangle {
// Size
@ -269,7 +326,7 @@ Item {
visible: iAmAdmin
color: hifi.colors.lightGrayText
}
// This Rectangle refers to the [?] popup button
// This Rectangle refers to the [?] popup button next to "NAMES"
Rectangle {
color: hifi.colors.tableBackgroundLight
width: 20
@ -297,7 +354,36 @@ Item {
onExited: helpText.color = hifi.colors.darkGray
}
}
// Explanitory popup upon clicking "[?]"
// This Rectangle refers to the [?] popup button next to "ADMIN"
Rectangle {
visible: iAmAdmin
color: adminTab.color
width: 20
height: 28
anchors.right: adminTab.right
anchors.rightMargin: 31
anchors.top: adminTab.top
anchors.topMargin: 2
RalewayRegular {
id: adminHelpText
text: "[?]"
size: hifi.fontSizes.tableHeading + 2
font.capitalization: Font.AllUppercase
color: hifi.colors.redHighlight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.fill: parent
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onClicked: adminPopup.visible = true
onEntered: adminHelpText.color = "#94132e"
onExited: adminHelpText.color = hifi.colors.redHighlight
}
}
// Explanitory popup upon clicking "[?]" next to "NAMES"
Item {
visible: false
id: namesPopup
@ -309,18 +395,22 @@ Item {
radius: hifi.dimensions.borderRadius
}
Rectangle {
width: Math.min(parent.width * 0.75, 400)
height: popupText.contentHeight*2
width: Math.max(parent.width * 0.75, 400)
height: popupText.contentHeight*1.5
anchors.centerIn: parent
radius: hifi.dimensions.borderRadius
color: "white"
FiraSansSemiBold {
id: popupText
text: "This is temporary text. It will eventually be used to explain what 'Names' means."
text: "Bold names in the list are Avatar Display Names.\n" +
"If a Display Name isn't set, a unique Session Display Name is assigned." +
"\n\nAdministrators of this domain can also see the Username or Machine ID associated with each avatar present."
size: hifi.fontSizes.textFieldInput
color: hifi.colors.darkGray
horizontalAlignment: Text.AlignHCenter
anchors.fill: parent
anchors.leftMargin: 15
anchors.rightMargin: 15
wrapMode: Text.WordWrap
}
}
@ -332,12 +422,47 @@ Item {
}
}
}
// Explanitory popup upon clicking "[?]" next to "ADMIN"
Item {
visible: false
id: adminPopup
anchors.fill: pal
Rectangle {
anchors.fill: parent
color: "black"
opacity: 0.5
radius: hifi.dimensions.borderRadius
}
Rectangle {
width: Math.max(parent.width * 0.75, 400)
height: adminPopupText.contentHeight*1.5
anchors.centerIn: parent
radius: hifi.dimensions.borderRadius
color: "white"
FiraSansSemiBold {
id: adminPopupText
text: 'Silencing a user mutes their microphone. Silenced users can unmute themselves by clicking the "UNMUTE" button on their HUD.\n\n' +
"Banning a user will remove them from this domain and prevent them from returning. You can un-ban users from your domain's settings page."
size: hifi.fontSizes.textFieldInput
color: hifi.colors.darkGray
horizontalAlignment: Text.AlignHCenter
anchors.fill: parent
anchors.leftMargin: 15
anchors.rightMargin: 15
wrapMode: Text.WordWrap
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
adminPopup.visible = false
}
}
}
property var userData: []
property var myData: ({displayName: "", userName: "", audioLevel: 0.0}) // valid dummy until set
property bool iAmAdmin: false
function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml
var i, data = optionalData || userData, length = data.length;
var data = optionalData || userModelData, length = data.length;
for (var i = 0; i < length; i++) {
if (data[i].sessionId === sessionId) {
return i;
@ -349,11 +474,24 @@ Item {
switch (message.method) {
case 'users':
var data = message.params;
var myIndex = findSessionIndex('', data);
iAmAdmin = Users.canKick;
myData = data[myIndex];
data.splice(myIndex, 1);
userData = data;
var index = -1;
index = findSessionIndex('', data);
if (index !== -1) {
iAmAdmin = Users.canKick;
myData = data[index];
data.splice(index, 1);
} else {
console.log("This user's data was not found in the user list. PAL will not function properly.");
}
userModelData = data;
for (var ignoredID in ignored) {
index = findSessionIndex(ignoredID);
if (index === -1) { // Add back any missing ignored to the PAL, because they sometimes take a moment to show up.
userModelData.push(ignored[ignoredID]);
} else { // Already appears in PAL; update properties of existing element in model data
userModelData[index] = ignored[ignoredID];
}
}
sortModel();
break;
case 'select':
@ -378,11 +516,15 @@ Item {
myData.userName = userName;
myCard.userName = userName; // Defensive programming
} else {
// Get the index in userModel and userData associated with the passed UUID
// Get the index in userModel and userModelData associated with the passed UUID
var userIndex = findSessionIndex(userId);
// Set the userName appropriately
userModel.get(userIndex).userName = userName;
userData[userIndex].userName = userName; // Defensive programming
if (userIndex != -1) {
// Set the userName appropriately
userModel.setProperty(userIndex, "userName", userName);
userModelData[userIndex].userName = userName; // Defensive programming
} else {
console.log("updateUsername() called with unknown UUID: ", userId);
}
}
break;
case 'updateAudioLevel':
@ -393,25 +535,28 @@ Item {
myData.audioLevel = audioLevel;
myCard.audioLevel = audioLevel; // Defensive programming
} else {
console.log("userid:" + userId);
var userIndex = findSessionIndex(userId);
userModel.get(userIndex).audioLevel = audioLevel;
userData[userIndex].audioLevel = audioLevel; // Defensive programming
if (userIndex != -1) {
userModel.setProperty(userIndex, "audioLevel", audioLevel);
userModelData[userIndex].audioLevel = audioLevel; // Defensive programming
} else {
console.log("updateUsername() called with unknown UUID: ", userId);
}
}
}
break;
case 'clearIgnored':
ignored = {};
break;
default:
console.log('Unrecognized message:', JSON.stringify(message));
}
}
ListModel {
id: userModel
}
function sortModel() {
var sortProperty = table.getColumn(table.sortIndicatorColumn).role;
var before = (table.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1;
var after = -1 * before;
userData.sort(function (a, b) {
userModelData.sort(function (a, b) {
var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase();
switch (true) {
case (aValue < bValue): return before;
@ -420,9 +565,10 @@ Item {
}
});
table.selection.clear();
userModel.clear();
var userIndex = 0;
userData.forEach(function (datum) {
userModelData.forEach(function (datum) {
function init(property) {
if (datum[property] === undefined) {
datum[property] = false;
@ -437,7 +583,7 @@ Item {
function noticeSelection() {
var userIds = [];
table.selection.forEach(function (userIndex) {
userIds.push(userData[userIndex].sessionId);
userIds.push(userModelData[userIndex].sessionId);
});
pal.sendToScript({method: 'selected', params: userIds});
}

View file

@ -95,7 +95,7 @@ Item {
readonly property color tableRowDarkEven: "#1c1c1c" // Equivalent to "#a6181818" over #404040 background
readonly property color tableBackgroundLight: tableRowLightEven
readonly property color tableBackgroundDark: tableRowDarkEven
readonly property color tableScrollHandleLight: tableRowLightOdd
readonly property color tableScrollHandleLight: "#8F8F8F"
readonly property color tableScrollHandleDark: "#707070"
readonly property color tableScrollBackgroundLight: tableRowLightEven
readonly property color tableScrollBackgroundDark: "#323232"

View file

@ -101,6 +101,7 @@
#include <RecordingScriptingInterface.h>
#include <RenderableWebEntityItem.h>
#include <RenderShadowTask.h>
#include <render/RenderFetchCullSortTask.h>
#include <RenderDeferredTask.h>
#include <RenderForwardTask.h>
#include <ResourceCache.h>
@ -1138,6 +1139,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_settingsTimer.moveToThread(&_settingsThread);
_settingsTimer.setSingleShot(false);
_settingsTimer.setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable
_settingsThread.setPriority(QThread::LowestPriority);
_settingsThread.start();
if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) {
@ -1834,11 +1836,13 @@ void Application::initializeGL() {
// Set up the render engine
render::CullFunctor cullFunctor = LODManager::shouldRender;
_renderEngine->addJob<RenderShadowTask>("RenderShadowTask", cullFunctor);
const auto items = _renderEngine->addJob<RenderFetchCullSortTask>("FetchCullSort", cullFunctor);
assert(items.canCast<RenderFetchCullSortTask::Output>());
static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD";
if (QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD)) {
_renderEngine->addJob<RenderForwardTask>("RenderForwardTask", cullFunctor);
_renderEngine->addJob<RenderForwardTask>("RenderForwardTask", items.get<RenderFetchCullSortTask::Output>());
} else {
_renderEngine->addJob<RenderDeferredTask>("RenderDeferredTask", cullFunctor);
_renderEngine->addJob<RenderDeferredTask>("RenderDeferredTask", items.get<RenderFetchCullSortTask::Output>());
}
_renderEngine->load();
_renderEngine->registerScene(_main3DScene);

View file

@ -44,6 +44,7 @@
#include "Util.h"
#include "world.h"
#include "InterfaceLogging.h"
#include "SceneScriptingInterface.h"
#include "SoftAttachmentModel.h"
#include <Rig.h>
@ -292,7 +293,10 @@ void Avatar::updateAvatarEntities() {
}
}
void Avatar::setShouldDie() {
// This will cause the avatar to be shrunk away and removed (the actual Avatar gets removed), but then it comes back.
_owningAvatarMixer.clear();
}
void Avatar::simulate(float deltaTime) {
PerformanceTimer perfTimer("simulate");
@ -459,6 +463,7 @@ bool Avatar::addToScene(AvatarSharedPointer self, std::shared_ptr<render::Scene>
attachmentModel->addToScene(scene, pendingChanges);
}
_inScene = true;
return true;
}
@ -469,6 +474,7 @@ void Avatar::removeFromScene(AvatarSharedPointer self, std::shared_ptr<render::S
for (auto& attachmentModel : _attachmentModels) {
attachmentModel->removeFromScene(scene, pendingChanges);
}
_inScene = false;
}
void Avatar::updateRenderItem(render::PendingChanges& pendingChanges) {
@ -1328,3 +1334,21 @@ void Avatar::setParentJointIndex(quint16 parentJointIndex) {
}
}
}
void Avatar::addToScene(AvatarSharedPointer myHandle) {
render::ScenePointer scene = qApp->getMain3DScene();
if (scene) {
render::PendingChanges pendingChanges;
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderAvatars() && !DependencyManager::get<NodeList>()->isIgnoringNode(getSessionUUID())) {
addToScene(myHandle, scene, pendingChanges);
}
scene->enqueuePendingChanges(pendingChanges);
} else {
qCWarning(interfaceapp) << "AvatarManager::addAvatar() : Unexpected null scene, possibly during application shutdown";
}
}
void Avatar::ensureInScene(AvatarSharedPointer self) {
if (!_inScene) {
addToScene(self);
}
}

View file

@ -178,6 +178,8 @@ public:
glm::vec3 getUncachedRightPalmPosition() const;
glm::quat getUncachedRightPalmRotation() const;
Q_INVOKABLE void setShouldDie();
public slots:
// FIXME - these should be migrated to use Pose data instead
@ -254,6 +256,9 @@ protected:
ThreadSafeValueCache<glm::vec3> _rightPalmPositionCache { glm::vec3() };
ThreadSafeValueCache<glm::quat> _rightPalmRotationCache { glm::quat() };
void addToScene(AvatarSharedPointer self);
void ensureInScene(AvatarSharedPointer self);
private:
int _leftPointerGeometryID { 0 };
int _rightPointerGeometryID { 0 };
@ -262,6 +267,7 @@ private:
bool _shouldAnimate { true };
bool _shouldSkipRender { false };
bool _isLookAtTarget { false };
bool _inScene { false };
float getBoundingRadius() const;

View file

@ -82,7 +82,11 @@ AvatarManager::AvatarManager(QObject* parent) :
// when we hear that the user has ignored an avatar by session UUID
// immediately remove that avatar instead of waiting for the absence of packets from avatar mixer
connect(nodeList.data(), "ignoredNode", this, "removeAvatar");
connect(nodeList.data(), &NodeList::ignoredNode, this, [=](const QUuid& nodeID, bool enabled) {
if (enabled) {
removeAvatar(nodeID);
}
});
}
AvatarManager::~AvatarManager() {
@ -159,6 +163,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
removeAvatar(avatarIterator.key());
++avatarIterator;
} else {
avatar->ensureInScene(avatar);
avatar->simulate(deltaTime);
++avatarIterator;
@ -219,16 +224,7 @@ AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWe
auto newAvatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer);
auto rawRenderableAvatar = std::static_pointer_cast<Avatar>(newAvatar);
render::ScenePointer scene = qApp->getMain3DScene();
if (scene) {
render::PendingChanges pendingChanges;
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderAvatars()) {
rawRenderableAvatar->addToScene(rawRenderableAvatar, scene, pendingChanges);
}
scene->enqueuePendingChanges(pendingChanges);
} else {
qCWarning(interfaceapp) << "AvatarManager::addAvatar() : Unexpected null scene, possibly during application shutdown";
}
rawRenderableAvatar->addToScene(rawRenderableAvatar);
return newAvatar;
}

View file

@ -93,7 +93,6 @@ private:
// virtual overrides
virtual AvatarSharedPointer newSharedAvatar() override;
virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) override;
virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
QVector<AvatarSharedPointer> _avatarFades;

View file

@ -125,6 +125,8 @@ void Stats::updateStats(bool force) {
STAT_UPDATE(framerate, qApp->getFps());
if (qApp->getActiveDisplayPlugin()) {
auto displayPlugin = qApp->getActiveDisplayPlugin();
auto stats = displayPlugin->getHardwareStats();
STAT_UPDATE(appdropped, stats["app_dropped_frame_count"].toInt());
STAT_UPDATE(renderrate, displayPlugin->renderRate());
STAT_UPDATE(presentrate, displayPlugin->presentRate());
STAT_UPDATE(presentnewrate, displayPlugin->newFramePresentRate());

View file

@ -39,6 +39,8 @@ class Stats : public QQuickItem {
// How often the display device reprojecting old frames
STATS_PROPERTY(float, stutterrate, 0)
STATS_PROPERTY(int, appdropped, 0)
STATS_PROPERTY(float, presentnewrate, 0)
STATS_PROPERTY(float, presentdroprate, 0)
STATS_PROPERTY(int, simrate, 0)
@ -135,6 +137,7 @@ public slots:
void forceUpdateStats() { updateStats(true); }
signals:
void appdroppedChanged();
void framerateChanged();
void expandedChanged();
void timingExpandedChanged();

View file

@ -25,6 +25,7 @@
#include "AudioLogging.h"
#include "SoundCache.h"
#include "AudioSRC.h"
#include "AudioHelpers.h"
int audioInjectorPtrMetaTypeId = qRegisterMetaType<AudioInjector*>();
@ -187,7 +188,7 @@ bool AudioInjector::injectLocally() {
return success;
}
const uchar MAX_INJECTOR_VOLUME = 0xFF;
const uchar MAX_INJECTOR_VOLUME = packFloatGainToByte(1.0f);
static const int64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = -1;
static const int64_t NEXT_FRAME_DELTA_IMMEDIATELY = 0;
@ -333,7 +334,7 @@ int64_t AudioInjector::injectNextFrame() {
_currentPacket->writePrimitive(_options.position);
_currentPacket->writePrimitive(_options.orientation);
quint8 volume = MAX_INJECTOR_VOLUME * _options.volume;
quint8 volume = packFloatGainToByte(_options.volume);
_currentPacket->seek(volumeOptionOffset);
_currentPacket->writePrimitive(volume);

View file

@ -63,7 +63,7 @@ public:
AudioFOA& getLocalFOA() { return _localFOA; }
bool isLocalOnly() const { return _options.localOnly; }
float getVolume() const { return glm::clamp(_options.volume, 0.0f, 1.0f); }
float getVolume() const { return _options.volume; }
glm::vec3 getPosition() const { return _options.position; }
glm::quat getOrientation() const { return _options.orientation; }
bool isStereo() const { return _options.stereo; }

View file

@ -18,6 +18,7 @@
#include <UUID.h>
#include "InjectedAudioStream.h"
#include "AudioHelpers.h"
InjectedAudioStream::InjectedAudioStream(const QUuid& streamIdentifier, bool isStereo, int numStaticJitterFrames) :
PositionalAudioStream(PositionalAudioStream::Injector, isStereo, numStaticJitterFrames),
@ -25,8 +26,6 @@ InjectedAudioStream::InjectedAudioStream(const QUuid& streamIdentifier, bool isS
_radius(0.0f),
_attenuationRatio(0) {}
const uchar MAX_INJECTOR_VOLUME = 255;
int InjectedAudioStream::parseStreamProperties(PacketType type,
const QByteArray& packetAfterSeqNum,
int& numAudioSamples) {
@ -62,7 +61,7 @@ int InjectedAudioStream::parseStreamProperties(PacketType type,
quint8 attenuationByte = 0;
packetStream >> attenuationByte;
_attenuationRatio = attenuationByte / (float)MAX_INJECTOR_VOLUME;
_attenuationRatio = unpackFloatGainFromByte(attenuationByte);
packetStream >> _ignorePenumbra;

View file

@ -112,7 +112,7 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer<ReceivedMessage> mess
// make sure this isn't our own avatar data or for a previously ignored node
auto nodeList = DependencyManager::get<NodeList>();
if (sessionUUID != _lastOwnerSessionUUID && !nodeList->isIgnoringNode(sessionUUID)) {
if (sessionUUID != _lastOwnerSessionUUID && (!nodeList->isIgnoringNode(sessionUUID) || nodeList->getRequestsDomainListData())) {
auto avatar = newOrExistingAvatar(sessionUUID, sendingNode);
// have the matching (or new) avatar parse the data from the packet
@ -145,7 +145,7 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
identity.uuid = EMPTY;
}
}
if (!nodeList->isIgnoringNode(identity.uuid)) {
if (!nodeList->isIgnoringNode(identity.uuid) || nodeList->getRequestsDomainListData()) {
// mesh URL for a UUID, find avatar in our list
auto avatar = newOrExistingAvatar(identity.uuid, sendingNode);
avatar->processAvatarIdentity(identity);

View file

@ -165,6 +165,7 @@ public:
if (newPlugin) {
bool hasVsync = true;
QThread::setPriority(newPlugin->getPresentPriority());
bool wantVsync = newPlugin->wantVsync();
_context->makeCurrent();
#if defined(Q_OS_WIN)

View file

@ -14,6 +14,7 @@
#include <memory>
#include <queue>
#include <QtCore/QThread>
#include <QtCore/QTimer>
#include <QtGui/QImage>
@ -80,6 +81,7 @@ protected:
void updateCompositeFramebuffer();
virtual QThread::Priority getPresentPriority() { return QThread::HighPriority; }
virtual void compositeLayers();
virtual void compositeScene();
virtual void compositeOverlay();

View file

@ -81,17 +81,24 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) {
_clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile();
}
void Node::parseIgnoreRequestMessage(QSharedPointer<ReceivedMessage> message) {
void Node::parseIgnoreRequestMessage(QSharedPointer<ReceivedMessage> message) {
bool addToIgnore;
message->readPrimitive(&addToIgnore);
while (message->getBytesLeftToRead()) {
// parse out the UUID being ignored from the packet
QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
addIgnoredNode(ignoredUUID);
if (addToIgnore) {
addIgnoredNode(ignoredUUID);
} else {
removeIgnoredNode(ignoredUUID);
}
}
}
void Node::addIgnoredNode(const QUuid& otherNodeID) {
if (!otherNodeID.isNull() && otherNodeID != _uuid) {
QReadLocker lock { &_ignoredNodeIDSetLock };
qCDebug(networking) << "Adding" << uuidStringWithoutCurlyBraces(otherNodeID) << "to ignore set for"
<< uuidStringWithoutCurlyBraces(_uuid);
@ -102,6 +109,20 @@ void Node::addIgnoredNode(const QUuid& otherNodeID) {
}
}
void Node::removeIgnoredNode(const QUuid& otherNodeID) {
if (!otherNodeID.isNull() && otherNodeID != _uuid) {
// insert/find are read locked concurrently. unsafe_erase is not concurrent, and needs a write lock.
QWriteLocker lock { &_ignoredNodeIDSetLock };
qCDebug(networking) << "Removing" << uuidStringWithoutCurlyBraces(otherNodeID) << "from ignore set for"
<< uuidStringWithoutCurlyBraces(_uuid);
// remove the session UUID from the set of ignored ones for this listening node
_ignoredNodeIDSet.unsafe_erase(otherNodeID);
} else {
qCWarning(networking) << "Node::removeIgnoredNode called with null ID or ID of ignoring node.";
}
}
void Node::parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message) {
bool enabled;
message->readPrimitive(&enabled);

View file

@ -21,6 +21,7 @@
#include <QtCore/QSharedPointer>
#include <QtCore/QUuid>
#include <QReadLocker>
#include <UUIDHasher.h>
#include <tbb/concurrent_unordered_set.h>
@ -73,7 +74,8 @@ public:
void parseIgnoreRequestMessage(QSharedPointer<ReceivedMessage> message);
void addIgnoredNode(const QUuid& otherNodeID);
bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); }
void removeIgnoredNode(const QUuid& otherNodeID);
bool isIgnoringNodeWithID(const QUuid& nodeID) const { QReadLocker lock { &_ignoredNodeIDSetLock }; return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); }
void parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message);
friend QDataStream& operator<<(QDataStream& out, const Node& node);
@ -97,6 +99,7 @@ private:
MovingPercentile _clockSkewMovingPercentile;
NodePermissions _permissions;
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDSet;
mutable QReadWriteLock _ignoredNodeIDSetLock;
std::atomic_bool _ignoreRadiusEnabled;
};

View file

@ -242,6 +242,10 @@ void NodeList::reset() {
_ignoredSetLock.lockForWrite();
_ignoredNodeIDs.clear();
_ignoredSetLock.unlock();
// lock and clear our set of personally muted IDs
_personalMutedSetLock.lockForWrite();
_personalMutedNodeIDs.clear();
_personalMutedSetLock.unlock();
// refresh the owner UUID to the NULL UUID
setSessionUUID(QUuid());
@ -777,7 +781,7 @@ void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationN
sendPacket(std::move(ignorePacket), *destinationNode);
}
void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) {
void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) {
// enumerate the nodes to send a reliable ignore packet to each that can leverage it
if (!nodeID.isNull() && _sessionUUID != nodeID) {
eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool {
@ -786,25 +790,36 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) {
} else {
return false;
}
}, [&nodeID, this](const SharedNodePointer& destinationNode) {
}, [&nodeID, ignoreEnabled, this](const SharedNodePointer& destinationNode) {
// create a reliable NLPacket with space for the ignore UUID
auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID, true);
auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID + sizeof(bool), true);
ignorePacket->writePrimitive(ignoreEnabled);
// write the node ID to the packet
ignorePacket->write(nodeID.toRfc4122());
qCDebug(networking) << "Sending packet to ignore node" << uuidStringWithoutCurlyBraces(nodeID);
qCDebug(networking) << "Sending packet to" << (destinationNode->getType() == NodeType::AudioMixer ? "AudioMixer" : "AvatarMixer") << "to"
<< (ignoreEnabled ? "ignore" : "unignore") << "node" << uuidStringWithoutCurlyBraces(nodeID);
// send off this ignore packet reliably to the matching node
sendPacket(std::move(ignorePacket), *destinationNode);
});
QReadLocker setLocker { &_ignoredSetLock };
// add this nodeID to our set of ignored IDs
_ignoredNodeIDs.insert(nodeID);
emit ignoredNode(nodeID);
if (ignoreEnabled) {
QReadLocker ignoredSetLocker{ &_ignoredSetLock }; // read lock for insert
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
// add this nodeID to our set of ignored IDs
_ignoredNodeIDs.insert(nodeID);
// add this nodeID to our set of personal muted IDs
_personalMutedNodeIDs.insert(nodeID);
emit ignoredNode(nodeID, true);
} else {
QWriteLocker ignoredSetLocker{ &_ignoredSetLock }; // write lock for unsafe_erase
QWriteLocker personalMutedSetLocker{ &_personalMutedSetLock }; // write lock for unsafe_erase
_ignoredNodeIDs.unsafe_erase(nodeID);
_personalMutedNodeIDs.unsafe_erase(nodeID);
emit ignoredNode(nodeID, false);
}
} else {
qWarning() << "NodeList::ignoreNodeBySessionID called with an invalid ID or an ID which matches the current session ID.";
@ -812,21 +827,94 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) {
}
bool NodeList::isIgnoringNode(const QUuid& nodeID) const {
QReadLocker setLocker { &_ignoredSetLock };
QReadLocker ignoredSetLocker{ &_ignoredSetLock };
return _ignoredNodeIDs.find(nodeID) != _ignoredNodeIDs.cend();
}
void NodeList::personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled) {
// cannot personal mute yourself, or nobody
if (!nodeID.isNull() && _sessionUUID != nodeID) {
auto audioMixer = soloNodeOfType(NodeType::AudioMixer);
if (audioMixer) {
if (isIgnoringNode(nodeID)) {
qCDebug(networking) << "You can't personally mute or unmute a node you're already ignoring.";
}
else {
// setup the packet
auto personalMutePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID + sizeof(bool), true);
personalMutePacket->writePrimitive(muteEnabled);
// write the node ID to the packet
personalMutePacket->write(nodeID.toRfc4122());
qCDebug(networking) << "Sending Personal Mute Packet to" << (muteEnabled ? "mute" : "unmute") << "node" << uuidStringWithoutCurlyBraces(nodeID);
sendPacket(std::move(personalMutePacket), *audioMixer);
if (muteEnabled) {
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
// add this nodeID to our set of personal muted IDs
_personalMutedNodeIDs.insert(nodeID);
} else {
QWriteLocker personalMutedSetLocker{ &_personalMutedSetLock }; // write lock for unsafe_erase
_personalMutedNodeIDs.unsafe_erase(nodeID);
}
}
} else {
qWarning() << "Couldn't find audio mixer to send node personal mute request";
}
} else {
qWarning() << "NodeList::personalMuteNodeBySessionID called with an invalid ID or an ID which matches the current session ID.";
}
}
bool NodeList::isPersonalMutingNode(const QUuid& nodeID) const {
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock };
return _personalMutedNodeIDs.find(nodeID) != _personalMutedNodeIDs.cend();
}
void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) {
if (newNode->getType() == NodeType::AudioMixer || newNode->getType() == NodeType::AvatarMixer) {
if (newNode->getType() == NodeType::AudioMixer) {
// this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session,
// so send that list along now (assuming it isn't empty)
QReadLocker setLocker { &_ignoredSetLock };
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock };
if (_personalMutedNodeIDs.size() > 0) {
// setup a packet list so we can send the stream of ignore IDs
auto personalMutePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true);
// Force the "enabled" flag in this packet to true
personalMutePacketList->writePrimitive(true);
// enumerate the ignored IDs and write them to the packet list
auto it = _personalMutedNodeIDs.cbegin();
while (it != _personalMutedNodeIDs.end()) {
personalMutePacketList->write(it->toRfc4122());
++it;
}
// send this NLPacketList to the new node
sendPacketList(std::move(personalMutePacketList), *newNode);
}
// also send them the current ignore radius state.
sendIgnoreRadiusStateToNode(newNode);
}
if (newNode->getType() == NodeType::AvatarMixer) {
// this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session,
// so send that list along now (assuming it isn't empty)
QReadLocker ignoredSetLocker{ &_ignoredSetLock };
if (_ignoredNodeIDs.size() > 0) {
// setup a packet list so we can send the stream of ignore IDs
auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true);
// Force the "enabled" flag in this packet to true
ignorePacketList->writePrimitive(true);
// enumerate the ignored IDs and write them to the packet list
auto it = _ignoredNodeIDs.cbegin();
while (it != _ignoredNodeIDs.end()) {
@ -930,3 +1018,18 @@ void NodeList::processUsernameFromIDReply(QSharedPointer<ReceivedMessage> messag
emit usernameFromIDReply(nodeUUIDString, username, machineFingerprintString);
}
void NodeList::setRequestsDomainListData(bool isRequesting) {
// Tell the avatar mixer whether I want to receive any additional data to which I might be entitled
if (_requestsDomainListData == isRequesting) {
return;
}
eachMatchingNode([](const SharedNodePointer& node)->bool {
return node->getType() == NodeType::AvatarMixer;
}, [this, isRequesting](const SharedNodePointer& destinationNode) {
auto packet = NLPacket::create(PacketType::RequestsDomainListData, sizeof(bool), true); // reliable
packet->writePrimitive(isRequesting);
sendPacket(std::move(packet), *destinationNode);
});
_requestsDomainListData = isRequesting;
}

View file

@ -76,12 +76,16 @@ public:
void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); }
void enableIgnoreRadius() { ignoreNodesInRadius(true); }
void disableIgnoreRadius() { ignoreNodesInRadius(false); }
void ignoreNodeBySessionID(const QUuid& nodeID);
void ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled);
bool isIgnoringNode(const QUuid& nodeID) const;
void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled);
bool isPersonalMutingNode(const QUuid& nodeID) const;
void kickNodeBySessionID(const QUuid& nodeID);
void muteNodeBySessionID(const QUuid& nodeID);
void requestUsernameFromSessionID(const QUuid& nodeID);
bool getRequestsDomainListData() { return _requestsDomainListData; }
void setRequestsDomainListData(bool isRequesting);
public slots:
void reset();
@ -109,7 +113,7 @@ public slots:
signals:
void limitOfSilentDomainCheckInsReached();
void receivedDomainServerList();
void ignoredNode(const QUuid& nodeID);
void ignoredNode(const QUuid& nodeID, bool enabled);
void ignoreRadiusEnabledChanged(bool isIgnored);
void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint);
@ -153,9 +157,12 @@ private:
HifiSockAddr _assignmentServerSocket;
bool _isShuttingDown { false };
QTimer _keepAlivePingTimer;
bool _requestsDomainListData;
mutable QReadWriteLock _ignoredSetLock;
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDs;
mutable QReadWriteLock _personalMutedSetLock;
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _personalMutedNodeIDs;
void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode);
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true };

View file

@ -53,7 +53,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::AvatarData:
case PacketType::BulkAvatarData:
case PacketType::KillAvatar:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::SessionDisplayName);
return static_cast<PacketVersion>(AvatarMixerPacketVersion::Unignore);
case PacketType::ICEServerHeartbeat:
return 18; // ICE Server Heartbeat signing
case PacketType::AssetGetInfo:
@ -78,7 +78,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::MicrophoneAudioNoEcho:
case PacketType::MicrophoneAudioWithEcho:
case PacketType::AudioStreamStats:
return static_cast<PacketVersion>(AudioVersion::SpaceBubbleChanges);
return static_cast<PacketVersion>(AudioVersion::HighDynamicRangeVolume);
default:
return 17;

View file

@ -104,7 +104,8 @@ public:
UsernameFromIDRequest,
UsernameFromIDReply,
ViewFrustum,
LAST_PACKET_TYPE = ViewFrustum
RequestsDomainListData,
LAST_PACKET_TYPE = RequestsDomainListData
};
};
@ -207,7 +208,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
SensorToWorldMat,
HandControllerJoints,
HasKillAvatarReason,
SessionDisplayName
SessionDisplayName,
Unignore
};
enum class DomainConnectRequestVersion : PacketVersion {
@ -242,6 +244,8 @@ enum class AudioVersion : PacketVersion {
Exactly10msAudioPackets,
TerminatingStreamStats,
SpaceBubbleChanges,
HasPersonalMute,
HighDynamicRangeVolume,
};
#endif // hifi_PacketHeaders_h

View file

@ -48,49 +48,19 @@ using namespace render;
extern void initOverlay3DPipelines(render::ShapePlumber& plumber);
extern void initDeferredPipelines(render::ShapePlumber& plumber);
RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) {
cullFunctor = cullFunctor ? cullFunctor : [](const RenderArgs*, const AABox&){ return true; };
RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) {
// Prepare the ShapePipelines
ShapePlumberPointer shapePlumber = std::make_shared<ShapePlumber>();
initDeferredPipelines(*shapePlumber);
// CPU jobs:
// Fetch and cull the items from the scene
auto spatialFilter = ItemFilter::Builder::visibleWorldItems().withoutLayered();
const auto spatialSelection = addJob<FetchSpatialTree>("FetchSceneSelection", spatialFilter);
const auto culledSpatialSelection = addJob<CullSpatialSelection>("CullSceneSelection", spatialSelection, cullFunctor, RenderDetails::ITEM, spatialFilter);
// Overlays are not culled
const auto nonspatialSelection = addJob<FetchNonspatialItems>("FetchOverlaySelection");
// Multi filter visible items into different buckets
const int NUM_FILTERS = 3;
const int OPAQUE_SHAPE_BUCKET = 0;
const int TRANSPARENT_SHAPE_BUCKET = 1;
const int LIGHT_BUCKET = 2;
const int BACKGROUND_BUCKET = 2;
MultiFilterItem<NUM_FILTERS>::ItemFilterArray spatialFilters = { {
ItemFilter::Builder::opaqueShape(),
ItemFilter::Builder::transparentShape(),
ItemFilter::Builder::light()
} };
MultiFilterItem<NUM_FILTERS>::ItemFilterArray nonspatialFilters = { {
ItemFilter::Builder::opaqueShape(),
ItemFilter::Builder::transparentShape(),
ItemFilter::Builder::background()
} };
const auto filteredSpatialBuckets = addJob<MultiFilterItem<NUM_FILTERS>>("FilterSceneSelection", culledSpatialSelection, spatialFilters).get<MultiFilterItem<NUM_FILTERS>::ItemBoundsArray>();
const auto filteredNonspatialBuckets = addJob<MultiFilterItem<NUM_FILTERS>>("FilterOverlaySelection", nonspatialSelection, nonspatialFilters).get<MultiFilterItem<NUM_FILTERS>::ItemBoundsArray>();
// Extract / Sort opaques / Transparents / Lights / Overlays
const auto opaques = addJob<DepthSortItems>("DepthSortOpaque", filteredSpatialBuckets[OPAQUE_SHAPE_BUCKET]);
const auto transparents = addJob<DepthSortItems>("DepthSortTransparent", filteredSpatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false));
const auto lights = filteredSpatialBuckets[LIGHT_BUCKET];
const auto overlayOpaques = addJob<DepthSortItems>("DepthSortOverlayOpaque", filteredNonspatialBuckets[OPAQUE_SHAPE_BUCKET]);
const auto overlayTransparents = addJob<DepthSortItems>("DepthSortOverlayTransparent", filteredNonspatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false));
const auto background = filteredNonspatialBuckets[BACKGROUND_BUCKET];
// Extract opaques / transparents / lights / overlays
const auto opaques = items[0];
const auto transparents = items[1];
const auto lights = items[2];
const auto overlayOpaques = items[3];
const auto overlayTransparents = items[4];
const auto background = items[5];
const auto spatialSelection = items[6];
// Prepare deferred, generate the shared Deferred Frame Transform
const auto deferredFrameTransform = addJob<GenerateDeferredFrameTransform>("DeferredFrameTransform");
@ -200,7 +170,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) {
addJob<DebugAmbientOcclusion>("DebugAmbientOcclusion", debugAmbientOcclusionInputs);
// Scene Octree Debuging job
// Scene Octree Debugging job
{
addJob<DrawSceneOctree>("DrawSceneOctree", spatialSelection);
addJob<DrawItemSelection>("DrawItemSelection", spatialSelection);
@ -228,26 +198,6 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) {
}
void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) {
// sanity checks
assert(sceneContext);
if (!sceneContext->_scene) {
return;
}
// Is it possible that we render without a viewFrustum ?
if (!(renderContext->args && renderContext->args->hasViewFrustum())) {
return;
}
auto config = std::static_pointer_cast<Config>(renderContext->jobConfig);
for (auto job : _jobs) {
job.run(sceneContext, renderContext);
}
}
void BeginGPURangeTimer::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer) {
timer = _gpuTimer;
gpu::doInBatch(renderContext->args->_context, [&](gpu::Batch& batch) {

View file

@ -13,18 +13,18 @@
#define hifi_RenderDeferredTask_h
#include <gpu/Pipeline.h>
#include <render/CullTask.h>
#include <render/RenderFetchCullSortTask.h>
#include "LightingModel.h"
class BeginGPURangeTimer {
public:
using JobModel = render::Job::ModelO<BeginGPURangeTimer, gpu::RangeTimerPointer>;
BeginGPURangeTimer(const std::string& name) : _gpuTimer(std::make_shared<gpu::RangeTimer>(name)) {}
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer);
protected:
gpu::RangeTimerPointer _gpuTimer;
};
@ -35,12 +35,12 @@ class EndGPURangeTimer {
public:
using Config = GPURangeTimerConfig;
using JobModel = render::Job::ModelI<EndGPURangeTimer, gpu::RangeTimerPointer, Config>;
EndGPURangeTimer() {}
void configure(const Config& config) {}
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::RangeTimerPointer& timer);
protected:
};
@ -192,20 +192,11 @@ public:
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer);
};
using RenderDeferredTaskConfig = render::GPUTaskConfig;
class RenderDeferredTask : public render::Task {
public:
using Config = RenderDeferredTaskConfig;
RenderDeferredTask(render::CullFunctor cullFunctor);
using JobModel = Model<RenderDeferredTask>;
void configure(const Config& config) {}
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext);
using JobModel = Model<RenderDeferredTask, Config>;
protected:
gpu::RangeTimerPointer _gpuTimer;
RenderDeferredTask(RenderFetchCullSortTask::Output items);
};
#endif // hifi_RenderDeferredTask_h

View file

@ -19,9 +19,6 @@
#include <ViewFrustum.h>
#include <gpu/Context.h>
#include <render/CullTask.h>
#include <render/SortTask.h>
#include "FramebufferCache.h"
#include "TextureCache.h"
@ -32,45 +29,14 @@
using namespace render;
RenderForwardTask::RenderForwardTask(CullFunctor cullFunctor) {
// CPU jobs:
// Fetch and cull the items from the scene
const auto spatialSelection = addJob<FetchSpatialTree>("FetchSceneSelection");
cullFunctor = cullFunctor ? cullFunctor : [](const RenderArgs*, const AABox&){ return true; };
auto spatialFilter = ItemFilter::Builder::visibleWorldItems().withoutLayered();
const auto culledSpatialSelection = addJob<CullSpatialSelection>("CullSceneSelection", spatialSelection, cullFunctor, RenderDetails::ITEM, spatialFilter);
// Overlays are not culled
const auto nonspatialSelection = addJob<FetchNonspatialItems>("FetchOverlaySelection");
// Multi filter visible items into different buckets
const int NUM_FILTERS = 3;
const int OPAQUE_SHAPE_BUCKET = 0;
const int TRANSPARENT_SHAPE_BUCKET = 1;
const int LIGHT_BUCKET = 2;
const int BACKGROUND_BUCKET = 2;
MultiFilterItem<NUM_FILTERS>::ItemFilterArray spatialFilters = { {
ItemFilter::Builder::opaqueShape(),
ItemFilter::Builder::transparentShape(),
ItemFilter::Builder::light()
} };
MultiFilterItem<NUM_FILTERS>::ItemFilterArray nonspatialFilters = { {
ItemFilter::Builder::opaqueShape(),
ItemFilter::Builder::transparentShape(),
ItemFilter::Builder::background()
} };
const auto filteredSpatialBuckets = addJob<MultiFilterItem<NUM_FILTERS>>("FilterSceneSelection", culledSpatialSelection, spatialFilters).get<MultiFilterItem<NUM_FILTERS>::ItemBoundsArray>();
const auto filteredNonspatialBuckets = addJob<MultiFilterItem<NUM_FILTERS>>("FilterOverlaySelection", nonspatialSelection, nonspatialFilters).get<MultiFilterItem<NUM_FILTERS>::ItemBoundsArray>();
// Extract / Sort opaques / Transparents / Lights / Overlays
const auto opaques = addJob<DepthSortItems>("DepthSortOpaque", filteredSpatialBuckets[OPAQUE_SHAPE_BUCKET]);
const auto transparents = addJob<DepthSortItems>("DepthSortTransparent", filteredSpatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false));
const auto lights = filteredSpatialBuckets[LIGHT_BUCKET];
const auto overlayOpaques = addJob<DepthSortItems>("DepthSortOverlayOpaque", filteredNonspatialBuckets[OPAQUE_SHAPE_BUCKET]);
const auto overlayTransparents = addJob<DepthSortItems>("DepthSortOverlayTransparent", filteredNonspatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false));
const auto background = filteredNonspatialBuckets[BACKGROUND_BUCKET];
RenderForwardTask::RenderForwardTask(RenderFetchCullSortTask::Output items) {
// Extract opaques / transparents / lights / overlays
const auto opaques = items[0];
const auto transparents = items[1];
const auto lights = items[2];
const auto overlayOpaques = items[3];
const auto overlayTransparents = items[4];
const auto background = items[5];
const auto framebuffer = addJob<PrepareFramebuffer>("PrepareFramebuffer");
@ -83,26 +49,6 @@ RenderForwardTask::RenderForwardTask(CullFunctor cullFunctor) {
addJob<Blit>("Blit", framebuffer);
}
void RenderForwardTask::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) {
// sanity checks
assert(sceneContext);
if (!sceneContext->_scene) {
return;
}
// Is it possible that we render without a viewFrustum ?
if (!(renderContext->args && renderContext->args->hasViewFrustum())) {
return;
}
auto config = std::static_pointer_cast<Config>(renderContext->jobConfig);
for (auto job : _jobs) {
job.run(sceneContext, renderContext);
}
}
void PrepareFramebuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, gpu::FramebufferPointer& framebuffer) {
auto framebufferCache = DependencyManager::get<FramebufferCache>();
auto framebufferSize = framebufferCache->getFrameBufferSize();

View file

@ -13,20 +13,14 @@
#define hifi_RenderForwardTask_h
#include <gpu/Pipeline.h>
#include <render/CullTask.h>
#include <render/RenderFetchCullSortTask.h>
#include "LightingModel.h"
using RenderForwardTaskConfig = render::GPUTaskConfig;
class RenderForwardTask : public render::Task {
public:
using Config = RenderForwardTaskConfig;
RenderForwardTask(render::CullFunctor cullFunctor);
using JobModel = Model<RenderForwardTask>;
void configure(const Config& config) {}
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext);
using JobModel = Model<RenderForwardTask, Config>;
RenderForwardTask(RenderFetchCullSortTask::Output items);
};
class PrepareFramebuffer {

View file

@ -115,6 +115,8 @@ RenderShadowTask::RenderShadowTask(CullFunctor cullFunctor) {
skinProgram, state);
}
const auto cachedMode = addJob<RenderShadowSetup>("Setup");
// CPU jobs:
// Fetch and cull the items from the scene
auto shadowFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered();
@ -127,6 +129,8 @@ RenderShadowTask::RenderShadowTask(CullFunctor cullFunctor) {
// GPU jobs: Render to shadow map
addJob<RenderShadowMap>("RenderShadowMap", sortedShapes, shapePlumber);
addJob<RenderShadowTeardown>("Teardown", cachedMode);
}
void RenderShadowTask::configure(const Config& configuration) {
@ -135,25 +139,13 @@ void RenderShadowTask::configure(const Config& configuration) {
Task::configure(configuration);
}
void RenderShadowTask::run(const SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) {
assert(sceneContext);
RenderArgs* args = renderContext->args;
// sanity checks
if (!sceneContext->_scene || !args) {
return;
}
void RenderShadowSetup::run(const SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, Output& output) {
auto lightStage = DependencyManager::get<DeferredLightingEffect>()->getLightStage();
const auto globalShadow = lightStage->getShadow(0);
// If the global light is not set, bail
if (!globalShadow) {
return;
}
// Cache old render args
RenderArgs::RenderMode mode = args->_renderMode;
RenderArgs* args = renderContext->args;
output = args->_renderMode;
auto nearClip = args->getViewFrustum().getNearClip();
float nearDepth = -args->_boomOffset.z;
@ -163,14 +155,12 @@ void RenderShadowTask::run(const SceneContextPointer& sceneContext, const render
// Set the keylight render args
args->pushViewFrustum(*(globalShadow->getFrustum()));
args->_renderMode = RenderArgs::SHADOW_RENDER_MODE;
}
// TODO: Allow runtime manipulation of culling ShouldRenderFunctor
for (auto job : _jobs) {
job.run(sceneContext, renderContext);
}
void RenderShadowTeardown::run(const SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Input& input) {
RenderArgs* args = renderContext->args;
// Reset the render args
args->popViewFrustum();
args->_renderMode = mode;
args->_renderMode = input;
};

View file

@ -49,7 +49,20 @@ public:
RenderShadowTask(render::CullFunctor shouldRender);
void configure(const Config& configuration);
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext);
};
class RenderShadowSetup {
public:
using Output = RenderArgs::RenderMode;
using JobModel = render::Job::ModelO<RenderShadowSetup, Output>;
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, Output& output);
};
class RenderShadowTeardown {
public:
using Input = RenderArgs::RenderMode;
using JobModel = render::Job::ModelI<RenderShadowTeardown, Input>;
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Input& input);
};
#endif // hifi_RenderShadowTask_h

View file

@ -52,11 +52,3 @@ void Engine::load() {
}
}
}
void Engine::run() {
for (auto job : _jobs) {
job.run(_sceneContext, _renderContext);
}
}

View file

@ -39,8 +39,8 @@ namespace render {
RenderContextPointer getRenderContext() const { return _renderContext; }
// Render a frame
// A frame must have a scene registered and a context set to render
void run();
// Must have a scene registered and a context set
void run() { assert(_sceneContext && _renderContext); Task::run(_sceneContext, _renderContext); }
protected:
SceneContextPointer _sceneContext;

View file

@ -0,0 +1,61 @@
//
// RenderFetchCullSortTask.cpp
// render/src/
//
// Created by Zach Pomerantz on 12/22/2016.
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "RenderFetchCullSortTask.h"
#include "CullTask.h"
#include "SortTask.h"
using namespace render;
RenderFetchCullSortTask::RenderFetchCullSortTask(CullFunctor cullFunctor) {
cullFunctor = cullFunctor ? cullFunctor : [](const RenderArgs*, const AABox&){ return true; };
// CPU jobs:
// Fetch and cull the items from the scene
auto spatialFilter = ItemFilter::Builder::visibleWorldItems().withoutLayered();
const auto spatialSelection = addJob<FetchSpatialTree>("FetchSceneSelection", spatialFilter);
const auto culledSpatialSelection = addJob<CullSpatialSelection>("CullSceneSelection", spatialSelection, cullFunctor, RenderDetails::ITEM, spatialFilter);
// Overlays are not culled
const auto nonspatialSelection = addJob<FetchNonspatialItems>("FetchOverlaySelection");
// Multi filter visible items into different buckets
const int NUM_FILTERS = 3;
const int OPAQUE_SHAPE_BUCKET = 0;
const int TRANSPARENT_SHAPE_BUCKET = 1;
const int LIGHT_BUCKET = 2;
const int BACKGROUND_BUCKET = 2;
MultiFilterItem<NUM_FILTERS>::ItemFilterArray spatialFilters = { {
ItemFilter::Builder::opaqueShape(),
ItemFilter::Builder::transparentShape(),
ItemFilter::Builder::light()
} };
MultiFilterItem<NUM_FILTERS>::ItemFilterArray nonspatialFilters = { {
ItemFilter::Builder::opaqueShape(),
ItemFilter::Builder::transparentShape(),
ItemFilter::Builder::background()
} };
const auto filteredSpatialBuckets = addJob<MultiFilterItem<NUM_FILTERS>>("FilterSceneSelection", culledSpatialSelection, spatialFilters).get<MultiFilterItem<NUM_FILTERS>::ItemBoundsArray>();
const auto filteredNonspatialBuckets = addJob<MultiFilterItem<NUM_FILTERS>>("FilterOverlaySelection", nonspatialSelection, nonspatialFilters).get<MultiFilterItem<NUM_FILTERS>::ItemBoundsArray>();
// Extract opaques / transparents / lights / overlays
const auto opaques = addJob<DepthSortItems>("DepthSortOpaque", filteredSpatialBuckets[OPAQUE_SHAPE_BUCKET]);
const auto transparents = addJob<DepthSortItems>("DepthSortTransparent", filteredSpatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false));
const auto lights = filteredSpatialBuckets[LIGHT_BUCKET];
const auto overlayOpaques = addJob<DepthSortItems>("DepthSortOverlayOpaque", filteredNonspatialBuckets[OPAQUE_SHAPE_BUCKET]);
const auto overlayTransparents = addJob<DepthSortItems>("DepthSortOverlayTransparent", filteredNonspatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false));
const auto background = filteredNonspatialBuckets[BACKGROUND_BUCKET];
setOutput(Output{{
opaques, transparents, lights, overlayOpaques, overlayTransparents, background, spatialSelection }});
}

View file

@ -0,0 +1,28 @@
//
// RenderFetchCullSortTask.h
// render/src/
//
// Created by Zach Pomerantz on 12/22/2016.
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_RenderFetchCullSortTask_h
#define hifi_RenderFetchCullSortTask_h
#include <gpu/Pipeline.h>
#include "Task.h"
#include "CullTask.h"
class RenderFetchCullSortTask : public render::Task {
public:
using Output = std::array<render::Varying, 7>;
using JobModel = ModelO<RenderFetchCullSortTask>;
RenderFetchCullSortTask(render::CullFunctor cullFunctor);
};
#endif // hifi_RenderFetchCullSortTask_h

View file

@ -45,8 +45,9 @@ public:
}
template <class T> Varying(const T& data) : _concept(std::make_shared<Model<T>>(data)) {}
template <class T> T& edit() { return std::static_pointer_cast<Model<T>>(_concept)->_data; }
template <class T> bool canCast() const { return !!std::dynamic_pointer_cast<Model<T>>(_concept); }
template <class T> const T& get() const { return std::static_pointer_cast<const Model<T>>(_concept)->_data; }
template <class T> T& edit() { return std::static_pointer_cast<Model<T>>(_concept)->_data; }
// access potential sub varyings contained in this one.
@ -440,6 +441,9 @@ template <class T, class C> void jobConfigure(T& data, const C& configuration) {
template<class T> void jobConfigure(T&, const JobConfig&) {
// nop, as the default JobConfig was used, so the data does not need a configure method
}
template<class T> void jobConfigure(T&, const TaskConfig&) {
// nop, as the default TaskConfig was used, so the data does not need a configure method
}
template <class T> void jobRun(T& data, const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const JobNoIO& input, JobNoIO& output) {
data.run(sceneContext, renderContext);
}
@ -591,24 +595,20 @@ class Task {
public:
using Config = TaskConfig;
using QConfigPointer = Job::QConfigPointer;
using None = Job::None;
template <class T, class C = Config, class I = None, class O = None> class Model : public Job::Concept {
template <class T, class C = Config> class Model : public Job::Concept {
public:
using Data = T;
using Input = I;
using Output = O;
using Config = C;
using Input = Job::None;
Data _data;
Varying _input;
Varying _output;
const Varying getInput() const override { return _input; }
const Varying getOutput() const override { return _output; }
const Varying getOutput() const override { return _data._output; }
template <class... A>
Model(const Varying& input, A&&... args) :
Concept(nullptr), _data(Data(std::forward<A>(args)...)), _input(input), _output(Output()) {
Concept(nullptr), _data(Data(std::forward<A>(args)...)) {
// Recreate the Config to use the templated type
_data.template createConfiguration<C>();
_config = _data.getConfiguration();
@ -620,16 +620,15 @@ public:
}
void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) override {
renderContext->jobConfig = std::static_pointer_cast<Config>(_config);
if (renderContext->jobConfig->alwaysEnabled || renderContext->jobConfig->enabled) {
jobRun(_data, sceneContext, renderContext, _input.get<I>(), _output.edit<O>());
auto config = std::static_pointer_cast<Config>(_config);
if (config->alwaysEnabled || config->enabled) {
for (auto job : _data._jobs) {
job.run(sceneContext, renderContext);
}
}
renderContext->jobConfig.reset();
}
};
template <class T, class I, class C = Config> using ModelI = Model<T, C, I, None>;
template <class T, class O, class C = Config> using ModelO = Model<T, C, None, O>;
template <class T, class I, class O, class C = Config> using ModelIO = Model<T, C, I, O>;
template <class T, class C = Config> using ModelO = Model<T, C>;
using Jobs = std::vector<Job>;
@ -655,6 +654,10 @@ public:
return addJob<T>(name, input, std::forward<A>(args)...);
}
template <class O> void setOutput(O&& output) {
_output = Varying(output);
}
template <class C> void createConfiguration() {
auto config = std::make_shared<C>();
if (_config) {
@ -688,11 +691,18 @@ public:
}
}
void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) {
for (auto job : _jobs) {
job.run(sceneContext, renderContext);
}
}
protected:
template <class T, class C, class I, class O> friend class Model;
template <class T, class C> friend class Model;
QConfigPointer _config;
Jobs _jobs;
Varying _output;
};
}

View file

@ -911,19 +911,11 @@ void ScriptEngine::run() {
break;
}
// determine how long before the next timer should fire, we'd ideally like to sleep just
// that long, so the next processEvents() will allow the timers to fire on time.
const std::chrono::microseconds minTimerTimeRemaining(USECS_PER_MSEC * getTimersRemainingTime());
// However, if we haven't yet slept at least as long as our average timer per frame, then we will
// punish the timers to at least wait as long as the average run time of the timers.
auto untilTimer = std::max(minTimerTimeRemaining, averageTimerPerFrame);
// choose the closest time point, our
auto remainingSleepUntil = std::chrono::duration_cast<std::chrono::microseconds>(sleepUntil - clock::now());
auto closestUntil = std::min(remainingSleepUntil, untilTimer);
auto thisSleepUntil = std::min(sleepUntil, clock::now() + closestUntil);
std::this_thread::sleep_until(thisSleepUntil);
// We only want to sleep a small amount so that any pending events (like timers or invokeMethod events)
// will be able to process quickly.
static const int SMALL_SLEEP_AMOUNT = 100;
auto smallSleepUntil = clock::now() + static_cast<std::chrono::microseconds>(SMALL_SLEEP_AMOUNT);
std::this_thread::sleep_until(smallSleepUntil);
}
#ifdef SCRIPT_DELAY_DEBUG
@ -1013,21 +1005,6 @@ void ScriptEngine::run() {
emit doneRunning();
}
quint64 ScriptEngine::getTimersRemainingTime() {
quint64 minimumTime = USECS_PER_SECOND; // anything larger than this can be ignored
QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);
while (i.hasNext()) {
i.next();
QTimer* timer = i.key();
int remainingTime = timer->remainingTime();
if (remainingTime >= 0) {
minimumTime = std::min((quint64)remainingTime, minimumTime);
}
}
return minimumTime;
}
// NOTE: This is private because it must be called on the same thread that created the timers, which is why
// we want to only call it in our own run "shutdown" processing.
void ScriptEngine::stopAllTimers() {
@ -1038,6 +1015,7 @@ void ScriptEngine::stopAllTimers() {
stopTimer(timer);
}
}
void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) {
// We could maintain a separate map of entityID => QTimer, but someone will have to prove to me that it's worth the complexity. -HRS
QVector<QTimer*> toDelete;

View file

@ -218,7 +218,6 @@ protected:
void init();
bool evaluatePending() const { return _evaluatesPending > 0; }
quint64 getTimersRemainingTime();
void timerFired();
void stopAllTimers();
void stopAllTimersForEntityScript(const EntityItemID& entityID);

View file

@ -21,9 +21,25 @@ UsersScriptingInterface::UsersScriptingInterface() {
connect(nodeList.data(), &NodeList::usernameFromIDReply, this, &UsersScriptingInterface::usernameFromIDReply);
}
void UsersScriptingInterface::ignore(const QUuid& nodeID) {
void UsersScriptingInterface::ignore(const QUuid& nodeID, bool ignoreEnabled) {
// ask the NodeList to ignore this user (based on the session ID of their node)
DependencyManager::get<NodeList>()->ignoreNodeBySessionID(nodeID);
DependencyManager::get<NodeList>()->ignoreNodeBySessionID(nodeID, ignoreEnabled);
}
bool UsersScriptingInterface::getIgnoreStatus(const QUuid& nodeID) {
// ask the NodeList for the Ignore status associated with the given session ID
return DependencyManager::get<NodeList>()->isIgnoringNode(nodeID);
}
void UsersScriptingInterface::personalMute(const QUuid& nodeID, bool muteEnabled) {
// ask the NodeList to mute the user with the given session ID
// "Personal Mute" only applies one way and is not global
DependencyManager::get<NodeList>()->personalMuteNodeBySessionID(nodeID, muteEnabled);
}
bool UsersScriptingInterface::getPersonalMuteStatus(const QUuid& nodeID) {
// ask the NodeList for the Personal Mute status associated with the given session ID
return DependencyManager::get<NodeList>()->isPersonalMutingNode(nodeID);
}
void UsersScriptingInterface::kick(const QUuid& nodeID) {
@ -61,3 +77,10 @@ void UsersScriptingInterface::disableIgnoreRadius() {
bool UsersScriptingInterface::getIgnoreRadiusEnabled() {
return DependencyManager::get<NodeList>()->getIgnoreRadiusEnabled();
}
bool UsersScriptingInterface::getRequestsDomainListData() {
return DependencyManager::get<NodeList>()->getRequestsDomainListData();
}
void UsersScriptingInterface::setRequestsDomainListData(bool isRequesting) {
DependencyManager::get<NodeList>()->setRequestsDomainListData(isRequesting);
}

View file

@ -24,6 +24,7 @@ class UsersScriptingInterface : public QObject, public Dependency {
SINGLETON_DEPENDENCY
Q_PROPERTY(bool canKick READ getCanKick)
Q_PROPERTY(bool requestsDomainListData READ getRequestsDomainListData WRITE setRequestsDomainListData)
public:
UsersScriptingInterface();
@ -34,8 +35,31 @@ public slots:
* Ignore another user.
* @function Users.ignore
* @param {nodeID} nodeID The node or session ID of the user you want to ignore.
* @param {bool} enable True for ignored; false for un-ignored.
*/
void ignore(const QUuid& nodeID);
void ignore(const QUuid& nodeID, bool ignoreEnabled = true);
/**jsdoc
* Gets a bool containing whether you have ignored the given Avatar UUID.
* @function Users.getIgnoreStatus
* @param {nodeID} nodeID The node or session ID of the user whose ignore status you want.
*/
bool getIgnoreStatus(const QUuid& nodeID);
/**jsdoc
* Mute another user for you and you only.
* @function Users.personalMute
* @param {nodeID} nodeID The node or session ID of the user you want to mute.
* @param {bool} enable True for enabled; false for disabled.
*/
void personalMute(const QUuid& nodeID, bool muteEnabled = true);
/**jsdoc
* Requests a bool containing whether you have personally muted the given Avatar UUID.
* @function Users.requestPersonalMuteStatus
* @param {nodeID} nodeID The node or session ID of the user whose personal mute status you want.
*/
bool getPersonalMuteStatus(const QUuid& nodeID);
/**jsdoc
* Kick another user.
@ -45,7 +69,7 @@ public slots:
void kick(const QUuid& nodeID);
/**jsdoc
* Mute another user.
* Mute another user for everyone.
* @function Users.mute
* @param {nodeID} nodeID The node or session ID of the user you want to mute.
*/
@ -105,6 +129,11 @@ signals:
* @function Users.usernameFromIDReply
*/
void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint);
private:
bool getRequestsDomainListData();
void setRequestsDomainListData(bool requests);
bool _requestsDomainListData;
};

View file

@ -0,0 +1,94 @@
//
// AudioHelpers.h
// libraries/shared/src
//
// Created by Ken Cooke on 1/4/17.
// 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
//
#ifndef hifi_AudioHelpers_h
#define hifi_AudioHelpers_h
#include <stdint.h>
const int IEEE754_MANT_BITS = 23;
const int IEEE754_EXPN_BIAS = 127;
//
// for x > 0.0f, returns log2(x)
// for x <= 0.0f, returns large negative value
//
// abs |error| < 2e-4, smooth (exact for x=2^N)
// rel |error| < 0.4 from precision loss very close to 1.0f
//
static inline float fastLog2f(float x) {
union { float f; int32_t i; } mant, bits = { x };
// split into mantissa and exponent
mant.i = (bits.i & ((1 << IEEE754_MANT_BITS) - 1)) | (IEEE754_EXPN_BIAS << IEEE754_MANT_BITS);
int32_t expn = (bits.i >> IEEE754_MANT_BITS) - IEEE754_EXPN_BIAS;
mant.f -= 1.0f;
// polynomial for log2(1+x) over x=[0,1]
x = (((-0.0821307180f * mant.f + 0.321188984f) * mant.f - 0.677784014f) * mant.f + 1.43872575f) * mant.f;
return x + expn;
}
//
// for -127 <= x < 128, returns exp2(x)
// otherwise, returns undefined
//
// rel |error| < 9e-6, smooth (exact for x=N)
//
static inline float fastExp2f(float x) {
union { float f; int32_t i; } xi;
// bias such that x > 0
x += IEEE754_EXPN_BIAS;
// split into integer and fraction
xi.i = (int32_t)x;
x -= xi.i;
// construct exp2(xi) as a float
xi.i <<= IEEE754_MANT_BITS;
// polynomial for exp2(x) over x=[0,1]
x = (((0.0135557472f * x + 0.0520323690f) * x + 0.241379763f) * x + 0.693032121f) * x + 1.0f;
return x * xi.f;
}
//
// Quantize a non-negative gain value to the nearest 0.5dB, and pack to a byte.
//
// Values above +30dB are clamped to +30dB
// Values below -97dB are clamped to -inf
// Value of 1.0 (+0dB) is reconstructed exactly
//
const float GAIN_CONVERSION_RATIO = 2.0f * 6.02059991f; // scale log2 to 0.5dB
const float GAIN_CONVERSION_OFFSET = 255 - 60.0f; // translate +30dB to max
static inline uint8_t packFloatGainToByte(float gain) {
float f = fastLog2f(gain) * GAIN_CONVERSION_RATIO + GAIN_CONVERSION_OFFSET;
int32_t i = (int32_t)(f + 0.5f); // quantize
uint8_t byte = (i < 0) ? 0 : ((i > 255) ? 255 : i); // clamp
return byte;
}
static inline float unpackFloatGainFromByte(uint8_t byte) {
float gain = (byte == 0) ? 0.0f : fastExp2f((byte - GAIN_CONVERSION_OFFSET) * (1.0f/GAIN_CONVERSION_RATIO));
return gain;
}
#endif // hifi_AudioHelpers_h

View file

@ -74,7 +74,9 @@ void Settings::endGroup() {
}
void Settings::setValue(const QString& name, const QVariant& value) {
_manager->setValue(name, value);
if (_manager->value(name) != value) {
_manager->setValue(name, value);
}
}
QVariant Settings::value(const QString& name, const QVariant& defaultValue) const {

View file

@ -24,6 +24,8 @@ public:
virtual QJsonObject getHardwareStats() const;
protected:
QThread::Priority getPresentPriority() override { return QThread::TimeCriticalPriority; }
bool internalActivate() override;
void hmdPresent() override;
bool isHmdMounted() const override;

View file

@ -0,0 +1,244 @@
// Tester, try testing these different settings.
//
// Changing the TIMER_HZ will show different performance results. It's expected that at 90hz you'll see a fair
// amount of variance, as Qt Timers simply aren't accurate enough. In general RPC peformance should match the timer
// without significant difference in variance.
var TIMER_HZ = 50; // Change this for different values
var TIMER_INTERVAL = 1000 / TIMER_HZ;
var TIMER_WORK_EFFORT = 0; // 1000 is light work, 1000000 ~= 30ms
var UPDATE_HZ = 60; // standard script update rate
var UPDATE_INTERVAL = 1000/UPDATE_HZ; // standard script update interval
var UPDATE_WORK_EFFORT = 0; // 1000 is light work, 1000000 ~= 30ms
var basePosition = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation()));
var timerBox = Entities.addEntity(
{ type: "Box",
position: basePosition,
dimensions: { x: 0.1, y: 0.1, z: 0.1 },
color: { red: 255, green: 0, blue: 255 },
dynamic: false,
collisionless: true
});
var lastTick = Date.now();
var deltaTick = 0;
var tickSamples = 0;
var totalVariance = 0;
var tickCount = 0;
var totalWork = 0;
var highVarianceCount = 0;
var varianceCount = 0;
print("Set interval = " + TIMER_INTERVAL);
var timerTime = 0.0;
var range = 0.5;
var rotationFactor = 0.5; // smaller == faster
var omega = 2.0 * Math.PI / rotationFactor;
var ticker = Script.setInterval(function() {
tickCount++;
var tickNow = Date.now();
deltaTick = tickNow - lastTick;
var variance = Math.abs(deltaTick - TIMER_INTERVAL);
totalVariance += variance;
if (variance > 1) {
varianceCount++;
}
if (variance > 5) {
highVarianceCount++;
}
var preWork = Date.now();
var y = 2;
for (var x = 0; x < TIMER_WORK_EFFORT; x++) {
y = y * y;
}
var postWork = Date.now();
deltaWork = postWork - preWork;
totalWork += deltaWork;
// move a box
var deltaTime = deltaTick / 1000;
timerTime += deltaTime;
rotation = Quat.angleAxis(timerTime * omega / Math.PI * 180.0, { x: 0, y: 1, z: 0 });
Entities.editEntity(timerBox,
{
position: { x: basePosition.x + Math.sin(timerTime * omega) / 2.0 * range,
y: basePosition.y,
z: basePosition.z },
//rotation: rotation
});
tickSamples = tickSamples + deltaTick;
lastTick = tickNow;
// report about every 5 seconds
if(tickCount == (TIMER_HZ * 5)) {
print("TIMER -- For " + tickCount + " samples average interval = " + tickSamples/tickCount + " ms"
+ " average variance:" + totalVariance/tickCount + " ms"
+ " min variance:" + varianceCount + " [" + (varianceCount/tickCount) * 100 + " %] "
+ " high variance:" + highVarianceCount + " [" + (highVarianceCount/tickCount) * 100 + " %] "
+ " average work:" + totalWork/tickCount + " ms");
tickCount = 0;
tickSamples = 0;
totalWork = 0;
totalVariance = 0;
varianceCount = 0;
highVarianceCount = 0;
}
}, TIMER_INTERVAL);
/////////////////////////////////////////////////////////////////////////
var rpcPosition = Vec3.sum(basePosition, { x:0, y: 0.2, z: 0});
var theRpcFunctionInclude = Script.resolvePath("testIntervalRpcFunction.js");
print("theRpcFunctionInclude:" + theRpcFunctionInclude);
var rpcBox = Entities.addEntity(
{ type: "Box",
position: rpcPosition,
dimensions: { x: 0.1, y: 0.1, z: 0.1 },
color: { red: 255, green: 255, blue: 0 },
dynamic: false,
collisionless: true,
script: theRpcFunctionInclude
});
var rpcLastTick = Date.now();
var rpcTotalTicks = 0;
var rpcCount = 0;
var rpcTotalVariance = 0;
var rpcTickCount = 0;
var rpcVarianceCount = 0;
var rpcHighVarianceCount = 0;
var rpcTicker = Script.setInterval(function() {
rpcTickCount++;
var tickNow = Date.now();
var deltaTick = tickNow - rpcLastTick;
var variance = Math.abs(deltaTick - TIMER_INTERVAL);
rpcTotalVariance += variance;
if (variance > 1) {
rpcVarianceCount++;
}
if (variance > 5) {
rpcHighVarianceCount++;
}
rpcTotalTicks += deltaTick;
rpcLastTick = tickNow;
var args = [range, rotationFactor, omega, TIMER_INTERVAL, TIMER_HZ];
Entities.callEntityMethod(rpcBox, "doRPC", args);
// report about every 5 seconds
if(rpcTickCount == (TIMER_HZ * 5)) {
print("RPCTIMER- For " + rpcTickCount + " samples average interval = " + rpcTotalTicks/rpcTickCount + " ms"
+ " average variance:" + rpcTotalVariance/rpcTickCount + " ms"
+ " min variance:" + rpcVarianceCount + " [" + (rpcVarianceCount/rpcTickCount) * 100 + " %] "
+ " high variance:" + rpcHighVarianceCount + " [" + (rpcHighVarianceCount/rpcTickCount) * 100 + " %] "
);
rpcTickCount = 0;
rpcTotalTicks = 0;
rpcTotalVariance = 0;
rpcVarianceCount = 0;
rpcHighVarianceCount = 0;
}
}, TIMER_INTERVAL);
var updateCount = 0;
var updateTotalElapsed = 0;
var lastUpdate = Date.now();
var updateTotalWork = 0;
var updateTotalVariance = 0;
var updateVarianceCount = 0;
var updateHighVarianceCount = 0;
var updatePosition = Vec3.sum(basePosition, { x:0, y: -0.2, z: 0});
var updateBox = Entities.addEntity(
{ type: "Box",
position: updatePosition,
dimensions: { x: 0.1, y: 0.1, z: 0.1 },
color: { red: 0, green: 255, blue: 255 },
dynamic: false,
collisionless: true
});
var updateTime = 0;
var updateFunction = function(deltaTime){
updateCount++;
var updateAt = Date.now();
deltaUpdate = updateAt - lastUpdate;
updateTotalElapsed += deltaUpdate;
lastUpdate = updateAt;
var variance = Math.abs(deltaUpdate - UPDATE_INTERVAL);
updateTotalVariance += variance;
if (variance > 1) {
updateVarianceCount++;
}
if (variance > 5) {
updateHighVarianceCount++;
}
var preWork = Date.now();
var y = 2;
for (var x = 0; x < UPDATE_WORK_EFFORT; x++) {
y = y * y;
}
var postWork = Date.now();
deltaWork = postWork - preWork;
updateTotalWork += deltaWork;
// move a box
updateTime += deltaTime;
rotation = Quat.angleAxis(updateTime * omega / Math.PI * 180.0, { x: 0, y: 1, z: 0 });
Entities.editEntity(updateBox,
{
position: { x: updatePosition.x + Math.sin(updateTime * omega) / 2.0 * range,
y: updatePosition.y,
z: updatePosition.z },
});
if(updateCount == (UPDATE_HZ * 5)) {
print("UPDATE -- For " + updateCount + " samples average update = " + updateTotalElapsed/updateCount + " ms"
+ " average variance:" + updateTotalVariance/updateCount + " ms"
+ " min variance:" + updateVarianceCount + " [" + (updateVarianceCount/updateCount) * 100 + " %] "
+ " high variance:" + updateHighVarianceCount + " [" + (updateHighVarianceCount/updateCount) * 100 + " %] "
+ " average work:" + updateTotalWork/updateCount + " ms");
updateCount = 0;
updateTotalElapsed = 0;
updateTotalWork = 0;
updateTotalVariance = 0;
updateVarianceCount = 0;
updateHighVarianceCount = 0;
}
};
Script.update.connect(updateFunction);
Script.scriptEnding.connect(function(){
Entities.deleteEntity(timerBox);
Entities.deleteEntity(rpcBox);
Entities.deleteEntity(updateBox);
});

View file

@ -0,0 +1,77 @@
(function() {
var x = false;
var y = false;
var z = false;
var entityLastTick = Date.now();
var entityTotalTicks = 0;
var entityCount = 0;
var entityTotalVariance = 0;
var entityTickCount = 0;
var entityVarianceCount = 0;
var entityHighVarianceCount = 0;
var _entityID;
var time = 0;
function Foo() { return; };
Foo.prototype = {
preload: function(entityID) { print('Foo preload'); _entityID = entityID; },
doRPC: function(entityID, args) {
var range = args[0];
var rotationFactor = args[1];
var omega = args[2];
var TIMER_INTERVAL = args[3];
var TIMER_HZ = args[4];
// first time, set our x,y,z
if (x === false) {
var position = Entities.getEntityProperties(_entityID, "position").position;
x = position.x;
y = position.y;
z = position.z;
}
entityTickCount++;
var tickNow = Date.now();
var deltaTick = tickNow - entityLastTick;
var variance = Math.abs(deltaTick - TIMER_INTERVAL);
entityTotalVariance += variance;
if (variance > 1) {
entityVarianceCount++;
}
if (variance > 5) {
entityHighVarianceCount++;
}
entityTotalTicks += deltaTick;
entityLastTick = tickNow;
// move self!!
var deltaTime = deltaTick / 1000;
time += deltaTime;
rotation = Quat.angleAxis(time * omega / Math.PI * 180.0, { x: 0, y: 1, z: 0 });
Entities.editEntity(_entityID,
{
position: { x: x + Math.sin(time * omega) / 2.0 * range,
y: y,
z: z },
});
if(entityTickCount == (TIMER_HZ * 5)) {
print("ENTITY -- For " + entityTickCount + " samples average interval = " + entityTotalTicks/entityTickCount + " ms"
+ " average variance:" + entityTotalVariance/entityTickCount + " ms"
+ " min variance:" + entityVarianceCount + " [" + (entityVarianceCount/entityTickCount) * 100 + " %] "
+ " high variance:" + entityHighVarianceCount + " [" + (entityHighVarianceCount/entityTickCount) * 100 + " %] "
);
entityTickCount = 0;
entityTotalTicks = 0;
entityTotalVariance = 0;
entityVarianceCount = 0;
entityHighVarianceCount = 0;
}
}
};
return new Foo();
});

View file

@ -202,6 +202,8 @@ var STATE_OVERLAY_TOUCHING = 8;
var holdEnabled = true;
var nearGrabEnabled = true;
var farGrabEnabled = true;
var myAvatarScalingEnabled = true;
var objectScalingEnabled = true;
// "collidesWith" is specified by comma-separated list of group names
// the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar
@ -824,7 +826,6 @@ function MyController(hand) {
};
this.update = function(deltaTime, timestamp) {
this.updateSmoothedTrigger();
// If both trigger and grip buttons squeezed and nothing is held, rescale my avatar!
@ -2232,7 +2233,6 @@ function MyController(hand) {
};
this.nearGrabbing = function(deltaTime, timestamp) {
this.grabPointSphereOff();
if (this.state == STATE_NEAR_GRABBING && (!this.triggerClicked && this.secondaryReleased())) {
@ -2405,6 +2405,10 @@ function MyController(hand) {
};
this.maybeScale = function(props) {
if (!objectScalingEnabled) {
return;
}
if (!this.shouldScale) {
// If both secondary triggers squeezed, and the non-holding hand is empty, start scaling
if (this.secondarySqueezed() && this.getOtherHandController().secondarySqueezed() && this.getOtherHandController().state === STATE_OFF) {
@ -2424,6 +2428,10 @@ function MyController(hand) {
}
this.maybeScaleMyAvatar = function() {
if (!myAvatarScalingEnabled) {
return;
}
if (!this.shouldScale) {
// If both secondary triggers squeezed, start scaling
if (this.secondarySqueezed() && this.getOtherHandController().secondarySqueezed()) {
@ -3104,6 +3112,14 @@ var handleHandMessages = function(channel, message, sender) {
print("farGrabEnabled: ", data.farGrabEnabled);
farGrabEnabled = data.farGrabEnabled;
}
if (data.myAvatarScalingEnabled !== undefined) {
print("myAvatarScalingEnabled: ", data.myAvatarScalingEnabled);
myAvatarScalingEnabled = data.myAvatarScalingEnabled;
}
if (data.objectScalingEnabled !== undefined) {
print("objectScalingEnabled: ", data.objectScalingEnabled);
objectScalingEnabled = data.objectScalingEnabled;
}
} else if (channel === 'Hifi-Hand-Grab') {
try {
data = JSON.parse(message);
@ -3152,9 +3168,65 @@ var handleHandMessages = function(channel, message, sender) {
Messages.messageReceived.connect(handleHandMessages);
var BASIC_TIMER_INTERVAL_MS = 20; // 20ms = 50hz good enough
var TARGET_UPDATE_HZ = 50; // 50hz good enough (no change in logic)
var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ;
var lastInterval = Date.now();
var intervalCount = 0;
var totalDelta = 0;
var totalVariance = 0;
var highVarianceCount = 0;
var veryhighVarianceCount = 0;
var updateTotalWork = 0;
var UPDATE_PERFORMANCE_DEBUGGING = false;
var updateIntervalTimer = Script.setInterval(function(){
update(BASIC_TIMER_INTERVAL_MS / 1000);
intervalCount++;
var thisInterval = Date.now();
var deltaTimeMsec = thisInterval - lastInterval;
var deltaTime = deltaTimeMsec / 1000;
lastInterval = thisInterval;
totalDelta += deltaTimeMsec;
var variance = Math.abs(deltaTimeMsec - BASIC_TIMER_INTERVAL_MS);
totalVariance += variance;
if (variance > 1) {
highVarianceCount++;
}
if (variance > 5) {
veryhighVarianceCount++;
}
// will call update for both hands
var preWork = Date.now();
update(deltaTime);
var postWork = Date.now();
var workDelta = postWork - preWork;
updateTotalWork += workDelta;
if (intervalCount == 100) {
if (UPDATE_PERFORMANCE_DEBUGGING) {
print("handControllerGrab.js -- For " + intervalCount + " samples average= " + totalDelta/intervalCount + " ms"
+ " average variance:" + totalVariance/intervalCount + " ms"
+ " high variance count:" + highVarianceCount + " [ " + (highVarianceCount/intervalCount) * 100 + "% ] "
+ " VERY high variance count:" + veryhighVarianceCount + " [ " + (veryhighVarianceCount/intervalCount) * 100 + "% ] "
+ " average work:" + updateTotalWork/intervalCount + " ms");
}
intervalCount = 0;
totalDelta = 0;
totalVariance = 0;
highVarianceCount = 0;
veryhighVarianceCount = 0;
updateTotalWork = 0;
}
}, BASIC_TIMER_INTERVAL_MS);
function cleanup() {

View file

@ -74,6 +74,9 @@ TOUCH_CONTROLLER_CONFIGURATION_LEFT = {
teleport: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Teleport.png"
},
both_triggers: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Grip-Trigger.png"
},
}
},
@ -212,7 +215,7 @@ TOUCH_CONTROLLER_CONFIGURATION_RIGHT = {
modelURL: BASE_URL + "Oculus-Labels-R.fbx",
naturalPosition: { x: 0.009739525616168976, y: -0.0017818436026573181, z: 0.016794726252555847 },
textureName: "Texture",
textureName: "blank",
defaultTextureLayer: "blank",
textureLayers: {
blank: {
@ -230,6 +233,9 @@ TOUCH_CONTROLLER_CONFIGURATION_RIGHT = {
teleport: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Teleport.png"
},
both_triggers: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Grip-Trigger.png"
},
}
},

View file

@ -1,6 +1,6 @@
"use strict";
/*jslint vars: true, plusplus: true, forin: true*/
/*globals Script, AvatarList, Camera, Overlays, OverlayWindow, Toolbars, Vec3, Quat, Controller, print, getControllerWorldLocation */
/*globals Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, OverlayWindow, Toolbars, Vec3, Quat, Controller, print, getControllerWorldLocation */
//
// pal.js
//
@ -48,7 +48,7 @@ ExtendedOverlay.prototype.select = function (selected) {
var selectedIds = [];
ExtendedOverlay.isSelected = function (id) {
return -1 !== selectedIds.indexOf(id);
}
};
ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier
return overlays[key];
};
@ -73,6 +73,46 @@ ExtendedOverlay.applyPickRay = function (pickRay, cb) { // cb(overlay) on the on
});
};
//
// Similar, for entities
//
function HighlightedEntity(id, entityProperties) {
this.id = id;
this.overlay = Overlays.addOverlay('cube', {
position: entityProperties.position,
rotation: entityProperties.rotation,
dimensions: entityProperties.dimensions,
solid: false,
color: {
red: 0xF3,
green: 0x91,
blue: 0x29
},
lineWidth: 1.0,
ignoreRayIntersection: true,
drawInFront: true
});
HighlightedEntity.overlays.push(this);
}
HighlightedEntity.overlays = [];
HighlightedEntity.clearOverlays = function clearHighlightedEntities() {
HighlightedEntity.overlays.forEach(function (highlighted) {
Overlays.deleteOverlay(highlighted.overlay);
});
HighlightedEntity.overlays = [];
};
HighlightedEntity.updateOverlays = function updateHighlightedEntities() {
HighlightedEntity.overlays.forEach(function (highlighted) {
var properties = Entities.getEntityProperties(highlighted.id, ['position', 'rotation', 'dimensions']);
Overlays.editOverlay(highlighted.overlay, {
position: properties.position,
rotation: properties.rotation,
dimensions: properties.dimensions
});
});
};
//
// The qml window and communications.
//
@ -93,6 +133,25 @@ pal.fromQml.connect(function (message) { // messages are {method, params}, like
var selected = ExtendedOverlay.isSelected(id);
overlay.select(selected);
});
HighlightedEntity.clearOverlays();
if (selectedIds.length) {
Entities.findEntitiesInFrustum(Camera.frustum).forEach(function (id) {
// Because lastEditedBy is per session, the vast majority of entities won't match,
// so it would probably be worth reducing marshalling costs by asking for just we need.
// However, providing property name(s) is advisory and some additional properties are
// included anyway. As it turns out, asking for 'lastEditedBy' gives 'position', 'rotation',
// and 'dimensions', too, so we might as well make use of them instead of making a second
// getEntityProperties call.
// It would be nice if we could harden this against future changes by specifying all
// and only these four in an array, but see
// https://highfidelity.fogbugz.com/f/cases/2728/Entities-getEntityProperties-id-lastEditedBy-name-lastEditedBy-doesn-t-work
var properties = Entities.getEntityProperties(id, 'lastEditedBy');
if (ExtendedOverlay.isSelected(properties.lastEditedBy)) {
new HighlightedEntity(id, properties);
}
});
}
break;
case 'refresh':
removeOverlays();
@ -117,7 +176,6 @@ function addAvatarNode(id) {
}
function populateUserList() {
var data = [];
var counter = 1;
AvatarList.getAvatarIdentifiers().sort().forEach(function (id) { // sorting the identifiers is just an aid for debugging
var avatar = AvatarList.getAvatar(id);
var avatarPalDatum = {
@ -132,10 +190,14 @@ function populateUserList() {
// Request the username from the given UUID
Users.requestUsernameFromID(id);
}
data.push(avatarPalDatum);
if (id) { // No overlay for ourself.
addAvatarNode(id);
// Request personal mute status and ignore status
// from NodeList (as long as we're not requesting it for our own ID)
if (id) {
avatarPalDatum['personalMute'] = Users.getPersonalMuteStatus(id);
avatarPalDatum['ignore'] = Users.getIgnoreStatus(id);
addAvatarNode(id); // No overlay for ourselves
}
data.push(avatarPalDatum);
print('PAL data:', JSON.stringify(avatarPalDatum));
});
pal.sendToQml({method: 'users', params: data});
@ -147,7 +209,7 @@ function usernameFromIDReply(id, username, machineFingerprint) {
// If the ID we've received is our ID...
if (MyAvatar.sessionUUID === id) {
// Set the data to contain specific strings.
data = ['', username]
data = ['', username];
} else {
// Set the data to contain the ID and the username (if we have one)
// or fingerprint (if we don't have a username) string.
@ -186,9 +248,11 @@ function updateOverlays() {
}
});
// We could re-populateUserList if anything added or removed, but not for now.
HighlightedEntity.updateOverlays();
}
function removeOverlays() {
selectedIds = [];
HighlightedEntity.clearOverlays();
ExtendedOverlay.some(function (overlay) { overlay.deleteOverlay(); });
}
@ -252,9 +316,11 @@ function off() {
}
triggerMapping.disable(); // It's ok if we disable twice.
removeOverlays();
Users.requestsDomainListData = false;
}
function onClicked() {
if (!pal.visible) {
Users.requestsDomainListData = true;
populateUserList();
pal.raise();
isWired = true;
@ -267,7 +333,7 @@ function onClicked() {
pal.setVisible(!pal.visible);
}
var AVERAGING_RATIO = 0.05
var AVERAGING_RATIO = 0.05;
var LOUDNESS_FLOOR = 11.0;
var LOUDNESS_SCALE = 2.8 / 5.0;
var LOG2 = Math.log(2.0);
@ -282,12 +348,12 @@ function getAudioLevel(id) {
var audioLevel = 0.0;
// we will do exponential moving average by taking some the last loudness and averaging
accumulatedLevels[id] = AVERAGING_RATIO * (accumulatedLevels[id] || 0 ) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness);
accumulatedLevels[id] = AVERAGING_RATIO * (accumulatedLevels[id] || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness);
// add 1 to insure we don't go log() and hit -infinity. Math.log is
// natural log, so to get log base 2, just divide by ln(2).
var logLevel = Math.log(accumulatedLevels[id] + 1) / LOG2;
if (logLevel <= LOUDNESS_FLOOR) {
audioLevel = logLevel / LOUDNESS_FLOOR * LOUDNESS_SCALE;
} else {
@ -309,7 +375,7 @@ Script.setInterval(function () {
var level = getAudioLevel(id);
// qml didn't like an object with null/empty string for a key, so...
var userId = id || 0;
param[userId]= level;
param[userId] = level;
});
pal.sendToQml({method: 'updateAudioLevel', params: param});
}
@ -326,6 +392,14 @@ button.clicked.connect(onClicked);
pal.visibleChanged.connect(onVisibleChanged);
pal.closed.connect(off);
Users.usernameFromIDReply.connect(usernameFromIDReply);
function clearIgnoredInQMLAndClosePAL() {
pal.sendToQml({ method: 'clearIgnored' });
if (pal.visible) {
onClicked(); // Close the PAL
}
}
Window.domainChanged.connect(clearIgnoredInQMLAndClosePAL);
Window.domainConnectionRefused.connect(clearIgnoredInQMLAndClosePAL);
//
// Cleanup.
@ -336,6 +410,8 @@ Script.scriptEnding.connect(function () {
pal.visibleChanged.disconnect(onVisibleChanged);
pal.closed.disconnect(off);
Users.usernameFromIDReply.disconnect(usernameFromIDReply);
Window.domainChanged.disconnect(clearIgnoredInQMLAndClosePAL);
Window.domainConnectionRefused.disconnect(clearIgnoredInQMLAndClosePAL);
off();
});

View file

@ -0,0 +1,130 @@
// springAway.js
//
// If you attach this entity script to an object, the object will spring away from
// your avatar's hands. Useful for making a beachball or basketball, because you
// can bounce it on your hands.
//
// You can change the force applied by the script by setting userData to contain
// a value 'strength', which will otherwise default to DEFAULT_STRENGTH
//
// Note that the use of dimensions.x as the size of the entity means that it
// needs to be spherical for this to look correct.
//
// Copyright 2016 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(){
var UPDATE_INTERVAL_MSECS = 1000/45; // Update the spring/object at 45Hz
var SETTINGS_INTERVAL_MSECS = 1000; // Periodically check user data for updates
var TOO_FAR = 5.0; // If the object
var DEFAULT_STRENGTH = 3.0; // How strong/stiff is the spring?
var entity;
var props;
var checkTimer = false;
var settingsTimer = false;
var strength = DEFAULT_STRENGTH;
var _this;
var WANT_DEBUG = false;
function debugPrint(string) {
if (WANT_DEBUG) {
print(string);
}
}
function howFarAway(position) {
return Vec3.distance(MyAvatar.position, position);
}
function springForce(position, center, radius) {
//
// Return a vector corresponding to a normalized spring force ranging from 1 at
// exact center to zero at distance radius from center.
//
var distance = Vec3.distance(position, center);
return Vec3.multiply(1.0 - distance / radius, Vec3.normalize(Vec3.subtract(center, position)));
}
function fingerPosition(which) {
//
// Get the worldspace position of either the tip of the index finger (jointName "RightHandIndex3", etc), or
// fall back to the controller position if that doesn't exist.
//
var joint = MyAvatar.getJointPosition(which === "RIGHT" ? "RightHandIndex3" : "LeftHandIndex3");
if (Vec3.length(joint) > 0) {
return joint;
} else {
return Vec3.sum(MyAvatar.position,
Vec3.multiplyQbyV(MyAvatar.orientation,
Controller.getPoseValue(which === "RIGHT" ? Controller.Standard.RightHand : Controller.Standard.LeftHand).translation));
}
}
this.preload = function(entityID) {
// Load the sound and range from the entity userData fields, and note the position of the entity.
debugPrint("springAway preload");
entity = entityID;
_this = this;
checkTimer = Script.setInterval(this.maybePush, UPDATE_INTERVAL_MSECS);
settingsTimer = Script.setInterval(this.checkSettings, SETTINGS_INTERVAL_MSECS);
};
this.maybePush = function() {
props = Entities.getEntityProperties(entity, [ "position", "dimensions", "velocity" ]);
// First, check if the entity is far enough away to not need to do anything with it
if (howFarAway(props.position) - props.dimensions.x / 2 > TOO_FAR) {
return;
}
var rightFingerPosition = fingerPosition("RIGHT");
var leftFingerPosition = fingerPosition("LEFT");
var addVelocity = { x: 0, y: 0, z: 0 };
if (Vec3.distance(leftFingerPosition, props.position) < props.dimensions.x / 2) {
addVelocity = Vec3.sum(addVelocity, Vec3.multiply(springForce(leftFingerPosition, props.position, props.dimensions.x), strength));
}
if (Vec3.distance(rightFingerPosition, props.position) < props.dimensions.x / 2) {
addVelocity = Vec3.sum(addVelocity, Vec3.multiply(springForce(rightFingerPosition, props.position, props.dimensions.x), strength));
}
if (Vec3.length(addVelocity) > 0) {
Entities.editEntity(entity, {
velocity: Vec3.sum(props.velocity, addVelocity)
});
}
}
this.checkSettings = function() {
var dataProps = Entities.getEntityProperties(entity, [ "userData" ]);
if (dataProps.userData) {
var data = JSON.parse(dataProps.userData);
if (data.strength) {
if (!(strength === data.strength)) {
debugPrint("Read new spring strength: " + data.strength);
}
strength = data.strength;
}
}
}
this.unload = function(entityID) {
debugPrint("springAway unload");
if (checkTimer) {
Script.clearInterval(checkTimer);
}
if (settingsTimer) {
Script.clearInterval(settingsTimer);
}
};
})

View file

@ -42,7 +42,7 @@ const appIcon = path.join(__dirname, '../resources/console.png');
const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days
const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/;
const HOME_CONTENT_URL = "http://cachefly.highfidelity.com/home-tutorial-release-5572.tar.gz";
const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-28.tar.gz";
function getBuildInfo() {
var buildInfoPath = null;

View file

@ -14,6 +14,8 @@
#include <gl/Config.h>
#include <gl/Context.h>
#include <QProcessEnvironment>
#include <QtCore/QDir>
#include <QtCore/QElapsedTimer>
#include <QtCore/QLoggingCategory>
@ -23,7 +25,6 @@
#include <QtCore/QThread>
#include <QtCore/QThreadPool>
#include <QtGui/QGuiApplication>
#include <QtGui/QResizeEvent>
#include <QtGui/QWindow>
@ -33,7 +34,6 @@
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QApplication>
#include <shared/RateCounter.h>
#include <shared/NetworkUtils.h>
#include <shared/FileLogger.h>
@ -60,8 +60,10 @@
#include <model-networking/ModelCache.h>
#include <GeometryCache.h>
#include <DeferredLightingEffect.h>
#include <render/RenderFetchCullSortTask.h>
#include <RenderShadowTask.h>
#include <RenderDeferredTask.h>
#include <RenderForwardTask.h>
#include <OctreeConstants.h>
#include <EntityTreeRenderer.h>
@ -537,7 +539,14 @@ public:
_initContext.makeCurrent();
// Render engine init
_renderEngine->addJob<RenderShadowTask>("RenderShadowTask", _cullFunctor);
_renderEngine->addJob<RenderDeferredTask>("RenderDeferredTask", _cullFunctor);
const auto items = _renderEngine->addJob<RenderFetchCullSortTask>("FetchCullSort", _cullFunctor);
assert(items.canCast<RenderFetchCullSortTask::Output>());
static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD";
if (QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD)) {
_renderEngine->addJob<RenderForwardTask>("RenderForwardTask", items.get<RenderFetchCullSortTask::Output>());
} else {
_renderEngine->addJob<RenderDeferredTask>("RenderDeferredTask", items.get<RenderFetchCullSortTask::Output>());
}
_renderEngine->load();
_renderEngine->registerScene(_main3DScene);

View file

@ -58,6 +58,9 @@ function info() {
}
}
const CONTROLLER_TOUCH = 'touch';
const CONTROLLER_VIVE = 'vive';
var NEAR_BOX_SPAWN_NAME = "tutorial/nearGrab/box_spawn";
var FAR_BOX_SPAWN_NAME = "tutorial/farGrab/box_spawn";
var GUN_SPAWN_NAME = "tutorial/gun_spawn";
@ -323,7 +326,7 @@ function hideEntitiesWithTag(tag) {
//});
}
/**
/**
* Return the entity properties for an entity with a given name if it is in our
* cached list of entities. Otherwise, return undefined.
*/
@ -371,6 +374,8 @@ function disableEverything() {
nearGrabEnabled: true,
holdEnabled: false,
farGrabEnabled: false,
myAvatarScalingEnabled: false,
objectScalingEnabled: false,
}));
setControllerPartLayer('touchpad', 'blank');
setControllerPartLayer('trigger', 'blank');
@ -399,6 +404,8 @@ function reenableEverything() {
nearGrabEnabled: true,
holdEnabled: true,
farGrabEnabled: true,
myAvatarScalingEnabled: true,
objectScalingEnabled: true,
}));
setControllerPartLayer('touchpad', 'blank');
setControllerPartLayer('trigger', 'blank');
@ -516,12 +523,14 @@ stepOrient.prototype = {
// STEP: Near Grab //
// //
///////////////////////////////////////////////////////////////////////////////
var stepNearGrab = function() {
var stepNearGrab = function(tutorialManager) {
this.name = 'nearGrab';
this.tag = "nearGrab";
this.tags = ["bothGrab", "nearGrab", "nearGrab-" + tutorialManager.controllerName];
this.tempTag = "nearGrab-temporary";
this.birdIDs = [];
this.controllerName = tutorialManager.controllerName;
Messages.subscribe("Entity-Exploded");
Messages.messageReceived.connect(this.onMessage.bind(this));
}
@ -530,12 +539,17 @@ stepNearGrab.prototype = {
this.finished = false;
this.onFinish = onFinish;
setControllerPartLayer('tips', 'trigger');
setControllerPartLayer('trigger', 'highlight');
if (this.controllerName === CONTROLLER_TOUCH) {
setControllerPartLayer('tips', 'both_triggers');
setControllerPartLayer('trigger', 'highlight');
setControllerPartLayer('grip', 'highlight');
} else {
setControllerPartLayer('tips', 'trigger');
setControllerPartLayer('trigger', 'highlight');
}
// Spawn content set
showEntitiesWithTag(this.tag, { visible: true });
showEntitiesWithTag('bothGrab', { visible: true });
// Show content set
showEntitiesWithTags(this.tags);
var boxSpawnPosition = getEntityWithName(NEAR_BOX_SPAWN_NAME).position;
function createBlock(fireworkNumber) {
@ -573,8 +587,8 @@ stepNearGrab.prototype = {
this.finished = true;
setControllerPartLayer('tips', 'blank');
setControllerPartLayer('trigger', 'normal');
hideEntitiesWithTag(this.tag, { visible: false});
hideEntitiesWithTag('bothGrab', { visible: false});
setControllerPartLayer('grip', 'normal');
hideEntitiesWithTags(this.tags);
deleteEntitiesWithTag(this.tempTag);
if (this.positionWatcher) {
this.positionWatcher.destroy();
@ -696,6 +710,7 @@ PositionWatcher.prototype = {
///////////////////////////////////////////////////////////////////////////////
var stepEquip = function(tutorialManager) {
const controllerName = tutorialManager.controllerName;
this.controllerName = controllerName;
this.name = 'equip';
@ -714,8 +729,13 @@ var stepEquip = function(tutorialManager) {
}
stepEquip.prototype = {
start: function(onFinish) {
setControllerPartLayer('tips', 'trigger');
setControllerPartLayer('trigger', 'highlight');
if (this.controllerName === CONTROLLER_TOUCH) {
setControllerPartLayer('tips', 'grip');
setControllerPartLayer('grip', 'highlight');
} else {
setControllerPartLayer('tips', 'trigger');
setControllerPartLayer('trigger', 'highlight');
}
Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({
holdEnabled: true,

View file

@ -52,6 +52,16 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = {
"tag": "farGrab"
}
},
"nearGrab-vive": {
"{88221a22-b710-4d35-852b-5257b0aa77dc}": {
"tag": "nearGrab-vive"
}
},
"nearGrab-touch": {
"{7c0f2fde-6c5c-459b-bf82-421979cebf2e}": {
"tag": "nearGrab-touch"
}
},
"nearGrab": {
"{55c861ef-60ca-4722-a6c5-9c6967966ec5}": {
"tag": "nearGrab"
@ -59,9 +69,6 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = {
"{644d655b-ae66-43b1-9bab-a44b9a8ad632}": {
"tag": "nearGrab"
},
"{88221a22-b710-4d35-852b-5257b0aa77dc}": {
"tag": "nearGrab"
},
"{8bf0baa1-88d0-448a-a782-100d4413bd82}": {
"tag": "nearGrab"
},
@ -70,13 +77,20 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = {
"tag": "nearGrab"
}
},
"equip-part1-touch": {
"{470f0634-8be7-4b52-a8bd-5183d489fcb6}": {
"tag": "equip-part1-touch"
}
},
"equip-part1-vive": {
"{97ced5e7-fc81-40f9-a9e8-f85b4b30f24c}": {
"tag": "equip-part1-vive"
}
},
"equip-part1": {
"{d73822ca-0a34-4cf4-a530-3258ac459a14}": {
"tag": "equip-part1"
},
"{97ced5e7-fc81-40f9-a9e8-f85b4b30f24c}": {
"tag": "equip-part1"
},
"{8572d991-5777-45df-97bf-7243d7b12f81}": {
"tag": "equip-part1"
},
@ -102,16 +116,17 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = {
"equip-part2-vive": {
"{b5d17eda-90ab-40cf-b973-efcecb2e992e}": {
"tag": "equip-part2-vive"
},
"{6307cd16-dd1d-4988-a339-578178436b45}": {
"tag": "equip-part2-vive"
}
},
"equip-part2-touch": {
"{69195139-e020-4739-bb2c-50faebc6860a}": {
"tag": "equip-part2-touch"
}
},
"equip-part2": {
"{6307cd16-dd1d-4988-a339-578178436b45}": {
"tag": "equip-part2"
},
"{9b0a99ae-221b-4e59-ba3c-d8e64a083774}": {
"tag": "equip-part2-touch"
}
},
"bothGrab": {