Merge branch 'master' of github.com:highfidelity/hifi into oculus-store-commerce

This commit is contained in:
Zach Fox 2018-10-29 14:57:23 -07:00
commit d47fd44a47
106 changed files with 8384 additions and 6303 deletions
CMakeLists.txt
assignment-client/src/avatars
interface
libraries
plugins/openvr/src
scripts
server-console/src
tests-manual/qml

View file

@ -66,7 +66,7 @@ if (ANDROID)
set(GLES_OPTION ON)
set(PLATFORM_QT_COMPONENTS AndroidExtras WebView)
else ()
set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets)
set(PLATFORM_QT_COMPONENTS WebEngine)
endif ()
if (USE_GLES AND (NOT ANDROID))

View file

@ -541,7 +541,7 @@ void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMess
// ...For those nodes, reset the lastBroadcastTime to 0
// so that the AvatarMixer will send Identity data to us
[&](const SharedNodePointer& node) {
nodeData->setLastBroadcastTime(node->getUUID(), 0);
nodeData->setLastBroadcastTime(node->getLocalID(), 0);
nodeData->resetSentTraitData(node->getLocalID());
}
);
@ -565,7 +565,8 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
// parse the identity packet and update the change timestamp if appropriate
bool identityChanged = false;
bool displayNameChanged = false;
avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged);
QDataStream avatarIdentityStream(message->getMessage());
avatar.processAvatarIdentity(avatarIdentityStream, identityChanged, displayNameChanged);
if (identityChanged) {
QMutexLocker nodeDataLocker(&nodeData->getMutex());
@ -637,7 +638,7 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage>
// Reset the lastBroadcastTime for the ignored avatar to 0
// so the AvatarMixer knows it'll have to send identity data about the ignored avatar
// to the ignorer if the ignorer unignores.
nodeData->setLastBroadcastTime(ignoredUUID, 0);
nodeData->setLastBroadcastTime(ignoredNode->getLocalID(), 0);
nodeData->resetSentTraitData(ignoredNode->getLocalID());
}
@ -647,7 +648,7 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage>
// to the ignored if the ignorer unignores.
AvatarMixerClientData* ignoredNodeData = reinterpret_cast<AvatarMixerClientData*>(ignoredNode->getLinkedData());
if (ignoredNodeData) {
ignoredNodeData->setLastBroadcastTime(senderNode->getUUID(), 0);
ignoredNodeData->setLastBroadcastTime(senderNode->getLocalID(), 0);
ignoredNodeData->resetSentTraitData(senderNode->getLocalID());
}
}

View file

@ -26,20 +26,20 @@ AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID
_avatar->setID(nodeID);
}
uint64_t AvatarMixerClientData::getLastOtherAvatarEncodeTime(QUuid otherAvatar) const {
std::unordered_map<QUuid, uint64_t>::const_iterator itr = _lastOtherAvatarEncodeTime.find(otherAvatar);
uint64_t AvatarMixerClientData::getLastOtherAvatarEncodeTime(NLPacket::LocalID otherAvatar) const {
const auto itr = _lastOtherAvatarEncodeTime.find(otherAvatar);
if (itr != _lastOtherAvatarEncodeTime.end()) {
return itr->second;
}
return 0;
}
void AvatarMixerClientData::setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, uint64_t time) {
std::unordered_map<QUuid, uint64_t>::iterator itr = _lastOtherAvatarEncodeTime.find(otherAvatar);
void AvatarMixerClientData::setLastOtherAvatarEncodeTime(NLPacket::LocalID otherAvatar, uint64_t time) {
auto itr = _lastOtherAvatarEncodeTime.find(otherAvatar);
if (itr != _lastOtherAvatarEncodeTime.end()) {
itr->second = time;
} else {
_lastOtherAvatarEncodeTime.emplace(std::pair<QUuid, uint64_t>(otherAvatar, time));
_lastOtherAvatarEncodeTime.emplace(std::pair<NLPacket::LocalID, uint64_t>(otherAvatar, time));
}
}
@ -220,7 +220,7 @@ void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedDa
}
}
uint64_t AvatarMixerClientData::getLastBroadcastTime(const QUuid& nodeUUID) const {
uint64_t AvatarMixerClientData::getLastBroadcastTime(NLPacket::LocalID nodeUUID) const {
// return the matching PacketSequenceNumber, or the default if we don't have it
auto nodeMatch = _lastBroadcastTimes.find(nodeUUID);
if (nodeMatch != _lastBroadcastTimes.end()) {
@ -229,9 +229,9 @@ uint64_t AvatarMixerClientData::getLastBroadcastTime(const QUuid& nodeUUID) cons
return 0;
}
uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& nodeUUID) const {
uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) const {
// return the matching PacketSequenceNumber, or the default if we don't have it
auto nodeMatch = _lastBroadcastSequenceNumbers.find(nodeUUID);
auto nodeMatch = _lastBroadcastSequenceNumbers.find(nodeID);
if (nodeMatch != _lastBroadcastSequenceNumbers.end()) {
return nodeMatch->second;
}
@ -252,7 +252,7 @@ void AvatarMixerClientData::ignoreOther(const Node* self, const Node* other) {
} else {
killPacket->writePrimitive(KillAvatarReason::YourAvatarEnteredTheirBubble);
}
setLastBroadcastTime(other->getUUID(), 0);
setLastBroadcastTime(other->getLocalID(), 0);
resetSentTraitData(other->getLocalID());
@ -331,9 +331,9 @@ AvatarMixerClientData::TraitsCheckTimestamp AvatarMixerClientData::getLastOtherA
}
}
void AvatarMixerClientData::cleanupKilledNode(const QUuid& nodeUUID, Node::LocalID nodeLocalID) {
removeLastBroadcastSequenceNumber(nodeUUID);
removeLastBroadcastTime(nodeUUID);
void AvatarMixerClientData::cleanupKilledNode(const QUuid&, Node::LocalID nodeLocalID) {
removeLastBroadcastSequenceNumber(nodeLocalID);
removeLastBroadcastTime(nodeLocalID);
_lastSentTraitsTimestamps.erase(nodeLocalID);
_sentTraitVersions.erase(nodeLocalID);
}

View file

@ -49,17 +49,16 @@ public:
const AvatarData* getConstAvatarData() const { return _avatar.get(); }
AvatarSharedPointer getAvatarSharedPointer() const { return _avatar; }
uint16_t getLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) const;
void setLastBroadcastSequenceNumber(NLPacket::LocalID nodeID, uint16_t sequenceNumber)
{ _lastBroadcastSequenceNumbers[nodeID] = sequenceNumber; }
Q_INVOKABLE void removeLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) { _lastBroadcastSequenceNumbers.erase(nodeID); }
bool isIgnoreRadiusEnabled() const { return _isIgnoreRadiusEnabled; }
void setIsIgnoreRadiusEnabled(bool enabled) { _isIgnoreRadiusEnabled = enabled; }
uint16_t getLastBroadcastSequenceNumber(const QUuid& nodeUUID) const;
void setLastBroadcastSequenceNumber(const QUuid& nodeUUID, uint16_t sequenceNumber)
{ _lastBroadcastSequenceNumbers[nodeUUID] = sequenceNumber; }
Q_INVOKABLE void removeLastBroadcastSequenceNumber(const QUuid& nodeUUID) { _lastBroadcastSequenceNumbers.erase(nodeUUID); }
uint64_t getLastBroadcastTime(const QUuid& nodeUUID) const;
void setLastBroadcastTime(const QUuid& nodeUUID, uint64_t broadcastTime) { _lastBroadcastTimes[nodeUUID] = broadcastTime; }
Q_INVOKABLE void removeLastBroadcastTime(const QUuid& nodeUUID) { _lastBroadcastTimes.erase(nodeUUID); }
uint64_t getLastBroadcastTime(NLPacket::LocalID nodeUUID) const;
void setLastBroadcastTime(NLPacket::LocalID nodeUUID, uint64_t broadcastTime) { _lastBroadcastTimes[nodeUUID] = broadcastTime; }
Q_INVOKABLE void removeLastBroadcastTime(NLPacket::LocalID nodeUUID) { _lastBroadcastTimes.erase(nodeUUID); }
Q_INVOKABLE void cleanupKilledNode(const QUuid& nodeUUID, Node::LocalID nodeLocalID);
@ -93,7 +92,7 @@ public:
void loadJSONStats(QJsonObject& jsonObject) const;
glm::vec3 getPosition() const { return _avatar ? _avatar->getWorldPosition() : glm::vec3(0); }
glm::vec3 getPosition() const { return _avatar ? _avatar->getClientGlobalPosition() : glm::vec3(0); }
bool isRadiusIgnoring(const QUuid& other) const;
void addToRadiusIgnoringSet(const QUuid& other);
void removeFromRadiusIgnoringSet(const QUuid& other);
@ -114,10 +113,10 @@ public:
const ConicalViewFrustums& getViewFrustums() const { return _currentViewFrustums; }
uint64_t getLastOtherAvatarEncodeTime(QUuid otherAvatar) const;
void setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, uint64_t time);
uint64_t getLastOtherAvatarEncodeTime(NLPacket::LocalID otherAvatar) const;
void setLastOtherAvatarEncodeTime(NLPacket::LocalID otherAvatar, uint64_t time);
QVector<JointData>& getLastOtherAvatarSentJoints(QUuid otherAvatar) { return _lastOtherAvatarSentJoints[otherAvatar]; }
QVector<JointData>& getLastOtherAvatarSentJoints(NLPacket::LocalID otherAvatar) { return _lastOtherAvatarSentJoints[otherAvatar]; }
void queuePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node);
int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed
@ -150,13 +149,13 @@ private:
AvatarSharedPointer _avatar { new AvatarData() };
uint16_t _lastReceivedSequenceNumber { 0 };
std::unordered_map<QUuid, uint16_t> _lastBroadcastSequenceNumbers;
std::unordered_map<QUuid, uint64_t> _lastBroadcastTimes;
std::unordered_map<NLPacket::LocalID, uint16_t> _lastBroadcastSequenceNumbers;
std::unordered_map<NLPacket::LocalID, uint64_t> _lastBroadcastTimes;
// this is a map of the last time we encoded an "other" avatar for
// sending to "this" node
std::unordered_map<QUuid, uint64_t> _lastOtherAvatarEncodeTime;
std::unordered_map<QUuid, QVector<JointData>> _lastOtherAvatarSentJoints;
std::unordered_map<NLPacket::LocalID, uint64_t> _lastOtherAvatarEncodeTime;
std::unordered_map<NLPacket::LocalID, QVector<JointData>> _lastOtherAvatarSentJoints;
uint64_t _identityChangeTimestamp;
bool _avatarSessionDisplayNameMustChange{ true };

View file

@ -68,13 +68,11 @@ void AvatarMixerSlave::processIncomingPackets(const SharedNodePointer& node) {
_stats.processIncomingPacketsElapsedTime += (end - start);
}
int AvatarMixerSlave::sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) {
if (destinationNode->getType() == NodeType::Agent && !destinationNode->isUpstream()) {
int AvatarMixerSlave::sendIdentityPacket(NLPacketList& packetList, const AvatarMixerClientData* nodeData, const Node& destinationNode) {
if (destinationNode.getType() == NodeType::Agent && !destinationNode.isUpstream()) {
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
identityPackets->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPackets), *destinationNode);
packetList.write(individualData);
_stats.numIdentityPackets++;
return individualData.size();
} else {
@ -247,12 +245,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// reset the internal state for correct random number distribution
distribution.reset();
// Estimate number to sort on number sent last frame (with min. of 20).
const int numToSendEst = std::max(int(nodeData->getNumAvatarsSentLastFrame() * 2.5f), 20);
// reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame();
// keep a counter of the number of considered avatars
int numOtherAvatars = 0;
// keep track of outbound data rate specifically for avatar data
int numAvatarDataBytes = 0;
int identityBytesSent = 0;
@ -261,7 +259,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// max number of avatarBytes per frame
int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND);
// keep track of the number of other avatars held back in this frame
int numAvatarsHeldBack = 0;
@ -279,10 +276,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
int minimumBytesPerAvatar = PALIsOpen ? AvatarDataPacket::AVATAR_HAS_FLAGS_SIZE + NUM_BYTES_RFC4122_UUID +
sizeof(AvatarDataPacket::AvatarGlobalPosition) + sizeof(AvatarDataPacket::AudioLoudness) : 0;
// setup a PacketList for the avatarPackets
auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData);
static auto maxAvatarDataBytes = avatarPacketList->getMaxSegmentSize() - NUM_BYTES_RFC4122_UUID;
// compute node bounding box
const float MY_AVATAR_BUBBLE_EXPANSION_FACTOR = 4.0f; // magic number determined emperically
AABox nodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR);
@ -350,8 +343,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored
if (nodeData->isIgnoreRadiusEnabled() || (avatarClientNodeData->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
// Perform the collision check between the two bounding boxes
const float OTHER_AVATAR_BUBBLE_EXPANSION_FACTOR = 2.4f; // magic number determined empirically
AABox otherNodeBox = computeBubbleBox(avatarClientNodeData->getAvatar(), OTHER_AVATAR_BUBBLE_EXPANSION_FACTOR);
AABox otherNodeBox = avatarClientNodeData->getAvatar().getDefaultBubbleBox();
if (nodeBox.touches(otherNodeBox)) {
nodeData->ignoreOther(destinationNode, avatarNode);
shouldIgnore = !getsAnyIgnored;
@ -364,7 +356,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
}
if (!shouldIgnore) {
AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getUUID());
AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getLocalID());
AvatarDataSequenceNumber lastSeqFromSender = avatarClientNodeData->getLastReceivedSequenceNumber();
// FIXME - This code does appear to be working. But it seems brittle.
@ -396,7 +388,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
if (!shouldIgnore) {
// sort this one for later
const AvatarData* avatarNodeData = avatarClientNodeData->getConstAvatarData();
auto lastEncodeTime = nodeData->getLastOtherAvatarEncodeTime(avatarNodeData->getSessionUUID());
auto lastEncodeTime = nodeData->getLastOtherAvatarEncodeTime(avatarNode->getLocalID());
sortedAvatars.push(SortableAvatar(avatarNodeData, avatarNode, lastEncodeTime));
}
@ -406,8 +398,13 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
int remainingAvatars = (int)sortedAvatars.size();
auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true);
auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
const int avatarPacketCapacity = avatarPacket->getPayloadCapacity();
int avatarSpaceAvailable = avatarPacketCapacity;
int numPacketsSent = 0;
auto identityPacketList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
const auto& sortedAvatarVector = sortedAvatars.getSortedVector();
const auto& sortedAvatarVector = sortedAvatars.getSortedVector(numToSendEst);
for (const auto& sortedAvatar : sortedAvatarVector) {
const Node* otherNode = sortedAvatar.getNode();
auto lastEncodeForOther = sortedAvatar.getTimestamp();
@ -432,21 +429,9 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
auto startAvatarDataPacking = chrono::high_resolution_clock::now();
++numOtherAvatars;
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
const AvatarData* otherAvatar = otherNodeData->getConstAvatarData();
// If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
// the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
if (otherAvatar->hasProcessedFirstIdentity()
&& nodeData->getLastBroadcastTime(otherNode->getUUID()) <= otherNodeData->getIdentityChangeTimestamp()) {
identityBytesSent += sendIdentityPacket(otherNodeData, node);
// remember the last time we sent identity details about this other node to the receiver
nodeData->setLastBroadcastTime(otherNode->getUUID(), usecTimestampNow());
}
// Typically all out-of-view avatars but such avatars' priorities will rise with time:
bool isLowerPriority = sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD;
@ -456,71 +441,56 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
} else if (!overBudget) {
detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData;
nodeData->incrementAvatarInView();
}
bool includeThisAvatar = true;
QVector<JointData>& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID());
// If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
// the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
if (otherAvatar->hasProcessedFirstIdentity()
&& nodeData->getLastBroadcastTime(otherNode->getLocalID()) <= otherNodeData->getIdentityChangeTimestamp()) {
identityBytesSent += sendIdentityPacket(*identityPacketList, otherNodeData, *destinationNode);
lastSentJointsForOther.resize(otherAvatar->getJointCount());
bool distanceAdjust = true;
glm::vec3 viewerPosition = myPosition;
AvatarDataPacket::HasFlags hasFlagsOut; // the result of the toByteArray
bool dropFaceTracking = false;
auto startSerialize = chrono::high_resolution_clock::now();
QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition,
&lastSentJointsForOther);
auto endSerialize = chrono::high_resolution_clock::now();
_stats.toByteArrayElapsedTime +=
(quint64) chrono::duration_cast<chrono::microseconds>(endSerialize - startSerialize).count();
if (bytes.size() > maxAvatarDataBytes) {
qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID()
<< "resulted in very large buffer of" << bytes.size() << "bytes - dropping facial data";
dropFaceTracking = true; // first try dropping the facial data
bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
if (bytes.size() > maxAvatarDataBytes) {
qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID()
<< "without facial data resulted in very large buffer of" << bytes.size()
<< "bytes - reducing to MinimumData";
bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
if (bytes.size() > maxAvatarDataBytes) {
qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID()
<< "MinimumData resulted in very large buffer of" << bytes.size()
<< "bytes - refusing to send avatar";
includeThisAvatar = false;
}
// remember the last time we sent identity details about this other node to the receiver
nodeData->setLastBroadcastTime(otherNode->getLocalID(), usecTimestampNow());
}
}
if (includeThisAvatar) {
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122());
numAvatarDataBytes += avatarPacketList->write(bytes);
avatarPacketList->endSegment();
QVector<JointData>& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getLocalID());
if (detail != AvatarData::NoData) {
_stats.numOthersIncluded++;
const bool distanceAdjust = true;
const bool dropFaceTracking = false;
AvatarDataPacket::SendStatus sendStatus;
sendStatus.sendUUID = true;
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
do {
auto startSerialize = chrono::high_resolution_clock::now();
QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
sendStatus, dropFaceTracking, distanceAdjust, myPosition,
&lastSentJointsForOther, avatarSpaceAvailable);
auto endSerialize = chrono::high_resolution_clock::now();
_stats.toByteArrayElapsedTime +=
(quint64)chrono::duration_cast<chrono::microseconds>(endSerialize - startSerialize).count();
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNodeData->getLastReceivedSequenceNumber());
nodeData->setLastOtherAvatarEncodeTime(otherNode->getUUID(), usecTimestampNow());
avatarPacket->write(bytes);
avatarSpaceAvailable -= bytes.size();
numAvatarDataBytes += bytes.size();
if (!sendStatus || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) {
// Weren't able to fit everything.
nodeList->sendPacket(std::move(avatarPacket), *destinationNode);
++numPacketsSent;
avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
avatarSpaceAvailable = avatarPacketCapacity;
}
} else {
// TODO? this avatar is not included now, and will probably not be included next frame.
// It would be nice if we could tweak its future sort priority to put it at the back of the list.
} while (!sendStatus);
if (detail != AvatarData::NoData) {
_stats.numOthersIncluded++;
// increment the number of avatars sent to this receiver
nodeData->incrementNumAvatarsSentLastFrame();
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(otherNode->getLocalID(),
otherNodeData->getLastReceivedSequenceNumber());
nodeData->setLastOtherAvatarEncodeTime(otherNode->getLocalID(), usecTimestampNow());
}
auto endAvatarDataPacking = chrono::high_resolution_clock::now();
@ -532,17 +502,21 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
remainingAvatars--;
}
if (nodeData->getNumAvatarsSentLastFrame() > numToSendEst) {
qCWarning(avatars) << "More avatars sent than upper estimate" << nodeData->getNumAvatarsSentLastFrame()
<< " / " << numToSendEst;
}
quint64 startPacketSending = usecTimestampNow();
// close the current packet so that we're always sending something
avatarPacketList->closeCurrentPacket(true);
if (avatarPacket->getPayloadSize() != 0) {
nodeList->sendPacket(std::move(avatarPacket), *destinationNode);
++numPacketsSent;
}
_stats.numPacketsSent += (int)avatarPacketList->getNumPackets();
_stats.numPacketsSent += numPacketsSent;
_stats.numBytesSent += numAvatarDataBytes;
// send the avatar data PacketList
nodeList->sendPacketList(std::move(avatarPacketList), *destinationNode);
// record the bytes sent for other avatar data in the AvatarMixerClientData
nodeData->recordSentAvatarData(numAvatarDataBytes);
@ -554,6 +528,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
nodeList->sendPacketList(std::move(traitsPacketList), *destinationNode);
}
// Send any AvatarIdentity packets:
identityPacketList->closeCurrentPacket();
if (identityBytesSent > 0) {
nodeList->sendPacketList(std::move(identityPacketList), *destinationNode);
}
// record the number of avatars held back this frame
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
@ -599,20 +579,20 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin
// so we always send a full update for this avatar
quint64 start = usecTimestampNow();
AvatarDataPacket::HasFlags flagsOut;
AvatarDataPacket::SendStatus sendStatus;
QVector<JointData> emptyLastJointSendData { otherAvatar->getJointCount() };
QByteArray avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData,
flagsOut, false, false, glm::vec3(0), nullptr);
sendStatus, false, false, glm::vec3(0), nullptr, 0);
quint64 end = usecTimestampNow();
_stats.toByteArrayElapsedTime += (end - start);
auto lastBroadcastTime = nodeData->getLastBroadcastTime(agentNode->getUUID());
auto lastBroadcastTime = nodeData->getLastBroadcastTime(agentNode->getLocalID());
if (lastBroadcastTime <= agentNodeData->getIdentityChangeTimestamp()
|| (start - lastBroadcastTime) >= REBROADCAST_IDENTITY_TO_DOWNSTREAM_EVERY_US) {
sendReplicatedIdentityPacket(*agentNode, agentNodeData, *node);
nodeData->setLastBroadcastTime(agentNode->getUUID(), start);
nodeData->setLastBroadcastTime(agentNode->getLocalID(), start);
}
// figure out how large our avatar byte array can be to fit in the packet list
@ -630,14 +610,14 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin
<< "-" << avatarByteArray.size() << "bytes";
avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData,
flagsOut, true, false, glm::vec3(0), nullptr);
sendStatus, true, false, glm::vec3(0), nullptr, 0);
if (avatarByteArray.size() > maxAvatarByteArraySize) {
qCWarning(avatars) << "Replicated avatar data without facial data still too large for"
<< otherAvatar->getSessionUUID() << "-" << avatarByteArray.size() << "bytes";
avatarByteArray = otherAvatar->toByteArray(AvatarData::MinimumData, 0, emptyLastJointSendData,
flagsOut, true, false, glm::vec3(0), nullptr);
sendStatus, true, false, glm::vec3(0), nullptr, 0);
}
}
@ -646,7 +626,7 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin
nodeData->incrementNumAvatarsSentLastFrame();
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(agentNode->getUUID(),
nodeData->setLastBroadcastSequenceNumber(agentNode->getLocalID(),
agentNodeData->getLastReceivedSequenceNumber());
// increment the number of avatars sent to this reciever

View file

@ -101,7 +101,7 @@ public:
void harvestStats(AvatarMixerSlaveStats& stats);
private:
int sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
int sendIdentityPacket(NLPacketList& packet, const AvatarMixerClientData* nodeData, const Node& destinationNode);
int sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode);
qint64 addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,

View file

@ -0,0 +1,140 @@
{
"version": "1.1",
"root": {
"id": "userAnimStateMachine",
"type": "stateMachine",
"data": {
"currentState": "idleAnim",
"states": [
{
"id": "idleAnim",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "postTransitAnim", "state": "postTransitAnim" },
{ "var": "preTransitAnim", "state": "preTransitAnim" }
]
},
{
"id": "preTransitAnim",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "idleAnim", "state": "idleAnim" },
{ "var": "transitAnim", "state": "transitAnim" }
]
},
{
"id": "transitAnim",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "preTransitAnim", "state": "preTransitAnim" },
{ "var": "postTransitAnim", "state": "postTransitAnim" }
]
},
{
"id": "postTransitAnim",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "transitAnim", "state": "transitAnim" },
{ "var": "idleAnim", "state": "idleAnim" }
]
},
{
"id": "userAnimA",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "idleAnim", "state": "idleAnim" },
{ "var": "userAnimB", "state": "userAnimB" }
]
},
{
"id": "userAnimB",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "idleAnim", "state": "idleAnim" },
{ "var": "userAnimA", "state": "userAnimA" }
]
}
]
},
"children": [
{
"id": "idleAnim",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle.fbx",
"startFrame": 0.0,
"endFrame": 90.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "preTransitAnim",
"type": "clip",
"data": {
"url": "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx",
"startFrame": 0.0,
"endFrame": 10.0,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "transitAnim",
"type": "clip",
"data": {
"url": "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx",
"startFrame": 11.0,
"endFrame": 11.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "postTransitAnim",
"type": "clip",
"data": {
"url": "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx",
"startFrame": 22.0,
"endFrame": 49.0,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "userAnimA",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle.fbx",
"startFrame": 0.0,
"endFrame": 90.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "userAnimB",
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/idle.fbx",
"startFrame": 0.0,
"endFrame": 90.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
}
]
}
}

View file

@ -51,32 +51,34 @@
{ "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" },
{ "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" },
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand"},
{ "from": "Vive.RightHand", "to": "Standard.RightHand"},
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand" },
{ "from": "Vive.RightHand", "to": "Standard.RightHand" },
{ "from": "Vive.Head", "to" : "Standard.Head" },
{
"from": "Vive.LeftFoot", "to" : "Standard.LeftFoot",
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.3}]
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}]
},
{
"from": "Vive.RightFoot", "to" : "Standard.RightFoot",
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.3}]
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}]
},
{
"from": "Vive.Hips", "to" : "Standard.Hips",
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.3}]
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}]
},
{
"from": "Vive.Spine2", "to" : "Standard.Spine2",
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.3}]
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}]
},
{
"from": "Vive.RightArm", "to" : "Standard.RightArm",
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}]
},
{
"from": "Vive.LeftArm", "to" : "Standard.LeftArm",
"filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}]
},
{ "from": "Vive.Head", "to" : "Standard.Head"},
{ "from": "Vive.RightArm", "to" : "Standard.RightArm" },
{ "from": "Vive.LeftArm", "to" : "Standard.LeftArm" },
{ "from": "Vive.TrackedObject00", "to" : "Standard.TrackedObject00" },
{ "from": "Vive.TrackedObject01", "to" : "Standard.TrackedObject01" },

View file

@ -175,7 +175,7 @@ TabBar {
method: "newEntityButtonClicked",
params: { buttonName: "newParticleButton" }
});
editTabView.currentIndex = 4
editTabView.currentIndex = 2
}
}
@ -279,21 +279,6 @@ TabBar {
}
}
EditTabButton {
title: "P"
active: true
enabled: true
property string originalUrl: ""
property Component visualItem: Component {
WebView {
id: particleExplorerWebView
url: Paths.defaultScripts + "/system/particle_explorer/particleExplorer.html"
enabled: true
}
}
}
function fromScript(message) {
switch (message.method) {
case 'selectTab':
@ -326,9 +311,6 @@ TabBar {
case 'grid':
editTabView.currentIndex = 3;
break;
case 'particle':
editTabView.currentIndex = 4;
break;
default:
console.warn('Attempt to switch to invalid tab:', id);
}

View file

@ -18,7 +18,6 @@ TabBar {
readonly property int create: 0
readonly property int properties: 1
readonly property int grid: 2
readonly property int particle: 3
}
readonly property HifiConstants hifi: HifiConstants {}
@ -182,7 +181,7 @@ TabBar {
method: "newEntityButtonClicked",
params: { buttonName: "newParticleButton" }
});
editTabView.currentIndex = tabIndex.particle
editTabView.currentIndex = tabIndex.properties
}
}
@ -271,21 +270,6 @@ TabBar {
}
}
EditTabButton {
title: "P"
active: true
enabled: true
property string originalUrl: ""
property Component visualItem: Component {
WebView {
id: particleExplorerWebView
url: Paths.defaultScripts + "/system/particle_explorer/particleExplorer.html"
enabled: true
}
}
}
function fromScript(message) {
switch (message.method) {
case 'selectTab':
@ -299,7 +283,7 @@ TabBar {
// Changes the current tab based on tab index or title as input
function selectTab(id) {
if (typeof id === 'number') {
if (id >= tabIndex.create && id <= tabIndex.particle) {
if (id >= tabIndex.create && id <= tabIndex.grid) {
editTabView.currentIndex = id;
} else {
console.warn('Attempt to switch to invalid tab:', id);
@ -315,9 +299,6 @@ TabBar {
case 'grid':
editTabView.currentIndex = tabIndex.grid;
break;
case 'particle':
editTabView.currentIndex = tabIndex.particle;
break;
default:
console.warn('Attempt to switch to invalid tab:', id);
}

View file

@ -822,11 +822,44 @@ Flickable {
}
}
Row {
id: outOfRangeDataStrategyRow
anchors.top: viveInDesktop.bottom
anchors.topMargin: 5
anchors.left: openVrConfiguration.left
anchors.leftMargin: leftMargin + 10
spacing: 15
RalewayRegular {
id: outOfRangeDataStrategyLabel
size: 12
text: "Out Of Range Data Strategy:"
color: hifi.colors.lightGrayText
topPadding: 5
}
HifiControls.ComboBox {
id: outOfRangeDataStrategyComboBox
height: 25
width: 100
editable: true
colorScheme: hifi.colorSchemes.dark
model: ["None", "Freeze", "Drop"]
label: ""
onCurrentIndexChanged: {
sendConfigurationSettings();
}
}
}
RalewayBold {
id: viveDesktopText
size: 10
size: 12
text: "Use " + stack.selectedPlugin + " devices in desktop mode"
color: hifi.colors.white
color: hifi.colors.lightGrayText
anchors {
left: viveInDesktop.right
@ -946,6 +979,7 @@ Flickable {
viveInDesktop.checked = desktopMode;
hmdInDesktop.checked = hmdDesktopPosition;
outOfRangeDataStrategyComboBox.currentIndex = outOfRangeDataStrategyComboBox.model.indexOf(settings.outOfRangeDataStrategy);
initializeButtonState();
updateCalibrationText();
@ -1107,7 +1141,8 @@ Flickable {
"armCircumference": armCircumference.realValue,
"shoulderWidth": shoulderWidth.realValue,
"desktopMode": viveInDesktop.checked,
"hmdDesktopTracking": hmdInDesktop.checked
"hmdDesktopTracking": hmdInDesktop.checked,
"outOfRangeDataStrategy": outOfRangeDataStrategyComboBox.model[outOfRangeDataStrategyComboBox.currentIndex]
}
return settingsObject;

View file

@ -52,6 +52,8 @@
#include <QTemporaryDir>
#include <gl/QOpenGLContextWrapper.h>
#include <gl/GLWindow.h>
#include <gl/GLHelpers.h>
#include <shared/FileUtils.h>
#include <shared/QtHelpers.h>
@ -971,9 +973,11 @@ OffscreenGLCanvas* _qmlShareContext { nullptr };
// and manually set THAT to be the shared context for the Chromium helper
#if !defined(DISABLE_QML)
OffscreenGLCanvas* _chromiumShareContext { nullptr };
Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context);
#endif
Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context);
Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context();
Setting::Handle<int> sessionRunTime{ "sessionRunTime", 0 };
const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f;
@ -1372,7 +1376,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_glWidget->setMouseTracking(true);
// Make sure the window is set to the correct size by processing the pending events
QCoreApplication::processEvents();
_glWidget->createContext();
// Create the main thread context, the GPU backend
initializeGL();
@ -2729,46 +2732,66 @@ void Application::initializeGL() {
_isGLInitialized = true;
}
if (!_glWidget->makeCurrent()) {
qCWarning(interfaceapp, "Unable to make window context current");
}
_glWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat());
// When loading QtWebEngineWidgets, it creates a global share context on startup.
// We have to account for this possibility by checking here for an existing
// global share context
auto globalShareContext = qt_gl_global_share_context();
#if !defined(DISABLE_QML)
// Build a shared canvas / context for the Chromium processes
{
// Disable signed distance field font rendering on ATI/AMD GPUs, due to
// https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app
std::string vendor{ (const char*)glGetString(GL_VENDOR) };
if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) {
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text"));
}
if (!globalShareContext) {
// Chromium rendering uses some GL functions that prevent nSight from capturing
// frames, so we only create the shared context if nsight is NOT active.
if (!nsightActive()) {
_chromiumShareContext = new OffscreenGLCanvas();
_chromiumShareContext->setObjectName("ChromiumShareContext");
_chromiumShareContext->create(_glWidget->qglContext());
auto format =QSurfaceFormat::defaultFormat();
#ifdef Q_OS_MAC
// On mac, the primary shared OpenGL context must be a 3.2 core context,
// or chromium flips out and spews error spam (but renders fine)
format.setMajorVersion(3);
format.setMinorVersion(2);
#endif
_chromiumShareContext->setFormat(format);
_chromiumShareContext->create();
if (!_chromiumShareContext->makeCurrent()) {
qCWarning(interfaceapp, "Unable to make chromium shared context current");
}
qt_gl_set_global_share_context(_chromiumShareContext->getContext());
globalShareContext = _chromiumShareContext->getContext();
qt_gl_set_global_share_context(globalShareContext);
_chromiumShareContext->doneCurrent();
// Restore the GL widget context
if (!_glWidget->makeCurrent()) {
qCWarning(interfaceapp, "Unable to make window context current");
}
} else {
qCWarning(interfaceapp) << "nSight detected, disabling chrome rendering";
}
}
#endif
_glWidget->createContext(globalShareContext);
if (!_glWidget->makeCurrent()) {
qCWarning(interfaceapp, "Unable to make window context current");
}
#if !defined(DISABLE_QML)
// Disable signed distance field font rendering on ATI/AMD GPUs, due to
// https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app
std::string vendor{ (const char*)glGetString(GL_VENDOR) };
if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) {
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text"));
}
#endif
if (!globalShareContext) {
globalShareContext = _glWidget->qglContext();
qt_gl_set_global_share_context(globalShareContext);
}
// Build a shared canvas / context for the QML rendering
{
_qmlShareContext = new OffscreenGLCanvas();
_qmlShareContext->setObjectName("QmlShareContext");
_qmlShareContext->create(_glWidget->qglContext());
_qmlShareContext->create(globalShareContext);
if (!_qmlShareContext->makeCurrent()) {
qCWarning(interfaceapp, "Unable to make QML shared context current");
}
@ -5817,6 +5840,42 @@ void Application::update(float deltaTime) {
controller::Pose pose = userInputMapper->getPoseState(action);
myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix));
}
static const std::vector<QString> trackedObjectStringLiterals = {
QStringLiteral("_TrackedObject00"), QStringLiteral("_TrackedObject01"), QStringLiteral("_TrackedObject02"), QStringLiteral("_TrackedObject03"),
QStringLiteral("_TrackedObject04"), QStringLiteral("_TrackedObject05"), QStringLiteral("_TrackedObject06"), QStringLiteral("_TrackedObject07"),
QStringLiteral("_TrackedObject08"), QStringLiteral("_TrackedObject09"), QStringLiteral("_TrackedObject10"), QStringLiteral("_TrackedObject11"),
QStringLiteral("_TrackedObject12"), QStringLiteral("_TrackedObject13"), QStringLiteral("_TrackedObject14"), QStringLiteral("_TrackedObject15")
};
// Controlled by the Developer > Avatar > Show Tracked Objects menu.
if (_showTrackedObjects) {
static const std::vector<controller::Action> trackedObjectActions = {
controller::Action::TRACKED_OBJECT_00, controller::Action::TRACKED_OBJECT_01, controller::Action::TRACKED_OBJECT_02, controller::Action::TRACKED_OBJECT_03,
controller::Action::TRACKED_OBJECT_04, controller::Action::TRACKED_OBJECT_05, controller::Action::TRACKED_OBJECT_06, controller::Action::TRACKED_OBJECT_07,
controller::Action::TRACKED_OBJECT_08, controller::Action::TRACKED_OBJECT_09, controller::Action::TRACKED_OBJECT_10, controller::Action::TRACKED_OBJECT_11,
controller::Action::TRACKED_OBJECT_12, controller::Action::TRACKED_OBJECT_13, controller::Action::TRACKED_OBJECT_14, controller::Action::TRACKED_OBJECT_15
};
int i = 0;
glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f);
for (auto& action : trackedObjectActions) {
controller::Pose pose = userInputMapper->getPoseState(action);
if (pose.valid) {
glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation);
glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation;
DebugDraw::getInstance().addMarker(trackedObjectStringLiterals[i], rot, pos, BLUE);
} else {
DebugDraw::getInstance().removeMarker(trackedObjectStringLiterals[i]);
}
i++;
}
} else if (_prevShowTrackedObjects) {
for (auto& key : trackedObjectStringLiterals) {
DebugDraw::getInstance().removeMarker(key);
}
}
_prevShowTrackedObjects = _showTrackedObjects;
}
updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process...
@ -5983,7 +6042,9 @@ void Application::update(float deltaTime) {
{
PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount());
PerformanceTimer perfTimer("overlays");
_overlays.update(deltaTime);
if (qApp->shouldPaint()) {
_overlays.update(deltaTime);
}
}
// Update _viewFrustum with latest camera and view frustum data...
@ -6068,8 +6129,10 @@ void Application::update(float deltaTime) {
PROFILE_RANGE_EX(app, "PostUpdateLambdas", 0xffff0000, (uint64_t)0);
PerformanceTimer perfTimer("postUpdateLambdas");
std::unique_lock<std::mutex> guard(_postUpdateLambdasLock);
for (auto& iter : _postUpdateLambdas) {
iter.second();
if (qApp->shouldPaint()) {
for (auto& iter : _postUpdateLambdas) {
iter.second();
}
}
_postUpdateLambdas.clear();
}
@ -8313,6 +8376,10 @@ void Application::setShowBulletConstraintLimits(bool value) {
_physicsEngine->setShowBulletConstraintLimits(value);
}
void Application::setShowTrackedObjects(bool value) {
_showTrackedObjects = value;
}
void Application::startHMDStandBySession() {
_autoSwitchDisplayModeSupportedHMDPlugin->startStandBySession();
}

View file

@ -498,6 +498,8 @@ private slots:
void setShowBulletConstraints(bool value);
void setShowBulletConstraintLimits(bool value);
void setShowTrackedObjects(bool value);
private:
void init();
bool handleKeyEventForFocusedEntityOrOverlay(QEvent* event);
@ -779,5 +781,8 @@ private:
std::atomic<bool> _pendingRenderEvent { true };
bool quitWhenFinished { false };
bool _showTrackedObjects { false };
bool _prevShowTrackedObjects { false };
};
#endif // hifi_Application_h

View file

@ -273,15 +273,71 @@ Menu::Menu() {
// Developer menu ----------------------------------
MenuWrapper* developerMenu = addMenu("Developer", "Developer");
// Developer > Scripting >>>
MenuWrapper* scriptingOptionsMenu = developerMenu->addMenu("Scripting");
// Developer > Scripting > Console...
addActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J,
DependencyManager::get<StandAloneJSConsole>().data(),
SLOT(toggleConsole()),
QAction::NoRole,
UNSPECIFIED_POSITION);
// Developer > Scripting > API Debugger
action = addActionToQMenuAndActionHash(scriptingOptionsMenu, "API Debugger");
connect(action, &QAction::triggered, [] {
QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation();
defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/tools/currentAPI.js");
DependencyManager::get<ScriptEngines>()->loadScript(defaultScriptsLoc.toString());
});
// Developer > Scripting > Entity Script Server Log
auto essLogAction = addActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::EntityScriptServerLog, 0,
qApp, SLOT(toggleEntityScriptServerLogDialog()));
{
auto nodeList = DependencyManager::get<NodeList>();
QObject::connect(nodeList.data(), &NodeList::canRezChanged, essLogAction, [essLogAction] {
auto nodeList = DependencyManager::get<NodeList>();
essLogAction->setEnabled(nodeList->getThisNodeCanRez());
});
essLogAction->setEnabled(nodeList->getThisNodeCanRez());
}
// Developer > Scripting > Script Log (HMD friendly)...
addActionToQMenuAndActionHash(scriptingOptionsMenu, "Script Log (HMD friendly)...", Qt::NoButton,
qApp, SLOT(showScriptLogs()));
// Developer > Scripting > Verbose Logging
addCheckableActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::VerboseLogging, 0, false,
qApp, SLOT(updateVerboseLogging()));
// Developer > Scripting > Enable Speech Control API
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
auto speechRecognizer = DependencyManager::get<SpeechRecognizer>();
QAction* speechRecognizerAction = addCheckableActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::ControlWithSpeech,
Qt::CTRL | Qt::SHIFT | Qt::Key_C,
speechRecognizer->getEnabled(),
speechRecognizer.data(),
SLOT(setEnabled(bool)),
UNSPECIFIED_POSITION);
connect(speechRecognizer.data(), SIGNAL(enabledUpdated(bool)), speechRecognizerAction, SLOT(setChecked(bool)));
#endif
// Developer > UI >>>
MenuWrapper* uiOptionsMenu = developerMenu->addMenu("UI");
action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::DesktopTabletToToolbar, 0,
qApp->getDesktopTabletBecomesToolbarSetting());
// Developer > UI > Show Overlays
addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::Overlays, 0, true);
// Developer > UI > Desktop Tablet Becomes Toolbar
connect(action, &QAction::triggered, [action] {
qApp->setDesktopTabletBecomesToolbarSetting(action->isChecked());
});
// Developer > UI > HMD Tablet Becomes Toolbar
action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::HMDTabletToToolbar, 0,
qApp->getHmdTabletBecomesToolbarSetting());
connect(action, &QAction::triggered, [action] {
@ -570,6 +626,8 @@ Menu::Menu() {
avatar.get(), SLOT(updateMotionBehaviorFromMenu()),
UNSPECIFIED_POSITION, "Developer");
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowTrackedObjects, 0, false, qApp, SLOT(setShowTrackedObjects(bool)));
// Developer > Hands >>>
MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands");
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false,
@ -684,10 +742,11 @@ Menu::Menu() {
addCheckableActionToQMenuAndActionHash(pickingOptionsMenu, MenuOption::ForceCoarsePicking, 0, false,
DependencyManager::get<PickManager>().data(), SLOT(setForceCoarsePicking(bool)));
// Developer > Display Crash Options
addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::DisplayCrashOptions, 0, true);
// Developer > Crash >>>
MenuWrapper* crashMenu = developerMenu->addMenu("Crash");
// Developer > Crash > Display Crash Options
addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true);
addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication()));
addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication()));
@ -722,59 +781,15 @@ Menu::Menu() {
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); });
// Developer > Stats
// Developer > Show Statistics
addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats);
// Developer > Show Animation Statistics
addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::AnimStats);
// Settings > Enable Speech Control API
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
auto speechRecognizer = DependencyManager::get<SpeechRecognizer>();
QAction* speechRecognizerAction = addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::ControlWithSpeech,
Qt::CTRL | Qt::SHIFT | Qt::Key_C,
speechRecognizer->getEnabled(),
speechRecognizer.data(),
SLOT(setEnabled(bool)),
UNSPECIFIED_POSITION);
connect(speechRecognizer.data(), SIGNAL(enabledUpdated(bool)), speechRecognizerAction, SLOT(setChecked(bool)));
#endif
// console
addActionToQMenuAndActionHash(developerMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J,
DependencyManager::get<StandAloneJSConsole>().data(),
SLOT(toggleConsole()),
QAction::NoRole,
UNSPECIFIED_POSITION);
// Developer > API Debugger
action = addActionToQMenuAndActionHash(developerMenu, "API Debugger");
connect(action, &QAction::triggered, [] {
QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation();
defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/tools/currentAPI.js");
DependencyManager::get<ScriptEngines>()->loadScript(defaultScriptsLoc.toString());
});
// Developer > Log...
// Developer > Log
addActionToQMenuAndActionHash(developerMenu, MenuOption::Log, Qt::CTRL | Qt::SHIFT | Qt::Key_L,
qApp, SLOT(toggleLogDialog()));
auto essLogAction = addActionToQMenuAndActionHash(developerMenu, MenuOption::EntityScriptServerLog, 0,
qApp, SLOT(toggleEntityScriptServerLogDialog()));
{
auto nodeList = DependencyManager::get<NodeList>();
QObject::connect(nodeList.data(), &NodeList::canRezChanged, essLogAction, [essLogAction] {
auto nodeList = DependencyManager::get<NodeList>();
essLogAction->setEnabled(nodeList->getThisNodeCanRez());
});
essLogAction->setEnabled(nodeList->getThisNodeCanRez());
}
addActionToQMenuAndActionHash(developerMenu, "Script Log (HMD friendly)...", Qt::NoButton,
qApp, SLOT(showScriptLogs()));
addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::VerboseLogging, 0, false,
qApp, SLOT(updateVerboseLogging()));
// Developer > Show Overlays
addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Overlays, 0, true);
#if 0 /// -------------- REMOVED FOR NOW --------------
addDisabledActionAndSeparator(navigateMenu, "History");

View file

@ -183,6 +183,7 @@ namespace MenuOption {
const QString RunClientScriptTests = "Run Client Script Tests";
const QString RunTimingTests = "Run Timing Tests";
const QString ScriptedMotorControl = "Enable Scripted Motor Control";
const QString ShowTrackedObjects = "Show Tracked Objects";
const QString SendWrongDSConnectVersion = "Send wrong DS connect version";
const QString SendWrongProtocolVersion = "Send wrong protocol version";
const QString SetHomeLocation = "Set Home Location";

View file

@ -135,7 +135,7 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm::
glm::vec3 palmPosition;
glm::quat palmRotation;
bool isTransitingWithAvatar = holdingAvatar->getTransit()->isTransiting();
bool isTransitingWithAvatar = holdingAvatar->getTransit()->isActive();
if (isTransitingWithAvatar != _isTransitingWithAvatar) {
_isTransitingWithAvatar = isTransitingWithAvatar;
auto ownerEntity = _ownerEntity.lock();
@ -424,7 +424,7 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) {
if (ownerEntity) {
ownerEntity->setDynamicDataDirty(true);
ownerEntity->setDynamicDataNeedsTransmit(true);
ownerEntity->setTransitingWithAvatar(myAvatar->getTransit()->isTransiting());
ownerEntity->setTransitingWithAvatar(myAvatar->getTransit()->isActive());
}
});
}

View file

@ -71,14 +71,12 @@ AvatarManager::AvatarManager(QObject* parent) :
}
});
const float AVATAR_TRANSIT_TRIGGER_DISTANCE = 1.0f;
const int AVATAR_TRANSIT_FRAME_COUNT = 11; // Based on testing
const int AVATAR_TRANSIT_FRAMES_PER_METER = 1; // Based on testing
_transitConfig._totalFrames = AVATAR_TRANSIT_FRAME_COUNT;
_transitConfig._triggerDistance = AVATAR_TRANSIT_TRIGGER_DISTANCE;
_transitConfig._minTriggerDistance = AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE;
_transitConfig._maxTriggerDistance = AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE;
_transitConfig._framesPerMeter = AVATAR_TRANSIT_FRAMES_PER_METER;
_transitConfig._isDistanceBased = true;
_transitConfig._isDistanceBased = AVATAR_TRANSIT_DISTANCE_BASED;
_transitConfig._abortDistance = AVATAR_TRANSIT_ABORT_DISTANCE;
}
AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) {
@ -126,13 +124,39 @@ void AvatarManager::setSpace(workload::SpacePointer& space ) {
_space = space;
}
void AvatarManager::handleTransitAnimations(AvatarTransit::Status status) {
switch (status) {
case AvatarTransit::Status::STARTED:
_myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("preTransitAnim");
break;
case AvatarTransit::Status::START_TRANSIT:
_myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("transitAnim");
break;
case AvatarTransit::Status::END_TRANSIT:
_myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("postTransitAnim");
break;
case AvatarTransit::Status::ENDED:
_myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("idleAnim");
break;
case AvatarTransit::Status::PRE_TRANSIT:
break;
case AvatarTransit::Status::POST_TRANSIT:
break;
case AvatarTransit::Status::IDLE:
break;
case AvatarTransit::Status::TRANSITING:
break;
case AvatarTransit::Status::ABORT_TRANSIT:
break;
}
}
void AvatarManager::updateMyAvatar(float deltaTime) {
bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings);
PerformanceWarning warn(showWarnings, "AvatarManager::updateMyAvatar()");
AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _transitConfig);
bool sendFirstTransitPackage = (status == AvatarTransit::Status::START_TRANSIT);
bool blockTransitData = (status == AvatarTransit::Status::TRANSITING);
AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _myAvatar->getSensorToWorldScale(), _transitConfig);
handleTransitAnimations(status);
_myAvatar->update(deltaTime);
render::Transaction transaction;
@ -142,18 +166,13 @@ void AvatarManager::updateMyAvatar(float deltaTime) {
quint64 now = usecTimestampNow();
quint64 dt = now - _lastSendAvatarDataTime;
if (sendFirstTransitPackage || (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused && !blockTransitData)) {
if (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused) {
// send head/hand data to the avatar mixer and voxel server
PerformanceTimer perfTimer("send");
if (sendFirstTransitPackage) {
_myAvatar->overrideNextPackagePositionData(_myAvatar->getTransit()->getEndPosition());
}
PerformanceTimer perfTimer("send");
_myAvatar->sendAvatarDataPacket();
_lastSendAvatarDataTime = now;
_myAvatarSendRate.increment();
}
}
@ -267,7 +286,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
if (inView && avatar->hasNewJointData()) {
numAvatarsUpdated++;
}
auto transitStatus = avatar->_transit.update(deltaTime, avatar->_globalPosition, _transitConfig);
auto transitStatus = avatar->_transit.update(deltaTime, avatar->_serverPosition, _transitConfig);
if (avatar->getIsNewAvatar() && (transitStatus == AvatarTransit::Status::START_TRANSIT || transitStatus == AvatarTransit::Status::ABORT_TRANSIT)) {
avatar->_transit.reset();
avatar->setIsNewAvatar(false);

View file

@ -221,6 +221,7 @@ private:
// frequently grabs a read lock on the hash to get a given avatar by ID
void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar,
KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
void handleTransitAnimations(AvatarTransit::Status status);
QVector<AvatarSharedPointer> _avatarsToFade;

View file

@ -1058,7 +1058,6 @@ void MyAvatar::updateSensorToWorldMatrix() {
updateJointFromController(controller::Action::RIGHT_HAND, _controllerRightHandMatrixCache);
if (hasSensorToWorldScaleChanged) {
setTransitScale(sensorToWorldScale);
emit sensorToWorldScaleChanged(sensorToWorldScale);
}

View file

@ -36,6 +36,7 @@ Rig::CharacterControllerState convertCharacterControllerState(CharacterControlle
static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) {
glm::mat4 worldToSensorMat = glm::inverse(myAvatar->getSensorToWorldMatrix());
// check for pinned hips.
auto hipsIndex = myAvatar->getJointIndex("Hips");
if (myAvatar->isJointPinned(hipsIndex)) {
@ -199,49 +200,38 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
if (avatarHeadPose.isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] & (uint8_t)Rig::ControllerFlags::Enabled)) {
bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS);
if (!_prevHipsValid) {
AnimPose hips = computeHipsInSensorFrame(myAvatar, isFlying);
_prevHips = hips;
}
AnimPose hips = computeHipsInSensorFrame(myAvatar, isFlying);
// timescale in seconds
const float TRANS_HORIZ_TIMESCALE = 0.15f;
const float TRANS_VERT_TIMESCALE = 0.01f; // We want the vertical component of the hips to follow quickly to prevent spine squash/stretch.
const float ROT_TIMESCALE = 0.15f;
const float FLY_IDLE_TRANSITION_TIMESCALE = 0.25f;
float transHorizAlpha, transVertAlpha, rotAlpha;
if (_flyIdleTimer < 0.0f) {
transHorizAlpha = glm::min(deltaTime / TRANS_HORIZ_TIMESCALE, 1.0f);
transVertAlpha = glm::min(deltaTime / TRANS_VERT_TIMESCALE, 1.0f);
rotAlpha = glm::min(deltaTime / ROT_TIMESCALE, 1.0f);
_smoothHipsHelper.setHorizontalTranslationTimescale(TRANS_HORIZ_TIMESCALE);
_smoothHipsHelper.setVerticalTranslationTimescale(TRANS_VERT_TIMESCALE);
_smoothHipsHelper.setRotationTimescale(ROT_TIMESCALE);
} else {
transHorizAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f);
transVertAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f);
rotAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f);
_smoothHipsHelper.setHorizontalTranslationTimescale(FLY_IDLE_TRANSITION_TIMESCALE);
_smoothHipsHelper.setVerticalTranslationTimescale(FLY_IDLE_TRANSITION_TIMESCALE);
_smoothHipsHelper.setRotationTimescale(FLY_IDLE_TRANSITION_TIMESCALE);
}
// smootly lerp hips, in sensorframe, with different coeff for horiz and vertical translation.
float hipsY = hips.trans().y;
hips.trans() = lerp(_prevHips.trans(), hips.trans(), transHorizAlpha);
hips.trans().y = lerp(_prevHips.trans().y, hipsY, transVertAlpha);
hips.rot() = safeLerp(_prevHips.rot(), hips.rot(), rotAlpha);
_prevHips = hips;
_prevHipsValid = true;
AnimPose sensorHips = computeHipsInSensorFrame(myAvatar, isFlying);
if (!_prevIsEstimatingHips) {
_smoothHipsHelper.teleport(sensorHips);
}
sensorHips = _smoothHipsHelper.update(sensorHips, deltaTime);
glm::mat4 invRigMat = glm::inverse(myAvatar->getTransform().getMatrix() * Matrices::Y_180);
AnimPose sensorToRigPose(invRigMat * myAvatar->getSensorToWorldMatrix());
params.primaryControllerPoses[Rig::PrimaryControllerType_Hips] = sensorToRigPose * hips;
params.primaryControllerPoses[Rig::PrimaryControllerType_Hips] = sensorToRigPose * sensorHips;
params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] = (uint8_t)Rig::ControllerFlags::Enabled | (uint8_t)Rig::ControllerFlags::Estimated;
// set spine2 if we have hand controllers
if (myAvatar->getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid() &&
myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() &&
!(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) {
myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() &&
!(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) {
AnimPose currentSpine2Pose;
AnimPose currentHeadPose;
@ -268,8 +258,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
}
}
_prevIsEstimatingHips = true;
} else {
_prevHipsValid = false;
_prevIsEstimatingHips = false;
}
params.isTalking = head->getTimeWithoutTalking() <= 1.5f;
@ -299,7 +290,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
auto velocity = myAvatar->getLocalVelocity() / myAvatar->getSensorToWorldScale();
auto position = myAvatar->getLocalPosition();
auto orientation = myAvatar->getLocalOrientation();
_rig.computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState);
_rig.computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState, myAvatar->getSensorToWorldScale());
// evaluate AnimGraph animation and update jointStates.
Model::updateRig(deltaTime, parentTransform);

View file

@ -10,6 +10,7 @@
#define hifi_MySkeletonModel_h
#include <avatars-renderer/SkeletonModel.h>
#include <AnimUtil.h>
#include "MyAvatar.h"
/// A skeleton loaded from a model.
@ -26,11 +27,12 @@ public:
private:
void updateFingers();
AnimPose _prevHips; // sensor frame
bool _prevHipsValid { false };
CriticallyDampedSpringPoseHelper _smoothHipsHelper; // sensor frame
bool _prevIsFlying { false };
float _flyIdleTimer { 0.0f };
float _prevIsEstimatingHips { false };
std::map<int, int> _jointRotationFrameOffsetMap;
};

View file

@ -23,6 +23,7 @@
#include <SandboxUtils.h>
#include <SharedUtil.h>
#include <NetworkAccessManager.h>
#include <gl/GLHelpers.h>
#include "AddressManager.h"
#include "Application.h"
@ -40,6 +41,18 @@ extern "C" {
#endif
int main(int argc, const char* argv[]) {
#ifdef Q_OS_MAC
auto format = getDefaultOpenGLSurfaceFormat();
// Deal with some weirdness in the chromium context sharing on Mac.
// The primary share context needs to be 3.2, so that the Chromium will
// succeed in it's creation of it's command stub contexts.
format.setVersion(3, 2);
// This appears to resolve the issues with corrupted fonts on OSX. No
// idea why.
qputenv("QT_ENABLE_GLYPH_CACHE_WORKAROUND", "true");
// https://i.kym-cdn.com/entries/icons/original/000/008/342/ihave.jpg
QSurfaceFormat::setDefaultFormat(format);
#endif
setupHifiApplication(BuildInfo::INTERFACE_NAME);
QStringList arguments;

View file

@ -115,6 +115,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) {
batch.resetViewTransform();
batch.setResourceTexture(0, _uiTexture);
geometryCache->renderUnitQuad(batch, glm::vec4(1), _qmlGeometryId);
batch.setResourceTexture(0, nullptr);
}
void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) {

View file

@ -51,6 +51,8 @@ public:
bool getMirrorFlag() const { return _mirrorFlag; }
void setMirrorFlag(bool mirrorFlag) { _mirrorFlag = mirrorFlag; }
float getFrame() const { return _frame; }
void loadURL(const QString& url);
protected:

View file

@ -24,7 +24,7 @@
#include "AnimUtil.h"
static const int MAX_TARGET_MARKERS = 30;
static const float JOINT_CHAIN_INTERP_TIME = 0.25f;
static const float JOINT_CHAIN_INTERP_TIME = 0.5f;
static void lookupJointInfo(const AnimInverseKinematics::JointChainInfo& jointChainInfo,
int indexA, int indexB,
@ -253,11 +253,25 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector<
if (numLoops == MAX_IK_LOOPS) {
for (size_t i = 0; i < _prevJointChainInfoVec.size(); i++) {
if (_prevJointChainInfoVec[i].timer > 0.0f) {
float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[i].timer) / JOINT_CHAIN_INTERP_TIME;
// ease in expo
alpha = 1.0f - powf(2.0f, -10.0f * alpha);
size_t chainSize = std::min(_prevJointChainInfoVec[i].jointInfoVec.size(), jointChainInfoVec[i].jointInfoVec.size());
for (size_t j = 0; j < chainSize; j++) {
jointChainInfoVec[i].jointInfoVec[j].rot = safeMix(_prevJointChainInfoVec[i].jointInfoVec[j].rot, jointChainInfoVec[i].jointInfoVec[j].rot, alpha);
jointChainInfoVec[i].jointInfoVec[j].trans = lerp(_prevJointChainInfoVec[i].jointInfoVec[j].trans, jointChainInfoVec[i].jointInfoVec[j].trans, alpha);
if (jointChainInfoVec[i].target.getType() != IKTarget::Type::Unknown) {
// if we are interping into an enabled target type, i.e. not off, lerp the rot and the trans.
for (size_t j = 0; j < chainSize; j++) {
jointChainInfoVec[i].jointInfoVec[j].rot = safeMix(_prevJointChainInfoVec[i].jointInfoVec[j].rot, jointChainInfoVec[i].jointInfoVec[j].rot, alpha);
jointChainInfoVec[i].jointInfoVec[j].trans = lerp(_prevJointChainInfoVec[i].jointInfoVec[j].trans, jointChainInfoVec[i].jointInfoVec[j].trans, alpha);
}
} else {
// if we are interping into a disabled target type, keep the rot & trans the same, but lerp the weight down to zero.
jointChainInfoVec[i].target.setType((int)_prevJointChainInfoVec[i].target.getType());
jointChainInfoVec[i].target.setWeight(_prevJointChainInfoVec[i].target.getWeight() * (1.0f - alpha));
jointChainInfoVec[i].jointInfoVec = _prevJointChainInfoVec[i].jointInfoVec;
}
}
}

View file

@ -201,14 +201,17 @@ const AnimPoseVec& AnimTwoBoneIK::evaluate(const AnimVariantMap& animVars, const
if (_interpType != InterpType::None) {
_interpAlpha += _interpAlphaVel * dt;
// ease in expo
float easeInAlpha = 1.0f - powf(2.0f, -10.0f * _interpAlpha);
if (_interpAlpha < 1.0f) {
AnimChain interpChain;
if (_interpType == InterpType::SnapshotToUnderPoses) {
interpChain = underChain;
interpChain.blend(_snapshotChain, _interpAlpha);
interpChain.blend(_snapshotChain, easeInAlpha);
} else if (_interpType == InterpType::SnapshotToSolve) {
interpChain = ikChain;
interpChain.blend(_snapshotChain, _interpAlpha);
interpChain.blend(_snapshotChain, easeInAlpha);
}
// copy interpChain into _poses
interpChain.outputRelativePoses(_poses);

View file

@ -38,4 +38,94 @@ AnimPose boneLookAt(const glm::vec3& target, const AnimPose& bone);
// and returns a bodyRot that is also z-forward and y-up
glm::quat computeBodyFacingFromHead(const glm::quat& headRot, const glm::vec3& up);
// Uses a approximation of a critically damped spring to smooth full AnimPoses.
// It provides seperate timescales for horizontal, vertical and rotation components.
// The timescale is roughly how much time it will take the spring will reach halfway toward it's target.
class CriticallyDampedSpringPoseHelper {
public:
CriticallyDampedSpringPoseHelper() : _prevPoseValid(false) {}
void setHorizontalTranslationTimescale(float timescale) {
_horizontalTranslationTimescale = timescale;
}
void setVerticalTranslationTimescale(float timescale) {
_verticalTranslationTimescale = timescale;
}
void setRotationTimescale(float timescale) {
_rotationTimescale = timescale;
}
AnimPose update(const AnimPose& pose, float deltaTime) {
if (!_prevPoseValid) {
_prevPose = pose;
_prevPoseValid = true;
}
const float horizontalTranslationAlpha = glm::min(deltaTime / _horizontalTranslationTimescale, 1.0f);
const float verticalTranslationAlpha = glm::min(deltaTime / _verticalTranslationTimescale, 1.0f);
const float rotationAlpha = glm::min(deltaTime / _rotationTimescale, 1.0f);
const float poseY = pose.trans().y;
AnimPose newPose = _prevPose;
newPose.trans() = lerp(_prevPose.trans(), pose.trans(), horizontalTranslationAlpha);
newPose.trans().y = lerp(_prevPose.trans().y, poseY, verticalTranslationAlpha);
newPose.rot() = safeLerp(_prevPose.rot(), pose.rot(), rotationAlpha);
_prevPose = newPose;
_prevPoseValid = true;
return newPose;
}
void teleport(const AnimPose& pose) {
_prevPoseValid = true;
_prevPose = pose;
}
protected:
AnimPose _prevPose;
float _horizontalTranslationTimescale { 0.15f };
float _verticalTranslationTimescale { 0.15f };
float _rotationTimescale { 0.15f };
bool _prevPoseValid;
};
class SnapshotBlendPoseHelper {
public:
SnapshotBlendPoseHelper() : _snapshotValid(false) {}
void setBlendDuration(float duration) {
_duration = duration;
}
void setSnapshot(const AnimPose& pose) {
_snapshotValid = true;
_snapshotPose = pose;
_timer = _duration;
}
AnimPose update(const AnimPose& targetPose, float deltaTime) {
_timer -= deltaTime;
if (_timer > 0.0f) {
float alpha = (_duration - _timer) / _duration;
// ease in expo
alpha = 1.0f - powf(2.0f, -10.0f * alpha);
AnimPose newPose = targetPose;
newPose.blend(_snapshotPose, alpha);
return newPose;
} else {
return targetPose;
}
}
protected:
AnimPose _snapshotPose;
float _duration { 1.0f };
float _timer { 0.0f };
bool _snapshotValid { false };
};
#endif

View file

@ -30,7 +30,9 @@
#include "AnimOverlay.h"
#include "AnimSkeleton.h"
#include "AnimUtil.h"
#include "AvatarConstants.h"
#include "IKTarget.h"
#include "PathUtils.h"
static int nextRigId = 1;
@ -133,6 +135,30 @@ void Rig::overrideAnimation(const QString& url, float fps, bool loop, float firs
_animVars.set("userAnimB", clipNodeEnum == UserAnimState::B);
}
void Rig::triggerNetworkAnimation(const QString& animName) {
_networkVars.set("idleAnim", false);
_networkVars.set("preTransitAnim", false);
_networkVars.set("transitAnim", false);
_networkVars.set("postTransitAnim", false);
_sendNetworkNode = true;
if (animName == "idleAnim") {
_networkVars.set("idleAnim", true);
_networkAnimState.clipNodeEnum = NetworkAnimState::Idle;
_sendNetworkNode = false;
} else if (animName == "preTransitAnim") {
_networkVars.set("preTransitAnim", true);
_networkAnimState.clipNodeEnum = NetworkAnimState::PreTransit;
} else if (animName == "transitAnim") {
_networkVars.set("transitAnim", true);
_networkAnimState.clipNodeEnum = NetworkAnimState::Transit;
} else if (animName == "postTransitAnim") {
_networkVars.set("postTransitAnim", true);
_networkAnimState.clipNodeEnum = NetworkAnimState::PostTransit;
}
}
void Rig::restoreAnimation() {
if (_userAnimState.clipNodeEnum != UserAnimState::None) {
_userAnimState.clipNodeEnum = UserAnimState::None;
@ -144,6 +170,16 @@ void Rig::restoreAnimation() {
}
}
void Rig::restoreNetworkAnimation() {
if (_networkAnimState.clipNodeEnum != NetworkAnimState::Idle) {
_networkAnimState.clipNodeEnum = NetworkAnimState::Idle;
_networkVars.set("idleAnim", true);
_networkVars.set("preTransitAnim", false);
_networkVars.set("transitAnim", false);
_networkVars.set("postTransitAnim", false);
}
}
QStringList Rig::getAnimationRoles() const {
if (_animNode) {
QStringList list;
@ -208,11 +244,17 @@ void Rig::restoreRoleAnimation(const QString& role) {
void Rig::destroyAnimGraph() {
_animSkeleton.reset();
_animLoader.reset();
_networkLoader.reset();
_animNode.reset();
_internalPoseSet._relativePoses.clear();
_internalPoseSet._absolutePoses.clear();
_internalPoseSet._overridePoses.clear();
_internalPoseSet._overrideFlags.clear();
_networkNode.reset();
_networkPoseSet._relativePoses.clear();
_networkPoseSet._absolutePoses.clear();
_networkPoseSet._overridePoses.clear();
_networkPoseSet._overrideFlags.clear();
_numOverrides = 0;
_leftEyeJointChildren.clear();
_rightEyeJointChildren.clear();
@ -229,14 +271,24 @@ void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOff
_internalPoseSet._relativePoses.clear();
_internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses();
_networkPoseSet._relativePoses.clear();
_networkPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses();
buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses);
buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses);
_internalPoseSet._overridePoses.clear();
_internalPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses();
_internalPoseSet._overrideFlags.clear();
_internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false);
_networkPoseSet._overridePoses.clear();
_networkPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses();
_networkPoseSet._overrideFlags.clear();
_networkPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false);
_numOverrides = 0;
buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses);
@ -270,6 +322,18 @@ void Rig::reset(const FBXGeometry& geometry) {
_internalPoseSet._overrideFlags.clear();
_internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false);
_networkPoseSet._relativePoses.clear();
_networkPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses();
buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses);
_networkPoseSet._overridePoses.clear();
_networkPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses();
_networkPoseSet._overrideFlags.clear();
_networkPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false);
_numOverrides = 0;
buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses);
@ -629,7 +693,8 @@ bool Rig::getRelativeDefaultJointTranslation(int index, glm::vec3& translationOu
}
}
void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState) {
void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity,
const glm::quat& worldRotation, CharacterControllerState ccState, float sensorToWorldScale) {
glm::vec3 forward = worldRotation * IDENTITY_FORWARD;
glm::vec3 workingVelocity = worldVelocity;
@ -924,9 +989,15 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
}
_animVars.set("isNotInAir", false);
// compute blend based on velocity
const float JUMP_SPEED = 3.5f;
float alpha = glm::clamp(-workingVelocity.y / JUMP_SPEED, -1.0f, 1.0f) + 1.0f;
// We want to preserve the apparent jump height in sensor space.
const float jumpHeight = std::max(sensorToWorldScale * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT);
// convert jump height to a initial jump speed with the given gravity.
const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight);
// compute inAirAlpha blend based on velocity
float alpha = glm::clamp((-workingVelocity.y * sensorToWorldScale) / jumpSpeed, -1.0f, 1.0f) + 1.0f;
_animVars.set("inAirAlpha", alpha);
}
@ -1049,26 +1120,56 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons
updateAnimationStateHandlers();
_animVars.setRigToGeometryTransform(_rigToGeometryTransform);
if (_networkNode) {
_networkVars.setRigToGeometryTransform(_rigToGeometryTransform);
}
AnimContext context(_enableDebugDrawIKTargets, _enableDebugDrawIKConstraints, _enableDebugDrawIKChains,
getGeometryToRigTransform(), rigToWorldTransform);
// evaluate the animation
AnimVariantMap triggersOut;
AnimVariantMap networkTriggersOut;
_internalPoseSet._relativePoses = _animNode->evaluate(_animVars, context, deltaTime, triggersOut);
if (_networkNode) {
_networkPoseSet._relativePoses = _networkNode->evaluate(_networkVars, context, deltaTime, networkTriggersOut);
const float NETWORK_ANIMATION_BLEND_FRAMES = 6.0f;
float alpha = 1.0f;
std::shared_ptr<AnimClip> clip;
if (_networkAnimState.clipNodeEnum == NetworkAnimState::PreTransit) {
clip = std::dynamic_pointer_cast<AnimClip>(_networkNode->findByName("preTransitAnim"));
if (clip) {
alpha = (clip->getFrame() - clip->getStartFrame()) / NETWORK_ANIMATION_BLEND_FRAMES;
}
} else if (_networkAnimState.clipNodeEnum == NetworkAnimState::PostTransit) {
clip = std::dynamic_pointer_cast<AnimClip>(_networkNode->findByName("postTransitAnim"));
if (clip) {
alpha = (clip->getEndFrame() - clip->getFrame()) / NETWORK_ANIMATION_BLEND_FRAMES;
}
}
if (_sendNetworkNode) {
for (size_t i = 0; i < _networkPoseSet._relativePoses.size(); i++) {
_networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], (alpha > 1.0f ? 1.0f : alpha));
}
}
}
if ((int)_internalPoseSet._relativePoses.size() != _animSkeleton->getNumJoints()) {
// animations haven't fully loaded yet.
_internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses();
}
if ((int)_networkPoseSet._relativePoses.size() != _animSkeleton->getNumJoints()) {
// animations haven't fully loaded yet.
_networkPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses();
}
_lastAnimVars = _animVars;
_animVars.clearTriggers();
_animVars = triggersOut;
_networkVars.clearTriggers();
_networkVars = networkTriggersOut;
_lastContext = context;
}
applyOverridePoses();
buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses);
buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses);
// copy internal poses to external poses
{
QWriteLocker writeLock(&_externalPoseSetLock);
@ -1574,6 +1675,7 @@ glm::vec3 Rig::calculateKneePoleVector(int footJointIndex, int kneeIndex, int up
void Rig::updateFromControllerParameters(const ControllerParameters& params, float dt) {
if (!_animSkeleton || !_animNode) {
_previousControllerParameters = params;
return;
}
@ -1584,7 +1686,9 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo
bool leftHandEnabled = params.primaryControllerFlags[PrimaryControllerType_LeftHand] & (uint8_t)ControllerFlags::Enabled;
bool rightHandEnabled = params.primaryControllerFlags[PrimaryControllerType_RightHand] & (uint8_t)ControllerFlags::Enabled;
bool hipsEnabled = params.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Enabled;
bool prevHipsEnabled = _previousControllerParameters.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Enabled;
bool hipsEstimated = params.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Estimated;
bool prevHipsEstimated = _previousControllerParameters.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Estimated;
bool leftFootEnabled = params.primaryControllerFlags[PrimaryControllerType_LeftFoot] & (uint8_t)ControllerFlags::Enabled;
bool rightFootEnabled = params.primaryControllerFlags[PrimaryControllerType_RightFoot] & (uint8_t)ControllerFlags::Enabled;
bool spine2Enabled = params.primaryControllerFlags[PrimaryControllerType_Spine2] & (uint8_t)ControllerFlags::Enabled;
@ -1623,9 +1727,26 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo
}
if (hipsEnabled) {
// Apply a bit of smoothing when the hips toggle between estimated and non-estimated poses.
// This should help smooth out problems with the vive tracker when the sensor is occluded.
if (prevHipsEnabled && hipsEstimated != prevHipsEstimated) {
// blend from a snapshot of the previous hips.
const float HIPS_BLEND_DURATION = 0.5f;
_hipsBlendHelper.setBlendDuration(HIPS_BLEND_DURATION);
_hipsBlendHelper.setSnapshot(_previousControllerParameters.primaryControllerPoses[PrimaryControllerType_Hips]);
} else if (!prevHipsEnabled) {
// we have no sensible value to blend from.
const float HIPS_BLEND_DURATION = 0.0f;
_hipsBlendHelper.setBlendDuration(HIPS_BLEND_DURATION);
_hipsBlendHelper.setSnapshot(params.primaryControllerPoses[PrimaryControllerType_Hips]);
}
AnimPose hips = _hipsBlendHelper.update(params.primaryControllerPoses[PrimaryControllerType_Hips], dt);
_animVars.set("hipsType", (int)IKTarget::Type::RotationAndPosition);
_animVars.set("hipsPosition", params.primaryControllerPoses[PrimaryControllerType_Hips].trans());
_animVars.set("hipsRotation", params.primaryControllerPoses[PrimaryControllerType_Hips].rot());
_animVars.set("hipsPosition", hips.trans());
_animVars.set("hipsRotation", hips.rot());
} else {
_animVars.set("hipsType", (int)IKTarget::Type::Unknown);
}
@ -1665,6 +1786,8 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo
}
}
}
_previousControllerParameters = params;
}
void Rig::initAnimGraph(const QUrl& url) {
@ -1672,11 +1795,14 @@ void Rig::initAnimGraph(const QUrl& url) {
_animGraphURL = url;
_animNode.reset();
_networkNode.reset();
// load the anim graph
_animLoader.reset(new AnimNodeLoader(url));
auto networkUrl = PathUtils::resourcesUrl("avatar/network-animation.json");
_networkLoader.reset(new AnimNodeLoader(networkUrl));
std::weak_ptr<AnimSkeleton> weakSkeletonPtr = _animSkeleton;
connect(_animLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr](AnimNode::Pointer nodeIn) {
connect(_animLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr, url](AnimNode::Pointer nodeIn) {
_animNode = nodeIn;
// abort load if the previous skeleton was deleted.
@ -1703,7 +1829,33 @@ void Rig::initAnimGraph(const QUrl& url) {
emit onLoadComplete();
});
connect(_animLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) {
qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str;
qCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str;
});
connect(_networkLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr, networkUrl](AnimNode::Pointer nodeIn) {
_networkNode = nodeIn;
// abort load if the previous skeleton was deleted.
auto sharedSkeletonPtr = weakSkeletonPtr.lock();
if (!sharedSkeletonPtr) {
return;
}
_networkNode->setSkeleton(sharedSkeletonPtr);
if (_networkAnimState.clipNodeEnum != NetworkAnimState::Idle) {
// restore the user animation we had before reset.
NetworkAnimState origState = _networkAnimState;
_networkAnimState = { NetworkAnimState::Idle, "", 30.0f, false, 0.0f, 0.0f };
if (_networkAnimState.clipNodeEnum == NetworkAnimState::PreTransit) {
triggerNetworkAnimation("preTransitAnim");
} else if (_networkAnimState.clipNodeEnum == NetworkAnimState::Transit) {
triggerNetworkAnimation("transitAnim");
} else if (_networkAnimState.clipNodeEnum == NetworkAnimState::PostTransit) {
triggerNetworkAnimation("postTransitAnim");
}
}
});
connect(_networkLoader.get(), &AnimNodeLoader::error, [networkUrl](int error, QString str) {
qCritical(animation) << "Error loading" << networkUrl.toDisplayString() << "code = " << error << "str =" << str;
});
}
}
@ -1782,13 +1934,13 @@ void Rig::copyJointsIntoJointData(QVector<JointData>& jointDataVec) const {
if (isIndexValid(i)) {
// rotations are in absolute rig frame.
glm::quat defaultAbsRot = geometryToRigPose.rot() * _animSkeleton->getAbsoluteDefaultPose(i).rot();
data.rotation = _internalPoseSet._absolutePoses[i].rot();
data.rotation = !_sendNetworkNode ? _internalPoseSet._absolutePoses[i].rot() : _networkPoseSet._absolutePoses[i].rot();
data.rotationIsDefaultPose = isEqual(data.rotation, defaultAbsRot);
// translations are in relative frame but scaled so that they are in meters,
// instead of geometry units.
glm::vec3 defaultRelTrans = _geometryOffset.scale() * _animSkeleton->getRelativeDefaultPose(i).trans();
data.translation = _geometryOffset.scale() * _internalPoseSet._relativePoses[i].trans();
data.translation = _geometryOffset.scale() * (!_sendNetworkNode ? _internalPoseSet._relativePoses[i].trans() : _networkPoseSet._relativePoses[i].trans());
data.translationIsDefaultPose = isEqual(data.translation, defaultRelTrans);
} else {
data.translationIsDefaultPose = true;

View file

@ -24,6 +24,7 @@
#include "AnimNode.h"
#include "AnimNodeLoader.h"
#include "SimpleMovingAverage.h"
#include "AnimUtil.h"
class Rig;
class AnimInverseKinematics;
@ -113,7 +114,10 @@ public:
void destroyAnimGraph();
void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame);
void triggerNetworkAnimation(const QString& animName);
void restoreAnimation();
void restoreNetworkAnimation();
QStringList getAnimationRoles() const;
void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame);
void restoreRoleAnimation(const QString& role);
@ -172,7 +176,8 @@ public:
AnimPose getJointPose(int jointIndex) const;
// Start or stop animations as needed.
void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState);
void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity,
const glm::quat& worldRotation, CharacterControllerState ccState, float sensorToWorldScale);
// Regardless of who started the animations or how many, update the joints.
void updateAnimations(float deltaTime, const glm::mat4& rootTransform, const glm::mat4& rigToWorldTransform);
@ -269,6 +274,7 @@ protected:
// Only accessed by the main thread
PoseSet _internalPoseSet;
PoseSet _networkPoseSet;
// Copy of the _poseSet for external threads.
PoseSet _externalPoseSet;
@ -300,9 +306,12 @@ protected:
QUrl _animGraphURL;
std::shared_ptr<AnimNode> _animNode;
std::shared_ptr<AnimNode> _networkNode;
std::shared_ptr<AnimSkeleton> _animSkeleton;
std::unique_ptr<AnimNodeLoader> _animLoader;
std::unique_ptr<AnimNodeLoader> _networkLoader;
AnimVariantMap _animVars;
AnimVariantMap _networkVars;
enum class RigRole {
Idle = 0,
@ -315,6 +324,25 @@ protected:
RigRole _state { RigRole::Idle };
RigRole _desiredState { RigRole::Idle };
float _desiredStateAge { 0.0f };
struct NetworkAnimState {
enum ClipNodeEnum {
Idle = 0,
PreTransit,
Transit,
PostTransit
};
NetworkAnimState() : clipNodeEnum(NetworkAnimState::Idle) {}
NetworkAnimState(ClipNodeEnum clipNodeEnumIn, const QString& urlIn, float fpsIn, bool loopIn, float firstFrameIn, float lastFrameIn) :
clipNodeEnum(clipNodeEnumIn), url(urlIn), fps(fpsIn), loop(loopIn), firstFrame(firstFrameIn), lastFrame(lastFrameIn) {}
ClipNodeEnum clipNodeEnum;
QString url;
float fps;
bool loop;
float firstFrame;
float lastFrame;
};
struct UserAnimState {
enum ClipNodeEnum {
@ -349,6 +377,7 @@ protected:
};
UserAnimState _userAnimState;
NetworkAnimState _networkAnimState;
std::map<QString, RoleAnimState> _roleAnimStates;
float _leftHandOverlayAlpha { 0.0f };
@ -382,9 +411,13 @@ protected:
int _rigId;
bool _headEnabled { false };
bool _sendNetworkNode { false };
AnimContext _lastContext;
AnimVariantMap _lastAnimVars;
SnapshotBlendPoseHelper _hipsBlendHelper;
ControllerParameters _previousControllerParameters;
};
#endif /* defined(__hifi__Rig__) */

View file

@ -114,27 +114,29 @@ void Avatar::setShowNamesAboveHeads(bool show) {
}
AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config) {
glm::vec3 currentPosition = _isTransiting ? _currentPosition : avatarPosition;
float oneFrameDistance = glm::length(currentPosition - _lastPosition);
const float MAX_TRANSIT_DISTANCE = 30.0f;
float scaledMaxTransitDistance = MAX_TRANSIT_DISTANCE * _scale;
if (oneFrameDistance > config._triggerDistance && !_isTransiting) {
if (oneFrameDistance < scaledMaxTransitDistance) {
start(deltaTime, _lastPosition, currentPosition, config);
float oneFrameDistance = _isActive ? glm::length(avatarPosition - _endPosition) : glm::length(avatarPosition - _lastPosition);
if (oneFrameDistance > (config._minTriggerDistance * _scale)) {
if (oneFrameDistance < (config._maxTriggerDistance * _scale)) {
start(deltaTime, _lastPosition, avatarPosition, config);
} else {
_lastPosition = currentPosition;
return Status::ABORT_TRANSIT;
_lastPosition = avatarPosition;
_status = Status::ABORT_TRANSIT;
}
}
_lastPosition = currentPosition;
_lastPosition = avatarPosition;
_status = updatePosition(deltaTime);
if (_isActive && oneFrameDistance > (config._abortDistance * _scale) && _status == Status::POST_TRANSIT) {
reset();
_status = Status::ENDED;
}
return _status;
}
void AvatarTransit::reset() {
_lastPosition = _endPosition;
_currentPosition = _endPosition;
_isTransiting = false;
_isActive = false;
}
void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const glm::vec3& endPosition, const AvatarTransit::TransitConfig& config) {
_startPosition = startPosition;
@ -143,12 +145,14 @@ void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const
_transitLine = endPosition - startPosition;
_totalDistance = glm::length(_transitLine);
_easeType = config._easeType;
const float REFERENCE_FRAMES_PER_SECOND = 30.0f;
_preTransitTime = AVATAR_PRE_TRANSIT_FRAME_COUNT / AVATAR_TRANSIT_FRAMES_PER_SECOND;
_postTransitTime = AVATAR_POST_TRANSIT_FRAME_COUNT / AVATAR_TRANSIT_FRAMES_PER_SECOND;
int transitFrames = (!config._isDistanceBased) ? config._totalFrames : config._framesPerMeter * _totalDistance;
_totalTime = (float)transitFrames / REFERENCE_FRAMES_PER_SECOND;
_currentTime = 0.0f;
_isTransiting = true;
_transitTime = (float)transitFrames / AVATAR_TRANSIT_FRAMES_PER_SECOND;
_totalTime = _transitTime + _preTransitTime + _postTransitTime;
_currentTime = _isActive ? _preTransitTime : 0.0f;
_isActive = true;
}
float AvatarTransit::getEaseValue(AvatarTransit::EaseType type, float value) {
@ -171,31 +175,37 @@ float AvatarTransit::getEaseValue(AvatarTransit::EaseType type, float value) {
AvatarTransit::Status AvatarTransit::updatePosition(float deltaTime) {
Status status = Status::IDLE;
if (_isTransiting) {
if (_isActive) {
float nextTime = _currentTime + deltaTime;
if (nextTime >= _totalTime) {
_currentPosition = _endPosition;
_isTransiting = false;
status = Status::END_TRANSIT;
} else {
if (nextTime < _preTransitTime) {
_currentPosition = _startPosition;
status = Status::PRE_TRANSIT;
if (_currentTime == 0) {
status = Status::STARTED;
}
} else if (nextTime < _totalTime - _postTransitTime){
status = Status::TRANSITING;
if (_currentTime <= _preTransitTime) {
status = Status::START_TRANSIT;
} else {
status = Status::TRANSITING;
float percentageIntoTransit = (nextTime - _preTransitTime) / _transitTime;
_currentPosition = _startPosition + getEaseValue(_easeType, percentageIntoTransit) * _transitLine;
}
} else {
status = Status::POST_TRANSIT;
_currentPosition = _endPosition;
if (nextTime >= _totalTime) {
_isActive = false;
status = Status::ENDED;
} else if (_currentTime < _totalTime - _postTransitTime) {
status = Status::END_TRANSIT;
}
float percentageIntoTransit = nextTime / _totalTime;
_currentPosition = _startPosition + getEaseValue(_easeType, percentageIntoTransit) * _transitLine;
}
_currentTime = nextTime;
}
return status;
}
bool AvatarTransit::getNextPosition(glm::vec3& nextPosition) {
nextPosition = _currentPosition;
return _isTransiting;
}
Avatar::Avatar(QThread* thread) :
_voiceSphereID(GeometryCache::UNKNOWN_ID)
{
@ -536,16 +546,10 @@ void Avatar::relayJointDataToChildren() {
void Avatar::simulate(float deltaTime, bool inView) {
PROFILE_RANGE(simulation, "simulate");
if (_transit.isTransiting()) {
glm::vec3 nextPosition;
if (_transit.getNextPosition(nextPosition)) {
_globalPosition = nextPosition;
_globalPositionChanged = usecTimestampNow();
if (!hasParent()) {
setLocalPosition(nextPosition);
}
}
_globalPosition = _transit.isActive() ? _transit.getCurrentPosition() : _serverPosition;
if (!hasParent()) {
setLocalPosition(_globalPosition);
}
_simulationRate.increment();
@ -558,7 +562,7 @@ void Avatar::simulate(float deltaTime, bool inView) {
PROFILE_RANGE(simulation, "updateJoints");
if (inView) {
Head* head = getHead();
if (_hasNewJointData || _transit.isTransiting()) {
if (_hasNewJointData || _transit.isActive()) {
_skeletonModel->getRig().copyJointsFromJointData(_jointData);
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
_skeletonModel->getRig().computeExternalPoses(rootTransform);
@ -703,10 +707,10 @@ static TextRenderer3D* textRenderer(TextRendererType type) {
return displayNameRenderer;
}
void Avatar::metaBlendshapeOperator(int blendshapeNumber, const QVector<BlendshapeOffset>& blendshapeOffsets, const QVector<int>& blendedMeshSizes,
const render::ItemIDs& subItemIDs) {
void Avatar::metaBlendshapeOperator(render::ItemID renderItemID, int blendshapeNumber, const QVector<BlendshapeOffset>& blendshapeOffsets,
const QVector<int>& blendedMeshSizes, const render::ItemIDs& subItemIDs) {
render::Transaction transaction;
transaction.updateItem<AvatarData>(_renderItemID, [blendshapeNumber, blendshapeOffsets, blendedMeshSizes,
transaction.updateItem<AvatarData>(renderItemID, [blendshapeNumber, blendshapeOffsets, blendedMeshSizes,
subItemIDs](AvatarData& avatar) {
auto avatarPtr = dynamic_cast<Avatar*>(&avatar);
if (avatarPtr) {
@ -726,7 +730,7 @@ void Avatar::addToScene(AvatarSharedPointer self, const render::ScenePointer& sc
_renderBound = getBounds();
transaction.resetItem(_renderItemID, avatarPayloadPointer);
using namespace std::placeholders;
_skeletonModel->addToScene(scene, transaction, std::bind(&Avatar::metaBlendshapeOperator, this, _1, _2, _3, _4));
_skeletonModel->addToScene(scene, transaction, std::bind(&Avatar::metaBlendshapeOperator, _renderItemID, _1, _2, _3, _4));
_skeletonModel->setTagMask(render::hifi::TAG_ALL_VIEWS);
_skeletonModel->setGroupCulled(true);
_skeletonModel->setCanCastShadow(true);
@ -950,7 +954,7 @@ void Avatar::fixupModelsInScene(const render::ScenePointer& scene) {
if (_skeletonModel->isRenderable() && _skeletonModel->needsFixupInScene()) {
_skeletonModel->removeFromScene(scene, transaction);
using namespace std::placeholders;
_skeletonModel->addToScene(scene, transaction, std::bind(&Avatar::metaBlendshapeOperator, this, _1, _2, _3, _4));
_skeletonModel->addToScene(scene, transaction, std::bind(&Avatar::metaBlendshapeOperator, _renderItemID, _1, _2, _3, _4));
_skeletonModel->setTagMask(render::hifi::TAG_ALL_VIEWS);
_skeletonModel->setGroupCulled(true);
@ -1995,22 +1999,12 @@ float Avatar::getUnscaledEyeHeightFromSkeleton() const {
}
}
AvatarTransit::Status Avatar::updateTransit(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config) {
AvatarTransit::Status Avatar::updateTransit(float deltaTime, const glm::vec3& avatarPosition, float avatarScale, const AvatarTransit::TransitConfig& config) {
std::lock_guard<std::mutex> lock(_transitLock);
_transit.setScale(avatarScale);
return _transit.update(deltaTime, avatarPosition, config);
}
void Avatar::setTransitScale(float scale) {
std::lock_guard<std::mutex> lock(_transitLock);
return _transit.setScale(scale);
}
void Avatar::overrideNextPackagePositionData(const glm::vec3& position) {
std::lock_guard<std::mutex> lock(_transitLock);
_overrideGlobalPosition = true;
_globalPositionOverride = position;
}
void Avatar::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) {
std::lock_guard<std::mutex> lock(_materialsLock);
_materials[parentMaterialName].push(material);

View file

@ -56,9 +56,13 @@ class AvatarTransit {
public:
enum Status {
IDLE = 0,
STARTED,
PRE_TRANSIT,
START_TRANSIT,
TRANSITING,
END_TRANSIT,
POST_TRANSIT,
ENDED,
ABORT_TRANSIT
};
@ -72,20 +76,20 @@ public:
struct TransitConfig {
TransitConfig() {};
int _totalFrames { 0 };
int _framesPerMeter { 0 };
float _framesPerMeter { 0.0f };
bool _isDistanceBased { false };
float _triggerDistance { 0 };
float _minTriggerDistance { 0.0f };
float _maxTriggerDistance { 0.0f };
float _abortDistance{ 0.0f };
EaseType _easeType { EaseType::EASE_OUT };
};
AvatarTransit() {};
Status update(float deltaTime, const glm::vec3& avatarPosition, const TransitConfig& config);
Status getStatus() { return _status; }
bool isTransiting() { return _isTransiting; }
bool isActive() { return _isActive; }
glm::vec3 getCurrentPosition() { return _currentPosition; }
bool getNextPosition(glm::vec3& nextPosition);
glm::vec3 getEndPosition() { return _endPosition; }
float getTransitTime() { return _totalTime; }
void setScale(float scale) { _scale = scale; }
void reset();
@ -93,7 +97,7 @@ private:
Status updatePosition(float deltaTime);
void start(float deltaTime, const glm::vec3& startPosition, const glm::vec3& endPosition, const TransitConfig& config);
float getEaseValue(AvatarTransit::EaseType type, float value);
bool _isTransiting { false };
bool _isActive { false };
glm::vec3 _startPosition;
glm::vec3 _endPosition;
@ -103,7 +107,10 @@ private:
glm::vec3 _transitLine;
float _totalDistance { 0.0f };
float _preTransitTime { 0.0f };
float _totalTime { 0.0f };
float _transitTime { 0.0f };
float _postTransitTime { 0.0f };
float _currentTime { 0.0f };
EaseType _easeType { EaseType::EASE_OUT };
Status _status { Status::IDLE };
@ -431,11 +438,7 @@ public:
virtual scriptable::ScriptableModelBase getScriptableModel() override;
std::shared_ptr<AvatarTransit> getTransit() { return std::make_shared<AvatarTransit>(_transit); };
AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config);
void setTransitScale(float scale);
void overrideNextPackagePositionData(const glm::vec3& position);
AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, float avatarScale, const AvatarTransit::TransitConfig& config);
signals:
void targetScaleChanged(float targetScale);
@ -623,8 +626,8 @@ protected:
LoadingStatus _loadingStatus { LoadingStatus::NoModel };
void metaBlendshapeOperator(int blendshapeNumber, const QVector<BlendshapeOffset>& blendshapeOffsets, const QVector<int>& blendedMeshSizes,
const render::ItemIDs& subItemIDs);
static void metaBlendshapeOperator(render::ItemID renderItemID, int blendshapeNumber, const QVector<BlendshapeOffset>& blendshapeOffsets,
const QVector<int>& blendedMeshSizes, const render::ItemIDs& subItemIDs);
};
#endif // hifi_Avatar_h

View file

@ -66,7 +66,7 @@ size_t AvatarDataPacket::maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients
}
size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints) {
const size_t validityBitsSize = (size_t)std::ceil(numJoints / (float)BITS_IN_BYTE);
const size_t validityBitsSize = calcBitVectorSize((int)numJoints);
size_t totalSize = sizeof(uint8_t); // numJoints
@ -228,18 +228,18 @@ float AvatarData::getDistanceBasedMinTranslationDistance(glm::vec3 viewerPositio
// we want to track outbound data in this case...
QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) {
AvatarDataPacket::HasFlags hasFlagsOut;
auto lastSentTime = _lastToByteArray;
_lastToByteArray = usecTimestampNow();
return AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(),
hasFlagsOut, dropFaceTracking, false, glm::vec3(0), nullptr,
&_outboundDataRate);
AvatarDataPacket::SendStatus sendStatus;
auto avatarByteArray = AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(),
sendStatus, dropFaceTracking, false, glm::vec3(0), nullptr, 0, &_outboundDataRate);
return avatarByteArray;
}
QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime,
const QVector<JointData>& lastSentJointData,
AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust,
glm::vec3 viewerPosition, QVector<JointData>* sentJointDataOut, AvatarDataRate* outboundDataRateOut) const {
AvatarDataPacket::SendStatus& sendStatus, bool dropFaceTracking, bool distanceAdjust,
glm::vec3 viewerPosition, QVector<JointData>* sentJointDataOut, int maxDataSize, AvatarDataRate* outboundDataRateOut) const {
bool cullSmallChanges = (dataDetail == CullSmallData);
bool sendAll = (dataDetail == SendAllData);
@ -247,11 +247,23 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
bool sendPALMinimum = (dataDetail == PALMinimum);
lazyInitHeadData();
ASSERT(maxDataSize == 0 || (size_t)maxDataSize >= AvatarDataPacket::MIN_BULK_PACKET_SIZE);
// Leading flags, to indicate how much data is actually included in the packet...
AvatarDataPacket::HasFlags wantedFlags = 0;
AvatarDataPacket::HasFlags includedFlags = 0;
AvatarDataPacket::HasFlags extraReturnedFlags = 0; // For partial joint data.
// special case, if we were asked for no data, then just include the flags all set to nothing
if (dataDetail == NoData) {
AvatarDataPacket::HasFlags packetStateFlags = 0;
QByteArray avatarDataByteArray(reinterpret_cast<char*>(&packetStateFlags), sizeof(packetStateFlags));
sendStatus.itemFlags = wantedFlags;
QByteArray avatarDataByteArray;
if (sendStatus.sendUUID) {
avatarDataByteArray.append(getSessionUUID().toRfc4122().data(), NUM_BYTES_RFC4122_UUID);
}
avatarDataByteArray.append((char*) &wantedFlags, sizeof wantedFlags);
return avatarDataByteArray;
}
@ -274,114 +286,141 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
// 3 translations * 6 bytes = 6.48kbps
//
auto parentID = getParentID();
bool hasAvatarGlobalPosition = true; // always include global position
bool hasAvatarOrientation = false;
bool hasAvatarBoundingBox = false;
bool hasAvatarScale = false;
bool hasLookAtPosition = false;
bool hasAudioLoudness = false;
bool hasSensorToWorldMatrix = false;
bool hasAdditionalFlags = false;
// local position, and parent info only apply to avatars that are parented. The local position
// and the parent info can change independently though, so we track their "changed since"
// separately
bool hasParentInfo = false;
bool hasAvatarLocalPosition = false;
bool hasFaceTrackerInfo = false;
bool hasJointData = false;
bool hasJointDefaultPoseFlags = false;
bool hasGrabJoints = false;
QUuid parentID;
glm::mat4 leftFarGrabMatrix;
glm::mat4 rightFarGrabMatrix;
glm::mat4 mouseFarGrabMatrix;
if (sendPALMinimum) {
hasAudioLoudness = true;
} else {
hasAvatarOrientation = sendAll || rotationChangedSince(lastSentTime);
hasAvatarBoundingBox = sendAll || avatarBoundingBoxChangedSince(lastSentTime);
hasAvatarScale = sendAll || avatarScaleChangedSince(lastSentTime);
hasLookAtPosition = sendAll || lookAtPositionChangedSince(lastSentTime);
hasAudioLoudness = sendAll || audioLoudnessChangedSince(lastSentTime);
hasSensorToWorldMatrix = sendAll || sensorToWorldMatrixChangedSince(lastSentTime);
hasAdditionalFlags = sendAll || additionalFlagsChangedSince(lastSentTime);
hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime);
hasAvatarLocalPosition = hasParent() && (sendAll ||
tranlationChangedSince(lastSentTime) ||
parentInfoChangedSince(lastSentTime));
if (sendStatus.itemFlags == 0) {
// New avatar ...
bool hasAvatarGlobalPosition = true; // always include global position
bool hasAvatarOrientation = false;
bool hasAvatarBoundingBox = false;
bool hasAvatarScale = false;
bool hasLookAtPosition = false;
bool hasAudioLoudness = false;
bool hasSensorToWorldMatrix = false;
bool hasJointData = false;
bool hasJointDefaultPoseFlags = false;
bool hasAdditionalFlags = false;
hasFaceTrackerInfo = !dropFaceTracking && (hasFaceTracker() || getHasScriptedBlendshapes()) &&
(sendAll || faceTrackerInfoChangedSince(lastSentTime));
hasJointData = sendAll || !sendMinimum;
hasJointDefaultPoseFlags = hasJointData;
if (hasJointData) {
bool leftValid;
leftFarGrabMatrix = _farGrabLeftMatrixCache.get(leftValid);
if (!leftValid) {
leftFarGrabMatrix = glm::mat4();
}
bool rightValid;
rightFarGrabMatrix = _farGrabRightMatrixCache.get(rightValid);
if (!rightValid) {
rightFarGrabMatrix = glm::mat4();
}
bool mouseValid;
mouseFarGrabMatrix = _farGrabMouseMatrixCache.get(mouseValid);
if (!mouseValid) {
mouseFarGrabMatrix = glm::mat4();
}
hasGrabJoints = (leftValid || rightValid || mouseValid);
// local position, and parent info only apply to avatars that are parented. The local position
// and the parent info can change independently though, so we track their "changed since"
// separately
bool hasParentInfo = false;
bool hasAvatarLocalPosition = false;
bool hasFaceTrackerInfo = false;
if (sendPALMinimum) {
hasAudioLoudness = true;
} else {
hasAvatarOrientation = sendAll || rotationChangedSince(lastSentTime);
hasAvatarBoundingBox = sendAll || avatarBoundingBoxChangedSince(lastSentTime);
hasAvatarScale = sendAll || avatarScaleChangedSince(lastSentTime);
hasLookAtPosition = sendAll || lookAtPositionChangedSince(lastSentTime);
hasAudioLoudness = sendAll || audioLoudnessChangedSince(lastSentTime);
hasSensorToWorldMatrix = sendAll || sensorToWorldMatrixChangedSince(lastSentTime);
hasAdditionalFlags = sendAll || additionalFlagsChangedSince(lastSentTime);
hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime);
hasAvatarLocalPosition = hasParent() && (sendAll ||
tranlationChangedSince(lastSentTime) ||
parentInfoChangedSince(lastSentTime));
hasFaceTrackerInfo = !dropFaceTracking && (hasFaceTracker() || getHasScriptedBlendshapes()) &&
(sendAll || faceTrackerInfoChangedSince(lastSentTime));
hasJointData = !sendMinimum;
hasJointDefaultPoseFlags = hasJointData;
}
wantedFlags =
(hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0)
| (hasAvatarBoundingBox ? AvatarDataPacket::PACKET_HAS_AVATAR_BOUNDING_BOX : 0)
| (hasAvatarOrientation ? AvatarDataPacket::PACKET_HAS_AVATAR_ORIENTATION : 0)
| (hasAvatarScale ? AvatarDataPacket::PACKET_HAS_AVATAR_SCALE : 0)
| (hasLookAtPosition ? AvatarDataPacket::PACKET_HAS_LOOK_AT_POSITION : 0)
| (hasAudioLoudness ? AvatarDataPacket::PACKET_HAS_AUDIO_LOUDNESS : 0)
| (hasSensorToWorldMatrix ? AvatarDataPacket::PACKET_HAS_SENSOR_TO_WORLD_MATRIX : 0)
| (hasAdditionalFlags ? AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS : 0)
| (hasParentInfo ? AvatarDataPacket::PACKET_HAS_PARENT_INFO : 0)
| (hasAvatarLocalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION : 0)
| (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0)
| (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0)
| (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0)
| (hasJointData ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0);
sendStatus.itemFlags = wantedFlags;
sendStatus.rotationsSent = 0;
sendStatus.translationsSent = 0;
} else { // Continuing avatar ...
wantedFlags = sendStatus.itemFlags;
if (wantedFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) {
// Must send joints for grab joints -
wantedFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA;
}
}
if (wantedFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) {
bool leftValid;
leftFarGrabMatrix = _farGrabLeftMatrixCache.get(leftValid);
if (!leftValid) {
leftFarGrabMatrix = glm::mat4();
}
bool rightValid;
rightFarGrabMatrix = _farGrabRightMatrixCache.get(rightValid);
if (!rightValid) {
rightFarGrabMatrix = glm::mat4();
}
bool mouseValid;
mouseFarGrabMatrix = _farGrabMouseMatrixCache.get(mouseValid);
if (!mouseValid) {
mouseFarGrabMatrix = glm::mat4();
}
if (!(leftValid || rightValid || mouseValid)) {
wantedFlags &= ~AvatarDataPacket::PACKET_HAS_GRAB_JOINTS;
}
}
if (wantedFlags & (AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS | AvatarDataPacket::PACKET_HAS_PARENT_INFO)) {
parentID = getParentID();
}
const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE +
(hasFaceTrackerInfo ? AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) : 0) +
(hasJointData ? AvatarDataPacket::maxJointDataSize(_jointData.size(), hasGrabJoints) : 0) +
(hasJointDefaultPoseFlags ? AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size()) : 0);
const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + NUM_BYTES_RFC4122_UUID +
AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) +
AvatarDataPacket::maxJointDataSize(_jointData.size(), true) +
AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size());
if (maxDataSize == 0) {
maxDataSize = (int)byteArraySize;
}
QByteArray avatarDataByteArray((int)byteArraySize, 0);
unsigned char* destinationBuffer = reinterpret_cast<unsigned char*>(avatarDataByteArray.data());
unsigned char* startPosition = destinationBuffer;
// Leading flags, to indicate how much data is actually included in the packet...
AvatarDataPacket::HasFlags packetStateFlags =
(hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0)
| (hasAvatarBoundingBox ? AvatarDataPacket::PACKET_HAS_AVATAR_BOUNDING_BOX : 0)
| (hasAvatarOrientation ? AvatarDataPacket::PACKET_HAS_AVATAR_ORIENTATION : 0)
| (hasAvatarScale ? AvatarDataPacket::PACKET_HAS_AVATAR_SCALE : 0)
| (hasLookAtPosition ? AvatarDataPacket::PACKET_HAS_LOOK_AT_POSITION : 0)
| (hasAudioLoudness ? AvatarDataPacket::PACKET_HAS_AUDIO_LOUDNESS : 0)
| (hasSensorToWorldMatrix ? AvatarDataPacket::PACKET_HAS_SENSOR_TO_WORLD_MATRIX : 0)
| (hasAdditionalFlags ? AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS : 0)
| (hasParentInfo ? AvatarDataPacket::PACKET_HAS_PARENT_INFO : 0)
| (hasAvatarLocalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION : 0)
| (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0)
| (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0)
| (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0)
| (hasGrabJoints ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0);
memcpy(destinationBuffer, &packetStateFlags, sizeof(packetStateFlags));
destinationBuffer += sizeof(packetStateFlags);
const unsigned char* const startPosition = destinationBuffer;
const unsigned char* const packetEnd = destinationBuffer + maxDataSize;
#define AVATAR_MEMCPY(src) \
memcpy(destinationBuffer, &(src), sizeof(src)); \
destinationBuffer += sizeof(src);
if (hasAvatarGlobalPosition) {
auto startSection = destinationBuffer;
if (_overrideGlobalPosition) {
AVATAR_MEMCPY(_globalPositionOverride);
} else {
AVATAR_MEMCPY(_globalPosition);
}
// If we want an item and there's sufficient space:
#define IF_AVATAR_SPACE(flag, space) \
if ((wantedFlags & AvatarDataPacket::flag) \
&& (packetEnd - destinationBuffer) >= (ptrdiff_t)(space) \
&& (includedFlags |= AvatarDataPacket::flag))
if (sendStatus.sendUUID) {
memcpy(destinationBuffer, getSessionUUID().toRfc4122(), NUM_BYTES_RFC4122_UUID);
destinationBuffer += NUM_BYTES_RFC4122_UUID;
}
unsigned char * packetFlagsLocation = destinationBuffer;
destinationBuffer += sizeof(wantedFlags);
IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof _globalPosition) {
auto startSection = destinationBuffer;
AVATAR_MEMCPY(_globalPosition);
int numBytes = destinationBuffer - startSection;
if (outboundDataRateOut) {
@ -389,7 +428,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
if (hasAvatarBoundingBox) {
IF_AVATAR_SPACE(PACKET_HAS_AVATAR_BOUNDING_BOX, sizeof _globalBoundingBoxDimensions + sizeof _globalBoundingBoxOffset) {
auto startSection = destinationBuffer;
AVATAR_MEMCPY(_globalBoundingBoxDimensions);
AVATAR_MEMCPY(_globalBoundingBoxOffset);
@ -400,7 +439,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
if (hasAvatarOrientation) {
IF_AVATAR_SPACE(PACKET_HAS_AVATAR_ORIENTATION, sizeof(AvatarDataPacket::SixByteQuat)) {
auto startSection = destinationBuffer;
auto localOrientation = getOrientationOutbound();
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, localOrientation);
@ -411,7 +450,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
if (hasAvatarScale) {
IF_AVATAR_SPACE(PACKET_HAS_AVATAR_SCALE, sizeof(AvatarDataPacket::AvatarScale)) {
auto startSection = destinationBuffer;
auto data = reinterpret_cast<AvatarDataPacket::AvatarScale*>(destinationBuffer);
auto scale = getDomainLimitedScale();
@ -424,7 +463,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
if (hasLookAtPosition) {
IF_AVATAR_SPACE(PACKET_HAS_LOOK_AT_POSITION, sizeof(_headData->getLookAtPosition()) ) {
auto startSection = destinationBuffer;
AVATAR_MEMCPY(_headData->getLookAtPosition());
int numBytes = destinationBuffer - startSection;
@ -433,7 +472,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
if (hasAudioLoudness) {
IF_AVATAR_SPACE(PACKET_HAS_AUDIO_LOUDNESS, sizeof(AvatarDataPacket::AudioLoudness)) {
auto startSection = destinationBuffer;
auto data = reinterpret_cast<AvatarDataPacket::AudioLoudness*>(destinationBuffer);
data->audioLoudness = packFloatGainToByte(getAudioLoudness() / AUDIO_LOUDNESS_SCALE);
@ -445,7 +484,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
if (hasSensorToWorldMatrix) {
IF_AVATAR_SPACE(PACKET_HAS_SENSOR_TO_WORLD_MATRIX, sizeof(AvatarDataPacket::SensorToWorldMatrix)) {
auto startSection = destinationBuffer;
auto data = reinterpret_cast<AvatarDataPacket::SensorToWorldMatrix*>(destinationBuffer);
glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix();
@ -463,7 +502,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
if (hasAdditionalFlags) {
IF_AVATAR_SPACE(PACKET_HAS_ADDITIONAL_FLAGS, sizeof (uint16_t)) {
auto startSection = destinationBuffer;
auto data = reinterpret_cast<AvatarDataPacket::AdditionalFlags*>(destinationBuffer);
@ -511,7 +550,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
if (hasParentInfo) {
IF_AVATAR_SPACE(PACKET_HAS_PARENT_INFO, sizeof(AvatarDataPacket::ParentInfo)) {
auto startSection = destinationBuffer;
auto parentInfo = reinterpret_cast<AvatarDataPacket::ParentInfo*>(destinationBuffer);
QByteArray referentialAsBytes = parentID.toRfc4122();
@ -525,7 +564,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
if (hasAvatarLocalPosition) {
IF_AVATAR_SPACE(PACKET_HAS_AVATAR_LOCAL_POSITION, sizeof(getLocalPosition()) ) {
auto startSection = destinationBuffer;
const auto localPosition = getLocalPosition();
AVATAR_MEMCPY(localPosition);
@ -536,11 +575,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
const auto& blendshapeCoefficients = _headData->getBlendshapeCoefficients();
// If it is connected, pack up the data
if (hasFaceTrackerInfo) {
IF_AVATAR_SPACE(PACKET_HAS_FACE_TRACKER_INFO, sizeof(AvatarDataPacket::FaceTrackerInfo) + (size_t)blendshapeCoefficients.size() * sizeof(float)) {
auto startSection = destinationBuffer;
auto faceTrackerInfo = reinterpret_cast<AvatarDataPacket::FaceTrackerInfo*>(destinationBuffer);
const auto& blendshapeCoefficients = _headData->getBlendshapeCoefficients();
// note: we don't use the blink and average loudness, we just use the numBlendShapes and
// compute the procedural info on the client side.
faceTrackerInfo->leftEyeBlink = _headData->_leftEyeBlink;
@ -560,125 +599,125 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
QVector<JointData> jointData;
if (hasJointData || hasJointDefaultPoseFlags) {
if (wantedFlags & (AvatarDataPacket::PACKET_HAS_JOINT_DATA | AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS)) {
QReadLocker readLock(&_jointDataLock);
jointData = _jointData;
}
const int numJoints = jointData.size();
assert(numJoints <= 255);
const int jointBitVectorSize = calcBitVectorSize(numJoints);
// If it is connected, pack up the data
if (hasJointData) {
// Start joints if room for at least the faux joints.
IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, 1 + 2 * jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE) {
// Allow for faux joints + translation bit-vector:
const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat)
+ jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE;
auto startSection = destinationBuffer;
// joint rotation data
int numJoints = jointData.size();
*destinationBuffer++ = (uint8_t)numJoints;
unsigned char* validityPosition = destinationBuffer;
unsigned char validity = 0;
int validityBit = 0;
int numValidityBytes = calcBitVectorSize(numJoints);
memset(validityPosition, 0, jointBitVectorSize);
#ifdef WANT_DEBUG
int rotationSentCount = 0;
unsigned char* beforeRotations = destinationBuffer;
#endif
destinationBuffer += numValidityBytes; // Move pointer past the validity bytes
destinationBuffer += jointBitVectorSize; // Move pointer past the validity bytes
// sentJointDataOut and lastSentJointData might be the same vector
if (sentJointDataOut) {
sentJointDataOut->resize(numJoints); // Make sure the destination is resized before using it
}
const JointData *const joints = jointData.data();
JointData *const sentJoints = sentJointDataOut ? sentJointDataOut->data() : nullptr;
float minRotationDOT = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinRotationDOT(viewerPosition) : AVATAR_MIN_ROTATION_DOT;
for (int i = 0; i < jointData.size(); i++) {
const JointData& data = jointData[i];
int i = sendStatus.rotationsSent;
for (; i < numJoints; ++i) {
const JointData& data = joints[i];
const JointData& last = lastSentJointData[i];
if (!data.rotationIsDefaultPose) {
// The dot product for larger rotations is a lower number.
// So if the dot() is less than the value, then the rotation is a larger angle of rotation
if (sendAll || last.rotationIsDefaultPose || (!cullSmallChanges && last.rotation != data.rotation)
|| (cullSmallChanges && fabsf(glm::dot(last.rotation, data.rotation)) < minRotationDOT) ) {
validity |= (1 << validityBit);
if (packetEnd - destinationBuffer >= minSizeForJoint) {
if (!data.rotationIsDefaultPose) {
// The dot product for larger rotations is a lower number,
// so if the dot() is less than the value, then the rotation is a larger angle of rotation
if (sendAll || last.rotationIsDefaultPose || (!cullSmallChanges && last.rotation != data.rotation)
|| (cullSmallChanges && fabsf(glm::dot(last.rotation, data.rotation)) < minRotationDOT)) {
validityPosition[i / BITS_IN_BYTE] |= 1 << (i % BITS_IN_BYTE);
#ifdef WANT_DEBUG
rotationSentCount++;
rotationSentCount++;
#endif
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation);
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation);
if (sentJointDataOut) {
(*sentJointDataOut)[i].rotation = data.rotation;
if (sentJoints) {
sentJoints[i].rotation = data.rotation;
}
}
}
} else {
break;
}
if (sentJointDataOut) {
(*sentJointDataOut)[i].rotationIsDefaultPose = data.rotationIsDefaultPose;
if (sentJoints) {
sentJoints[i].rotationIsDefaultPose = data.rotationIsDefaultPose;
}
if (++validityBit == BITS_IN_BYTE) {
*validityPosition++ = validity;
validityBit = validity = 0;
}
}
if (validityBit != 0) {
*validityPosition++ = validity;
}
sendStatus.rotationsSent = i;
// joint translation data
validityPosition = destinationBuffer;
validity = 0;
validityBit = 0;
#ifdef WANT_DEBUG
int translationSentCount = 0;
unsigned char* beforeTranslations = destinationBuffer;
#endif
destinationBuffer += numValidityBytes; // Move pointer past the validity bytes
memset(destinationBuffer, 0, jointBitVectorSize);
destinationBuffer += jointBitVectorSize; // Move pointer past the validity bytes
float minTranslation = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinTranslationDistance(viewerPosition) : AVATAR_MIN_TRANSLATION;
float maxTranslationDimension = 0.0;
for (int i = 0; i < jointData.size(); i++) {
const JointData& data = jointData[i];
i = sendStatus.translationsSent;
for (; i < numJoints; ++i) {
const JointData& data = joints[i];
const JointData& last = lastSentJointData[i];
if (!data.translationIsDefaultPose) {
if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation)
|| (cullSmallChanges && glm::distance(data.translation, lastSentJointData[i].translation) > minTranslation)) {
validity |= (1 << validityBit);
if (packetEnd - destinationBuffer >= minSizeForJoint) {
if (!data.translationIsDefaultPose) {
if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation)
|| (cullSmallChanges && glm::distance(data.translation, lastSentJointData[i].translation) > minTranslation)) {
validityPosition[i / BITS_IN_BYTE] |= 1 << (i % BITS_IN_BYTE);
#ifdef WANT_DEBUG
translationSentCount++;
translationSentCount++;
#endif
maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension);
maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension);
maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension);
maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension);
maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension);
maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension);
destinationBuffer +=
packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX);
destinationBuffer +=
packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX);
if (sentJointDataOut) {
(*sentJointDataOut)[i].translation = data.translation;
if (sentJoints) {
sentJoints[i].translation = data.translation;
}
}
}
} else {
break;
}
if (sentJointDataOut) {
(*sentJointDataOut)[i].translationIsDefaultPose = data.translationIsDefaultPose;
if (sentJoints) {
sentJoints[i].translationIsDefaultPose = data.translationIsDefaultPose;
}
if (++validityBit == BITS_IN_BYTE) {
*validityPosition++ = validity;
validityBit = validity = 0;
}
}
if (validityBit != 0) {
*validityPosition++ = validity;
}
sendStatus.translationsSent = i;
// faux joints
Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix());
@ -691,7 +730,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(),
TRANSLATION_COMPRESSION_RADIX);
if (hasGrabJoints) {
IF_AVATAR_SPACE(PACKET_HAS_GRAB_JOINTS, sizeof (AvatarDataPacket::FarGrabJoints)) {
// the far-grab joints may range further than 3 meters, so we can't use packFloatVec3ToSignedTwoByteFixed etc
auto startSection = destinationBuffer;
@ -733,18 +772,20 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
#endif
if (sendStatus.rotationsSent != numJoints || sendStatus.translationsSent != numJoints) {
extraReturnedFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA;
}
int numBytes = destinationBuffer - startSection;
if (outboundDataRateOut) {
outboundDataRateOut->jointDataRate.increment(numBytes);
}
}
if (hasJointDefaultPoseFlags) {
IF_AVATAR_SPACE(PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS, 1 + 2 * jointBitVectorSize) {
auto startSection = destinationBuffer;
// write numJoints
int numJoints = jointData.size();
*destinationBuffer++ = (uint8_t)numJoints;
// write rotationIsDefaultPose bits
@ -763,6 +804,10 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
}
memcpy(packetFlagsLocation, &includedFlags, sizeof(includedFlags));
// Return dropped items.
sendStatus.itemFlags = (wantedFlags & ~includedFlags) | extraReturnedFlags;
int avatarDataSize = destinationBuffer - startPosition;
if (avatarDataSize > (int)byteArraySize) {
@ -771,6 +816,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
}
return avatarDataByteArray.left(avatarDataSize);
#undef AVATAR_MEMCPY
#undef IF_AVATAR_SPACE
}
// NOTE: This is never used in a "distanceAdjust" mode, so it's ok that it doesn't use a variable minimum rotation/translation
@ -887,20 +935,22 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
offset = glm::vec3(row * SPACE_BETWEEN_AVATARS, 0.0f, col * SPACE_BETWEEN_AVATARS);
}
auto newValue = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset;
if (_globalPosition != newValue) {
_globalPosition = newValue;
_serverPosition = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset;
auto oneStepDistance = glm::length(_globalPosition - _serverPosition);
if (oneStepDistance <= AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE || oneStepDistance >= AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE) {
_globalPosition = _serverPosition;
// if we don't have a parent, make sure to also set our local position
if (!hasParent()) {
setLocalPosition(_serverPosition);
}
}
if (_globalPosition != _serverPosition) {
_globalPositionChanged = now;
}
sourceBuffer += sizeof(AvatarDataPacket::AvatarGlobalPosition);
int numBytesRead = sourceBuffer - startSection;
_globalPositionRate.increment(numBytesRead);
_globalPositionUpdateRate.increment();
// if we don't have a parent, make sure to also set our local position
if (!hasParent()) {
setLocalPosition(newValue);
}
}
if (hasAvatarBoundingBox) {
@ -921,6 +971,8 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
_avatarBoundingBoxChanged = now;
}
_defaultBubbleBox = computeBubbleBox();
sourceBuffer += sizeof(AvatarDataPacket::AvatarBoundingBox);
int numBytesRead = sourceBuffer - startSection;
_avatarBoundingBoxRate.increment(numBytesRead);
@ -1730,11 +1782,9 @@ glm::quat AvatarData::getOrientationOutbound() const {
return (getLocalOrientation());
}
void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& identityChanged,
void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identityChanged,
bool& displayNameChanged) {
QDataStream packetStream(identityData);
QUuid avatarSessionID;
// peek the sequence number, this will tell us if we should be processing this identity packet at all
@ -1749,17 +1799,18 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
<< (udt::SequenceNumber::Type) incomingSequenceNumber;
}
if (incomingSequenceNumber > _identitySequenceNumber) {
Identity identity;
Identity identity;
packetStream
>> identity.attachmentData
>> identity.displayName
>> identity.sessionDisplayName
>> identity.isReplicated
>> identity.lookAtSnappingEnabled
packetStream
>> identity.attachmentData
>> identity.displayName
>> identity.sessionDisplayName
>> identity.isReplicated
>> identity.lookAtSnappingEnabled
;
if (incomingSequenceNumber > _identitySequenceNumber) {
// set the store identity sequence number to match the incoming identity
_identitySequenceNumber = incomingSequenceNumber;
@ -2115,10 +2166,6 @@ void AvatarData::sendAvatarDataPacket(bool sendAll) {
}
}
if (_overrideGlobalPosition) {
_overrideGlobalPosition = false;
}
doneEncoding(cullSmallData);
static AvatarDataSequenceNumber sequenceNumber = 0;
@ -2909,3 +2956,21 @@ void AvatarEntityMapFromScriptValue(const QScriptValue& object, AvatarEntityMap&
value[EntityID] = binaryEntityProperties;
}
}
const float AvatarData::DEFAULT_BUBBLE_SCALE = 2.4f; // magic number determined empirically
AABox AvatarData::computeBubbleBox(float bubbleScale) const {
AABox box = AABox(_globalBoundingBoxOffset - _globalBoundingBoxDimensions, _globalBoundingBoxDimensions);
glm::vec3 size = box.getScale();
size *= bubbleScale;
const glm::vec3 MIN_BUBBLE_SCALE(0.3f, 1.3f, 0.3);
size= glm::max(size, MIN_BUBBLE_SCALE);
box.setScaleStayCentered(size);
return box;
}
AABox AvatarData::getDefaultBubbleBox() const {
AABox bubbleBox(_defaultBubbleBox);
bubbleBox.translate(_globalPosition);
return bubbleBox;
}

View file

@ -296,6 +296,17 @@ namespace AvatarDataPacket {
} PACKED_END;
const size_t FAR_GRAB_JOINTS_SIZE = 84;
static_assert(sizeof(FarGrabJoints) == FAR_GRAB_JOINTS_SIZE, "AvatarDataPacket::FarGrabJoints size doesn't match.");
static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE;
static const size_t FAUX_JOINTS_SIZE = 2 * (sizeof(SixByteQuat) + sizeof(SixByteTrans));
struct SendStatus {
HasFlags itemFlags { 0 };
bool sendUUID { false };
int rotationsSent { 0 }; // ie: index of next unsent joint
int translationsSent { 0 };
operator bool() { return itemFlags == 0; }
};
}
const float MAX_AUDIO_LOUDNESS = 1000.0f; // close enough for mouth animation
@ -327,6 +338,17 @@ const float AVATAR_DISTANCE_LEVEL_5 = 200.0f; // meters
// This is the start location in the Sandbox (xyz: 6270, 211, 6000).
const glm::vec3 START_LOCATION(6270, 211, 6000);
// Avatar Transit Constants
const float AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE = 1.0f;
const float AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE = 30.0f;
const int AVATAR_TRANSIT_FRAME_COUNT = 11;
const float AVATAR_TRANSIT_FRAMES_PER_METER = 0.5f;
const float AVATAR_TRANSIT_ABORT_DISTANCE = 0.1f;
const bool AVATAR_TRANSIT_DISTANCE_BASED = true;
const float AVATAR_TRANSIT_FRAMES_PER_SECOND = 30.0f;
const float AVATAR_PRE_TRANSIT_FRAME_COUNT = 10.0f;
const float AVATAR_POST_TRANSIT_FRAME_COUNT = 27.0f;
enum KeyState {
NO_KEY_DOWN = 0,
INSERT_KEY_DOWN,
@ -452,8 +474,8 @@ public:
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false);
virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector<JointData>& lastSentJointData,
AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition,
QVector<JointData>* sentJointDataOut, AvatarDataRate* outboundDataRateOut = nullptr) const;
AvatarDataPacket::SendStatus& sendStatus, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition,
QVector<JointData>* sentJointDataOut, int maxDataSize = 0, AvatarDataRate* outboundDataRateOut = nullptr) const;
virtual void doneEncoding(bool cullSmallChanges);
@ -960,7 +982,7 @@ public:
// identityChanged returns true if identity has changed, false otherwise.
// identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange.
void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged);
void processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, bool& displayNameChanged);
qint64 packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination,
AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION);
@ -1101,6 +1123,7 @@ public:
glm::vec3 getClientGlobalPosition() const { return _globalPosition; }
AABox getGlobalBoundingBox() const { return AABox(_globalPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions, _globalBoundingBoxDimensions); }
AABox getDefaultBubbleBox() const;
/**jsdoc
* @function MyAvatar.getAvatarEntityData
@ -1193,6 +1216,9 @@ public:
void setReplicaIndex(int replicaIndex) { _replicaIndex = replicaIndex; }
int getReplicaIndex() { return _replicaIndex; }
static const float DEFAULT_BUBBLE_SCALE; /* = 2.4 */
AABox computeBubbleBox(float bubbleScale = DEFAULT_BUBBLE_SCALE) const;
void setIsNewAvatar(bool isNewAvatar) { _isNewAvatar = isNewAvatar; }
bool getIsNewAvatar() { return _isNewAvatar; }
@ -1378,8 +1404,7 @@ protected:
// where Entities are located. This is currently only used by the mixer to decide how often to send
// updates about one avatar to another.
glm::vec3 _globalPosition { 0, 0, 0 };
glm::vec3 _globalPositionOverride { 0, 0, 0 };
bool _overrideGlobalPosition { false };
glm::vec3 _serverPosition { 0, 0, 0 };
quint64 _globalPositionChanged { 0 };
quint64 _avatarBoundingBoxChanged { 0 };
@ -1430,6 +1455,8 @@ protected:
glm::vec3 _globalBoundingBoxDimensions;
glm::vec3 _globalBoundingBoxOffset;
AABox _defaultBubbleBox;
mutable ReadWriteLockable _avatarEntitiesLock;
AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar
AvatarEntityIDs _avatarEntityForRecording; // create new entities id for avatar recording

View file

@ -85,8 +85,9 @@ std::vector<AvatarSharedPointer> AvatarReplicas::takeReplicas(const QUuid& paren
void AvatarReplicas::processAvatarIdentity(const QUuid& parentID, const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged) {
if (_replicasMap.find(parentID) != _replicasMap.end()) {
auto &replicas = _replicasMap[parentID];
QDataStream identityDataStream(identityData);
for (auto avatar : replicas) {
avatar->processAvatarIdentity(identityData, identityChanged, displayNameChanged);
avatar->processAvatarIdentity(identityDataStream, identityChanged, displayNameChanged);
}
}
}
@ -284,39 +285,45 @@ AvatarSharedPointer AvatarHashMap::parseAvatarData(QSharedPointer<ReceivedMessag
}
void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
QDataStream avatarIdentityStream(message->getMessage());
// peek the avatar UUID from the incoming packet
QUuid identityUUID = QUuid::fromRfc4122(message->peek(NUM_BYTES_RFC4122_UUID));
while (!avatarIdentityStream.atEnd()) {
// peek the avatar UUID from the incoming packet
avatarIdentityStream.startTransaction();
QUuid identityUUID;
avatarIdentityStream >> identityUUID;
avatarIdentityStream.rollbackTransaction();
if (identityUUID.isNull()) {
qCDebug(avatars) << "Refusing to process identity packet for null avatar ID";
return;
}
// make sure this isn't for an ignored avatar
auto nodeList = DependencyManager::get<NodeList>();
static auto EMPTY = QUuid();
{
QReadLocker locker(&_hashLock);
auto me = _avatarHash.find(EMPTY);
if ((me != _avatarHash.end()) && (identityUUID == me.value()->getSessionUUID())) {
// We add MyAvatar to _avatarHash with an empty UUID. Code relies on this. In order to correctly handle an
// identity packet for ourself (such as when we are assigned a sessionDisplayName by the mixer upon joining),
// we make things match here.
identityUUID = EMPTY;
if (identityUUID.isNull()) {
qCDebug(avatars) << "Refusing to process identity packet for null avatar ID";
return;
}
// make sure this isn't for an ignored avatar
auto nodeList = DependencyManager::get<NodeList>();
static auto EMPTY = QUuid();
{
QReadLocker locker(&_hashLock);
auto me = _avatarHash.find(EMPTY);
if ((me != _avatarHash.end()) && (identityUUID == me.value()->getSessionUUID())) {
// We add MyAvatar to _avatarHash with an empty UUID. Code relies on this. In order to correctly handle an
// identity packet for ourself (such as when we are assigned a sessionDisplayName by the mixer upon joining),
// we make things match here.
identityUUID = EMPTY;
}
}
if (!nodeList->isIgnoringNode(identityUUID) || nodeList->getRequestsDomainListData()) {
// mesh URL for a UUID, find avatar in our list
bool isNewAvatar;
auto avatar = newOrExistingAvatar(identityUUID, sendingNode, isNewAvatar);
bool identityChanged = false;
bool displayNameChanged = false;
// In this case, the "sendingNode" is the Avatar Mixer.
avatar->processAvatarIdentity(avatarIdentityStream, identityChanged, displayNameChanged);
_replicas.processAvatarIdentity(identityUUID, message->getMessage(), identityChanged, displayNameChanged);
}
}
if (!nodeList->isIgnoringNode(identityUUID) || nodeList->getRequestsDomainListData()) {
// mesh URL for a UUID, find avatar in our list
bool isNewAvatar;
auto avatar = newOrExistingAvatar(identityUUID, sendingNode, isNewAvatar);
bool identityChanged = false;
bool displayNameChanged = false;
// In this case, the "sendingNode" is the Avatar Mixer.
avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged);
_replicas.processAvatarIdentity(identityUUID, message->getMessage(), identityChanged, displayNameChanged);
}
}

View file

@ -31,6 +31,7 @@
#include "filters/RotateFilter.h"
#include "filters/LowVelocityFilter.h"
#include "filters/ExponentialSmoothingFilter.h"
#include "filters/AccelerationLimiterFilter.h"
using namespace controller;
@ -51,6 +52,7 @@ REGISTER_FILTER_CLASS_INSTANCE(PostTransformFilter, "postTransform")
REGISTER_FILTER_CLASS_INSTANCE(RotateFilter, "rotate")
REGISTER_FILTER_CLASS_INSTANCE(LowVelocityFilter, "lowVelocity")
REGISTER_FILTER_CLASS_INSTANCE(ExponentialSmoothingFilter, "exponentialSmoothing")
REGISTER_FILTER_CLASS_INSTANCE(AccelerationLimiterFilter, "accelerationLimiter")
const QString JSON_FILTER_TYPE = QStringLiteral("type");
const QString JSON_FILTER_PARAMS = QStringLiteral("params");

View file

@ -0,0 +1,192 @@
//
// Created by Anthony Thibault 2018/11/09
// Copyright 2018 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 "AccelerationLimiterFilter.h"
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include "../../UserInputMapper.h"
#include "../../Input.h"
#include <DependencyManager.h>
#include <QDebug>
#include <StreamUtils.h>
static const QString JSON_ROTATION_ACCELERATION_LIMIT = QStringLiteral("rotationAccelerationLimit");
static const QString JSON_TRANSLATION_ACCELERATION_LIMIT = QStringLiteral("translationAccelerationLimit");
static const QString JSON_TRANSLATION_SNAP_THRESHOLD = QStringLiteral("translationSnapThreshold");
static const QString JSON_ROTATION_SNAP_THRESHOLD = QStringLiteral("rotationSnapThreshold");
static glm::vec3 angularVelFromDeltaRot(const glm::quat& deltaQ, float dt) {
// Measure the angular velocity of a delta rotation quaternion by using quaternion logarithm.
// The logarithm of a unit quternion returns the axis of rotation with a length of one half the angle of rotation in the imaginary part.
// The real part will be 0. Then we multiply it by 2 / dt. turning it into the angular velocity, (except for the extra w = 0 part).
glm::quat omegaQ((2.0f / dt) * glm::log(deltaQ));
return glm::vec3(omegaQ.x, omegaQ.y, omegaQ.z);
}
static glm::quat deltaRotFromAngularVel(const glm::vec3& omega, float dt) {
// Convert angular velocity into a delta quaternion by using quaternion exponent.
// The exponent of quaternion will return a delta rotation around the axis of the imaginary part, by twice the angle as determined by the length of that imaginary part.
// It is the inverse of the logarithm step in angularVelFromDeltaRot
glm::quat omegaQ(0.0f, omega.x, omega.y, omega.z);
return glm::exp((dt / 2.0f) * omegaQ);
}
static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, const glm::vec3& x2, const glm::vec3& x3,
float dt, float accLimit, float snapThreshold) {
// measure the linear velocities of this step and the previoius step
glm::vec3 v1 = (x3 - x1) / (2.0f * dt);
glm::vec3 v0 = (x2 - x0) / (2.0f * dt);
// compute the acceleration
const glm::vec3 a = (v1 - v0) / dt;
// clamp the acceleration if it is over the limit
float aLen = glm::length(a);
// pick limit based on if we are moving faster then our target
float distToTarget = glm::length(x3 - x2);
if (aLen > accLimit && distToTarget > snapThreshold) {
// Solve for a new `v1`, such that `a` does not exceed `aLimit`
// This combines two steps:
// 1) Computing a limited accelration in the direction of `a`, but with a magnitute of `aLimit`:
// `newA = a * (aLimit / aLen)`
// 2) Computing new `v1`
// `v1 = newA * dt + v0`
// We combine the scalars from step 1 and step 2 into a single term to avoid having to do multiple scalar-vec3 multiplies.
v1 = a * ((accLimit * dt) / aLen) + v0;
// apply limited v1 to compute filtered x3
return v1 * dt + x2;
} else {
// did not exceed limit, no filtering necesary
return x3;
}
}
static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, const glm::quat& q2In, const glm::quat& q3In,
float dt, float accLimit, float snapThreshold) {
// ensure quaternions have the same polarity
glm::quat q0 = q0In;
glm::quat q1 = glm::dot(q0In, q1In) < 0.0f ? -q1In : q1In;
glm::quat q2 = glm::dot(q1In, q2In) < 0.0f ? -q2In : q2In;
glm::quat q3 = glm::dot(q2In, q3In) < 0.0f ? -q3In : q3In;
// measure the angular velocities of this step and the previous step
glm::vec3 w1 = angularVelFromDeltaRot(q3 * glm::inverse(q1), 2.0f * dt);
glm::vec3 w0 = angularVelFromDeltaRot(q2 * glm::inverse(q0), 2.0f * dt);
const glm::vec3 a = (w1 - w0) / dt;
float aLen = glm::length(a);
// clamp the acceleration if it is over the limit
float angleToTarget = glm::angle(q3 * glm::inverse(q2));
if (aLen > accLimit && angleToTarget > snapThreshold) {
// solve for a new w1, such that a does not exceed the accLimit
w1 = a * ((accLimit * dt) / aLen) + w0;
// apply limited w1 to compute filtered q3
return deltaRotFromAngularVel(w1, dt) * q2;
} else {
// did not exceed limit, no filtering necesary
return q3;
}
}
namespace controller {
Pose AccelerationLimiterFilter::apply(Pose value) const {
if (value.isValid()) {
// to perform filtering in sensor space, we need to compute the transformations.
auto userInputMapper = DependencyManager::get<UserInputMapper>();
const InputCalibrationData calibrationData = userInputMapper->getInputCalibrationData();
glm::mat4 sensorToAvatarMat = glm::inverse(calibrationData.avatarMat) * calibrationData.sensorToWorldMat;
glm::mat4 avatarToSensorMat = glm::inverse(calibrationData.sensorToWorldMat) * calibrationData.avatarMat;
// transform pose into sensor space.
Pose sensorValue = value.transform(avatarToSensorMat);
if (_prevValid) {
const float DELTA_TIME = 0.01111111f;
glm::vec3 unfilteredTranslation = sensorValue.translation;
sensorValue.translation = filterTranslation(_prevPos[0], _prevPos[1], _prevPos[2], sensorValue.translation,
DELTA_TIME, _translationAccelerationLimit, _translationSnapThreshold);
glm::quat unfilteredRot = sensorValue.rotation;
sensorValue.rotation = filterRotation(_prevRot[0], _prevRot[1], _prevRot[2], sensorValue.rotation,
DELTA_TIME, _rotationAccelerationLimit, _rotationSnapThreshold);
// remember previous values.
_prevPos[0] = _prevPos[1];
_prevPos[1] = _prevPos[2];
_prevPos[2] = sensorValue.translation;
_prevRot[0] = _prevRot[1];
_prevRot[1] = _prevRot[2];
_prevRot[2] = sensorValue.rotation;
_unfilteredPrevPos[0] = _unfilteredPrevPos[1];
_unfilteredPrevPos[1] = _unfilteredPrevPos[2];
_unfilteredPrevPos[2] = unfilteredTranslation;
_unfilteredPrevRot[0] = _unfilteredPrevRot[1];
_unfilteredPrevRot[1] = _unfilteredPrevRot[2];
_unfilteredPrevRot[2] = unfilteredRot;
// transform back into avatar space
return sensorValue.transform(sensorToAvatarMat);
} else {
// initialize previous values with the current sample.
_prevPos[0] = sensorValue.translation;
_prevPos[1] = sensorValue.translation;
_prevPos[2] = sensorValue.translation;
_prevRot[0] = sensorValue.rotation;
_prevRot[1] = sensorValue.rotation;
_prevRot[2] = sensorValue.rotation;
_unfilteredPrevPos[0] = sensorValue.translation;
_unfilteredPrevPos[1] = sensorValue.translation;
_unfilteredPrevPos[2] = sensorValue.translation;
_unfilteredPrevRot[0] = sensorValue.rotation;
_unfilteredPrevRot[1] = sensorValue.rotation;
_unfilteredPrevRot[2] = sensorValue.rotation;
_prevValid = true;
// no previous value to smooth with, so return value unchanged
return value;
}
} else {
// mark previous poses as invalid.
_prevValid = false;
// return invalid value unchanged
return value;
}
}
bool AccelerationLimiterFilter::parseParameters(const QJsonValue& parameters) {
if (parameters.isObject()) {
auto obj = parameters.toObject();
if (obj.contains(JSON_ROTATION_ACCELERATION_LIMIT) && obj.contains(JSON_TRANSLATION_ACCELERATION_LIMIT) &&
obj.contains(JSON_ROTATION_SNAP_THRESHOLD) && obj.contains(JSON_TRANSLATION_SNAP_THRESHOLD)) {
_rotationAccelerationLimit = (float)obj[JSON_ROTATION_ACCELERATION_LIMIT].toDouble();
_translationAccelerationLimit = (float)obj[JSON_TRANSLATION_ACCELERATION_LIMIT].toDouble();
_rotationSnapThreshold = (float)obj[JSON_ROTATION_SNAP_THRESHOLD].toDouble();
_translationSnapThreshold = (float)obj[JSON_TRANSLATION_SNAP_THRESHOLD].toDouble();
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,41 @@
//
// Created by Anthony Thibault 2018/11/09
// Copyright 2018 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_Controllers_Filters_Acceleration_Limiter_h
#define hifi_Controllers_Filters_Acceleration_Limiter_h
#include "../Filter.h"
namespace controller {
class AccelerationLimiterFilter : public Filter {
REGISTER_FILTER_CLASS(AccelerationLimiterFilter);
public:
AccelerationLimiterFilter() {}
float apply(float value) const override { return value; }
Pose apply(Pose value) const override;
bool parseParameters(const QJsonValue& parameters) override;
private:
float _rotationAccelerationLimit { FLT_MAX };
float _translationAccelerationLimit { FLT_MAX };
float _rotationSnapThreshold { 0.0f };
float _translationSnapThreshold { 0.0f };
mutable glm::vec3 _prevPos[3]; // sensor space
mutable glm::quat _prevRot[3]; // sensor space
mutable glm::vec3 _unfilteredPrevPos[3]; // sensor space
mutable glm::quat _unfilteredPrevRot[3]; // sensor space
mutable bool _prevValid { false };
};
}
#endif

View file

@ -35,7 +35,7 @@ namespace controller {
if (_prevSensorValue.isValid()) {
// exponential smoothing filter
sensorValue.translation = _translationConstant * sensorValue.getTranslation() + (1.0f - _translationConstant) * _prevSensorValue.getTranslation();
sensorValue.rotation = safeMix(sensorValue.getRotation(), _prevSensorValue.getRotation(), _rotationConstant);
sensorValue.rotation = safeMix(sensorValue.getRotation(), _prevSensorValue.getRotation(), (1.0f - _rotationConstant));
// remember previous sensor space value.
_prevSensorValue = sensorValue;

View file

@ -88,6 +88,7 @@ public:
// Move the OpenGL context to the present thread
// Extra code because of the widget 'wrapper' context
_context = context;
_context->doneCurrent();
_context->moveToThread(this);
}
@ -179,7 +180,9 @@ public:
_context->makeCurrent();
{
PROFILE_RANGE(render, "PluginPresent")
gl::globalLock();
currentPlugin->present();
gl::globalRelease(false);
CHECK_GL_ERROR();
}
_context->doneCurrent();

View file

@ -261,6 +261,7 @@ void WebEntityRenderer::doRender(RenderArgs* args) {
DependencyManager::get<GeometryCache>()->bindWebBrowserProgram(batch, fadeRatio < OPAQUE_ALPHA_THRESHOLD);
DependencyManager::get<GeometryCache>()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio), _geometryId);
batch.popProjectionJitter();
batch.setResourceTexture(0, nullptr);
}
bool WebEntityRenderer::hasWebSurface() {

View file

@ -3803,6 +3803,16 @@ bool EntityItemProperties::queryAACubeRelatedPropertyChanged() const {
return parentRelatedPropertyChanged() || dimensionsChanged();
}
bool EntityItemProperties::grabbingRelatedPropertyChanged() const {
const GrabPropertyGroup& grabProperties = getGrab();
return grabProperties.triggerableChanged() || grabProperties.grabbableChanged() ||
grabProperties.grabFollowsControllerChanged() || grabProperties.grabKinematicChanged() ||
grabProperties.equippableChanged() || grabProperties.equippableLeftPositionChanged() ||
grabProperties.equippableRightPositionChanged() || grabProperties.equippableLeftRotationChanged() ||
grabProperties.equippableRightRotationChanged() || grabProperties.equippableIndicatorURLChanged() ||
grabProperties.equippableIndicatorScaleChanged() || grabProperties.equippableIndicatorOffsetChanged();
}
// Checking Certifiable Properties
#define ADD_STRING_PROPERTY(n, N) if (!get##N().isEmpty()) json[#n] = get##N()
#define ADD_ENUM_PROPERTY(n, N) json[#n] = get##N##AsString()

View file

@ -108,6 +108,7 @@ public:
bool getScalesWithParent() const;
bool parentRelatedPropertyChanged() const;
bool queryAACubeRelatedPropertyChanged() const;
bool grabbingRelatedPropertyChanged() const;
AABox getAABox() const;

View file

@ -16,6 +16,9 @@
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrentRun>
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>
#include <shared/QtHelpers.h>
#include <VariantMapToScriptValue.h>
@ -37,6 +40,7 @@
#include "WebEntityItem.h"
#include <EntityScriptClient.h>
#include <Profile.h>
#include "GrabPropertyGroup.h"
const QString GRABBABLE_USER_DATA = "{\"grabbableKey\":{\"grabbable\":true}}";
const QString NOT_GRABBABLE_USER_DATA = "{\"grabbableKey\":{\"grabbable\":false}}";
@ -237,6 +241,235 @@ EntityItemProperties convertPropertiesFromScriptSemantics(const EntityItemProper
}
void synchronizeSpatialKey(const GrabPropertyGroup& grabProperties, QJsonObject& grabbableKey, bool& userDataChanged) {
if (grabProperties.equippableLeftPositionChanged() ||
grabProperties.equippableRightPositionChanged() ||
grabProperties.equippableRightRotationChanged() ||
grabProperties.equippableIndicatorURLChanged() ||
grabProperties.equippableIndicatorScaleChanged() ||
grabProperties.equippableIndicatorOffsetChanged()) {
QJsonObject spatialKey = grabbableKey["spatialKey"].toObject();
if (grabProperties.equippableLeftPositionChanged()) {
if (grabProperties.getEquippableLeftPosition() == INITIAL_LEFT_EQUIPPABLE_POSITION) {
spatialKey.remove("leftRelativePosition");
} else {
spatialKey["leftRelativePosition"] =
QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableLeftPosition()));
}
}
if (grabProperties.equippableRightPositionChanged()) {
if (grabProperties.getEquippableRightPosition() == INITIAL_RIGHT_EQUIPPABLE_POSITION) {
spatialKey.remove("rightRelativePosition");
} else {
spatialKey["rightRelativePosition"] =
QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableRightPosition()));
}
}
if (grabProperties.equippableLeftRotationChanged()) {
spatialKey["relativeRotation"] =
QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableLeftRotation()));
} else if (grabProperties.equippableRightRotationChanged()) {
spatialKey["relativeRotation"] =
QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableRightRotation()));
}
grabbableKey["spatialKey"] = spatialKey;
userDataChanged = true;
}
}
void synchronizeGrabbableKey(const GrabPropertyGroup& grabProperties, QJsonObject& userData, bool& userDataChanged) {
if (grabProperties.triggerableChanged() ||
grabProperties.grabbableChanged() ||
grabProperties.grabFollowsControllerChanged() ||
grabProperties.grabKinematicChanged() ||
grabProperties.equippableChanged() ||
grabProperties.equippableLeftPositionChanged() ||
grabProperties.equippableRightPositionChanged() ||
grabProperties.equippableRightRotationChanged()) {
QJsonObject grabbableKey = userData["grabbableKey"].toObject();
if (grabProperties.triggerableChanged()) {
if (grabProperties.getTriggerable()) {
grabbableKey["triggerable"] = true;
} else {
grabbableKey.remove("triggerable");
}
}
if (grabProperties.grabbableChanged()) {
if (grabProperties.getGrabbable()) {
grabbableKey.remove("grabbable");
} else {
grabbableKey["grabbable"] = false;
}
}
if (grabProperties.grabFollowsControllerChanged()) {
if (grabProperties.getGrabFollowsController()) {
grabbableKey.remove("ignoreIK");
} else {
grabbableKey["ignoreIK"] = false;
}
}
if (grabProperties.grabKinematicChanged()) {
if (grabProperties.getGrabKinematic()) {
grabbableKey.remove("kinematic");
} else {
grabbableKey["kinematic"] = false;
}
}
if (grabProperties.equippableChanged()) {
if (grabProperties.getEquippable()) {
grabbableKey["equippable"] = true;
} else {
grabbableKey.remove("equippable");
}
}
if (grabbableKey.contains("spatialKey")) {
synchronizeSpatialKey(grabProperties, grabbableKey, userDataChanged);
}
userData["grabbableKey"] = grabbableKey;
userDataChanged = true;
}
}
void synchronizeGrabJoints(const GrabPropertyGroup& grabProperties, QJsonObject& joints) {
QJsonArray rightHand = joints["RightHand"].toArray();
QJsonObject rightHandPosition = rightHand.size() > 0 ? rightHand[0].toObject() : QJsonObject();
QJsonObject rightHandRotation = rightHand.size() > 1 ? rightHand[1].toObject() : QJsonObject();
QJsonArray leftHand = joints["LeftHand"].toArray();
QJsonObject leftHandPosition = leftHand.size() > 0 ? leftHand[0].toObject() : QJsonObject();
QJsonObject leftHandRotation = leftHand.size() > 1 ? leftHand[1].toObject() : QJsonObject();
if (grabProperties.equippableLeftPositionChanged()) {
leftHandPosition =
QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableLeftPosition())).toObject();
}
if (grabProperties.equippableRightPositionChanged()) {
rightHandPosition =
QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableRightPosition())).toObject();
}
if (grabProperties.equippableLeftRotationChanged()) {
leftHandRotation =
QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableLeftRotation())).toObject();
}
if (grabProperties.equippableRightRotationChanged()) {
rightHandRotation =
QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableRightRotation())).toObject();
}
rightHand = QJsonArray();
rightHand.append(rightHandPosition);
rightHand.append(rightHandRotation);
joints["RightHand"] = rightHand;
leftHand = QJsonArray();
leftHand.append(leftHandPosition);
leftHand.append(leftHandRotation);
joints["LeftHand"] = leftHand;
}
void synchronizeEquipHotspot(const GrabPropertyGroup& grabProperties, QJsonObject& userData, bool& userDataChanged) {
if (grabProperties.equippableLeftPositionChanged() ||
grabProperties.equippableRightPositionChanged() ||
grabProperties.equippableRightRotationChanged() ||
grabProperties.equippableIndicatorURLChanged() ||
grabProperties.equippableIndicatorScaleChanged() ||
grabProperties.equippableIndicatorOffsetChanged()) {
QJsonArray equipHotspots = userData["equipHotspots"].toArray();
QJsonObject equipHotspot = equipHotspots[0].toObject();
QJsonObject joints = equipHotspot["joints"].toObject();
synchronizeGrabJoints(grabProperties, joints);
if (grabProperties.equippableIndicatorURLChanged()) {
equipHotspot["modelURL"] = grabProperties.getEquippableIndicatorURL();
}
if (grabProperties.equippableIndicatorScaleChanged()) {
QJsonObject scale =
QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableIndicatorScale())).toObject();
equipHotspot["radius"] = scale;
equipHotspot["modelScale"] = scale;
}
if (grabProperties.equippableIndicatorOffsetChanged()) {
equipHotspot["position"] =
QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableIndicatorOffset())).toObject();
}
equipHotspot["joints"] = joints;
equipHotspots = QJsonArray();
equipHotspots.append(equipHotspot);
userData["equipHotspots"] = equipHotspots;
userDataChanged = true;
}
}
void synchronizeWearable(const GrabPropertyGroup& grabProperties, QJsonObject& userData, bool& userDataChanged) {
if (grabProperties.equippableLeftPositionChanged() ||
grabProperties.equippableRightPositionChanged() ||
grabProperties.equippableRightRotationChanged() ||
grabProperties.equippableIndicatorURLChanged() ||
grabProperties.equippableIndicatorScaleChanged() ||
grabProperties.equippableIndicatorOffsetChanged()) {
QJsonObject wearable = userData["wearable"].toObject();
QJsonObject joints = wearable["joints"].toObject();
synchronizeGrabJoints(grabProperties, joints);
wearable["joints"] = joints;
userData["wearable"] = wearable;
userDataChanged = true;
}
}
void synchronizeEditedGrabProperties(EntityItemProperties& properties, const QString& previousUserdata) {
// After sufficient warning to content creators, we should be able to remove this.
if (properties.grabbingRelatedPropertyChanged()) {
// This edit touches a new-style grab property, so make userData agree...
GrabPropertyGroup& grabProperties = properties.getGrab();
bool userDataChanged { false };
// if the edit changed userData, use the updated version coming along with the edit. If not, use
// what was already in the entity.
QByteArray userDataString;
if (properties.userDataChanged()) {
userDataString = properties.getUserData().toUtf8();
} else {
userDataString = previousUserdata.toUtf8();;
}
QJsonObject userData = QJsonDocument::fromJson(userDataString).object();
if (userData.contains("grabbableKey")) {
synchronizeGrabbableKey(grabProperties, userData, userDataChanged);
}
if (userData.contains("equipHotspots")) {
synchronizeEquipHotspot(grabProperties, userData, userDataChanged);
}
if (userData.contains("wearable")) {
synchronizeWearable(grabProperties, userData, userDataChanged);
}
if (userDataChanged) {
properties.setUserData(QJsonDocument(userData).toJson());
}
} else if (properties.userDataChanged()) {
// This edit touches userData (and doesn't touch a new-style grab property). Check for grabbableKey in the
// userdata and make the new-style grab properties agree
convertGrabUserDataToProperties(properties);
}
}
QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties, bool clientOnly) {
PROFILE_RANGE(script_entities, __FUNCTION__);
@ -257,6 +490,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
propertiesWithSimID = convertPropertiesFromScriptSemantics(propertiesWithSimID, scalesWithParent);
propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged());
synchronizeEditedGrabProperties(propertiesWithSimID, QString());
EntityItemID id;
// If we have a local entity tree set, then also update it.
@ -559,6 +793,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
simulationOwner = entity->getSimulationOwner();
});
QString previousUserdata;
if (entity) {
if (properties.hasSimulationRestrictedChanges()) {
if (_bidOnSimulationOwnership) {
@ -597,6 +832,8 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
// make sure the properties has a type, so that the encode can know which properties to include
properties.setType(entity->getType());
previousUserdata = entity->getUserData();
} else if (_bidOnSimulationOwnership) {
// bail when simulation participants don't know about entity
return QUuid();
@ -605,6 +842,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
// How to check for this cheaply?
properties = convertPropertiesFromScriptSemantics(properties, properties.getScalesWithParent());
synchronizeEditedGrabProperties(properties, previousUserdata);
properties.setLastEditedBy(sessionID);
// done reading and modifying properties --> start write

View file

@ -2493,6 +2493,118 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer
return true;
}
void convertGrabUserDataToProperties(EntityItemProperties& properties) {
GrabPropertyGroup& grabProperties = properties.getGrab();
QJsonObject userData = QJsonDocument::fromJson(properties.getUserData().toUtf8()).object();
QJsonValue grabbableKeyValue = userData["grabbableKey"];
if (grabbableKeyValue.isObject()) {
QJsonObject grabbableKey = grabbableKeyValue.toObject();
QJsonValue wantsTrigger = grabbableKey["wantsTrigger"];
if (wantsTrigger.isBool()) {
grabProperties.setTriggerable(wantsTrigger.toBool());
}
QJsonValue triggerable = grabbableKey["triggerable"];
if (triggerable.isBool()) {
grabProperties.setTriggerable(triggerable.toBool());
}
QJsonValue grabbable = grabbableKey["grabbable"];
if (grabbable.isBool()) {
grabProperties.setGrabbable(grabbable.toBool());
}
QJsonValue ignoreIK = grabbableKey["ignoreIK"];
if (ignoreIK.isBool()) {
grabProperties.setGrabFollowsController(ignoreIK.toBool());
}
QJsonValue kinematic = grabbableKey["kinematic"];
if (kinematic.isBool()) {
grabProperties.setGrabKinematic(kinematic.toBool());
}
QJsonValue equippable = grabbableKey["equippable"];
if (equippable.isBool()) {
grabProperties.setEquippable(equippable.toBool());
}
if (grabbableKey["spatialKey"].isObject()) {
QJsonObject spatialKey = grabbableKey["spatialKey"].toObject();
grabProperties.setEquippable(true);
if (spatialKey["leftRelativePosition"].isObject()) {
grabProperties.setEquippableLeftPosition(qMapToVec3(spatialKey["leftRelativePosition"].toVariant()));
}
if (spatialKey["rightRelativePosition"].isObject()) {
grabProperties.setEquippableRightPosition(qMapToVec3(spatialKey["rightRelativePosition"].toVariant()));
}
if (spatialKey["relativeRotation"].isObject()) {
grabProperties.setEquippableLeftRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant()));
grabProperties.setEquippableRightRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant()));
}
}
}
QJsonValue wearableValue = userData["wearable"];
if (wearableValue.isObject()) {
QJsonObject wearable = wearableValue.toObject();
QJsonObject joints = wearable["joints"].toObject();
if (joints["LeftHand"].isArray()) {
QJsonArray leftHand = joints["LeftHand"].toArray();
if (leftHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant()));
grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant()));
}
}
if (joints["RightHand"].isArray()) {
QJsonArray rightHand = joints["RightHand"].toArray();
if (rightHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant()));
grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant()));
}
}
}
QJsonValue equipHotspotsValue = userData["equipHotspots"];
if (equipHotspotsValue.isArray()) {
QJsonArray equipHotspots = equipHotspotsValue.toArray();
if (equipHotspots.size() > 0) {
// just take the first one
QJsonObject firstHotSpot = equipHotspots[0].toObject();
QJsonObject joints = firstHotSpot["joints"].toObject();
if (joints["LeftHand"].isArray()) {
QJsonArray leftHand = joints["LeftHand"].toArray();
if (leftHand.size() == 2) {
grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant()));
grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant()));
}
}
if (joints["RightHand"].isArray()) {
QJsonArray rightHand = joints["RightHand"].toArray();
if (rightHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant()));
grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant()));
}
}
QJsonValue indicatorURL = firstHotSpot["modelURL"];
if (indicatorURL.isString()) {
grabProperties.setEquippableIndicatorURL(indicatorURL.toString());
}
QJsonValue indicatorScale = firstHotSpot["modelScale"];
if (indicatorScale.isDouble()) {
grabProperties.setEquippableIndicatorScale(glm::vec3((float)indicatorScale.toDouble()));
} else if (indicatorScale.isObject()) {
grabProperties.setEquippableIndicatorScale(qMapToVec3(indicatorScale.toVariant()));
}
QJsonValue indicatorOffset = firstHotSpot["position"];
if (indicatorOffset.isObject()) {
grabProperties.setEquippableIndicatorOffset(qMapToVec3(indicatorOffset.toVariant()));
}
}
}
}
bool EntityTree::readFromMap(QVariantMap& map) {
// These are needed to deal with older content (before adding inheritance modes)
int contentVersion = map["Version"].toInt();
@ -2639,104 +2751,7 @@ bool EntityTree::readFromMap(QVariantMap& map) {
// convert old grab-related userData to new grab properties
if (contentVersion < (int)EntityVersion::GrabProperties) {
QJsonObject userData = QJsonDocument::fromJson(properties.getUserData().toUtf8()).object();
QJsonObject grabbableKey = userData["grabbableKey"].toObject();
QJsonValue wantsTrigger = grabbableKey["wantsTrigger"];
GrabPropertyGroup& grabProperties = properties.getGrab();
if (wantsTrigger.isBool()) {
grabProperties.setTriggerable(wantsTrigger.toBool());
}
QJsonValue triggerable = grabbableKey["triggerable"];
if (triggerable.isBool()) {
grabProperties.setTriggerable(triggerable.toBool());
}
QJsonValue grabbable = grabbableKey["grabbable"];
if (grabbable.isBool()) {
grabProperties.setGrabbable(grabbable.toBool());
}
QJsonValue ignoreIK = grabbableKey["ignoreIK"];
if (ignoreIK.isBool()) {
grabProperties.setGrabFollowsController(ignoreIK.toBool());
}
QJsonValue kinematic = grabbableKey["kinematic"];
if (kinematic.isBool()) {
grabProperties.setGrabKinematic(kinematic.toBool());
}
if (grabbableKey["spatialKey"].isObject()) {
QJsonObject spatialKey = grabbableKey["spatialKey"].toObject();
grabProperties.setEquippable(true);
if (spatialKey["leftRelativePosition"].isObject()) {
grabProperties.setEquippableLeftPosition(qMapToVec3(spatialKey["leftRelativePosition"].toVariant()));
}
if (spatialKey["rightRelativePosition"].isObject()) {
grabProperties.setEquippableRightPosition(qMapToVec3(spatialKey["rightRelativePosition"].toVariant()));
}
if (spatialKey["relativeRotation"].isObject()) {
grabProperties.setEquippableLeftRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant()));
grabProperties.setEquippableRightRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant()));
}
}
QJsonObject wearable = userData["wearable"].toObject();
QJsonObject joints = wearable["joints"].toObject();
if (joints["LeftHand"].isArray()) {
QJsonArray leftHand = joints["LeftHand"].toArray();
if (leftHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant()));
grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant()));
}
}
if (joints["RightHand"].isArray()) {
QJsonArray rightHand = joints["RightHand"].toArray();
if (rightHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant()));
grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant()));
}
}
if (userData["equipHotspots"].isArray()) {
QJsonArray equipHotspots = userData["equipHotspots"].toArray();
if (equipHotspots.size() > 0) {
// just take the first one
QJsonObject firstHotSpot = equipHotspots[0].toObject();
QJsonObject joints = firstHotSpot["joints"].toObject();
if (joints["LeftHand"].isArray()) {
QJsonArray leftHand = joints["LeftHand"].toArray();
if (leftHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant()));
grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant()));
}
}
if (joints["RightHand"].isArray()) {
QJsonArray rightHand = joints["RightHand"].toArray();
if (rightHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant()));
grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant()));
}
}
QJsonValue indicatorURL = firstHotSpot["modelURL"];
if (indicatorURL.isString()) {
grabProperties.setEquippableIndicatorURL(indicatorURL.toString());
}
QJsonValue indicatorScale = firstHotSpot["modelScale"];
if (indicatorScale.isDouble()) {
grabProperties.setEquippableIndicatorScale(glm::vec3((float)indicatorScale.toDouble()));
} else if (indicatorScale.isObject()) {
grabProperties.setEquippableIndicatorScale(qMapToVec3(indicatorScale.toVariant()));
}
QJsonValue indicatorOffset = firstHotSpot["position"];
if (indicatorOffset.isObject()) {
grabProperties.setEquippableIndicatorOffset(qMapToVec3(indicatorOffset.toVariant()));
}
}
}
convertGrabUserDataToProperties(properties);
}
// Zero out the spread values that were fixed in version ParticleEntityFix so they behave the same as before

View file

@ -424,4 +424,6 @@ private:
std::map<QString, QString> _namedPaths;
};
void convertGrabUserDataToProperties(EntityItemProperties& properties);
#endif // hifi_EntityTree_h

View file

@ -51,6 +51,9 @@ void GrabPropertyGroup::copyFromScriptValue(const QScriptValue& object, bool& _d
COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableLeftRotation, quat, setEquippableLeftRotation);
COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableRightPosition, vec3, setEquippableRightPosition);
COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableRightRotation, quat, setEquippableRightRotation);
COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableIndicatorURL, QString, setEquippableIndicatorURL);
COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableIndicatorScale, vec3, setEquippableIndicatorScale);
COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableIndicatorOffset, vec3, setEquippableIndicatorOffset);
}
void GrabPropertyGroup::merge(const GrabPropertyGroup& other) {
@ -63,6 +66,9 @@ void GrabPropertyGroup::merge(const GrabPropertyGroup& other) {
COPY_PROPERTY_IF_CHANGED(equippableLeftRotation);
COPY_PROPERTY_IF_CHANGED(equippableRightPosition);
COPY_PROPERTY_IF_CHANGED(equippableRightRotation);
COPY_PROPERTY_IF_CHANGED(equippableIndicatorURL);
COPY_PROPERTY_IF_CHANGED(equippableIndicatorScale);
COPY_PROPERTY_IF_CHANGED(equippableIndicatorOffset);
}
void GrabPropertyGroup::debugDump() const {
@ -77,6 +83,9 @@ void GrabPropertyGroup::debugDump() const {
qCDebug(entities) << " _equippableLeftRotation:" << _equippableLeftRotation;
qCDebug(entities) << " _equippableRightPosition:" << _equippableRightPosition;
qCDebug(entities) << " _equippableRightRotation:" << _equippableRightRotation;
qCDebug(entities) << " _equippableIndicatorURL:" << _equippableIndicatorURL;
qCDebug(entities) << " _equippableIndicatorScale:" << _equippableIndicatorScale;
qCDebug(entities) << " _equippableIndicatorOffset:" << _equippableIndicatorOffset;
}
void GrabPropertyGroup::listChangedProperties(QList<QString>& out) {
@ -107,6 +116,15 @@ void GrabPropertyGroup::listChangedProperties(QList<QString>& out) {
if (equippableRightRotationChanged()) {
out << "grab-equippableRightRotation";
}
if (equippableIndicatorURLChanged()) {
out << "grab-equippableIndicatorURL";
}
if (equippableIndicatorScaleChanged()) {
out << "grab-equippableIndicatorScale";
}
if (equippableIndicatorOffsetChanged()) {
out << "grab-equippableIndicatorOffset";
}
}
bool GrabPropertyGroup::appendToEditPacket(OctreePacketData* packetData,
@ -184,6 +202,9 @@ void GrabPropertyGroup::markAllChanged() {
_equippableLeftRotationChanged = true;
_equippableRightPositionChanged = true;
_equippableRightRotationChanged = true;
_equippableIndicatorURLChanged = true;
_equippableIndicatorScaleChanged = true;
_equippableIndicatorOffsetChanged = true;
}
EntityPropertyFlags GrabPropertyGroup::getChangedProperties() const {
@ -215,6 +236,9 @@ void GrabPropertyGroup::getProperties(EntityItemProperties& properties) const {
COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableLeftRotation, getEquippableLeftRotation);
COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableRightPosition, getEquippableRightPosition);
COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableRightRotation, getEquippableRightRotation);
COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableIndicatorURL, getEquippableIndicatorURL);
COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableIndicatorScale, getEquippableIndicatorScale);
COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableIndicatorOffset, getEquippableIndicatorOffset);
}
bool GrabPropertyGroup::setProperties(const EntityItemProperties& properties) {
@ -231,6 +255,12 @@ bool GrabPropertyGroup::setProperties(const EntityItemProperties& properties) {
setEquippableRightPosition);
SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(Grab, EquippableRightRotation, equippableRightRotation,
setEquippableRightRotation);
SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(Grab, EquippableIndicatorURL, equippableIndicatorURL,
setEquippableIndicatorURL);
SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(Grab, EquippableIndicatorScale, equippableIndicatorScale,
setEquippableIndicatorScale);
SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(Grab, EquippableIndicatorOffset, equippableIndicatorOffset,
setEquippableIndicatorOffset);
return somethingChanged;
}

View file

@ -90,10 +90,10 @@ bool operator==(const Properties& a, const Properties& b) {
return
(a.color == b.color) &&
(a.alpha == b.alpha) &&
(a.radiusStart == b.radiusStart) &&
(a.radius == b.radius) &&
(a.spin == b.spin) &&
(a.rotateWithEntity == b.rotateWithEntity) &&
(a.radiusStart == b.radiusStart) &&
(a.lifespan == b.lifespan) &&
(a.maxParticles == b.maxParticles) &&
(a.emission == b.emission) &&
@ -117,18 +117,7 @@ bool Properties::valid() const {
(alpha.range.start == glm::clamp(alpha.range.start, MINIMUM_ALPHA, MAXIMUM_ALPHA)) &&
(alpha.range.finish == glm::clamp(alpha.range.finish, MINIMUM_ALPHA, MAXIMUM_ALPHA)) &&
(alpha.gradient.spread == glm::clamp(alpha.gradient.spread, MINIMUM_ALPHA, MAXIMUM_ALPHA)) &&
(lifespan == glm::clamp(lifespan, MINIMUM_LIFESPAN, MAXIMUM_LIFESPAN)) &&
(emission.rate == glm::clamp(emission.rate, MINIMUM_EMIT_RATE, MAXIMUM_EMIT_RATE)) &&
(emission.speed.target == glm::clamp(emission.speed.target, MINIMUM_EMIT_SPEED, MAXIMUM_EMIT_SPEED)) &&
(emission.speed.spread == glm::clamp(emission.speed.spread, MINIMUM_EMIT_SPEED, MAXIMUM_EMIT_SPEED)) &&
(emission.dimensions == glm::clamp(emission.dimensions, vec3(MINIMUM_EMIT_DIMENSION), vec3(MAXIMUM_EMIT_DIMENSION))) &&
(radiusStart == glm::clamp(radiusStart, MINIMUM_EMIT_RADIUS_START, MAXIMUM_EMIT_RADIUS_START)) &&
(polar.start == glm::clamp(polar.start, MINIMUM_POLAR, MAXIMUM_POLAR)) &&
(polar.finish == glm::clamp(polar.finish, MINIMUM_POLAR, MAXIMUM_POLAR)) &&
(azimuth.start == glm::clamp(azimuth.start, MINIMUM_AZIMUTH, MAXIMUM_AZIMUTH)) &&
(azimuth.finish == glm::clamp(azimuth.finish, MINIMUM_AZIMUTH, MAXIMUM_AZIMUTH)) &&
(emission.acceleration.target == glm::clamp(emission.acceleration.target, vec3(MINIMUM_EMIT_ACCELERATION), vec3(MAXIMUM_EMIT_ACCELERATION))) &&
(emission.acceleration.spread == glm::clamp(emission.acceleration.spread, vec3(MINIMUM_ACCELERATION_SPREAD), vec3(MAXIMUM_ACCELERATION_SPREAD))) &&
(radius.gradient.target == glm::clamp(radius.gradient.target, MINIMUM_PARTICLE_RADIUS, MAXIMUM_PARTICLE_RADIUS)) &&
(radius.range.start == glm::clamp(radius.range.start, MINIMUM_PARTICLE_RADIUS, MAXIMUM_PARTICLE_RADIUS)) &&
(radius.range.finish == glm::clamp(radius.range.finish, MINIMUM_PARTICLE_RADIUS, MAXIMUM_PARTICLE_RADIUS)) &&
@ -136,7 +125,19 @@ bool Properties::valid() const {
(spin.gradient.target == glm::clamp(spin.gradient.target, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN)) &&
(spin.range.start == glm::clamp(spin.range.start, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN)) &&
(spin.range.finish == glm::clamp(spin.range.finish, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN)) &&
(spin.gradient.spread == glm::clamp(spin.gradient.spread, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN));
(spin.gradient.spread == glm::clamp(spin.gradient.spread, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN)) &&
(lifespan == glm::clamp(lifespan, MINIMUM_LIFESPAN, MAXIMUM_LIFESPAN)) &&
(maxParticles == glm::clamp(maxParticles, MINIMUM_MAX_PARTICLES, MAXIMUM_MAX_PARTICLES)) &&
(emission.rate == glm::clamp(emission.rate, MINIMUM_EMIT_RATE, MAXIMUM_EMIT_RATE)) &&
(emission.speed.target == glm::clamp(emission.speed.target, MINIMUM_EMIT_SPEED, MAXIMUM_EMIT_SPEED)) &&
(emission.speed.spread == glm::clamp(emission.speed.spread, MINIMUM_EMIT_SPEED, MAXIMUM_EMIT_SPEED)) &&
(emission.acceleration.target == glm::clamp(emission.acceleration.target, vec3(MINIMUM_EMIT_ACCELERATION), vec3(MAXIMUM_EMIT_ACCELERATION))) &&
(emission.acceleration.spread == glm::clamp(emission.acceleration.spread, vec3(MINIMUM_ACCELERATION_SPREAD), vec3(MAXIMUM_ACCELERATION_SPREAD)) &&
(emission.dimensions == glm::clamp(emission.dimensions, vec3(MINIMUM_EMIT_DIMENSION), vec3(MAXIMUM_EMIT_DIMENSION))) &&
(polar.start == glm::clamp(polar.start, MINIMUM_POLAR, MAXIMUM_POLAR)) &&
(polar.finish == glm::clamp(polar.finish, MINIMUM_POLAR, MAXIMUM_POLAR)) &&
(azimuth.start == glm::clamp(azimuth.start, MINIMUM_AZIMUTH, MAXIMUM_AZIMUTH)) &&
(azimuth.finish == glm::clamp(azimuth.finish, MINIMUM_AZIMUTH, MAXIMUM_AZIMUTH)));
}
bool Properties::emitting() const {

View file

@ -177,9 +177,10 @@ namespace particle {
Properties& operator =(const Properties& other) {
color = other.color;
alpha = other.alpha;
radiusStart = other.radiusStart;
radius = other.radius;
spin = other.spin;
rotateWithEntity = other.rotateWithEntity;
radius = other.radius;
lifespan = other.lifespan;
maxParticles = other.maxParticles;
emission = other.emission;

View file

@ -25,25 +25,24 @@
#include "GLLogging.h"
#include "Config.h"
#include "GLHelpers.h"
#include "QOpenGLContextWrapper.h"
using namespace gl;
bool Context::enableDebugLogger() {
#if defined(Q_OS_MAC)
// OSX does not support GL_KHR_debug or GL_ARB_debug_output
return false;
#else
#if defined(DEBUG) || defined(USE_GLES)
static bool enableDebugLogger = true;
#else
static const QString DEBUG_FLAG("HIFI_DEBUG_OPENGL");
static bool enableDebugLogger = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG);
#endif
static std::once_flag once;
std::call_once(once, [&] {
// If the previous run crashed, force GL debug logging on
if (qApp->property(hifi::properties::CRASHED).toBool()) {
enableDebugLogger = true;
}
});
return enableDebugLogger;
#endif
}
@ -68,8 +67,6 @@ void Context::updateSwapchainMemoryUsage(size_t prevSize, size_t newSize) {
}
Context* Context::PRIMARY = nullptr;
Context::Context() {}
Context::Context(QWindow* window) {
@ -97,9 +94,6 @@ void Context::release() {
_context = nullptr;
#endif
_window = nullptr;
if (PRIMARY == this) {
PRIMARY = nullptr;
}
updateSwapchainMemoryCounter();
}
@ -235,16 +229,9 @@ typedef HGLRC(APIENTRYP PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hDC, HGLRC hShare
GLAPI PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB;
GLAPI PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB;
Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context();
void Context::create() {
if (!PRIMARY) {
PRIMARY = static_cast<Context*>(qApp->property(hifi::properties::gl::PRIMARY_CONTEXT).value<void*>());
}
if (PRIMARY) {
_version = PRIMARY->_version;
}
void Context::create(QOpenGLContext* shareContext) {
assert(0 != _hwnd);
assert(0 == _hdc);
auto hwnd = _hwnd;
@ -338,7 +325,10 @@ void Context::create() {
contextAttribs.push_back(0);
}
contextAttribs.push_back(0);
auto shareHglrc = PRIMARY ? PRIMARY->_hglrc : 0;
if (!shareContext) {
shareContext = qt_gl_global_share_context();
}
HGLRC shareHglrc = (HGLRC)QOpenGLContextWrapper::nativeContext(shareContext);
_hglrc = wglCreateContextAttribsARB(_hdc, shareHglrc, &contextAttribs[0]);
}
@ -346,11 +336,6 @@ void Context::create() {
throw std::runtime_error("Could not create GL context");
}
if (!PRIMARY) {
PRIMARY = this;
qApp->setProperty(hifi::properties::gl::PRIMARY_CONTEXT, QVariant::fromValue((void*)PRIMARY));
}
if (!makeCurrent()) {
throw std::runtime_error("Could not make context current");
}
@ -363,12 +348,11 @@ void Context::create() {
#endif
OffscreenContext::~OffscreenContext() {
_window->deleteLater();
}
void OffscreenContext::create() {
void OffscreenContext::create(QOpenGLContext* shareContext) {
if (!_window) {
_window = new QWindow();
_window->setFlags(Qt::MSWindowsOwnDC);
@ -379,5 +363,5 @@ void OffscreenContext::create() {
qCDebug(glLogging) << "New Offscreen GLContext, window size = " << windowSize.width() << " , " << windowSize.height();
QGuiApplication::processEvents();
}
Parent::create();
Parent::create(shareContext);
}

View file

@ -21,6 +21,7 @@ class QSurface;
class QWindow;
class QOpenGLContext;
class QThread;
class QOpenGLDebugMessage;
#if defined(Q_OS_WIN)
#define GL_CUSTOM_CONTEXT
@ -30,7 +31,6 @@ namespace gl {
class Context {
protected:
QWindow* _window { nullptr };
static Context* PRIMARY;
static void destroyContext(QOpenGLContext* context);
#if defined(GL_CUSTOM_CONTEXT)
uint32_t _version { 0x0401 };
@ -48,6 +48,9 @@ namespace gl {
public:
static bool enableDebugLogger();
static void debugMessageHandler(const QOpenGLDebugMessage &debugMessage);
static void setupDebugLogging(QOpenGLContext* context);
Context();
Context(QWindow* window);
void release();
@ -59,14 +62,14 @@ namespace gl {
static void makeCurrent(QOpenGLContext* context, QSurface* surface);
void swapBuffers();
void doneCurrent();
virtual void create();
virtual void create(QOpenGLContext* shareContext = nullptr);
QOpenGLContext* qglContext();
void moveToThread(QThread* thread);
static size_t evalSurfaceMemoryUsage(uint32_t width, uint32_t height, uint32_t pixelSize);
static size_t getSwapchainMemoryUsage();
static void updateSwapchainMemoryUsage(size_t prevSize, size_t newSize);
private:
static std::atomic<size_t> _totalSwapchainMemoryUsage;
@ -81,7 +84,7 @@ namespace gl {
QWindow* _window { nullptr };
public:
virtual ~OffscreenContext();
void create() override;
void create(QOpenGLContext* shareContext = nullptr) override;
};
}

View file

@ -17,6 +17,8 @@
#include <QtPlatformHeaders/QWGLNativeContext>
#endif
#include <QtGui/QOpenGLDebugMessage>
#include "GLHelpers.h"
using namespace gl;
@ -47,6 +49,32 @@ void Context::moveToThread(QThread* thread) {
qglContext()->moveToThread(thread);
}
void Context::debugMessageHandler(const QOpenGLDebugMessage& debugMessage) {
auto severity = debugMessage.severity();
switch (severity) {
case QOpenGLDebugMessage::NotificationSeverity:
case QOpenGLDebugMessage::LowSeverity:
return;
default:
break;
}
qDebug(glLogging) << debugMessage;
return;
}
void Context::setupDebugLogging(QOpenGLContext *context) {
QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(context);
QObject::connect(logger, &QOpenGLDebugLogger::messageLogged, nullptr, [](const QOpenGLDebugMessage& message){
Context::debugMessageHandler(message);
});
if (logger->initialize()) {
logger->enableMessages();
logger->startLogging(QOpenGLDebugLogger::SynchronousLogging);
} else {
qCWarning(glLogging) << "OpenGL context does not support debugging";
}
}
#ifndef GL_CUSTOM_CONTEXT
bool Context::makeCurrent() {
updateSwapchainMemoryCounter();
@ -65,21 +93,29 @@ void Context::doneCurrent() {
}
}
Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context();
const QSurfaceFormat& getDefaultOpenGLSurfaceFormat();
void Context::create() {
void Context::create(QOpenGLContext* shareContext) {
_context = new QOpenGLContext();
if (PRIMARY) {
_context->setShareContext(PRIMARY->qglContext());
} else {
PRIMARY = this;
_context->setFormat(_window->format());
if (!shareContext) {
shareContext = qt_gl_global_share_context();
}
_context->setFormat(getDefaultOpenGLSurfaceFormat());
_context->create();
_context->setShareContext(shareContext);
_context->create();
_swapchainPixelSize = evalGLFormatSwapchainPixelSize(_context->format());
updateSwapchainMemoryCounter();
if (!makeCurrent()) {
throw std::runtime_error("Could not make context current");
}
if (enableDebugLogger()) {
setupDebugLogging(_context);
}
doneCurrent();
}
#endif

View file

@ -13,6 +13,8 @@
#include <QtGui/QOpenGLContext>
#include <QtGui/QOpenGLDebugLogger>
#include "Context.h"
size_t evalGLFormatSwapchainPixelSize(const QSurfaceFormat& format) {
size_t pixelSize = format.redBufferSize() + format.greenBufferSize() + format.blueBufferSize() + format.alphaBufferSize();
// We don't apply the length of the swap chain into this pixelSize since it is not vsible for the Process (on windows).
@ -34,14 +36,54 @@ bool gl::disableGl45() {
#endif
}
#ifdef Q_OS_MAC
#define SERIALIZE_GL_RENDERING
#endif
#ifdef SERIALIZE_GL_RENDERING
// This terrible terrible hack brought to you by the complete lack of reasonable
// OpenGL debugging tools on OSX. Without this serialization code, the UI textures
// frequently become 'glitchy' and get composited onto the main scene in what looks
// like a partially rendered state.
// This looks very much like either state bleeding across the contexts, or bad
// synchronization for the shared OpenGL textures. However, previous attempts to resolve
// it, even with gratuitous use of glFinish hasn't improved the situation
static std::mutex _globalOpenGLLock;
void gl::globalLock() {
_globalOpenGLLock.lock();
}
void gl::globalRelease(bool finish) {
if (finish) {
glFinish();
}
_globalOpenGLLock.unlock();
}
#else
void gl::globalLock() {}
void gl::globalRelease(bool finish) {}
#endif
void gl::getTargetVersion(int& major, int& minor) {
#if defined(USE_GLES)
major = 3;
minor = 2;
#else
#if defined(Q_OS_MAC)
major = 4;
minor = 1;
#else
major = 4;
minor = disableGl45() ? 1 : 5;
#endif
#endif
}
const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() {
@ -57,6 +99,9 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() {
#else
format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
#endif
if (gl::Context::enableDebugLogger()) {
format.setOption(QSurfaceFormat::DebugContext);
}
// Qt Quick may need a depth and stencil buffer. Always make sure these are available.
format.setDepthBufferSize(DEFAULT_GL_DEPTH_BUFFER_BITS);
format.setStencilBufferSize(DEFAULT_GL_STENCIL_BUFFER_BITS);
@ -64,7 +109,6 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() {
::gl::getTargetVersion(major, minor);
format.setMajorVersion(major);
format.setMinorVersion(minor);
QSurfaceFormat::setDefaultFormat(format);
});
return format;
}

View file

@ -35,6 +35,9 @@ int glVersionToInteger(QString glVersion);
bool isRenderThread();
namespace gl {
void globalLock();
void globalRelease(bool finish = true);
void withSavedContext(const std::function<void()>& f);
bool checkGLError(const char* name);

View file

@ -63,10 +63,10 @@ int GLWidget::getDeviceHeight() const {
return height() * (windowHandle() ? (float)windowHandle()->devicePixelRatio() : 1.0f);
}
void GLWidget::createContext() {
void GLWidget::createContext(QOpenGLContext* shareContext) {
_context = new gl::Context();
_context->setWindow(windowHandle());
_context->create();
_context->create(shareContext);
_context->makeCurrent();
_context->clear();
_context->doneCurrent();

View file

@ -29,7 +29,7 @@ public:
int getDeviceHeight() const;
QSize getDeviceSize() const { return QSize(getDeviceWidth(), getDeviceHeight()); }
QPaintEngine* paintEngine() const override;
void createContext();
void createContext(QOpenGLContext* shareContext = nullptr);
bool makeCurrent();
void doneCurrent();
void swapBuffers();

View file

@ -22,7 +22,7 @@ void GLWindow::createContext(QOpenGLContext* shareContext) {
void GLWindow::createContext(const QSurfaceFormat& format, QOpenGLContext* shareContext) {
_context = new gl::Context();
_context->setWindow(this);
_context->create();
_context->create(shareContext);
_context->makeCurrent();
_context->clear();
}

View file

@ -33,6 +33,7 @@ OffscreenGLCanvas::OffscreenGLCanvas() :
_context(new QOpenGLContext),
_offscreenSurface(new QOffscreenSurface)
{
setFormat(getDefaultOpenGLSurfaceFormat());
}
OffscreenGLCanvas::~OffscreenGLCanvas() {
@ -49,12 +50,15 @@ OffscreenGLCanvas::~OffscreenGLCanvas() {
}
void OffscreenGLCanvas::setFormat(const QSurfaceFormat& format) {
_context->setFormat(format);
}
bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) {
if (nullptr != sharedContext) {
sharedContext->doneCurrent();
_context->setShareContext(sharedContext);
}
_context->setFormat(getDefaultOpenGLSurfaceFormat());
if (!_context->create()) {
qFatal("Failed to create OffscreenGLCanvas context");
}
@ -69,38 +73,16 @@ bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) {
if (!_context->makeCurrent(_offscreenSurface)) {
qFatal("Unable to make offscreen surface current");
}
_context->doneCurrent();
#else
if (!_offscreenSurface->isValid()) {
qFatal("Offscreen surface is invalid");
}
#endif
if (gl::Context::enableDebugLogger()) {
_context->makeCurrent(_offscreenSurface);
QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(this);
connect(logger, &QOpenGLDebugLogger::messageLogged, this, &OffscreenGLCanvas::onMessageLogged);
logger->initialize();
logger->enableMessages();
logger->startLogging(QOpenGLDebugLogger::SynchronousLogging);
_context->doneCurrent();
}
return true;
}
void OffscreenGLCanvas::onMessageLogged(const QOpenGLDebugMessage& debugMessage) {
auto severity = debugMessage.severity();
switch (severity) {
case QOpenGLDebugMessage::NotificationSeverity:
case QOpenGLDebugMessage::LowSeverity:
return;
default:
break;
}
qDebug(glLogging) << debugMessage;
return;
}
bool OffscreenGLCanvas::makeCurrent() {
bool result = _context->makeCurrent(_offscreenSurface);
if (glGetString) {

View file

@ -18,11 +18,13 @@
class QOpenGLContext;
class QOffscreenSurface;
class QOpenGLDebugMessage;
class QSurfaceFormat;
class OffscreenGLCanvas : public QObject {
public:
OffscreenGLCanvas();
~OffscreenGLCanvas();
void setFormat(const QSurfaceFormat& format);
bool create(QOpenGLContext* sharedContext = nullptr);
bool makeCurrent();
void doneCurrent();
@ -35,9 +37,6 @@ public:
void setThreadContext();
static bool restoreThreadContext();
private slots:
void onMessageLogged(const QOpenGLDebugMessage &debugMessage);
protected:
void clearThreadContext();

View file

@ -13,6 +13,10 @@
#include <QOpenGLContext>
#ifdef Q_OS_WIN
#include <QtPlatformHeaders/QWGLNativeContext>
#endif
uint32_t QOpenGLContextWrapper::currentContextVersion() {
QOpenGLContext* context = QOpenGLContext::currentContext();
if (!context) {
@ -45,6 +49,19 @@ void QOpenGLContextWrapper::setFormat(const QSurfaceFormat& format) {
_context->setFormat(format);
}
#ifdef Q_OS_WIN
void* QOpenGLContextWrapper::nativeContext(QOpenGLContext* context) {
HGLRC result = 0;
if (context != nullptr) {
auto nativeHandle = context->nativeHandle();
if (nativeHandle.canConvert<QWGLNativeContext>()) {
result = nativeHandle.value<QWGLNativeContext>().context();
}
}
return result;
}
#endif
bool QOpenGLContextWrapper::create() {
return _context->create();
}

View file

@ -13,6 +13,7 @@
#define hifi_QOpenGLContextWrapper_h
#include <stdint.h>
#include <QtGlobal>
class QOpenGLContext;
class QSurface;
@ -21,6 +22,10 @@ class QThread;
class QOpenGLContextWrapper {
public:
#ifdef Q_OS_WIN
static void* nativeContext(QOpenGLContext* context);
#endif
QOpenGLContextWrapper();
QOpenGLContextWrapper(QOpenGLContext* context);
virtual ~QOpenGLContextWrapper();

View file

@ -708,37 +708,37 @@ void GLBackend::do_glColor4f(const Batch& batch, size_t paramOffset) {
void GLBackend::releaseBuffer(GLuint id, Size size) const {
Lock lock(_trashMutex);
_buffersTrash.push_back({ id, size });
_currentFrameTrash.buffersTrash.push_back({ id, size });
}
void GLBackend::releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const {
Lock lock(_trashMutex);
_externalTexturesTrash.push_back({ id, recycler });
_currentFrameTrash.externalTexturesTrash.push_back({ id, recycler });
}
void GLBackend::releaseTexture(GLuint id, Size size) const {
Lock lock(_trashMutex);
_texturesTrash.push_back({ id, size });
_currentFrameTrash.texturesTrash.push_back({ id, size });
}
void GLBackend::releaseFramebuffer(GLuint id) const {
Lock lock(_trashMutex);
_framebuffersTrash.push_back(id);
_currentFrameTrash.framebuffersTrash.push_back(id);
}
void GLBackend::releaseShader(GLuint id) const {
Lock lock(_trashMutex);
_shadersTrash.push_back(id);
_currentFrameTrash.shadersTrash.push_back(id);
}
void GLBackend::releaseProgram(GLuint id) const {
Lock lock(_trashMutex);
_programsTrash.push_back(id);
_currentFrameTrash.programsTrash.push_back(id);
}
void GLBackend::releaseQuery(GLuint id) const {
Lock lock(_trashMutex);
_queriesTrash.push_back(id);
_currentFrameTrash.queriesTrash.push_back(id);
}
void GLBackend::queueLambda(const std::function<void()> lambda) const {
@ -746,6 +746,81 @@ void GLBackend::queueLambda(const std::function<void()> lambda) const {
_lambdaQueue.push_back(lambda);
}
void GLBackend::FrameTrash::cleanup() {
glWaitSync(fence, 0, GL_TIMEOUT_IGNORED);
glDeleteSync(fence);
{
std::vector<GLuint> ids;
ids.reserve(buffersTrash.size());
for (auto pair : buffersTrash) {
ids.push_back(pair.first);
}
if (!ids.empty()) {
glDeleteBuffers((GLsizei)ids.size(), ids.data());
}
}
{
std::vector<GLuint> ids;
ids.reserve(framebuffersTrash.size());
for (auto id : framebuffersTrash) {
ids.push_back(id);
}
if (!ids.empty()) {
glDeleteFramebuffers((GLsizei)ids.size(), ids.data());
}
}
{
std::vector<GLuint> ids;
ids.reserve(texturesTrash.size());
for (auto pair : texturesTrash) {
ids.push_back(pair.first);
}
if (!ids.empty()) {
glDeleteTextures((GLsizei)ids.size(), ids.data());
}
}
{
if (!externalTexturesTrash.empty()) {
std::vector<GLsync> fences;
fences.resize(externalTexturesTrash.size());
for (size_t i = 0; i < externalTexturesTrash.size(); ++i) {
fences[i] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
// External texture fences will be read in another thread/context, so we need a flush
glFlush();
size_t index = 0;
for (auto pair : externalTexturesTrash) {
auto fence = fences[index++];
pair.second(pair.first, fence);
}
}
}
for (auto id : programsTrash) {
glDeleteProgram(id);
}
for (auto id : shadersTrash) {
glDeleteShader(id);
}
{
std::vector<GLuint> ids;
ids.reserve(queriesTrash.size());
for (auto id : queriesTrash) {
ids.push_back(id);
}
if (!ids.empty()) {
glDeleteQueries((GLsizei)ids.size(), ids.data());
}
}
}
void GLBackend::recycle() const {
PROFILE_RANGE(render_gpu_gl, __FUNCTION__)
{
@ -759,112 +834,16 @@ void GLBackend::recycle() const {
}
}
{
std::vector<GLuint> ids;
std::list<std::pair<GLuint, Size>> buffersTrash;
{
Lock lock(_trashMutex);
std::swap(_buffersTrash, buffersTrash);
}
ids.reserve(buffersTrash.size());
for (auto pair : buffersTrash) {
ids.push_back(pair.first);
}
if (!ids.empty()) {
glDeleteBuffers((GLsizei)ids.size(), ids.data());
}
while (!_previousFrameTrashes.empty()) {
_previousFrameTrashes.front().cleanup();
_previousFrameTrashes.pop_front();
}
_previousFrameTrashes.emplace_back();
{
std::vector<GLuint> ids;
std::list<GLuint> framebuffersTrash;
{
Lock lock(_trashMutex);
std::swap(_framebuffersTrash, framebuffersTrash);
}
ids.reserve(framebuffersTrash.size());
for (auto id : framebuffersTrash) {
ids.push_back(id);
}
if (!ids.empty()) {
glDeleteFramebuffers((GLsizei)ids.size(), ids.data());
}
}
{
std::vector<GLuint> ids;
std::list<std::pair<GLuint, Size>> texturesTrash;
{
Lock lock(_trashMutex);
std::swap(_texturesTrash, texturesTrash);
}
ids.reserve(texturesTrash.size());
for (auto pair : texturesTrash) {
ids.push_back(pair.first);
}
if (!ids.empty()) {
glDeleteTextures((GLsizei)ids.size(), ids.data());
}
}
{
std::list<std::pair<GLuint, Texture::ExternalRecycler>> externalTexturesTrash;
{
Lock lock(_trashMutex);
std::swap(_externalTexturesTrash, externalTexturesTrash);
}
if (!externalTexturesTrash.empty()) {
std::vector<GLsync> fences;
fences.resize(externalTexturesTrash.size());
for (size_t i = 0; i < externalTexturesTrash.size(); ++i) {
fences[i] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
// External texture fences will be read in another thread/context, so we need a flush
glFlush();
size_t index = 0;
for (auto pair : externalTexturesTrash) {
auto fence = fences[index++];
pair.second(pair.first, fence);
}
}
}
{
std::list<GLuint> programsTrash;
{
Lock lock(_trashMutex);
std::swap(_programsTrash, programsTrash);
}
for (auto id : programsTrash) {
glDeleteProgram(id);
}
}
{
std::list<GLuint> shadersTrash;
{
Lock lock(_trashMutex);
std::swap(_shadersTrash, shadersTrash);
}
for (auto id : shadersTrash) {
glDeleteShader(id);
}
}
{
std::vector<GLuint> ids;
std::list<GLuint> queriesTrash;
{
Lock lock(_trashMutex);
std::swap(_queriesTrash, queriesTrash);
}
ids.reserve(queriesTrash.size());
for (auto id : queriesTrash) {
ids.push_back(id);
}
if (!ids.empty()) {
glDeleteQueries((GLsizei)ids.size(), ids.data());
}
Lock lock(_trashMutex);
_previousFrameTrashes.back().swap(_currentFrameTrash);
_previousFrameTrashes.back().fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
_textureManagement._transferEngine->manageMemory();

View file

@ -419,16 +419,34 @@ protected:
static const size_t INVALID_OFFSET = (size_t)-1;
bool _inRenderTransferPass{ false };
int _currentDraw{ -1 };
std::list<std::string> profileRanges;
struct FrameTrash {
GLsync fence = nullptr;
std::list<std::pair<GLuint, Size>> buffersTrash;
std::list<std::pair<GLuint, Size>> texturesTrash;
std::list<std::pair<GLuint, Texture::ExternalRecycler>> externalTexturesTrash;
std::list<GLuint> framebuffersTrash;
std::list<GLuint> shadersTrash;
std::list<GLuint> programsTrash;
std::list<GLuint> queriesTrash;
void swap(FrameTrash& other) {
buffersTrash.swap(other.buffersTrash);
texturesTrash.swap(other.texturesTrash);
externalTexturesTrash.swap(other.externalTexturesTrash);
framebuffersTrash.swap(other.framebuffersTrash);
shadersTrash.swap(other.shadersTrash);
programsTrash.swap(other.programsTrash);
queriesTrash.swap(other.queriesTrash);
}
void cleanup();
};
mutable Mutex _trashMutex;
mutable std::list<std::pair<GLuint, Size>> _buffersTrash;
mutable std::list<std::pair<GLuint, Size>> _texturesTrash;
mutable std::list<std::pair<GLuint, Texture::ExternalRecycler>> _externalTexturesTrash;
mutable std::list<GLuint> _framebuffersTrash;
mutable std::list<GLuint> _shadersTrash;
mutable std::list<GLuint> _programsTrash;
mutable std::list<GLuint> _queriesTrash;
mutable FrameTrash _currentFrameTrash;
mutable std::list<FrameTrash> _previousFrameTrashes;
std::list<std::string> profileRanges;
mutable std::list<std::function<void()>> _lambdaQueue;
void renderPassTransfer(const Batch& batch);

View file

@ -766,14 +766,16 @@ void CharacterController::updateState() {
SET_STATE(State::InAir, "takeoff done");
// compute jumpSpeed based on the scaled jump height for the default avatar in default gravity.
float jumpSpeed = sqrtf(2.0f * DEFAULT_AVATAR_GRAVITY * _scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT);
const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT);
const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight);
velocity += jumpSpeed * _currentUp;
_rigidBody->setLinearVelocity(velocity);
}
break;
case State::InAir: {
const float JUMP_SPEED = _scaleFactor * DEFAULT_AVATAR_JUMP_SPEED;
if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) {
const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT);
const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight);
if ((velocity.dot(_currentUp) <= (jumpSpeed / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) {
SET_STATE(State::Ground, "hit ground");
} else if (_flyingAllowed) {
btVector3 desiredVelocity = _targetVelocity;

View file

@ -12,6 +12,7 @@
#include <gl/Config.h>
#include <gl/QOpenGLContextWrapper.h>
#include <gl/GLHelpers.h>
#include <QtQuick/QQuickWindow>
@ -114,6 +115,7 @@ void RenderEventHandler::onRender() {
PROFILE_RANGE(render_qml_gl, __FUNCTION__);
gl::globalLock();
if (!_shared->preRender()) {
return;
}
@ -139,11 +141,12 @@ void RenderEventHandler::onRender() {
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
// Fence will be used in another thread / context, so a flush is required
glFlush();
_shared->updateTextureAndFence({ texture, fence });
// Fence will be used in another thread / context, so a flush is required
_shared->_quickWindow->resetOpenGLState();
}
gl::globalRelease();
}
void RenderEventHandler::onQuit() {
@ -167,4 +170,5 @@ void RenderEventHandler::onQuit() {
moveToThread(qApp->thread());
QThread::currentThread()->quit();
}
#endif
#endif

View file

@ -51,8 +51,10 @@ uint32_t TextureCache::acquireTexture(const QSize& size) {
if (!textureSet.returnedTextures.empty()) {
auto textureAndFence = textureSet.returnedTextures.front();
textureSet.returnedTextures.pop_front();
glWaitSync((GLsync)textureAndFence.second, 0, GL_TIMEOUT_IGNORED);
glDeleteSync((GLsync)textureAndFence.second);
if (textureAndFence.second) {
glWaitSync((GLsync)textureAndFence.second, 0, GL_TIMEOUT_IGNORED);
glDeleteSync((GLsync)textureAndFence.second);
}
return textureAndFence.first;
}
return createTexture(size);
@ -101,9 +103,11 @@ void TextureCache::destroyTexture(uint32_t texture) {
void TextureCache::destroy(const Value& textureAndFence) {
const auto& fence = textureAndFence.second;
// FIXME prevents crash on shutdown, but we should migrate to a global functions object owned by the shared context.
glWaitSync((GLsync)fence, 0, GL_TIMEOUT_IGNORED);
glDeleteSync((GLsync)fence);
if (fence) {
// FIXME prevents crash on shutdown, but we should migrate to a global functions object owned by the shared context.
glWaitSync((GLsync)fence, 0, GL_TIMEOUT_IGNORED);
glDeleteSync((GLsync)fence);
}
destroyTexture(textureAndFence.first);
}

View file

@ -242,9 +242,13 @@ ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int meshIndex, in
#ifdef Q_OS_MAC
// On mac AMD, we specifically need to have a _meshBlendshapeBuffer bound when using a deformed mesh pipeline
// it cannot be null otherwise we crash in the drawcall using a deformed pipeline with a skinned only (not blendshaped) mesh
if ((_isBlendShaped || _isSkinned)) {
glm::vec4 data;
_meshBlendshapeBuffer = std::make_shared<gpu::Buffer>(sizeof(glm::vec4), reinterpret_cast<const gpu::Byte*>(&data));
if (_isBlendShaped) {
std::vector<BlendshapeOffset> data(_meshNumVertices);
const auto blendShapeBufferSize = _meshNumVertices * sizeof(BlendshapeOffset);
_meshBlendshapeBuffer = std::make_shared<gpu::Buffer>(blendShapeBufferSize, reinterpret_cast<const gpu::Byte*>(data.data()), blendShapeBufferSize);
} else if (_isSkinned) {
BlendshapeOffset data;
_meshBlendshapeBuffer = std::make_shared<gpu::Buffer>(sizeof(BlendshapeOffset), reinterpret_cast<const gpu::Byte*>(&data), sizeof(BlendshapeOffset));
}
#endif

View file

@ -70,9 +70,10 @@ const float DEFAULT_AVATAR_MAX_WALKING_BACKWARD_SPEED = 2.2f; // meters / second
const float DEFAULT_AVATAR_MAX_FLYING_SPEED = 30.0f; // meters / second
const float DEFAULT_AVATAR_WALK_SPEED_THRESHOLD = 0.15f;
const float DEFAULT_AVATAR_GRAVITY = -5.0f; // meters / second^2
const float DEFAULT_AVATAR_JUMP_SPEED = 3.5f; // meters / second
const float DEFAULT_AVATAR_JUMP_HEIGHT = (DEFAULT_AVATAR_JUMP_SPEED * DEFAULT_AVATAR_JUMP_SPEED) / (2.0f * DEFAULT_AVATAR_GRAVITY); // meters
const float DEFAULT_AVATAR_GRAVITY = -5.0f; // meters / second^2 (world)
const float DEFAULT_AVATAR_JUMP_SPEED = 3.5f; // meters / second (sensor)
const float DEFAULT_AVATAR_JUMP_HEIGHT = (DEFAULT_AVATAR_JUMP_SPEED * DEFAULT_AVATAR_JUMP_SPEED) / (2.0f * -DEFAULT_AVATAR_GRAVITY); // meters (sensor)
const float DEFAULT_AVATAR_MIN_JUMP_HEIGHT = 0.25f; // meters (world) // hack
const float DEFAULT_AVATAR_FALL_HEIGHT = 20.0f; // meters
const float DEFAULT_AVATAR_MIN_HOVER_HEIGHT = 2.5f; // meters

View file

@ -68,8 +68,14 @@ namespace PrioritySortUtil {
void reserve(size_t num) {
_vector.reserve(num);
}
const std::vector<T>& getSortedVector() {
std::sort(_vector.begin(), _vector.end(), [](const T& left, const T& right) { return left.getPriority() > right.getPriority(); });
const std::vector<T>& getSortedVector(int numToSort = 0) {
if (numToSort == 0 || numToSort >= (int)_vector.size()) {
std::sort(_vector.begin(), _vector.end(),
[](const T& left, const T& right) { return left.getPriority() > right.getPriority(); });
} else {
std::partial_sort(_vector.begin(), _vector.begin() + numToSort, _vector.end(),
[](const T& left, const T& right) { return left.getPriority() > right.getPriority(); });
}
return _vector;
}
@ -99,6 +105,9 @@ namespace PrioritySortUtil {
float radius = glm::max(thing.getRadius(), MIN_RADIUS);
// Other item's angle from view centre:
float cosineAngle = glm::dot(offset, view.getDirection()) / distance;
if (cosineAngle > 0.0f) {
cosineAngle = std::sqrt(cosineAngle);
}
float age = float((_usecCurrentTime - thing.getTimestamp()) / USECS_PER_SECOND);
// the "age" term accumulates at the sum of all weights

View file

@ -129,6 +129,28 @@ static glm::mat4 calculateResetMat() {
return glm::mat4();
}
static QString outOfRangeDataStrategyToString(ViveControllerManager::OutOfRangeDataStrategy strategy) {
switch (strategy) {
default:
case ViveControllerManager::OutOfRangeDataStrategy::None:
return "None";
case ViveControllerManager::OutOfRangeDataStrategy::Freeze:
return "Freeze";
case ViveControllerManager::OutOfRangeDataStrategy::Drop:
return "Drop";
}
}
static ViveControllerManager::OutOfRangeDataStrategy stringToOutOfRangeDataStrategy(const QString& string) {
if (string == "Drop") {
return ViveControllerManager::OutOfRangeDataStrategy::Drop;
} else if (string == "Freeze") {
return ViveControllerManager::OutOfRangeDataStrategy::Freeze;
} else {
return ViveControllerManager::OutOfRangeDataStrategy::None;
}
}
bool ViveControllerManager::isDesktopMode() {
if (_container) {
return !_container->getActiveDisplayPlugin()->isHmd();
@ -288,8 +310,10 @@ void ViveControllerManager::loadSettings() {
if (_inputDevice) {
const double DEFAULT_ARM_CIRCUMFERENCE = 0.33;
const double DEFAULT_SHOULDER_WIDTH = 0.48;
const QString DEFAULT_OUT_OF_RANGE_STRATEGY = "Drop";
_inputDevice->_armCircumference = settings.value("armCircumference", QVariant(DEFAULT_ARM_CIRCUMFERENCE)).toDouble();
_inputDevice->_shoulderWidth = settings.value("shoulderWidth", QVariant(DEFAULT_SHOULDER_WIDTH)).toDouble();
_inputDevice->_outOfRangeDataStrategy = stringToOutOfRangeDataStrategy(settings.value("outOfRangeDataStrategy", QVariant(DEFAULT_OUT_OF_RANGE_STRATEGY)).toString());
}
}
settings.endGroup();
@ -303,6 +327,7 @@ void ViveControllerManager::saveSettings() const {
if (_inputDevice) {
settings.setValue(QString("armCircumference"), _inputDevice->_armCircumference);
settings.setValue(QString("shoulderWidth"), _inputDevice->_shoulderWidth);
settings.setValue(QString("outOfRangeDataStrategy"), outOfRangeDataStrategyToString(_inputDevice->_outOfRangeDataStrategy));
}
}
settings.endGroup();
@ -446,6 +471,8 @@ void ViveControllerManager::InputDevice::configureCalibrationSettings(const QJso
hmdDesktopTracking = iter.value().toBool();
} else if (iter.key() == "desktopMode") {
hmdDesktopMode = iter.value().toBool();
} else if (iter.key() == "outOfRangeDataStrategy") {
_outOfRangeDataStrategy = stringToOutOfRangeDataStrategy(iter.value().toString());
}
iter++;
}
@ -468,6 +495,7 @@ QJsonObject ViveControllerManager::InputDevice::configurationSettings() {
configurationSettings["puckCount"] = (int)_validTrackedObjects.size();
configurationSettings["armCircumference"] = (double)_armCircumference * M_TO_CM;
configurationSettings["shoulderWidth"] = (double)_shoulderWidth * M_TO_CM;
configurationSettings["outOfRangeDataStrategy"] = outOfRangeDataStrategyToString(_outOfRangeDataStrategy);
return configurationSettings;
}
@ -484,6 +512,10 @@ void ViveControllerManager::InputDevice::emitCalibrationStatus() {
emit inputConfiguration->calibrationStatus(status);
}
static controller::Pose buildPose(const glm::mat4& mat, const glm::vec3& linearVelocity, const glm::vec3& angularVelocity) {
return controller::Pose(extractTranslation(mat), glmExtractRotation(mat), linearVelocity, angularVelocity);
}
void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) {
uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex;
printDeviceTrackingResultChange(deviceIndex);
@ -492,35 +524,48 @@ void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceInde
_nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid &&
poseIndex <= controller::TRACKED_OBJECT_15) {
mat4& mat = mat4();
vec3 linearVelocity = vec3();
vec3 angularVelocity = vec3();
// check if the device is tracking out of range, then process the correct pose depending on the result.
if (_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult != vr::TrackingResult_Running_OutOfRange) {
mat = _nextSimPoseData.poses[deviceIndex];
linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex];
angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex];
} else {
mat = _lastSimPoseData.poses[deviceIndex];
linearVelocity = _lastSimPoseData.linearVelocities[deviceIndex];
angularVelocity = _lastSimPoseData.angularVelocities[deviceIndex];
// make sure that we do not overwrite the pose in the _lastSimPose with incorrect data.
_nextSimPoseData.poses[deviceIndex] = _lastSimPoseData.poses[deviceIndex];
_nextSimPoseData.linearVelocities[deviceIndex] = _lastSimPoseData.linearVelocities[deviceIndex];
_nextSimPoseData.angularVelocities[deviceIndex] = _lastSimPoseData.angularVelocities[deviceIndex];
controller::Pose pose;
switch (_outOfRangeDataStrategy) {
case OutOfRangeDataStrategy::Drop:
default:
// Drop - Mark all non Running_OK results as invald
if (_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult == vr::TrackingResult_Running_OK) {
pose = buildPose(_nextSimPoseData.poses[deviceIndex], _nextSimPoseData.linearVelocities[deviceIndex], _nextSimPoseData.angularVelocities[deviceIndex]);
} else {
pose.valid = false;
}
break;
case OutOfRangeDataStrategy::None:
// None - Ignore eTrackingResult all together
pose = buildPose(_nextSimPoseData.poses[deviceIndex], _nextSimPoseData.linearVelocities[deviceIndex], _nextSimPoseData.angularVelocities[deviceIndex]);
break;
case OutOfRangeDataStrategy::Freeze:
// Freeze - Dont invalide non Running_OK poses, instead just return the last good pose.
if (_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult == vr::TrackingResult_Running_OK) {
pose = buildPose(_nextSimPoseData.poses[deviceIndex], _nextSimPoseData.linearVelocities[deviceIndex], _nextSimPoseData.angularVelocities[deviceIndex]);
} else {
pose = buildPose(_lastSimPoseData.poses[deviceIndex], _lastSimPoseData.linearVelocities[deviceIndex], _lastSimPoseData.angularVelocities[deviceIndex]);
// make sure that we do not overwrite the pose in the _lastSimPose with incorrect data.
_nextSimPoseData.poses[deviceIndex] = _lastSimPoseData.poses[deviceIndex];
_nextSimPoseData.linearVelocities[deviceIndex] = _lastSimPoseData.linearVelocities[deviceIndex];
_nextSimPoseData.angularVelocities[deviceIndex] = _lastSimPoseData.angularVelocities[deviceIndex];
}
break;
}
controller::Pose pose(extractTranslation(mat), glmExtractRotation(mat), linearVelocity, angularVelocity);
if (pose.valid) {
// transform into avatar frame
glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat;
_poseStateMap[poseIndex] = pose.transform(controllerToAvatar);
// transform into avatar frame
glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat;
_poseStateMap[poseIndex] = pose.transform(controllerToAvatar);
// but _validTrackedObjects remain in sensor frame
_validTrackedObjects.push_back(std::make_pair(poseIndex, pose));
_trackedControllers++;
// but _validTrackedObjects remain in sensor frame
_validTrackedObjects.push_back(std::make_pair(poseIndex, pose));
_trackedControllers++;
} else {
// insert invalid pose into state map
_poseStateMap[poseIndex] = pose;
}
} else {
controller::Pose invalidPose;
_poseStateMap[poseIndex] = invalidPose;

View file

@ -60,11 +60,18 @@ public:
virtual void saveSettings() const override;
virtual void loadSettings() override;
enum class OutOfRangeDataStrategy {
None,
Freeze,
Drop
};
private:
class InputDevice : public controller::InputDevice {
public:
InputDevice(vr::IVRSystem*& system);
bool isHeadControllerMounted() const { return _overrideHead; }
private:
// Device functions
controller::Input::NamedVector getAvailableInputs() const override;
@ -162,6 +169,7 @@ private:
FilteredStick _filteredLeftStick;
FilteredStick _filteredRightStick;
std::string _headsetName {""};
OutOfRangeDataStrategy _outOfRangeDataStrategy { OutOfRangeDataStrategy::Drop };
std::vector<PuckPosePair> _validTrackedObjects;
std::map<uint32_t, glm::mat4> _pucksPostOffset;

View file

@ -45,7 +45,7 @@ if (Window.interstitialModeEnabled) {
}
// add a menu item for debugging
var MENU_CATEGORY = "Developer";
var MENU_CATEGORY = "Developer > Scripting";
var MENU_ITEM = "Debug defaultScripts.js";
var SETTINGS_KEY = '_debugDefaultScriptsIsChecked';

View file

@ -0,0 +1,222 @@
var LEFT_HAND_INDEX = 0;
var RIGHT_HAND_INDEX = 1;
var LEFT_FOOT_INDEX = 2;
var RIGHT_FOOT_INDEX = 3;
var HIPS_INDEX = 4;
var SPINE2_INDEX = 5;
var mappingJson = {
name: "com.highfidelity.testing.accelerationTest",
channels: [
{
from: "Standard.LeftHand",
to: "Actions.LeftHand",
filters: [
{
type: "accelerationLimiter",
rotationAccelerationLimit: 2000.0,
translationAccelerationLimit: 100.0,
}
]
},
{
from: "Standard.RightHand",
to: "Actions.RightHand",
filters: [
{
type: "accelerationLimiter",
rotationAccelerationLimit: 2000.0,
translationAccelerationLimit: 100.0,
}
]
},
{
from: "Standard.LeftFoot",
to: "Actions.LeftFoot",
filters: [
{
type: "accelerationLimiter",
rotationAccelerationLimit: 2000.0,
translationAccelerationLimit: 100.0,
}
]
},
{
from: "Standard.RightFoot",
to: "Actions.RightFoot",
filters: [
{
type: "accelerationLimiter",
rotationAccelerationLimit: 2000.0,
translationAccelerationLimit: 100.0,
}
]
},
{
from: "Standard.Hips",
to: "Actions.Hips",
filters: [
{
type: "accelerationLimiter",
rotationAccelerationLimit: 2000.0,
translationAccelerationLimit: 100.0,
}
]
},
{
from: "Standard.Spine2",
to: "Actions.Spine2",
filters: [
{
type: "accelerationLimiter",
rotationAccelerationLimit: 2000.0,
translationAccelerationLimit: 100.0,
}
]
}
]
};
//
// tablet app boiler plate
//
var TABLET_BUTTON_NAME = "ACCFILT";
var HTML_URL = "https://s3.amazonaws.com/hifi-public/tony/html/accelerationFilterApp.html?2";
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var tabletButton = tablet.addButton({
text: TABLET_BUTTON_NAME,
icon: "https://s3.amazonaws.com/hifi-public/tony/icons/tpose-i.svg",
activeIcon: "https://s3.amazonaws.com/hifi-public/tony/icons/tpose-a.svg"
});
tabletButton.clicked.connect(function () {
if (shown) {
tablet.gotoHomeScreen();
} else {
tablet.gotoWebScreen(HTML_URL);
}
});
var shown = false;
function onScreenChanged(type, url) {
if (type === "Web" && url === HTML_URL) {
tabletButton.editProperties({isActive: true});
if (!shown) {
// hook up to event bridge
tablet.webEventReceived.connect(onWebEventReceived);
shownChanged(true);
}
shown = true;
} else {
tabletButton.editProperties({isActive: false});
if (shown) {
// disconnect from event bridge
tablet.webEventReceived.disconnect(onWebEventReceived);
shownChanged(false);
}
shown = false;
}
}
function getTranslationAccelerationLimit(i) {
return mappingJson.channels[i].filters[0].translationAccelerationLimit;
}
function setTranslationAccelerationLimit(i, value) {
mappingJson.channels[i].filters[0].translationAccelerationLimit = value;
mappingChanged();
}
function getRotationAccelerationLimit(i) {
return mappingJson.channels[i].filters[0].rotationAccelerationLimit;
}
function setRotationAccelerationLimit(i, value) {
mappingJson.channels[i].filters[0].rotationAccelerationLimit = value; mappingChanged();
}
function onWebEventReceived(msg) {
if (msg.name === "init-complete") {
var values = [
{name: "left-hand-translation-acceleration-limit", val: getTranslationAccelerationLimit(LEFT_HAND_INDEX), checked: false},
{name: "left-hand-rotation-acceleration-limit", val: getRotationAccelerationLimit(LEFT_HAND_INDEX), checked: false},
{name: "right-hand-translation-acceleration-limit", val: getTranslationAccelerationLimit(RIGHT_HAND_INDEX), checked: false},
{name: "right-hand-rotation-acceleration-limit", val: getRotationAccelerationLimit(RIGHT_HAND_INDEX), checked: false},
{name: "left-foot-translation-acceleration-limit", val: getTranslationAccelerationLimit(LEFT_FOOT_INDEX), checked: false},
{name: "left-foot-rotation-acceleration-limit", val: getRotationAccelerationLimit(LEFT_FOOT_INDEX), checked: false},
{name: "right-foot-translation-acceleration-limit", val: getTranslationAccelerationLimit(RIGHT_FOOT_INDEX), checked: false},
{name: "right-foot-rotation-acceleration-limit", val: getRotationAccelerationLimit(RIGHT_FOOT_INDEX), checked: false},
{name: "hips-translation-acceleration-limit", val: getTranslationAccelerationLimit(HIPS_INDEX), checked: false},
{name: "hips-rotation-acceleration-limit", val: getRotationAccelerationLimit(HIPS_INDEX), checked: false},
{name: "spine2-translation-acceleration-limit", val: getTranslationAccelerationLimit(SPINE2_INDEX), checked: false},
{name: "spine2-rotation-acceleration-limit", val: getRotationAccelerationLimit(SPINE2_INDEX), checked: false}
];
tablet.emitScriptEvent(JSON.stringify(values));
} else if (msg.name === "left-hand-translation-acceleration-limit") {
setTranslationAccelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "left-hand-rotation-acceleration-limit") {
setRotationAccelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "right-hand-translation-acceleration-limit") {
setTranslationAccelerationLimit(RIGHT_HAND_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "right-hand-rotation-acceleration-limit") {
setRotationAccelerationLimit(RIGHT_HAND_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "left-foot-translation-acceleration-limit") {
setTranslationAccelerationLimit(LEFT_FOOT_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "left-foot-rotation-acceleration-limit") {
setRotationAccelerationLimit(LEFT_FOOT_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "right-foot-translation-acceleration-limit") {
setTranslationAccelerationLimit(RIGHT_FOOT_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "right-foot-rotation-acceleration-limit") {
setRotationAccelerationLimit(RIGHT_FOOT_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "hips-translation-acceleration-limit") {
setTranslationAccelerationLimit(HIPS_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "hips-rotation-acceleration-limit") {
setRotationAccelerationLimit(HIPS_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "spine2-translation-acceleration-limit") {
setTranslationAccelerationLimit(SPINE2_INDEX, parseInt(msg.val, 10));
} else if (msg.name === "spine2-rotation-acceleration-limit") {
setRotationAccelerationLimit(SPINE2_INDEX, parseInt(msg.val, 10));
}
}
tablet.screenChanged.connect(onScreenChanged);
function shutdownTabletApp() {
tablet.removeButton(tabletButton);
if (shown) {
tablet.webEventReceived.disconnect(onWebEventReceived);
tablet.gotoHomeScreen();
}
tablet.screenChanged.disconnect(onScreenChanged);
}
//
// end tablet app boiler plate
//
var mapping;
function mappingChanged() {
if (mapping) {
mapping.disable();
}
mapping = Controller.parseMapping(JSON.stringify(mappingJson));
mapping.enable();
}
function shownChanged(newShown) {
if (newShown) {
mappingChanged();
} else {
mapping.disable();
}
}
mappingChanged();
Script.scriptEnding.connect(function() {
if (mapping) {
mapping.disable();
}
tablet.removeButton(tabletButton);
});

View file

@ -19,6 +19,23 @@ if (scripts.length >= 2) {
return;
}
var SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME = "Developer"
var SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME = "Suppress messages from default scripts in Debug Window";
var DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS = 'debugWindowSuppressDefaultScripts';
var suppressDefaultScripts = Settings.getValue(DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS, false)
Menu.addMenuItem({
menuName: SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME,
menuItemName: SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME,
isCheckable: true,
isChecked: suppressDefaultScripts
});
Menu.menuItemEvent.connect(function(menuItem) {
if (menuItem === SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME) {
suppressDefaultScripts = Menu.isOptionChecked(SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME);
}
});
// Set up the qml ui
var qml = Script.resolvePath('debugWindow.qml');
@ -61,17 +78,24 @@ window.visibleChanged.connect(function() {
window.closed.connect(function () { Script.stop(); });
function shouldLogMessage(scriptFileName) {
return !suppressDefaultScripts
|| (scriptFileName !== "defaultScripts.js" && scriptFileName != "controllerScripts.js");
}
var getFormattedDate = function() {
var date = new Date();
return date.getMonth() + "/" + date.getDate() + " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds();
};
var sendToLogWindow = function(type, message, scriptFileName) {
var typeFormatted = "";
if (type) {
typeFormatted = type + " - ";
if (shouldLogMessage(scriptFileName)) {
var typeFormatted = "";
if (type) {
typeFormatted = type + " - ";
}
window.sendToQml("[" + getFormattedDate() + "] " + "[" + scriptFileName + "] " + typeFormatted + message);
}
window.sendToQml("[" + getFormattedDate() + "] " + "[" + scriptFileName + "] " + typeFormatted + message);
};
ScriptDiscoveryService.printedMessage.connect(function(message, scriptFileName) {
@ -95,6 +119,10 @@ ScriptDiscoveryService.clearDebugWindow.connect(function() {
});
Script.scriptEnding.connect(function () {
Settings.setValue(DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS,
Menu.isOptionChecked(SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME));
Menu.removeMenuItem(SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME, SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME);
var geometry = JSON.stringify({
x: window.position.x,
y: window.position.y,

View file

@ -0,0 +1,240 @@
var LEFT_HAND_INDEX = 0;
var RIGHT_HAND_INDEX = 1;
var LEFT_FOOT_INDEX = 2;
var RIGHT_FOOT_INDEX = 3;
var HIPS_INDEX = 4;
var SPINE2_INDEX = 5;
var HAND_SMOOTHING_TRANSLATION = 0.3;
var HAND_SMOOTHING_ROTATION = 0.15;
var FOOT_SMOOTHING_TRANSLATION = 0.3;
var FOOT_SMOOTHING_ROTATION = 0.15;
var TORSO_SMOOTHING_TRANSLATION = 0.3;
var TORSO_SMOOTHING_ROTATION = 0.16;
var mappingJson = {
name: "com.highfidelity.testing.exponentialFilterApp",
channels: [
{
from: "Standard.LeftHand",
to: "Actions.LeftHand",
filters: [
{
type: "exponentialSmoothing",
translation: HAND_SMOOTHING_TRANSLATION,
rotation: HAND_SMOOTHING_ROTATION
}
]
},
{
from: "Standard.RightHand",
to: "Actions.RightHand",
filters: [
{
type: "exponentialSmoothing",
translation: HAND_SMOOTHING_TRANSLATION,
rotation: HAND_SMOOTHING_ROTATION
}
]
},
{
from: "Standard.LeftFoot",
to: "Actions.LeftFoot",
filters: [
{
type: "exponentialSmoothing",
translation: FOOT_SMOOTHING_TRANSLATION,
rotation: FOOT_SMOOTHING_ROTATION
}
]
},
{
from: "Standard.RightFoot",
to: "Actions.RightFoot",
filters: [
{
type: "exponentialSmoothing",
translation: FOOT_SMOOTHING_TRANSLATION,
rotation: FOOT_SMOOTHING_ROTATION
}
]
},
{
from: "Standard.Hips",
to: "Actions.Hips",
filters: [
{
type: "exponentialSmoothing",
translation: TORSO_SMOOTHING_TRANSLATION,
rotation: TORSO_SMOOTHING_ROTATION
}
]
},
{
from: "Standard.Spine2",
to: "Actions.Spine2",
filters: [
{
type: "exponentialSmoothing",
translation: TORSO_SMOOTHING_TRANSLATION,
rotation: TORSO_SMOOTHING_ROTATION
}
]
}
]
};
//
// tablet app boiler plate
//
var TABLET_BUTTON_NAME = "EXPFILT";
var HTML_URL = "https://s3.amazonaws.com/hifi-public/tony/html/exponentialFilterApp.html?7";
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var tabletButton = tablet.addButton({
text: TABLET_BUTTON_NAME,
icon: "https://s3.amazonaws.com/hifi-public/tony/icons/tpose-i.svg",
activeIcon: "https://s3.amazonaws.com/hifi-public/tony/icons/tpose-a.svg"
});
tabletButton.clicked.connect(function () {
if (shown) {
tablet.gotoHomeScreen();
} else {
tablet.gotoWebScreen(HTML_URL);
}
});
var shown = false;
function onScreenChanged(type, url) {
if (type === "Web" && url === HTML_URL) {
tabletButton.editProperties({isActive: true});
if (!shown) {
// hook up to event bridge
tablet.webEventReceived.connect(onWebEventReceived);
shownChanged(true);
}
shown = true;
} else {
tabletButton.editProperties({isActive: false});
if (shown) {
// disconnect from event bridge
tablet.webEventReceived.disconnect(onWebEventReceived);
shownChanged(false);
}
shown = false;
}
}
function getTranslation(i) {
return mappingJson.channels[i].filters[0].translation;
}
function setTranslation(i, value) {
mappingJson.channels[i].filters[0].translation = value;
mappingChanged();
}
function getRotation(i) {
return mappingJson.channels[i].filters[0].rotation;
}
function setRotation(i, value) {
mappingJson.channels[i].filters[0].rotation = value; mappingChanged();
}
function onWebEventReceived(msg) {
if (msg.name === "init-complete") {
var values = [
{name: "enable-filtering", val: filterEnabled ? "on" : "off", checked: false},
{name: "left-hand-translation", val: getTranslation(LEFT_HAND_INDEX), checked: false},
{name: "left-hand-rotation", val: getRotation(LEFT_HAND_INDEX), checked: false},
{name: "right-hand-translation", val: getTranslation(RIGHT_HAND_INDEX), checked: false},
{name: "right-hand-rotation", val: getRotation(RIGHT_HAND_INDEX), checked: false},
{name: "left-foot-translation", val: getTranslation(LEFT_FOOT_INDEX), checked: false},
{name: "left-foot-rotation", val: getRotation(LEFT_FOOT_INDEX), checked: false},
{name: "right-foot-translation", val: getTranslation(RIGHT_FOOT_INDEX), checked: false},
{name: "right-foot-rotation", val: getRotation(RIGHT_FOOT_INDEX), checked: false},
{name: "hips-translation", val: getTranslation(HIPS_INDEX), checked: false},
{name: "hips-rotation", val: getRotation(HIPS_INDEX), checked: false},
{name: "spine2-translation", val: getTranslation(SPINE2_INDEX), checked: false},
{name: "spine2-rotation", val: getRotation(SPINE2_INDEX), checked: false}
];
tablet.emitScriptEvent(JSON.stringify(values));
} else if (msg.name === "enable-filtering") {
if (msg.val === "on") {
filterEnabled = true;
} else if (msg.val === "off") {
filterEnabled = false;
}
mappingChanged();
} else if (msg.name === "left-hand-translation") {
setTranslation(LEFT_HAND_INDEX, Number(msg.val));
} else if (msg.name === "left-hand-rotation") {
setRotation(LEFT_HAND_INDEX, Number(msg.val));
} else if (msg.name === "right-hand-translation") {
setTranslation(RIGHT_HAND_INDEX, Number(msg.val));
} else if (msg.name === "right-hand-rotation") {
setRotation(RIGHT_HAND_INDEX, Number(msg.val));
} else if (msg.name === "left-foot-translation") {
setTranslation(LEFT_FOOT_INDEX, Number(msg.val));
} else if (msg.name === "left-foot-rotation") {
setRotation(LEFT_FOOT_INDEX, Number(msg.val));
} else if (msg.name === "right-foot-translation") {
setTranslation(RIGHT_FOOT_INDEX, Number(msg.val));
} else if (msg.name === "right-foot-rotation") {
setRotation(RIGHT_FOOT_INDEX, Number(msg.val));
} else if (msg.name === "hips-translation") {
setTranslation(HIPS_INDEX, Number(msg.val));
} else if (msg.name === "hips-rotation") {
setRotation(HIPS_INDEX, Number(msg.val));
} else if (msg.name === "spine2-translation") {
setTranslation(SPINE2_INDEX, Number(msg.val));
} else if (msg.name === "spine2-rotation") {
setRotation(SPINE2_INDEX, Number(msg.val));
}
}
tablet.screenChanged.connect(onScreenChanged);
function shutdownTabletApp() {
tablet.removeButton(tabletButton);
if (shown) {
tablet.webEventReceived.disconnect(onWebEventReceived);
tablet.gotoHomeScreen();
}
tablet.screenChanged.disconnect(onScreenChanged);
}
//
// end tablet app boiler plate
//
var filterEnabled = true;
var mapping;
function mappingChanged() {
if (mapping) {
mapping.disable();
}
if (filterEnabled) {
mapping = Controller.parseMapping(JSON.stringify(mappingJson));
mapping.enable();
}
}
function shownChanged(newShown) {
if (newShown) {
mappingChanged();
} else {
mapping.disable();
}
}
mappingChanged();
Script.scriptEnding.connect(function() {
if (mapping) {
mapping.disable();
}
tablet.removeButton(tabletButton);
});

View file

@ -21,16 +21,14 @@ function shutdown() {
var BLUE = {x: 0, y: 0, z: 1, w: 1};
function update(dt) {
if (Controller.Hardware.Vive) {
TRACKED_OBJECT_POSES.forEach(function (key) {
var pose = Controller.getPoseValue(Controller.Standard[key]);
if (pose.valid) {
DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE);
} else {
DebugDraw.removeMyAvatarMarker(key);
}
});
}
TRACKED_OBJECT_POSES.forEach(function (key) {
var pose = Controller.getPoseValue(Controller.Standard[key]);
if (pose.valid) {
DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE);
} else {
DebugDraw.removeMyAvatarMarker(key);
}
});
}
init();

View file

@ -0,0 +1,438 @@
//
// Created by Anthony J. Thibault on 2017/06/20
// Modified by Robbie Uvanni to support multiple pucks and easier placement of pucks on entities, on 2017/08/01
// 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
//
// When this script is running, a new app button, named "PUCKTACH", will be added to the toolbar/tablet.
// Click this app to bring up the puck attachment panel.
//
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
/* global Xform */
Script.include("/~/system/libraries/Xform.js");
(function() { // BEGIN LOCAL_SCOPE
var TABLET_BUTTON_NAME = "PUCKATTACH";
var TABLET_APP_URL = "https://s3.amazonaws.com/hifi-public/tony/html/filtered-puck-attach.html?2";
var NUM_TRACKED_OBJECTS = 16;
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var tabletButton = tablet.addButton({
text: TABLET_BUTTON_NAME,
icon: "https://s3.amazonaws.com/hifi-public/tony/icons/puck-i.svg",
activeIcon: "https://s3.amazonaws.com/hifi-public/tony/icons/puck-a.svg"
});
var shown = false;
function onScreenChanged(type, url) {
if (type === "Web" && url === TABLET_APP_URL) {
tabletButton.editProperties({isActive: true});
if (!shown) {
// hook up to event bridge
tablet.webEventReceived.connect(onWebEventReceived);
shownChanged(true);
}
shown = true;
} else {
tabletButton.editProperties({isActive: false});
if (shown) {
// disconnect from event bridge
tablet.webEventReceived.disconnect(onWebEventReceived);
shownChanged(false);
}
shown = false;
}
}
tablet.screenChanged.connect(onScreenChanged);
function pad(num, size) {
var tempString = "000000000" + num;
return tempString.substr(tempString.length - size);
}
function indexToTrackedObjectName(index) {
return "TrackedObject" + pad(index, 2);
}
function getAvailableTrackedObjects() {
var available = [];
var i;
for (i = 0; i < NUM_TRACKED_OBJECTS; i++) {
var key = indexToTrackedObjectName(i);
var pose = Controller.getPoseValue(Controller.Standard[key]);
if (pose && pose.valid) {
available.push(i);
}
}
return available;
}
function sendAvailableTrackedObjects() {
tablet.emitScriptEvent(JSON.stringify({
pucks: getAvailableTrackedObjects(),
selectedPuck: ((lastPuck === undefined) ? -1 : lastPuck.name)
}));
}
function getRelativePosition(origin, rotation, offset) {
var relativeOffset = Vec3.multiplyQbyV(rotation, offset);
var worldPosition = Vec3.sum(origin, relativeOffset);
return worldPosition;
}
function getPropertyForEntity(entityID, propertyName) {
return Entities.getEntityProperties(entityID, [propertyName])[propertyName];
}
function entityExists(entityID) {
return Object.keys(Entities.getEntityProperties(entityID)).length > 0;
}
var VIVE_PUCK_MODEL = "https://s3.amazonaws.com/hifi-public/tony/vive_tracker_puck_y180z180.obj";
var VIVE_PUCK_DIMENSIONS = { x: 0.0945, y: 0.0921, z: 0.0423 }; // 1/1000th scale of model
var VIVE_PUCK_SEARCH_DISTANCE = 1.5; // metres
var VIVE_PUCK_SPAWN_DISTANCE = 0.5; // metres
var VIVE_PUCK_TRACKED_OBJECT_MAX_DISTANCE = 10.0; // metres
var VIVE_PUCK_NAME = "Tracked Puck";
var trackedPucks = { };
var lastPuck;
var DEFAULT_ROTATION_SMOOTHING_CONSTANT = 1.0; // no smoothing
var DEFAULT_TRANSLATION_SMOOTHING_CONSTANT = 1.0; // no smoothing
var DEFAULT_TRANSLATION_ACCELERATION_LIMIT = 1000; // only extreme accelerations are smoothed
var DEFAULT_ROTATION_ACCELERATION_LIMIT = 10000; // only extreme accelerations are smoothed
var DEFAULT_TRANSLATION_SNAP_THRESHOLD = 0; // no snapping
var DEFAULT_ROTATION_SNAP_THRESHOLD = 0; // no snapping
function buildMappingJson() {
var obj = {name: "com.highfidelity.testing.filteredPuckAttach", channels: []};
var i;
for (i = 0; i < NUM_TRACKED_OBJECTS; i++) {
obj.channels.push({
from: "Vive." + indexToTrackedObjectName(i),
to: "Standard." + indexToTrackedObjectName(i),
filters: [
{
type: "accelerationLimiter",
translationAccelerationLimit: DEFAULT_TRANSLATION_ACCELERATION_LIMIT,
rotationAccelerationLimit: DEFAULT_ROTATION_ACCELERATION_LIMIT,
translationSnapThreshold: DEFAULT_TRANSLATION_SNAP_THRESHOLD,
rotationSnapThreshold: DEFAULT_ROTATION_SNAP_THRESHOLD,
},
{
type: "exponentialSmoothing",
translation: DEFAULT_TRANSLATION_SMOOTHING_CONSTANT,
rotation: DEFAULT_ROTATION_SMOOTHING_CONSTANT
}
]
});
}
return obj;
}
var mappingJson = buildMappingJson();
var mapping;
function mappingChanged() {
if (mapping) {
mapping.disable();
}
mapping = Controller.parseMapping(JSON.stringify(mappingJson));
mapping.enable();
}
function shownChanged(newShown) {
if (newShown) {
mappingChanged();
} else {
mapping.disable();
}
}
function setTranslationAccelerationLimit(value) {
var i;
for (i = 0; i < NUM_TRACKED_OBJECTS; i++) {
mappingJson.channels[i].filters[0].translationAccelerationLimit = value;
}
mappingChanged();
}
function setTranslationSnapThreshold(value) {
// convert from mm
var MM_PER_M = 1000;
var meters = value / MM_PER_M;
var i;
for (i = 0; i < NUM_TRACKED_OBJECTS; i++) {
mappingJson.channels[i].filters[0].translationSnapThreshold = meters;
}
mappingChanged();
}
function setRotationAccelerationLimit(value) {
var i;
for (i = 0; i < NUM_TRACKED_OBJECTS; i++) {
mappingJson.channels[i].filters[0].rotationAccelerationLimit = value;
}
mappingChanged();
}
function setRotationSnapThreshold(value) {
// convert from degrees
var PI_IN_DEGREES = 180;
var radians = value * (Math.pi / PI_IN_DEGREES);
var i;
for (i = 0; i < NUM_TRACKED_OBJECTS; i++) {
mappingJson.channels[i].filters[0].translationSnapThreshold = radians;
}
mappingChanged();
}
function setTranslationSmoothingConstant(value) {
var i;
for (i = 0; i < NUM_TRACKED_OBJECTS; i++) {
mappingJson.channels[i].filters[1].translation = value;
}
mappingChanged();
}
function setRotationSmoothingConstant(value) {
var i;
for (i = 0; i < NUM_TRACKED_OBJECTS; i++) {
mappingJson.channels[i].filters[1].rotation = value;
}
mappingChanged();
}
function createPuck(puck) {
// create a puck entity and add it to our list of pucks
var action = indexToTrackedObjectName(puck.puckno);
var pose = Controller.getPoseValue(Controller.Standard[action]);
if (pose && pose.valid) {
var spawnOffset = Vec3.multiply(Vec3.FRONT, VIVE_PUCK_SPAWN_DISTANCE);
var spawnPosition = getRelativePosition(MyAvatar.position, MyAvatar.orientation, spawnOffset);
// should be an overlay
var puckEntityProperties = {
name: "Tracked Puck",
type: "Model",
modelURL: VIVE_PUCK_MODEL,
dimensions: VIVE_PUCK_DIMENSIONS,
position: spawnPosition,
userData: '{ "grabbableKey": { "grabbable": true, "kinematic": false } }'
};
var puckEntityID = Entities.addEntity(puckEntityProperties);
// if we've already created this puck, destroy it
if (trackedPucks.hasOwnProperty(puck.puckno)) {
destroyPuck(puck.puckno);
}
// if we had an unfinalized puck, destroy it
if (lastPuck !== undefined) {
destroyPuck(lastPuck.name);
}
// create our new unfinalized puck
trackedPucks[puck.puckno] = {
puckEntityID: puckEntityID,
trackedEntityID: ""
};
lastPuck = trackedPucks[puck.puckno];
lastPuck.name = Number(puck.puckno);
}
}
function finalizePuck(puckName) {
// find nearest entity and change its parent to the puck
if (!trackedPucks.hasOwnProperty(puckName)) {
print('2');
return;
}
if (lastPuck === undefined) {
print('3');
return;
}
if (lastPuck.name !== Number(puckName)) {
print('1');
return;
}
var puckPosition = getPropertyForEntity(lastPuck.puckEntityID, "position");
var foundEntities = Entities.findEntities(puckPosition, VIVE_PUCK_SEARCH_DISTANCE);
var foundEntity;
var leastDistance = Number.MAX_VALUE;
for (var i = 0; i < foundEntities.length; i++) {
var entity = foundEntities[i];
if (getPropertyForEntity(entity, "name") !== VIVE_PUCK_NAME) {
var entityPosition = getPropertyForEntity(entity, "position");
var d = Vec3.distance(entityPosition, puckPosition);
if (d < leastDistance) {
leastDistance = d;
foundEntity = entity;
}
}
}
if (foundEntity) {
lastPuck.trackedEntityID = foundEntity;
// remember the userdata and collisionless flag for the tracked entity since
// we're about to remove it and make it ungrabbable and collisionless
lastPuck.trackedEntityUserData = getPropertyForEntity(foundEntity, "userData");
lastPuck.trackedEntityCollisionFlag = getPropertyForEntity(foundEntity, "collisionless");
// update properties of the tracked entity
Entities.editEntity(lastPuck.trackedEntityID, {
"parentID": lastPuck.puckEntityID,
"userData": '{ "grabbableKey": { "grabbable": false } }',
"collisionless": 1
});
// remove reference to puck since it is now calibrated and finalized
lastPuck = undefined;
}
}
function updatePucks() {
// for each puck, update its position and orientation
for (var puckName in trackedPucks) {
if (!trackedPucks.hasOwnProperty(puckName)) {
continue;
}
var action = indexToTrackedObjectName(puckName);
var pose = Controller.getPoseValue(Controller.Standard[action]);
if (pose && pose.valid) {
var puck = trackedPucks[puckName];
if (puck.trackedEntityID) {
if (entityExists(puck.trackedEntityID)) {
var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position);
var puckXform = new Xform(pose.rotation, pose.translation);
var finalXform = Xform.mul(avatarXform, puckXform);
var d = Vec3.distance(MyAvatar.position, finalXform.pos);
if (d > VIVE_PUCK_TRACKED_OBJECT_MAX_DISTANCE) {
print('tried to move tracked object too far away: ' + d);
return;
}
Entities.editEntity(puck.puckEntityID, {
position: finalXform.pos,
rotation: finalXform.rot
});
// in case someone grabbed both entities and destroyed the
// child/parent relationship
Entities.editEntity(puck.trackedEntityID, {
parentID: puck.puckEntityID
});
} else {
destroyPuck(puckName);
}
}
}
}
}
function destroyPuck(puckName) {
// unparent entity and delete its parent
if (!trackedPucks.hasOwnProperty(puckName)) {
return;
}
var puck = trackedPucks[puckName];
var puckEntityID = puck.puckEntityID;
var trackedEntityID = puck.trackedEntityID;
// remove the puck as a parent entity and restore the tracked entities
// former userdata and collision flag
Entities.editEntity(trackedEntityID, {
"parentID": "{00000000-0000-0000-0000-000000000000}",
"userData": puck.trackedEntityUserData,
"collisionless": puck.trackedEntityCollisionFlag
});
delete trackedPucks[puckName];
// in some cases, the entity deletion may occur before the parent change
// has been processed, resulting in both the puck and the tracked entity
// to be deleted so we wait 100ms before deleting the puck, assuming
// that the parent change has occured
var DELETE_TIMEOUT = 100; // ms
Script.setTimeout(function() {
// delete the puck
Entities.deleteEntity(puckEntityID);
}, DELETE_TIMEOUT);
}
function destroyPucks() {
// remove all pucks and unparent entities
for (var puckName in trackedPucks) {
if (trackedPucks.hasOwnProperty(puckName)) {
destroyPuck(puckName);
}
}
}
function onWebEventReceived(msg) {
var obj = {};
try {
obj = JSON.parse(msg);
} catch (err) {
return;
}
switch (obj.cmd) {
case "ready":
sendAvailableTrackedObjects();
break;
case "create":
createPuck(obj);
break;
case "finalize":
finalizePuck(obj.puckno);
break;
case "destroy":
destroyPuck(obj.puckno);
break;
case "translation-acceleration-limit":
setTranslationAccelerationLimit(Number(obj.val));
break;
case "translation-snap-threshold":
setTranslationSnapThreshold(Number(obj.val));
break;
case "rotation-acceleration-limit":
setRotationAccelerationLimit(Number(obj.val));
break;
case "rotation-snap-threshold":
setRotationSnapThreshold(Number(obj.val));
break;
case "translation-smoothing-constant":
setTranslationSmoothingConstant(Number(obj.val));
break;
case "rotation-smoothing-constant":
setRotationSmoothingConstant(Number(obj.val));
break;
}
}
Script.update.connect(updatePucks);
Script.scriptEnding.connect(function () {
tablet.removeButton(tabletButton);
destroyPucks();
if (shown) {
tablet.webEventReceived.disconnect(onWebEventReceived);
tablet.gotoHomeScreen();
}
tablet.screenChanged.disconnect(onScreenChanged);
if (mapping) {
mapping.disable();
}
});
tabletButton.clicked.connect(function () {
if (shown) {
tablet.gotoHomeScreen();
} else {
tablet.gotoWebScreen(TABLET_APP_URL);
}
});
}()); // END LOCAL_SCOPE

View file

@ -85,7 +85,7 @@ function entityExists(entityID) {
return Object.keys(Entities.getEntityProperties(entityID)).length > 0;
}
var VIVE_PUCK_MODEL = "http://content.highfidelity.com/seefo/production/puck-attach/vive_tracker_puck.obj";
var VIVE_PUCK_MODEL = "https://s3.amazonaws.com/hifi-public/tony/vive_tracker_puck_y180z180.obj";
var VIVE_PUCK_DIMENSIONS = { x: 0.0945, y: 0.0921, z: 0.0423 }; // 1/1000th scale of model
var VIVE_PUCK_SEARCH_DISTANCE = 1.5; // metres
var VIVE_PUCK_SPAWN_DISTANCE = 0.5; // metres
@ -304,4 +304,4 @@ tabletButton.clicked.connect(function () {
tablet.gotoWebScreen(TABLET_APP_URL);
}
});
}()); // END LOCAL_SCOPE
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,500 @@
{
"shape": {
"tooltip": "The shape of this entity's geometry."
},
"color": {
"tooltip": "The RGB value of this entity."
},
"text": {
"tooltip": "The text to display on the entity."
},
"textColor": {
"tooltip": "The color of the text."
},
"backgroundColor": {
"tooltip": "The color of the background."
},
"lineHeight": {
"tooltip": "The height of each line of text. This determines the size of the text."
},
"faceCamera": {
"tooltip": "If enabled, the entity follows the camera of each user, creating a billboard effect."
},
"flyingAllowed": {
"tooltip": "If enabled, users can fly in the zone."
},
"ghostingAllowed": {
"tooltip": "If enabled, users with avatar collisions turned off will not collide with content in the zone."
},
"filterURL": {
"tooltip": "The URL of a JS file that checks for changes to entity properties within the zone. Runs periodically."
},
"keyLightMode": {
"tooltip": "Configures the key light in the zone. This light is directional."
},
"keyLight.color": {
"tooltip": "The color of the key light."
},
"keyLight.intensity": {
"tooltip": "The intensity of the key light."
},
"keyLight.direction.y": {
"tooltip": "The angle in deg at which light emits. Starts in the entity's -z direction, and rotates around its y axis."
},
"keyLight.direction.x": {
"tooltip": "The angle in deg at which light emits. Starts in the entity's -z direction, and rotates around its x axis."
},
"keyLight.castShadows": {
"tooltip": "If enabled, shadows are cast. The entity or avatar casting the shadow must also have Cast Shadows enabled."
},
"skyboxMode": {
"tooltip": "Configures the skybox in the zone. The skybox is a cube map image."
},
"skybox.color": {
"tooltip": "If the URL is blank, this changes the color of the sky, otherwise it modifies the color of the skybox."
},
"skybox.url": {
"tooltip": "A cube map image that is used to render the sky."
},
"ambientLightMode": {
"tooltip": "Configures the ambient light in the zone. Use this if you want your skybox to reflect light on the content."
},
"ambientLight.ambientIntensity": {
"tooltip": "The intensity of the ambient light."
},
"ambientLight.ambientURL": {
"tooltip": "A cube map image that defines the color of the light coming from each direction."
},
"hazeMode": {
"tooltip": "Configures the haze in the scene."
},
"haze.hazeRange": {
"tooltip": "How far the haze extends out. This is measured in meters."
},
"haze.hazeAltitudeEffect": {
"tooltip": "If enabled, this adjusts the haze intensity as it gets higher."
},
"haze.hazeBaseRef": {
"tooltip": "The base of the altitude range. Measured in entity space."
},
"haze.hazeCeiling": {
"tooltip": "The ceiling of the altitude range. Measured in entity space."
},
"haze.hazeColor": {
"tooltip": "The color of the haze."
},
"haze.hazeBackgroundBlend": {
"tooltip": "How much the skybox shows through the haze. The higher the value, the more it shows through."
},
"haze.hazeEnableGlare": {
"tooltip": "If enabled, a glare is enabled on the skybox, based on the key light."
},
"haze.hazeGlareColor": {
"tooltip": "The color of the glare based on the key light."
},
"haze.hazeGlareAngle": {
"tooltip": "The angular size of the glare and how much it encompasses the skybox, based on the key light."
},
"bloomMode": {
"tooltip": "Configures how much bright areas of the scene glow."
},
"bloom.bloomIntensity": {
"tooltip": "The intensity, or brightness, of the bloom effect."
},
"bloom.bloomThreshold": {
"tooltip": "The cutoff of the bloom. The higher the value, the more only bright areas of the scene will glow."
},
"bloom.bloomSize": {
"tooltip": "The radius of bloom. The higher the value, the larger the bloom."
},
"modelURL": {
"tooltip": "A mesh model from an FBX or OBJ file."
},
"shapeType": {
"tooltip": "The shape of the collision hull used if collisions are enabled. This affects how an entity collides."
},
"compoundShapeURL": {
"tooltip": "The OBJ file to use for the compound shape if Collision Shape is \"compound\"."
},
"animation.url": {
"tooltip": "An animation to play on the model."
},
"animation.running": {
"tooltip": "If enabled, the animation on the model will play automatically."
},
"animation.allowTranslation": {
"tooltip": "If enabled, this allows an entity to move in space during an animation."
},
"animation.loop": {
"tooltip": "If enabled, then the animation will continuously repeat."
},
"animation.hold": {
"tooltip": "If enabled, then rotations and translations of the last frame played are maintained when the animation stops."
},
"animation.currentFrame": {
"tooltip": "The current frame being played in the animation."
},
"animation.firstFrame": {
"tooltip": "The first frame to play in the animation."
},
"animation.lastFrame": {
"tooltip": "The last frame to play in the animation."
},
"animation.fps": {
"tooltip": "The speed of the animation."
},
"textures": {
"tooltip": "A JSON string containing a texture. Use a name from the Original Texture property to override it."
},
"originalTextures": {
"tooltip": "A JSON string containing the original texture used on the model."
},
"image": {
"tooltip": "The URL for the image source.",
"jsPropertyName": "textures"
},
"sourceUrl": {
"tooltip": "The URL for the web page source."
},
"dpi": {
"tooltip": "The resolution to display the page at, in pixels per inch. Use this to resize your web source in the frame."
},
"isEmitting": {
"tooltip": "If enabled, then particles are emitted."
},
"lifespan": {
"tooltip": "How long each particle lives, measured in seconds."
},
"maxParticles": {
"tooltip": "The maximum number of particles to render at one time. Older particles are swapped out for new ones."
},
"particleTextures": {
"tooltip": "The URL of a JPG or PNG image file to display for each particle.",
"jsPropertyName": "textures"
},
"emitRate": {
"tooltip": "The number of particles per second to emit."
},
"emitSpeed": {
"tooltip": "The speed that each particle is emitted at, measured in m/s."
},
"speedSpread": {
"tooltip": "The spread in speeds at which particles are emitted at, resulting in a variety of speeds."
},
"emitDimensions": {
"tooltip": "The outer limit radius in dimensions that the particles can be emitted from."
},
"emitOrientation": {
"tooltip": "The orientation of particle emission relative to the entity's axes."
},
"emitRadiusStart": {
"tooltip": "The inner limit radius in dimensions that the particles start emitting from."
},
"emitterShouldTrail": {
"tooltip": "If enabled, then particles are \"left behind\" as the emitter moves, otherwise they are not."
},
"particleRadius": {
"tooltip": "The size of each particle."
},
"radiusStart": {
"tooltip": ""
},
"radiusFinish": {
"tooltip": ""
},
"radiusSpread": {
"tooltip": "The spread in size that each particle is given, resulting in a variety of sizes."
},
"particleColor": {
"tooltip": "The color of each particle.",
"jsPropertyName": "color"
},
"colorSpread": {
"tooltip": "The spread in color that each particle is given, resulting in a variety of colors."
},
"alpha": {
"tooltip": "The alpha of each particle."
},
"alphaStart": {
"tooltip": ""
},
"alphaFinish": {
"tooltip": ""
},
"alphaSpread": {
"tooltip": "The spread in alpha that each particle is given, resulting in a variety of alphas."
},
"emitAcceleration": {
"tooltip": "The acceleration that is applied to each particle during its lifetime."
},
"accelerationSpread": {
"tooltip": "The spread in accelerations that each particle is given, resulting in a variety of accelerations."
},
"particleSpin": {
"tooltip": "The spin of each particle in the system."
},
"spinStart": {
"tooltip": ""
},
"spinFinish": {
"tooltip": ""
},
"spinSpread": {
"tooltip": "The spread in spin that each particle is given, resulting in a variety of spins."
},
"rotateWithEntity": {
"tooltip": "If enabled, each particle will spin relative to the roation of the entity as a whole."
},
"polarStart": {
"tooltip": "The angle in deg at which particles are emitted. Starts in the entity's -z direction, and rotates around its y axis."
},
"polarFinish": {
"tooltip": ""
},
"azimuthStart": {
"tooltip": "The angle in deg at which particles are emitted. Starts in the entity's -z direction, and rotates around its y axis."
},
"azimuthFinish": {
"tooltip": ""
},
"lightColor": {
"tooltip": "The color of the light emitted.",
"jsPropertyName": "color"
},
"intensity": {
"tooltip": "The brightness of the light."
},
"falloffRadius": {
"tooltip": "The distance from the light's center where the intensity is reduced."
},
"isSpotlight": {
"tooltip": "If enabled, then the light is directional, otherwise the light is a point light which emits light in all directions."
},
"exponent": {
"tooltip": "Affects the softness of the spotlight beam; the higher the value, the softer the beam."
},
"cutoff": {
"tooltip": "Affects the size of the spotlight beam; the higher the value, the larger the beam."
},
"materialURL": {
"tooltip": "The URL to an external JSON file or \"materialData\", \"materialData?<material name> to use Material Data."
},
"materialData": {
"tooltip": "Can be used instead of a JSON file when material set to materialData."
},
"materialNameToReplace": {
"tooltip": "Material name of parent entity to map this material entity on.",
"jsPropertyName": "parentMaterialName"
},
"submeshToReplace": {
"tooltip": "Submesh index of the parent entity to map this material on.",
"jsPropertyName": "parentMaterialName"
},
"selectSubmesh": {
"tooltip": "If enabled, \"Select Submesh\" property will show up, otherwise \"Material Name to Replace\" will be shown.",
"skipJSProperty": true
},
"priority": {
"tooltip": "The priority of the material, where a larger number means higher priority. Original materials = 0."
},
"materialMappingPos": {
"tooltip": "The offset position of the bottom left of the material within the parent's UV space."
},
"materialMappingScale": {
"tooltip": "How many times the material will repeat in each direction within the parent's UV space."
},
"materialMappingRot": {
"tooltip": "How much to rotate the material within the parent's UV-space, in degrees."
},
"id": {
"tooltip": "The unique identifier of this entity."
},
"name": {
"tooltip": "The name of this entity."
},
"description": {
"tooltip": "Use this field to describe the entity."
},
"position": {
"tooltip": "The global position of this entity."
},
"rotation": {
"tooltip": "The rotation of the entity with respect to world coordinates."
},
"dimensions": {
"tooltip": "The global dimensions of this entity."
},
"scale": {
"tooltip": "The global scaling of this entity.",
"skipJSProperty": true
},
"registrationPoint": {
"tooltip": "The point in the entity at which the entity is rotated about."
},
"visible": {
"tooltip": "If enabled, this entity will be visible."
},
"locked": {
"tooltip": "If enabled, this entity will be locked."
},
"collisionless": {
"tooltip": "If enabled, this entity will collide with other entities or avatars."
},
"dynamic": {
"tooltip": "If enabled, this entity has collisions associated with it that can affect its movement."
},
"collidesWithStatic": {
"tooltip": "If enabled, this entity will collide with other non-moving, static entities.",
"jsPropertyName": "collidesWith"
},
"collidesWithDynamic": {
"tooltip": "If enabled, this entity will collide with other dynamic entities.",
"jsPropertyName": "collidesWith"
},
"collidesWithKinematic": {
"tooltip": "If enabled, this entity will collide with other kinematic entities (they have velocity but are not dynamic).",
"jsPropertyName": "collidesWith"
},
"collidesWithOtherAvatar": {
"tooltip": "If enabled, this entity will collide with other user's avatars.",
"jsPropertyName": "collidesWith"
},
"collidesWithMyAvatar": {
"tooltip": "If enabled, this entity will collide with your own avatar.",
"jsPropertyName": "collidesWith"
},
"collisionSoundURL": {
"tooltip": "The URL of a sound to play when the entity collides with something else."
},
"grab.grabbable": {
"tooltip": "If enabled, this entity will allow grabbing input and will be moveable."
},
"grab.triggerable": {
"tooltip": "If enabled, the collider on this entity is used for triggering events."
},
"cloneable": {
"tooltip": "If enabled, this entity can be duplicated."
},
"cloneLifetime": {
"tooltip": "The lifetime for clones of this entity."
},
"cloneLimit": {
"tooltip": "The total number of clones of this entity that can exist in the domain at any given time."
},
"cloneDynamic": {
"tooltip": "If enabled, then clones created from this entity will be dynamic, allowing the clone to collide."
},
"cloneAvatarEntity": {
"tooltip": "If enabled, then clones created from this entity will be created as avatar entities."
},
"grab.grabFollowsController": {
"tooltip": "If enabled, grabbed entities will follow the movements of your hand controller instead of your avatar's hand."
},
"canCastShadow": {
"tooltip": "If enabled, this geometry of this entity casts shadows when a shadow-casting light source shines on it."
},
"parentID": {
"tooltip": "The ID of the entity or avatar that this entity is parented to."
},
"parentJointIndex": {
"tooltip": "If the entity is parented to an avatar, this joint defines where on the avatar the entity is parented."
},
"href": {
"tooltip": "The URL that will be opened when a user clicks on this entity. Useful for web pages and portals."
},
"script": {
"tooltip": "The URL to an external JS file to add behaviors to the client."
},
"serverScripts": {
"tooltip": "The URL to an external JS file to add behaviors to the server."
},
"serverScriptsStatus": {
"tooltip": "The status of the server script, if provided. This shows if it's running or has an error.",
"skipJSProperty": true
},
"hasLifetime": {
"tooltip": "If enabled, the entity will disappear after a certain amount of time specified by Lifetime.",
"jsPropertyName": "lifetime"
},
"lifetime": {
"tooltip": "The time this entity will exist in the environment for."
},
"userData": {
"tooltip": "Used to store extra data about the entity in JSON format."
},
"velocity": {
"tooltip": "The linear velocity vector of the entity. The velocity at which this entity moves forward in space."
},
"damping": {
"tooltip": "The linear damping to slow down the linear velocity of an entity over time."
},
"angularVelocity": {
"tooltip": "The angular velocity of the entity in rad/s with respect to its axes, about its pivot point."
},
"angularDamping": {
"tooltip": "The angular damping to slow down the angular velocity of an entity over time."
},
"restitution": {
"tooltip": "If enabled, the entity can bounce against other objects that also have Bounciness."
},
"friction": {
"tooltip": "The friction applied to slow down an entity when it's moving against another entity."
},
"density": {
"tooltip": "The density of the entity. The higher the density, the harder the entity is to move."
},
"gravity": {
"tooltip": "The acceleration due to gravity that the entity should move with, in world space."
},
"acceleration": {
"tooltip": "A acceleration that the entity should move with, in world space."
},
"alignToGrid": {
"tooltip": "Used to align entities to the grid, or floor of the environment.",
"skipJSProperty": true
},
"createModel": {
"tooltip": "An entity that is based on a custom mesh created from an .OBJ or .FBX.",
"skipJSProperty": true
},
"createShape": {
"tooltip": "An entity that has many different primitive shapes.",
"skipJSProperty": true
},
"createLight": {
"tooltip": "An entity that emits light.",
"skipJSProperty": true
},
"createText": {
"tooltip": "An entity that displays text on a panel.",
"skipJSProperty": true
},
"createImage": {
"tooltip": "An entity that displays an image on a panel.",
"skipJSProperty": true
},
"createWeb": {
"tooltip": "An entity that displays a web page on a panel.",
"skipJSProperty": true
},
"createZone": {
"tooltip": "An entity that can be used for skyboxes, lighting, and can constrain or change avatar behaviors.",
"skipJSProperty": true
},
"createParticle": {
"tooltip": "An entity that emits particles.",
"skipJSProperty": true
},
"createMaterial": {
"tooltip": "An entity that creates a material that can be attached to a Shape or Model.",
"skipJSProperty": true
},
"useAssetServer": {
"tooltip": "A server that hosts content and assets. You can't take items that are hosted here into other domains.",
"skipJSProperty": true
},
"importNewEntity": {
"tooltip": "Import a local or hosted file that can be used across domains.",
"skipJSProperty": true
}
}

View file

@ -167,16 +167,6 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
};
var alreadyWarned = {};
function warnAboutUserData(props) {
if (alreadyWarned[props.id]) {
return;
}
print("Warning -- overriding grab properties with userData for " + props.id + " / " + props.name);
alreadyWarned[props.id] = true;
}
(function() {
var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints";
@ -200,7 +190,6 @@ function warnAboutUserData(props) {
function getWearableData(props) {
if (props.grab.equippable) {
// if equippable is true, we know this was already converted from the old userData style to properties
return {
joints: {
LeftHand: [ props.grab.equippableLeftPosition, props.grab.equippableLeftRotation ],
@ -211,67 +200,7 @@ function warnAboutUserData(props) {
indicatorOffset: props.grab.equippableIndicatorOffset
};
} else {
// check for old userData equippability. The JSON reader will convert userData to properties
// in EntityTree.cpp, but this won't catch things created from scripts or some items in
// the market. Eventually we'll remove this section.
try {
if (!props.userDataParsed) {
props.userDataParsed = JSON.parse(props.userData);
}
var userDataParsed = props.userDataParsed;
// userData: { wearable: { joints: { LeftHand: {...}, RightHand: {...} } } }
if (userDataParsed.wearable && userDataParsed.wearable.joints) {
warnAboutUserData(props);
userDataParsed.wearable.indicatorURL = "";
userDataParsed.wearable.indicatorScale = { x: 1, y: 1, z: 1 };
userDataParsed.wearable.indicatorOffset = { x: 0, y: 0, z: 0 };
return userDataParsed.wearable;
}
// userData: { equipHotspots: { joints: { LeftHand: {...}, RightHand: {...} } } }
// https://highfidelity.atlassian.net/wiki/spaces/HOME/pages/51085337/Authoring+Equippable+Entities
if (userDataParsed.equipHotspots &&
userDataParsed.equipHotspots.length > 0 &&
userDataParsed.equipHotspots[0].joints) {
warnAboutUserData(props);
var hotSpot = userDataParsed.equipHotspots[0];
var indicatorScale = { x: hotSpot.radius, y: hotSpot.radius, z: hotSpot.radius };
if (hotSpot.modelURL && hotSpot.modelURL !== "") {
indicatorScale = hotSpot.modelScale;
}
return {
joints: hotSpot.joints,
indicatorURL: hotSpot.modelURL,
indicatorScale: indicatorScale,
indicatorOffset: hotSpot.position,
};
}
// userData:{grabbableKey:{spatialKey:{leftRelativePosition:{...},rightRelativePosition:{...}}}}
if (userDataParsed.grabbableKey &&
userDataParsed.grabbableKey.spatialKey) {
warnAboutUserData(props);
var joints = {};
joints.LeftHand = [ { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0, w: 1 } ];
joints.RightHand = [ { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0, w: 1 } ];
if (userDataParsed.grabbableKey.spatialKey.leftRelativePosition) {
joints.LeftHand = [userDataParsed.grabbableKey.spatialKey.leftRelativePosition,
userDataParsed.grabbableKey.spatialKey.relativeRotation];
}
if (userDataParsed.grabbableKey.spatialKey.rightRelativePosition) {
joints.RightHand = [userDataParsed.grabbableKey.spatialKey.rightRelativePosition,
userDataParsed.grabbableKey.spatialKey.relativeRotation];
}
return { joints: joints };
}
} catch (err) {
// don't spam logs
}
return null;
return null
}
}

View file

@ -12,7 +12,7 @@
/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager,
Overlays, OverlayWebWindow, UserActivityLogger, Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera,
progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool, OverlaySystemWindow */
progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, OverlaySystemWindow */
(function() { // BEGIN LOCAL_SCOPE
@ -32,7 +32,6 @@ Script.include([
"libraries/gridTool.js",
"libraries/entityList.js",
"libraries/utils.js",
"particle_explorer/particleExplorerTool.js",
"libraries/entityIconOverlayManager.js"
]);
@ -42,6 +41,9 @@ var TITLE_OFFSET = 60;
var CREATE_TOOLS_WIDTH = 490;
var MAX_DEFAULT_ENTITY_LIST_HEIGHT = 942;
var IMAGE_MODEL = "https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx";
var DEFAULT_IMAGE = "https://hifi-content.s3.amazonaws.com/DomainContent/production/no-image.jpg";
var createToolsWindow = new CreateWindow(
Script.resourcesPath() + "qml/hifi/tablet/EditTools.qml",
'Create Tools',
@ -109,28 +111,6 @@ var entityListTool = new EntityListTool(shouldUseEditTabletApp);
selectionManager.addEventListener(function () {
selectionDisplay.updateHandles();
entityIconOverlayManager.updatePositions();
// Update particle explorer
var needToDestroyParticleExplorer = false;
if (selectionManager.selections.length === 1) {
var selectedEntityID = selectionManager.selections[0];
if (selectedEntityID === selectedParticleEntityID) {
return;
}
var type = Entities.getEntityProperties(selectedEntityID, "type").type;
if (type === "ParticleEffect") {
selectParticleEntity(selectedEntityID);
} else {
needToDestroyParticleExplorer = true;
}
} else {
needToDestroyParticleExplorer = true;
}
if (needToDestroyParticleExplorer && selectedParticleEntityID !== null) {
selectedParticleEntityID = null;
particleExplorerTool.destroyWebView();
}
});
var KEY_P = 80; //Key code for letter p used for Parenting hotkey.
@ -294,6 +274,202 @@ function checkEditPermissionsAndUpdate() {
}
}
const DEFAULT_ENTITY_PROPERTIES = {
All: {
description: "",
rotation: { x: 0, y: 0, z: 0, w: 1 },
collidesWith: "static,dynamic,kinematic,otherAvatar",
collisionSoundURL: "",
cloneable: false,
ignoreIK: true,
canCastShadow: true,
href: "",
script: "",
serverScripts:"",
velocity: {
x: 0,
y: 0,
z: 0
},
damping: 0,
angularVelocity: {
x: 0,
y: 0,
z: 0
},
angularDamping: 0,
restitution: 0.5,
friction: 0.5,
density: 1000,
gravity: {
x: 0,
y: 0,
z: 0
},
acceleration: {
x: 0,
y: 0,
z: 0
},
dynamic: false,
},
Shape: {
shape: "Box",
dimensions: { x: 0.2, y: 0.2, z: 0.2 },
color: { red: 0, green: 180, blue: 239 },
},
Text: {
text: "Text",
dimensions: {
x: 0.65,
y: 0.3,
z: 0.01
},
textColor: { red: 255, green: 255, blue: 255 },
backgroundColor: { red: 0, green: 0, blue: 0 },
lineHeight: 0.06,
faceCamera: false,
},
Zone: {
dimensions: {
x: 10,
y: 10,
z: 10
},
flyingAllowed: true,
ghostingAllowed: true,
filter: "",
keyLightMode: "inherit",
keyLightColor: { red: 255, green: 255, blue: 255 },
keyLight: {
intensity: 1.0,
direction: {
x: 0.0,
y: -0.707106769084930, // 45 degrees
z: 0.7071067690849304
},
castShadows: true
},
ambientLightMode: "inherit",
ambientLight: {
ambientIntensity: 0.5,
ambientURL: ""
},
hazeMode: "inherit",
haze: {
hazeRange: 1000,
hazeAltitudeEffect: false,
hazeBaseRef: 0,
hazeColor: {
red: 128,
green: 154,
blue: 179
},
hazeBackgroundBlend: 0,
hazeEnableGlare: false,
hazeGlareColor: {
red: 255,
green: 229,
blue: 179
},
},
bloomMode: "inherit"
},
Model: {
collisionShape: "none",
compoundShapeURL: "",
animation: {
url: "",
running: false,
allowTranslation: false,
loop: true,
hold: false,
currentFrame: 0,
firstFrame: 0,
lastFrame: 100000,
fps: 30.0,
}
},
Image: {
dimensions: {
x: 0.5385,
y: 0.2819,
z: 0.0092
},
shapeType: "box",
collisionless: true,
modelURL: IMAGE_MODEL,
textures: JSON.stringify({ "tex.picture": "" })
},
Web: {
dimensions: {
x: 1.6,
y: 0.9,
z: 0.01
},
sourceUrl: "https://highfidelity.com/",
dpi: 30,
},
ParticleEffect: {
lifespan: 1.5,
maxParticles: 10,
textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png",
emitRate: 5.5,
emitSpeed: 0,
speedSpread: 0,
emitDimensions: { x: 0, y: 0, z: 0 },
emitOrientation: { x: 0, y: 0, z: 0, w: 1 },
emitterShouldTrail: true,
particleRadius: 0.25,
radiusStart: 0,
radiusFinish: 0.1,
radiusSpread: 0,
particleColor: {
red: 255,
green: 255,
blue: 255
},
colorSpread: {
red: 0,
green: 0,
blue: 0
},
alpha: 0,
alphaStart: 1,
alphaFinish: 0,
alphaSpread: 0,
emitAcceleration: {
x: 0,
y: 2.5,
z: 0
},
accelerationSpread: {
x: 0,
y: 0,
z: 0
},
particleSpin: 0,
spinStart: 0,
spinFinish: 0,
spinSpread: 0,
rotateWithEntity: false,
polarStart: 0,
polarFinish: 0,
azimuthStart: -Math.PI,
azimuthFinish: Math.PI
},
Light: {
color: { red: 255, green: 255, blue: 255 },
intensity: 5.0,
dimensions: DEFAULT_LIGHT_DIMENSIONS,
falloffRadius: 1.0,
isSpotlight: false,
exponent: 1.0,
cutoff: 75.0,
dimensions: { x: 20, y: 20, z: 20 },
},
};
var toolBar = (function () {
var EDIT_SETTING = "io.highfidelity.isEditing"; // for communication with other scripts
var that = {},
@ -303,11 +479,29 @@ var toolBar = (function () {
dialogWindow = null,
tablet = null;
function applyProperties(originalProperties, newProperties) {
for (var key in newProperties) {
originalProperties[key] = newProperties[key];
}
}
function createNewEntity(properties) {
var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS;
var position = getPositionToCreateEntity();
var entityID = null;
applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.All);
var type = properties.type;
if (type == "Box" || type == "Sphere") {
applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Shape);
} else if (type == "Image") {
properties.type = "Model";
applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Image);
} else {
applyProperties(properties, DEFAULT_ENTITY_PROPERTIES[type]);
}
if (position !== null && position !== undefined) {
var direction;
if (Camera.mode === "entity" || Camera.mode === "independent") {
@ -363,10 +557,6 @@ var toolBar = (function () {
properties: properties
}], [], true);
if (properties.type === "ParticleEffect") {
selectParticleEntity(entityID);
}
var POST_ADJUST_ENTITY_TYPES = ["Model"];
if (POST_ADJUST_ENTITY_TYPES.indexOf(properties.type) !== -1) {
// Adjust position of entity per bounding box after it has been created and auto-resized.
@ -385,7 +575,7 @@ var toolBar = (function () {
Entities.editEntity(entityID, {
position: position
});
selectionManager._update();
selectionManager._update(false, this);
} else if (dimensionsCheckCount < MAX_DIMENSIONS_CHECKS) {
Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL);
}
@ -397,9 +587,9 @@ var toolBar = (function () {
properties.type + " would be out of bounds.");
}
selectionManager.clearSelections();
selectionManager.clearSelections(this);
entityListTool.sendUpdate();
selectionManager.setSelections([entityID]);
selectionManager.setSelections([entityID], this);
Window.setFocus();
@ -550,7 +740,7 @@ var toolBar = (function () {
}
deletedEntityTimer = Script.setTimeout(function () {
if (entitiesToDelete.length > 0) {
selectionManager.removeEntities(entitiesToDelete);
selectionManager.removeEntities(entitiesToDelete, this);
}
entityListTool.removeEntities(entitiesToDelete, selectionManager.selections);
entitiesToDelete = [];
@ -679,14 +869,12 @@ var toolBar = (function () {
addButton("newLightButton", function () {
createNewEntity({
type: "Light",
dimensions: DEFAULT_LIGHT_DIMENSIONS,
isSpotlight: false,
color: {
red: 150,
green: 150,
blue: 150
},
constantAttenuation: 1,
linearAttenuation: 0,
quadraticAttenuation: 0,
@ -698,116 +886,30 @@ var toolBar = (function () {
addButton("newTextButton", function () {
createNewEntity({
type: "Text",
dimensions: {
x: 0.65,
y: 0.3,
z: 0.01
},
backgroundColor: {
red: 64,
green: 64,
blue: 64
},
textColor: {
red: 255,
green: 255,
blue: 255
},
text: "some text",
lineHeight: 0.06
});
});
addButton("newImageButton", function () {
var IMAGE_MODEL = "https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx";
var DEFAULT_IMAGE = "https://hifi-content.s3.amazonaws.com/DomainContent/production/no-image.jpg";
createNewEntity({
type: "Model",
dimensions: {
x: 0.5385,
y: 0.2819,
z: 0.0092
},
shapeType: "box",
collisionless: true,
modelURL: IMAGE_MODEL,
textures: JSON.stringify({ "tex.picture": DEFAULT_IMAGE })
type: "Image",
});
});
addButton("newWebButton", function () {
createNewEntity({
type: "Web",
dimensions: {
x: 1.6,
y: 0.9,
z: 0.01
},
sourceUrl: "https://highfidelity.com/"
});
});
addButton("newZoneButton", function () {
createNewEntity({
type: "Zone",
dimensions: {
x: 10,
y: 10,
z: 10
}
});
});
addButton("newParticleButton", function () {
createNewEntity({
type: "ParticleEffect",
isEmitting: true,
emitterShouldTrail: true,
color: {
red: 200,
green: 200,
blue: 200
},
colorSpread: {
red: 0,
green: 0,
blue: 0
},
colorStart: {
red: 200,
green: 200,
blue: 200
},
colorFinish: {
red: 0,
green: 0,
blue: 0
},
emitAcceleration: {
x: -0.5,
y: 2.5,
z: -0.5
},
accelerationSpread: {
x: 0.5,
y: 1,
z: 0.5
},
emitRate: 5.5,
emitSpeed: 0,
speedSpread: 0,
lifespan: 1.5,
maxParticles: 10,
particleRadius: 0.25,
radiusStart: 0,
radiusFinish: 0.1,
radiusSpread: 0,
alpha: 0,
alphaStart: 1,
alphaFinish: 0,
polarStart: 0,
polarFinish: 0,
textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png"
});
});
@ -866,7 +968,7 @@ var toolBar = (function () {
gridTool.setVisible(false);
grid.setEnabled(false);
propertiesTool.setVisible(false);
selectionManager.clearSelections();
selectionManager.clearSelections(this);
cameraManager.disable();
selectionDisplay.disableTriggerMapping();
tablet.landscape = false;
@ -994,7 +1096,7 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) {
var entity = entityIconOverlayManager.findEntity(data.overlayID);
if (entity !== null) {
selectionManager.setSelections([entity]);
selectionManager.setSelections([entity], this);
}
}
}
@ -1141,7 +1243,7 @@ function mouseClickEvent(event) {
if (result === null || result === undefined) {
if (!event.isShifted) {
selectionManager.clearSelections();
selectionManager.clearSelections(this);
}
return;
}
@ -1185,17 +1287,10 @@ function mouseClickEvent(event) {
orientation = MyAvatar.orientation;
intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation));
if (event.isShifted) {
particleExplorerTool.destroyWebView();
}
if (properties.type !== "ParticleEffect") {
particleExplorerTool.destroyWebView();
}
if (!event.isShifted) {
selectionManager.setSelections([foundEntity]);
selectionManager.setSelections([foundEntity], this);
} else {
selectionManager.addEntity(foundEntity, true);
selectionManager.addEntity(foundEntity, true, this);
}
if (wantDebug) {
@ -1493,7 +1588,7 @@ function selectAllEtitiesInCurrentSelectionBox(keepIfTouching) {
}
}
}
selectionManager.setSelections(entities);
selectionManager.setSelections(entities, this);
}
}
@ -1610,8 +1705,6 @@ function deleteSelectedEntities() {
if (SelectionManager.hasSelection()) {
var deletedIDs = [];
selectedParticleEntityID = null;
particleExplorerTool.destroyWebView();
SelectionManager.saveProperties();
var savedProperties = [];
var newSortedSelection = sortSelectedEntities(selectionManager.selections);
@ -1633,7 +1726,7 @@ function deleteSelectedEntities() {
}
if (savedProperties.length > 0) {
SelectionManager.clearSelections();
SelectionManager.clearSelections(this);
pushCommandForSelections([], savedProperties);
entityListTool.deleteEntities(deletedIDs);
}
@ -1650,7 +1743,7 @@ function toggleSelectedEntitiesLocked() {
});
}
entityListTool.sendUpdate();
selectionManager._update();
selectionManager._update(false, this);
}
}
@ -1664,7 +1757,7 @@ function toggleSelectedEntitiesVisible() {
});
}
entityListTool.sendUpdate();
selectionManager._update();
selectionManager._update(false, this);
}
}
@ -1861,7 +1954,7 @@ function importSVO(importURL) {
}
if (isActive) {
selectionManager.setSelections(pastedEntityIDs);
selectionManager.setSelections(pastedEntityIDs, this);
}
} else {
Window.notifyEditError("Can't import entities: entities would be out of bounds.");
@ -1909,7 +2002,7 @@ function deleteKey(value) {
}
function deselectKey(value) {
if (value === 0) { // on release
selectionManager.clearSelections();
selectionManager.clearSelections(this);
}
}
function toggleKey(value) {
@ -2069,7 +2162,7 @@ function applyEntityProperties(data) {
// We might be getting an undo while edit.js is disabled. If that is the case, don't set
// our selections, causing the edit widgets to display.
if (isActive) {
selectionManager.setSelections(selectedEntityIDs);
selectionManager.setSelections(selectedEntityIDs, this);
}
}
@ -2218,9 +2311,11 @@ var PropertiesTool = function (opts) {
if (entity.properties.rotation !== undefined) {
entity.properties.rotation = Quat.safeEulerAngles(entity.properties.rotation);
}
if (entity.properties.emitOrientation !== undefined) {
entity.properties.emitOrientation = Quat.safeEulerAngles(entity.properties.emitOrientation);
}
if (entity.properties.keyLight !== undefined && entity.properties.keyLight.direction !== undefined) {
entity.properties.keyLight.direction = Vec3.multiply(RADIANS_TO_DEGREES,
Vec3.toPolar(entity.properties.keyLight.direction));
entity.properties.keyLight.direction = Vec3.toPolar(entity.properties.keyLight.direction);
entity.properties.keyLight.direction.z = 0.0;
}
selections.push(entity);
@ -2256,23 +2351,29 @@ var PropertiesTool = function (opts) {
data.properties.angularVelocity = Vec3.ZERO;
}
if (data.properties.rotation !== undefined) {
var rotation = data.properties.rotation;
data.properties.rotation = Quat.fromPitchYawRollDegrees(rotation.x, rotation.y, rotation.z);
data.properties.rotation = Quat.fromVec3Degrees(data.properties.rotation);
}
if (data.properties.emitOrientation !== undefined) {
data.properties.emitOrientation = Quat.fromVec3Degrees(data.properties.emitOrientation);
}
if (data.properties.keyLight !== undefined && data.properties.keyLight.direction !== undefined) {
data.properties.keyLight.direction = Vec3.fromPolar(
data.properties.keyLight.direction.x * DEGREES_TO_RADIANS,
data.properties.keyLight.direction.y * DEGREES_TO_RADIANS
);
var currentKeyLightDirection = Vec3.toPolar(Entities.getEntityProperties(selectionManager.selections[0], ['keyLight.direction']).keyLight.direction);
if (data.properties.keyLight.direction.x === undefined) {
data.properties.keyLight.direction.x = currentKeyLightDirection.x;
}
if (data.properties.keyLight.direction.y === undefined) {
data.properties.keyLight.direction.y = currentKeyLightDirection.y;
}
data.properties.keyLight.direction = Vec3.fromPolar(data.properties.keyLight.direction.x, data.properties.keyLight.direction.y);
}
Entities.editEntity(selectionManager.selections[0], data.properties);
if (data.properties.name !== undefined || data.properties.modelURL !== undefined || data.properties.materialURL !== undefined ||
data.properties.visible !== undefined || data.properties.locked !== undefined) {
data.properties.visible !== undefined || data.properties.locked !== undefined) {
entityListTool.sendUpdate();
}
}
pushCommandForSelections();
selectionManager._update();
selectionManager._update(false, this);
} else if (data.type === 'parent') {
parentSelectedEntities();
} else if (data.type === 'unparent') {
@ -2301,7 +2402,7 @@ var PropertiesTool = function (opts) {
});
}
pushCommandForSelections();
selectionManager._update();
selectionManager._update(false, this);
}
} else if (data.action === "moveAllToGrid") {
if (selectionManager.hasSelection()) {
@ -2321,7 +2422,7 @@ var PropertiesTool = function (opts) {
});
}
pushCommandForSelections();
selectionManager._update();
selectionManager._update(false, this);
}
} else if (data.action === "resetToNaturalDimensions") {
if (selectionManager.hasSelection()) {
@ -2342,7 +2443,7 @@ var PropertiesTool = function (opts) {
}
}
pushCommandForSelections();
selectionManager._update();
selectionManager._update(false, this);
}
} else if (data.action === "previewCamera") {
if (selectionManager.hasSelection()) {
@ -2360,7 +2461,7 @@ var PropertiesTool = function (opts) {
});
}
pushCommandForSelections();
selectionManager._update();
selectionManager._update(false, this);
}
} else if (data.action === "reloadClientScripts") {
if (selectionManager.hasSelection()) {
@ -2380,6 +2481,12 @@ var PropertiesTool = function (opts) {
}
} else if (data.type === "propertiesPageReady") {
updateSelections(true);
} else if (data.type === "tooltipsRequest") {
emitScriptEvent({
type: 'tooltipsReply',
tooltips: Script.require('./assets/data/createAppTooltips.json'),
hmdActive: HMD.active,
});
}
};
@ -2613,31 +2720,6 @@ propertyMenu.onSelectMenuItem = function (name) {
var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace");
var propertiesTool = new PropertiesTool();
var particleExplorerTool = new ParticleExplorerTool(createToolsWindow);
var selectedParticleEntityID = null;
function selectParticleEntity(entityID) {
selectedParticleEntityID = entityID;
var properties = Entities.getEntityProperties(entityID);
if (properties.emitOrientation) {
properties.emitOrientation = Quat.safeEulerAngles(properties.emitOrientation);
}
particleExplorerTool.destroyWebView();
particleExplorerTool.createWebView();
particleExplorerTool.setActiveParticleEntity(entityID);
// Switch to particle explorer
var selectTabMethod = { method: 'selectTab', params: { id: 'particle' } };
if (shouldUseEditTabletApp()) {
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
tablet.sendToQml(selectTabMethod);
} else {
createToolsWindow.sendToQml(selectTabMethod);
}
}
entityListTool.webView.webEventReceived.connect(function(data) {
try {
@ -2651,20 +2733,6 @@ entityListTool.webView.webEventReceived.connect(function(data) {
parentSelectedEntities();
} else if (data.type === 'unparent') {
unparentSelectedEntities();
} else if (data.type === "selectionUpdate") {
var ids = data.entityIds;
if (ids.length === 1) {
if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") {
if (JSON.stringify(selectedParticleEntityID) === JSON.stringify(ids[0])) {
// This particle entity is already selected, so return
return;
}
// Destroy the old particles web view first
} else {
selectedParticleEntityID = 0;
particleExplorerTool.destroyWebView();
}
}
}
});

File diff suppressed because one or more lines are too long

View file

@ -16,6 +16,7 @@
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
<script type="text/javascript" src="js/spinButtons.js"></script>
<script type="text/javascript" src="js/listView.js"></script>
<script type="text/javascript" src="js/entityListContextMenu.js"></script>
<script type="text/javascript" src="js/entityList.js"></script>
</head>
<body onload='loaded();' id="entity-list-body">
@ -29,12 +30,25 @@
<input type="button" class="red" id="delete" value="Delete" />
</div>
<div id="entity-list">
<div id="search-area">
<span class="icon-input"><input type="text" class="search" id="filter" placeholder="Filter" /><span>Y</span></span>
<input type="button" id="in-view" class="glyph" value="&#xe007;" />
<div id="radius-and-unit" class="number">
<div id="filter-area">
<div class="multiselect">
<div class="select-box" id="filter-type-select-box">
<select>
<option id="filter-type-text">All Types</option>
</select>
<div class="over-select"></div>
</div>
<div id="filter-type-checkboxes">
<!-- type options with checkbox, icon, and label are added at runtime in entityList -->
</div>
</div>
<div id="filter-search-and-icon">
<span class="icon-input"><input type="search" class="search" id="filter-search" placeholder="Search" /><span>Y</span></span>
</div>
<input type="button" id="filter-in-view" class="glyph" value="&#xe007;" />
<div id="filter-radius-and-unit" class="number">
<label for="radius">Search radius <span class="unit">m</span></label>
<input type="number" id="radius" value="100" />
<input type="text" id="filter-radius" maxlength="9" value="100" />
</div>
</div>
<div id="entity-table-scroll">
@ -87,9 +101,8 @@
</tr>
</tbody>
</table>
<div id="no-entities">
No entities found <span id="no-entities-in-view">in view</span> within a <span id="no-entities-radius">100</span> meter radius. Try moving to a different location and refreshing.
There are no entities to display. Please check your filters or create an entity to begin.
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
<!--
<!--
// entityProperties.html
//
// Created by Ryan Huffman on 13 Nov 2014
@ -20,882 +20,15 @@
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
<script type="text/javascript" src="js/spinButtons.js"></script>
<script type="text/javascript" src="js/underscore-min.js"></script>
<script type="text/javascript" src="js/createAppTooltip.js"></script>
<script type="text/javascript" src="js/entityProperties.js"></script>
<script src="js/jsoneditor.min.js"></script>
</head>
<body onload='loaded();'>
<div id="properties-list">
<fieldset id="properties-header">
<div id="type" class="property value">
<span id="type-icon"></span><label id="property-type"><i>No selection</i></label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-locked">
<label for="property-locked"><span>&#xe006;</span>&nbsp;Locked</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-visible">
<label for="property-visible"><span>&#xe007;</span>&nbsp;Visible</label>
</div>
</fieldset>
<fieldset id="general" class="major">
<div class="shape-group shape-section property dropdown" id="shape-list">
<label for="property-shape">Shape</label>
<select name="SelectShape" id="property-shape">
<option value="Cube">Box</option>
<option value="Sphere">Sphere</option>
<option value="Tetrahedron">Tetrahedron</option>
<option value="Octahedron">Octahedron</option>
<option value="Icosahedron">Icosahedron</option>
<option value="Dodecahedron">Dodecahedron</option>
<option value="Hexagon">Hexagon</option>
<option value="Triangle">Triangle</option>
<option value="Octagon">Octagon</option>
<option value="Cylinder">Cylinder</option>
<option value="Cone">Cone</option>
<option value="Circle">Circle</option>
<option value="Quad">Quad</option>
</select>
</div>
<div class="property text">
<label for="property-name">Name</label>
<input type="text" id="property-name">
</div>
<div class="physical-group color-section property rgb fstuple" id="base-color-section">
<div class="color-picker" id="property-color-control2"></div>
<label>Entity color</label>
<div class="tuple">
<div><input type="number" class="red" id="property-color-red"><label for="property-color-red">Red:</label></div>
<div><input type="number" class="green" id="property-color-green"><label for="property-color-green">Green:</label></div>
<div><input type="number" class="blue" id="property-color-blue"><label for="property-color-blue">Blue:</label></div>
</div>
</div>
</fieldset>
<fieldset id="collision-info" class="major">
<legend class="section-header"> Collision<span class=".collapse-icon">M</span> </legend>
<fieldset class="minor">
<div class="behavior-group property checkbox">
<input type="checkbox" id="property-collisionless">
<label for="property-collisionless">Collisionless</label>
</div>
<div class="behavior-group property checkbox">
<input type="checkbox" id="property-dynamic">
<label for="property-dynamic">Dynamic</label>
</div>
</fieldset>
<fieldset class="minor">
<div class="behavior-group two-column">
<fieldset class="column">
<legend class="sub-section-header">
Collides With
</legend>
<div class="checkbox-sub-props">
<div class="property checkbox">
<input type="checkbox" id="property-collide-static">
<label for="property-collide-static">Static entities</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-collide-dynamic">
<label for="property-collide-dynamic">Dynamic entities</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-collide-kinematic">
<label for="property-collide-kinematic">Kinematic entities</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-collide-myAvatar">
<label for="property-collide-myAvatar">My avatar</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-collide-otherAvatar">
<label for="property-collide-otherAvatar">Other avatars</label>
</div>
</div>
</fieldset>
<fieldset class="column">
<legend class="sub-section-header">
Grabbing
</legend>
<div class="checkbox-sub-props">
<div class="property checkbox">
<input type="checkbox" id="property-grabbable">
<label for="property-grabbable">Grabbable</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-triggerable">
<label for="property-triggerable">Triggerable</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-cloneable">
<label for="property-cloneable">Cloneable</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-grab-follows-controller">
<label for="property-grab-follows-controller">Follow Controller</label>
</div>
</div>
</fieldset>
<fieldset class="column" id="group-cloneable-group" style="display:none;">
<legend class="sub-section-header">
<span>Cloneable Settings</span>
</legend>
<fieldset class="minor">
<div><label>Clone Lifetime</label><input type="number" data-user-data-type="cloneLifetime" id="property-cloneable-lifetime"></div>
</fieldset>
<fieldset class="minor">
<div><label>Clone Limit</label><input type="number" data-user-data-type="cloneLimit" id="property-cloneable-limit"></div>
</fieldset>
<div class="property checkbox">
<input type="checkbox" id="property-cloneable-dynamic">
<label for="property-cloneable-dynamic">Clone Dynamic</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-cloneable-avatarEntity">
<label for="property-cloneable-avatarEntity">Clone Avatar Entity</label>
</div>
</fieldset>
</div>
</fieldset>
</fieldset>
<fieldset id="physical" class="major">
<legend class="section-header physical-group">
Physical<span class=".collapse-icon"> M</span>
</legend>
<fieldset class="minor">
<fieldset class="physical-group property xyz fstuple">
<legend>Linear velocity <span class="unit">m/s</span></legend>
<div class="tuple">
<div><input type="number" class="x" id="property-lvel-x"><label for="property-lvel-x">X:</label></div>
<div><input type="number" class="y" id="property-lvel-y"><label for="property-lvel-y">Y:</label></div>
<div><input type="number" class="z" id="property-lvel-z"><label for="property-lvel-z">Z:</label></div>
</div>
</fieldset>
<div class="physical-group property number">
<label>Linear damping</label>
<input type="number" id="property-ldamping">
</div>
</fieldset>
<fieldset class="minor">
<fieldset class="physical-group property pyr fstuple">
<legend>Angular velocity <span class="unit">deg/s</span></legend>
<div class="tuple">
<div><input type="number" class="pitch" id="property-avel-x"><label for="property-avel-x">Pitch:</label></div>
<div><input type="number" class="yaw" id="property-avel-y"><label for="property-avel-y">Yaw:</label></div>
<div><input type="number" class="roll" id="property-avel-z"><label for="property-avel-z">Roll:</label></div>
</div>
</fieldset>
<div class="physical-group property number">
<label>Angular damping</label>
<input type="number" id="property-adamping">
</div>
</fieldset>
<fieldset class="physical-group property gen fstuple">
<div class="tuple">
<div><label>Restitution</label><input type="number" id="property-restitution"></div>
<div><label>Friction</label><input type="number" id="property-friction"></div>
<div><label>Density</label><input type="number" id="property-density"></div>
</div>
</fieldset>
<fieldset class="minor">
<fieldset class="physical-group property xyz fstuple">
<legend>Gravity <span class="unit">m/s<sup>2</sup></span></legend>
<div class="tuple">
<div><input type="number" class="x" id="property-grav-x"><label for="property-grav-x">X:</label></div>
<div><input type="number" class="y" id="property-grav-y"><label for="property-grav-y">Y:</label></div>
<div><input type="number" class="z" id="property-grav-z"><label for="property-grav-z">Z:</label></div>
</div>
</fieldset>
<fieldset class="physical-group property xyz fstuple">
<legend>Acceleration <span class="unit">m/s<sup>2</sup></span></legend>
<div class="tuple">
<div><input type="number" class="x" id="property-lacc-x"><label for="property-lacc-x">X:</label></div>
<div><input type="number" class="y" id="property-lacc-y"><label for="property-lacc-y">Y:</label></div>
<div><input type="number" class="z" id="property-lacc-z"><label for="property-lacc-z">Z:</label></div>
</div>
</fieldset>
</fieldset>
</fieldset>
<fieldset id="spatial" class="major">
<legend class="section-header spatial-group">
Spatial<span class=".collapse-icon" >M</span>
</legend>
<fieldset class="spatial-group property xyz fstuple">
<legend>Position <span class="unit">m</span></legend>
<div class="tuple">
<div><input type="number" class="x" id="property-pos-x"><label for="property-pos-x">X:</label></div>
<div><input type="number" class="y" id="property-pos-y"><label for="property-pos-y">Y:</label></div>
<div><input type="number" class="z" id="property-pos-z"><label for="property-pos-z">Z:</label></div>
</div>
</fieldset>
<fieldset class="spatial-group property pyr fstuple">
<legend>Rotation <span class="unit">deg</span></legend>
<div class="tuple">
<div><input type="number" class="pitch" id="property-rot-x" step="0.1"><label for="property-rot-x">Pitch:</label></div>
<div><input type="number" class="yaw" id="property-rot-y" step="0.1"><label for="property-rot-y">Yaw:</label></div>
<div><input type="number" class="roll" id="property-rot-z" step="0.1"><label for="property-rot-z">Roll:</label></div>
</div>
</fieldset>
<fieldset class="spatial-group property xyz fstuple">
<legend>Dimensions <span class="unit">m</span></legend>
<div class="tuple">
<div><input type="number" class="x" id="property-dim-x" step="0.1"><label for="property-dim-x">X:</label></div>
<div><input type="number" class="y" id="property-dim-y" step="0.1"><label for="property-dim-y">Y:</label></div>
<div><input type="number" class="z" id="property-dim-z" step="0.1"><label for="property-dim-z">Z:</label></div>
</div>
</fieldset>
<fieldset class="spatial-group property xyz fstuple">
<legend>Registration <span class="unit">(pivot offset as ratio of dimension)</span></legend>
<div class="tuple">
<div><input type="number" class="x" id="property-reg-x" step="0.1"><label for="property-reg-x">X:</label></div>
<div><input type="number" class="y" id="property-reg-y" step="0.1"><label for="property-reg-y">Y:</label></div>
<div><input type="number" class="z" id="property-reg-z" step="0.1"><label for="property-reg-z">Z:</label></div>
</div>
</fieldset>
<fieldset class="spatial-group property gen fsrow">
<legend>Scale <span class="unit">%</span></legend>
<div class="row">
<input type="number" id="dimension-rescale-pct" value=100>
<input type="button" class="blue" id="dimension-rescale-button" value="Rescale">
<input type="button" class="red" id="reset-to-natural-dimensions" value="Reset Dimensions">
</div>
</fieldset>
<div class="spatial-group row">
<div class="property text">
<label for="property-parent-id">Parent ID</label>
<input type="text" id="property-parent-id">
</div>
<div class="property number">
<label for="property-parent-joint-index">Parent joint index</label>
<input type="number" id="property-parent-joint-index">
</div>
</div>
<div class="spatial-group ">
<div class="property text">
<label>Align</label>
</div>
<div class="row">
<div class="buttons">
<input type="button" id="move-selection-to-grid" value="Selection to Grid">
<input type="button" id="move-all-to-grid" value="All to Grid">
</div>
</div>
</div>
</fieldset>
<fieldset id="behavior" class="major">
<legend class="section-header behavior-group">
Behavior<span class=".collapse-icon">M</span>
</legend>
<div class="property textarea">
<label for="property-user-data">User data</label>
<br>
<div class="row">
<input type="button" class="red" id="userdata-clear" value="Clear User Data">
<input type="button" class="blue" id="userdata-new-editor" value="Edit as JSON">
<input disabled type="button" class="black" id="userdata-save" value="Save User Data">
<span id="userdata-saved">Saved!</span>
</div>
<div id="static-userdata"></div>
<div id="userdata-editor"></div>
<textarea id="property-user-data"></textarea>
</div>
<div id="id" class="property value">
<label>ID:</label>
<input type="text" id="property-id" readonly>
</div>
<div class="can-cast-shadow-section property checkbox">
<input type="checkbox" id="property-can-cast-shadow">
<label for="property-can-cast-shadow">Can cast shadow</label>
</div>
<fieldset class="minor">
<div class="behavior-group property url ">
<label for="property-collision-sound-url">Collision sound URL</label>
<input type="text" id="property-collision-sound-url">
</div>
<div class="behavior-group property number">
<label>Lifetime <span class="unit">s</span></label>
<input type="number" id="property-lifetime">
</div>
</fieldset>
<fieldset class="minor">
<div class="behavior-group property url refresh">
<input type="hidden" id="property-script-timestamp" class="value">
<label for="property-script-url">Script URL</label>
<input type="text" id="property-script-url">
<input type="button" id="reload-script-button" class="glyph" value="F">
</div>
</fieldset>
<fieldset class="minor">
<div class="behavior-group property url refresh">
<label for="property-server-scripts">Server Script URL</label>
<input type="text" id="property-server-scripts">
<input type="button" id="reload-server-scripts-button" class="glyph" value="F">
</div>
<div class="behavior-group property">
<label for="server-script-status">Server Script Status</label>
<span id="server-script-status"></span>
</div>
<div class="behavior-group property">
<textarea id="server-script-error"></textarea>
</div>
</fieldset>
<div class="property text">
<label for="property-description">Description</label>
<input type="text" id="property-description">
</div>
</fieldset>
<fieldset id="hyperlink" class="major">
<legend class="section-header hyperlink-group hyperlink-section">
Hyperlink<span class=".collapse-icon">M</span>
</legend>
<div class="hyperlink-group hyperlink-section property url">
<label for="property-hyperlink-href">Href - hifi://address</label>
<input type="text" id="property-hyperlink-href">
</div>
</fieldset>
<fieldset id="light" class="major">
<legend class="section-header light-group light-section">
Light<span class=".collapse-icon">M</span>
</legend>
<fieldset class="light-group light-section property rgb fstuple">
<div class="color-picker" id="property-light-color"></div>
<legend>Light color</legend>
<div class="tuple">
<div><input type="number" class="red" id="property-light-color-red"><label for="property-light-color-red">Red:</label></div>
<div><input type="number" class="green" id="property-light-color-green"><label for="property-light-color-green">Green:</label></div>
<div><input type="number" class="blue" id="property-light-color-blue"><label for="property-light-color-blue">Blue:</label></div>
</div>
</fieldset>
<fieldset class="light-group light-section property gen fstuple">
<div class="tuple">
<div><label>Intensity</label><input type="number" id="property-light-intensity" min="0" step="0.1"></div>
<div><label>Fall-off radius <span class="unit">m</span></label><input type="number" id="property-light-falloff-radius" min="0" step="0.1"></div>
<div></div>
</div>
</fieldset>
<div class="light-group light-section property checkbox">
<input type="checkbox" id="property-light-spot-light">
<label for="property-light-spot-light">Spotlight</label>
</div>
<fieldset class="light-group light-section property gen fstuple">
<div class="tuple">
<div><label>Spotlight exponent</label><input type="number" id="property-light-exponent" step="0.01"></div>
<div><label>Spotlight cut-off</label><input type="number" id="property-light-cutoff" step="0.01"></div>
<div></div>
</div>
</fieldset>
</fieldset>
<fieldset id="model" class="major">
<legend class="section-header model-group model-section zone-section">
Model<span class=".collapse-icon">M</span>
</legend>
<fieldset class="minor">
<div class="model-group model-section property url ">
<label for="property-model-url">Model URL</label>
<input type="text" id="property-model-url">
</div>
<div class="model-group model-section zone-section property dropdown">
<label>Collision shape type</label>
<select name="SelectShapeType" id="property-shape-type">
<option value="none">No Collision</option>
<option value="box">Box</option>
<option value="sphere">Sphere</option>
<option value="compound">Compound</option>
<option value="simple-hull">Basic - Whole model</option>
<option value="simple-compound">Good - Sub-meshes</option>
<option value="static-mesh">Exact - All polygons (non-dynamic only)</option>
</select>
</div>
<div class="model-group model-section zone-section property url ">
<label for="property-compound-shape-url">Compound shape URL</label>
<input type="text" id="property-compound-shape-url">
</div>
</fieldset>
<fieldset class="minor">
<div class="model-group model-section property url ">
<label for="property-model-animation-url">Animation URL</label>
<input type="text" id="property-model-animation-url">
</div>
<div class="model-group model-section two-column">
<div class="column">
<div class="property checkbox">
<input type="checkbox" id="property-model-animation-playing">
<label for="property-model-animation-playing">Animation playing</label>
</div>
<div class="property checkbox indent">
<input type="checkbox" id="property-model-animation-loop">
<label for="property-model-animation-loop">Animation loop</label>
</div>
<div class="property checkbox indent">
<input type="checkbox" id="property-model-animation-hold">
<label for="property-model-animation-hold">Animation hold</label>
</div>
<div class="property checkbox indent">
<input type="checkbox" id="property-model-animation-allow-translation">
<label for="property-model-animation-allow-translation">Animation Allow Translation</label>
</div>
<div id="animation-fps" class="property number">
<label>Animation FPS</label>
<input type="number" id="property-model-animation-fps">
</div>
</div>
<div class="column">
<div class="property number">
<label>Animation frame</label>
<input type="number" id="property-model-animation-frame">
</div>
<div class="property number">
<label>First frame</label>
<input type="number" id="property-model-animation-first-frame">
</div>
<div class="property number">
<label>Last frame</label>
<input type="number" id="property-model-animation-last-frame">
</div>
</div>
</div>
</fieldset>
<fieldset class="minor">
<div class="model-group model-section property textarea">
<label for="property-model-textures">Textures</label>
<textarea id="property-model-textures"></textarea>
</div>
<div class="model-group model-section property textarea">
<label for="property-model-original-textures">Original textures</label>
<textarea id="property-model-original-textures" readonly></textarea>
</div>
</fieldset>
</fieldset>
<fieldset id="zone" class="major">
<legend class="section-header zone-group zone-section">
Zone<span class=".collapse-icon">M</span>
</legend>
<fieldset class="minor">
<div class="zone-group zone-section property checkbox">
<input type="checkbox" id="property-zone-flying-allowed">
<label for="property-zone-flying-allowed">Flying allowed</label>
</div>
<div class="zone-group zone-section property checkbox">
<input type="checkbox" id="property-zone-ghosting-allowed">
<label for="property-zone-ghosting-allowed">Ghosting allowed</label>
</div>
</fieldset>
<fieldset class="minor">
<div class="zone-group zone-section property url ">
<label for="property-zone-filter-url">Filter URL</label>
<input type="text" id="property-zone-filter-url">
</div>
<div class="sub-section-header zone-group zone-section">
<label>Key Light</label>
</div>
<form>
<input type="radio" name="keyLightMode" value="inherit" id="property-zone-key-light-mode-inherit" checked> Inherit
<input type="radio" name="keyLightMode" value="disabled" id="property-zone-key-light-mode-disabled"> Off
<input type="radio" name="keyLightMode" value="enabled" id="property-zone-key-light-mode-enabled"> On
</form>
<div class="zone-section keylight-section zone-group property rgb">
<div class="color-picker" id="property-zone-key-light-color"></div>
<label>Key light color</label>
<div class="tuple">
<div><input type="number" class="red" id="property-zone-key-light-color-red" min="0" max="255" step="1"><label for="property-zone-key-light-color-red">Red:</label></div>
<div><input type="number" class="green" id="property-zone-key-light-color-green" min="0" max="255" step="1"><label for="property-zone-key-light-color-green">Green:</label></div>
<div><input type="number" class="blue" id="property-zone-key-light-color-blue" min="0" max="255" step="1"><label for="property-zone-key-light-color-blue">Blue:</label></div>
</div>
</div>
<div class="zone-section keylight-section zone-group property number">
<label>Light intensity</label>
<input type="number" id="property-zone-key-intensity" min="0" max="10" step="0.1">
</div>
<div class="zone-group zone-section keylight-section property gen">
<div class="tuple">
<div><label>Light altitude <span class="unit">deg</span></label><input type="number" id="property-zone-key-light-direction-x"></div>
<div><label>Light azimuth <span class="unit">deg</span></label><input type="number" id="property-zone-key-light-direction-y"></div>
<div></div>
</div>
</div>
<div class="zone-group zone-section keylight-section property checkbox">
<input type="checkbox" id="property-zone-key-light-cast-shadows">
<label for="property-zone-key-light-cast-shadows">Cast Shadows</label>
</div>
<fieldset class="minor">
<legend class="sub-section-header zone-group zone-section background-section">
Skybox
</legend>
<form>
<input type="radio" name="skyboxMode" value="inherit" id="property-zone-skybox-mode-inherit" checked> Inherit
<input type="radio" name="skyboxMode" value="disabled" id="property-zone-skybox-mode-disabled"> Off
<input type="radio" name="skyboxMode" value="enabled" id="property-zone-skybox-mode-enabled"> On
</form>
<fieldset class="zone-group zone-section skybox-section property rgb fstuple">
<div class="color-picker" id="property-zone-skybox-color"></div>
<legend>Skybox color</legend>
<div class="tuple">
<div>
<input type="number" class="red" id="property-zone-skybox-color-red">
<label for="property-zone-skybox-color-red">Red:</label>
</div>
<div>
<input type="number" class="green" id="property-zone-skybox-color-green">
<label for="property-zone-skybox-color-green">Green:</label>
</div>
<div>
<input type="number" class="blue" id="property-zone-skybox-color-blue">
<label for="property-zone-skybox-color-blue">Blue:</label>
</div>
</div>
</fieldset>
<div class="zone-group zone-section skybox-section property url ">
<br>
<label for="property-zone-skybox-url" width="80">Skybox URL</label>
<input type="text" id="property-zone-skybox-url">
<br>
<br>
<input type="button" id="copy-skybox-url-to-ambient-url" value="Copy URL to Ambient">
</div>
</fieldset>
<div class="sub-section-header zone-group zone-section">
<label>Ambient Light</label>
</div>
<form>
<input type="radio" name="ambientLightMode" value="inherit" id="property-zone-ambient-light-mode-inherit" checked> Inherit
<input type="radio" name="ambientLightMode" value="disabled" id="property-zone-ambient-light-mode-disabled"> Off
<input type="radio" name="ambientLightMode" value="enabled" id="property-zone-ambient-light-mode-enabled"> On
</form>
<div class="zone-group zone-section ambient-section property number">
<label>Ambient intensity</label>
<input type="number" id="property-zone-key-ambient-intensity" min="0" max="10" step="0.1">
</div>
<div class="zone-group zone-section ambient-section property url ">
<label for="property-zone-key-ambient-url">Ambient URL</label>
<input type="text" id="property-zone-key-ambient-url">
</div>
</fieldset>
<fieldset class="minor">
<legend class="sub-section-header zone-group zone-section">
Haze
</legend>
<form>
<input type="radio" name="hazeMode" value="inherit" id="property-zone-haze-mode-inherit" checked> Inherit
<input type="radio" name="hazeMode" value="disabled" id="property-zone-haze-mode-disabled"> Off
<input type="radio" name="hazeMode" value="enabled" id="property-zone-haze-mode-enabled"> On
</form>
<fieldset class="zone-group zone-section haze-section property gen fstuple">
<div class="tuple">
<div>
<label>Range<span class="unit">m</span></label>
<input type="number" id="property-zone-haze-range" min="5" max="10000" step="5">
</div>
</div>
</fieldset>
<div class="zone-group zone-section haze-section property checkbox">
<input type="checkbox" id="property-zone-haze-altitude-effect">
<label for="property-zone-haze-altitude-effect">Use Altitude</label>
</div>
<fieldset class="zone-group zone-section haze-section property gen fstuple">
<div class="tuple">
<div><label>Base<span class="unit">m</span></label><input type="number" id="property-zone-haze-base" min="-1000" max="1000" step="10"></div>
<div><label>Ceiling<span class="unit">m</span></label><input type="number" id="property-zone-haze-ceiling" min="-1000" max="5000" step="10"></div>
</div>
</fieldset>
<fieldset class="zone-group zone-section haze-section property rgb fstuple">
<div class="color-picker" id="property-zone-haze-color"></div>
<legend>Haze Color</legend>
<div class="tuple">
<div><input type="number" class="red" id="property-zone-haze-color-red" min="0" max="255" step="1">
<label for="property-zone-haze-blend-in-color-red">Red:</label></div>
<div><input type="number" class="green" id="property-zone-haze-color-green" min="0" max="255" step="1">
<label for="property-zone-haze-blend-in-color-green">Green:</label></div>
<div><input type="number" class="blue" id="property-zone-haze-color-blue" min="0" max="255" step="1">
<label for="property-zone-haze-blend-in-color-blue">Blue:</label></div>
</div>
</fieldset>
<fieldset class="zone-group zone-section haze-section property gen fstuple">
<div class="tuple">
<div>
<br>
<table>
<td><label>Background Blend&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0.0</label></td>
<td><input type="range" class="slider" id="property-zone-haze-background-blend" min="0.0" max="1.0" step="0.01" value="0.0"></td>
<td><label>1.0</label></td>
</table>
</div>
</div>
</fieldset>
<div class="zone-group zone-section haze-section property checkbox">
<input type="checkbox" id="property-zone-haze-enable-light-blend">
<label for="property-zone-haze-enable-light-blend">Enable Glare</label>
</div>
<fieldset class="zone-group zone-section haze-section property rgb fstuple">
<div class="color-picker" id="property-zone-haze-glare-color"></div>
<legend>Glare Color</legend>
<div class="tuple">
<div><input type="number" class="red" id="property-zone-haze-glare-color-red" min="0" max="255" step="1">
<label for="property-zone-haze-blend-out-color-red">Red:</label></div>
<div><input type="number" class="green" id="property-zone-haze-glare-color-green" min="0" max="255" step="1">
<label for="property-zone-haze-blend-out-color-green">Green:</label></div>
<div><input type="number" class="blue" id="property-zone-haze-glare-color-blue" min="0" max="255" step="1">
<label for="property-zone-haze-blend-out-color-blue">Blue:</label></div>
</div>
</fieldset>
<fieldset class="zone-group zone-section haze-section property gen fstuple">
<div class="tuple">
<div>
<br>
<table>
<td><label>Glare Angle&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0</label></td>
<td><input type="range" class="slider" id="property-zone-haze-blend-angle" min="0" max="180" step="1"></td>
<td><label>180</label>
</table>
</div>
</div>
</fieldset>
<!--div class="zone-group zone-section haze-section property checkbox">
<input type="checkbox" id="property-zone-haze-attenuate-keylight">
<label for="property-zone-haze-attenuate-keylight">Attenuate Keylight</label>
</div>
<fieldset class="zone-group zone-section haze-section property gen fstuple">
<div class="tuple">
<div><label>Range<span class="unit">m</span></label><input type="number" id="property-zone-haze-keylight-range"
min="5" max="1000000" step="5"></div>
<div><label>Altitude<span class="unit">m</span></label><input type="number" id="property-zone-haze-keylight-altitude"
min="-1000" max="50000" step="10"></div>
</div>
</fieldset-->
</fieldset>
<fieldset class="minor">
<legend class="sub-section-header zone-group zone-section">
Bloom
</legend>
<form>
<input type="radio" name="bloomMode" value="inherit" id="property-zone-bloom-mode-inherit" checked> Inherit
<input type="radio" name="bloomMode" value="disabled" id="property-zone-bloom-mode-disabled"> Off
<input type="radio" name="bloomMode" value="enabled" id="property-zone-bloom-mode-enabled"> On
</form>
<fieldset class="zone-group zone-section bloom-section property gen fstuple">
<div class="tuple">
<div>
<br>
<table>
<td><label>Bloom Intensity&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0</label></td>
<td><input type="range" class="slider" id="property-zone-bloom-intensity" min="0" max="1" step="0.01"></td>
<td><label>1</label>
</table>
</div>
</div>
</fieldset>
<fieldset class="zone-group zone-section bloom-section property gen fstuple">
<div class="tuple">
<div>
<br>
<table>
<td><label>Bloom Threshold&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0</label></td>
<td><input type="range" class="slider" id="property-zone-bloom-threshold" min="0" max="1" step="0.01"></td>
<td><label>1</label>
</table>
</div>
</div>
</fieldset>
<fieldset class="zone-group zone-section bloom-section property gen fstuple">
<div class="tuple">
<div>
<br>
<table>
<td><label>Bloom Size&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0</label></td>
<td><input type="range" class="slider" id="property-zone-bloom-size" min="0" max="2" step="0.01"></td>
<td><label>2</label>
</table>
</div>
</div>
</fieldset>
</fieldset>
</fieldset>
<fieldset id="text" class="major">
<legend class="section-header text-group text-section">
Text<span class=".collapse-icon">M</span>
</legend>
<div class="text-group text-section property text">
<label for="property-text-text">Text content</label>
<input type="text" id="property-text-text">
</div>
<div class="text-group text-section property checkbox">
<input type="checkbox" id="property-text-face-camera">
<label for="property-text-face-camera">&nbsp;Face Camera</label>
</div>
<div class="text-group text-section property number">
<label>Line height <span class="unit">m</span></label>
<input type="number" id="property-text-line-height" min="0" step="0.005">
</div>
<div class="text-group text-section property rgb">
<div class="color-picker" id="property-text-text-color"></div>
<label>Text color</label>
<div class="tuple">
<div><input type="number" class="red" id="property-text-text-color-red"><label for="property-text-text-color-red">Red:</label></div>
<div><input type="number" class="green" id="property-text-text-color-green"><label for="property-text-text-color-green">Green:</label></div>
<div><input type="number" class="blue" id="property-text-text-color-blue"><label for="property-text-text-color-blue">Blue:</label></div>
</div>
</div>
<fieldset class="text-group text-section property rgb fstuple">
<div class="color-picker" id="property-text-background-color"></div>
<legend>Background color</legend>
<div class="tuple">
<div><input type="number" class="red" id="property-text-background-color-red"><label for="roperty-text-background-color-red">Red:</label></div>
<div><input type="number" class="green" id="property-text-background-color-green"><label for="property-text-background-color-green">Green:</label></div>
<div><input type="number" class="blue" id="property-text-background-color-blue"><label for="property-text-background-color-blue">Blue:</label></div>
</div>
</fieldset>
</fieldset>
<fieldset id="image" class="major">
<legend class="section-header image-group image-section">
Image<span>M</span>
</legend>
<div class="image-group image-section property url ">
<label for="property-image-url">Image URL</label>
<input type="text" id="property-image-url">
</div>
</fieldset>
<fieldset id="web" class="major">
<legend class="section-header web-group web-section">
Web<span class=".collapse-icon">M</span>
</legend>
<div class="web-group web-section property url ">
<label for="property-web-source-url">Source URL</label>
<input type="text" id="property-web-source-url">
</div>
<div class="web-group web-section property dpi ">
<label for="property-web-dpi">Resolution (DPI)</label>
<input type="number" id="property-web-dpi">
</div>
</fieldset>
<fieldset id="polyvox" class="major">
<legend class="section-header spatial-group poly-vox-section property xyz">
Voxel volume size <span>m</span>
</legend>
<div class="tuple">
<div><input type="number" class="x" id="property-voxel-volume-size-x"><label for="property-voxel-volume-size-x">X:</label></div>
<div><input type="number" class="y" id="property-voxel-volume-size-y"><label for="property-voxel-volume-size-y">Y:</label></div>
<div><input type="number" class="z" id="property-voxel-volume-size-z"><label for="property-voxel-volume-size-z">Z:</label></div>
</div>
<div class="spatial-group poly-vox-section property dropdown">
<label>Surface extractor</label>
<select name="SelectVoxelSurfaceStyle" id="property-voxel-surface-style">
<option value="0">Marching cubes</option>
<option value="1">Cubic</option>
<option value="2">Edged cubic</option>
<option value="3">Edged marching cubes</option>
</select>
</div>
<div class="spatial-group poly-vox-section property url ">
<label for="property-x-texture-url">X-axis texture URL</label>
<input type="text" id="property-x-texture-url">
</div>
<div class="spatial-group poly-vox-section property url ">
<label for="property-y-texture-url">Y-axis texture URL</label>
<input type="text" id="property-y-texture-url">
</div>
<div class="spatial-group poly-vox-section property url ">
<label for="property-z-texture-url">Z-axis texture URL</label>
<input type="text" id="property-z-texture-url">
</div>
</fieldset>
<fieldset id="material" class="major">
<legend class="section-header material-group material-section">
Material<span>M</span>
</legend>
<fieldset class="minor">
<div class="material-group material-section property url">
<label for="property-material-url">Material URL</label>
<input type="text" id="property-material-url">
</div>
<div class="property textarea">
<label for="property-material-data">Material data</label>
<br><br>
<div class="row">
<input type="button" class="red" id="materialdata-clear" value="Clear Material Data">
<input type="button" class="blue" id="materialdata-new-editor" value="Edit as JSON">
<input disabled type="button" class="black" id="materialdata-save" value="Save Material Data">
<span id="materialdata-saved" visible="false">Saved!</span>
</div>
<div id="static-naterialdata"></div>
<div id="materialdata-editor"></div>
<textarea id="property-material-data"></textarea>
</div>
</fieldset>
<fieldset class="minor">
<div class="material-group material-section property text" id="property-parent-material-id-string-container">
<label for="property-parent-material-id-string">Material Name to Replace </label>
<input type="text" id="property-parent-material-id-string">
</div>
<div class="material-group material-section property number" id="property-parent-material-id-number-container">
<label for="property-parent-material-id-number">Submesh to Replace </label>
<input type="number" min="0" step="1" value="0" id="property-parent-material-id-number">
</div>
<div class="material-group material-section property checkbox">
<input type="checkbox" id="property-parent-material-id-checkbox">
<label for="property-parent-material-id-checkbox">Select Submesh</label>
</div>
<div class="material-group material-section property number">
<label>Priority </label>
<input type="number" id="property-priority" min="0">
</div>
<fieldset>
<!-- TODO: support 3D projected materials
<div class="material-group material-section property dropdown">
<label>Material mode </label>
<select name="SelectMaterialMode" id="property-material-mapping-mode">
<option value="uv">UV space material</option>
<option value="projected">3D projected material</option>
</select>
</div>
-->
<div class="material-group material-section property xy fstuple">
<label>Material Position </label>
<div class="tuple">
<div><input type="number" class="x" id="property-material-mapping-pos-x" min="0" max="1" step="0.1"><label for="property-material-mapping-pos-x">X:</label></div>
<div><input type="number" class="y" id="property-material-mapping-pos-y" min="0" max="1" step="0.1"><label for="property-material-mapping-pos-y">Y:</label></div>
</div>
<div class="material-group material-section property wh fstuple">
<label>Material Scale </label>
<div class="tuple">
<div><input type="number" class="x" id="property-material-mapping-scale-x" min="0" step="0.1"><label for="property-material-mapping-scale-x">Width:</label></div>
<div><input type="number" class="y" id="property-material-mapping-scale-y" min="0" step="0.1"><label for="property-material-mapping-scale-y">Height:</label></div>
</div>
<div class="material-group material-section property number">
<label>Material Rotation <span class="unit">deg</span></label>
<input type="number" id="property-material-mapping-rot" step="0.1">
</div>
</fieldset>
</fieldset>
</fieldset>
<!-- each property is added at runtime in entityProperties -->
</div>
</body>

View file

@ -0,0 +1,116 @@
// createAppTooltip.js
//
// Created by Thijs Wenker on 17 Oct 2018
// Copyright 2018 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
const CREATE_APP_TOOLTIP_OFFSET = 20;
const TOOLTIP_DELAY = 500; // ms
const TOOLTIP_DEBUG = false;
function CreateAppTooltip() {
this._tooltipData = null;
this._tooltipDiv = null;
this._delayTimeout = null;
this._isEnabled = false;
}
CreateAppTooltip.prototype = {
_tooltipData: null,
_tooltipDiv: null,
_delayTimeout: null,
_isEnabled: null,
_removeTooltipIfExists: function() {
if (this._delayTimeout !== null) {
window.clearTimeout(this._delayTimeout);
this._delayTimeout = null;
}
if (this._tooltipDiv !== null) {
this._tooltipDiv.remove();
this._tooltipDiv = null;
}
},
setIsEnabled: function(isEnabled) {
this._isEnabled = isEnabled;
},
setTooltipData: function(tooltipData) {
this._tooltipData = tooltipData;
},
registerTooltipElement: function(element, tooltipID) {
element.addEventListener("mouseover", function() {
if (!this._isEnabled) {
return;
}
this._removeTooltipIfExists();
this._delayTimeout = window.setTimeout(function() {
let tooltipData = this._tooltipData[tooltipID];
if (!tooltipData || tooltipData.tooltip === "") {
if (!TOOLTIP_DEBUG) {
return;
}
tooltipData = {tooltip: 'PLEASE SET THIS TOOLTIP'};
}
let elementRect = element.getBoundingClientRect();
let elTip = document.createElement("div");
elTip.className = "createAppTooltip";
let elTipDescription = document.createElement("div");
elTipDescription.className = "createAppTooltipDescription";
elTipDescription.innerText = tooltipData.tooltip;
elTip.appendChild(elTipDescription);
let jsAttribute = tooltipID;
if (tooltipData.jsPropertyName) {
jsAttribute = tooltipData.jsPropertyName;
}
if (!tooltipData.skipJSProperty) {
let elTipJSAttribute = document.createElement("div");
elTipJSAttribute.className = "createAppTooltipJSAttribute";
elTipJSAttribute.innerText = `JS Attribute: ${jsAttribute}`;
elTip.appendChild(elTipJSAttribute);
}
document.body.appendChild(elTip);
let elementTop = window.pageYOffset + elementRect.top;
let desiredTooltipTop = elementTop + element.clientHeight + CREATE_APP_TOOLTIP_OFFSET;
let desiredTooltipLeft = window.pageXOffset + elementRect.left;
if ((window.innerHeight + window.pageYOffset) < (desiredTooltipTop + elTip.clientHeight)) {
// show above when otherwise out of bounds
elTip.style.top = elementTop - CREATE_APP_TOOLTIP_OFFSET - elTip.clientHeight;
} else {
// show tooltip on below by default
elTip.style.top = desiredTooltipTop;
}
if ((window.innerWidth + window.pageXOffset) < (desiredTooltipLeft + elTip.clientWidth)) {
elTip.style.left = document.body.clientWidth + window.pageXOffset - elTip.offsetWidth;
} else {
elTip.style.left = desiredTooltipLeft;
}
this._tooltipDiv = elTip;
}.bind(this), TOOLTIP_DELAY);
}.bind(this), false);
element.addEventListener("mouseout", function() {
if (!this._isEnabled) {
return;
}
this._removeTooltipIfExists();
}.bind(this), false);
}
};

View file

@ -13,7 +13,7 @@ const DESCENDING_STRING = '&#x25BE;';
const LOCKED_GLYPH = "&#xe006;";
const VISIBLE_GLYPH = "&#xe007;";
const TRANSPARENCY_GLYPH = "&#xe00b;";
const BAKED_GLYPH = "&#xe01a;"
const BAKED_GLYPH = "&#xe01a;";
const SCRIPT_GLYPH = "k";
const BYTES_PER_MEGABYTE = 1024 * 1024;
const IMAGE_MODEL_NAME = 'default-image-model.fbx';
@ -23,6 +23,7 @@ const FILTER_IN_VIEW_ATTRIBUTE = "pressed";
const WINDOW_NONVARIABLE_HEIGHT = 227;
const NUM_COLUMNS = 12;
const EMPTY_ENTITY_ID = "0";
const MAX_LENGTH_RADIUS = 9;
const DELETE = 46; // Key code for the delete key.
const KEY_P = 80; // Key code for letter p used for Parenting hotkey.
@ -54,10 +55,34 @@ const COMPARE_ASCENDING = function(a, b) {
}
return 1;
}
};
const COMPARE_DESCENDING = function(a, b) {
return COMPARE_ASCENDING(b, a);
}
};
const FILTER_TYPES = [
"Shape",
"Model",
"Image",
"Light",
"Zone",
"Web",
"Material",
"ParticleEffect",
"Text",
];
const ICON_FOR_TYPE = {
Shape: "n",
Model: "&#xe008;",
Image: "&#xe02a;",
Light: "p",
Zone: "o",
Web: "q",
Material: "&#xe00b;",
ParticleEffect: "&#xe004;",
Text: "l",
};
// List of all entities
var entities = [];
@ -70,8 +95,14 @@ var selectedEntities = [];
var entityList = null; // The ListView
/**
* @type EntityListContextMenu
*/
var entityListContextMenu = null;
var currentSortColumn = 'type';
var currentSortOrder = ASCENDING_SORT;
var typeFilters = [];
var isFilterInView = false;
var showExtraInfo = false;
@ -105,9 +136,12 @@ function loaded() {
elToggleLocked = document.getElementById("locked");
elToggleVisible = document.getElementById("visible");
elDelete = document.getElementById("delete");
elFilter = document.getElementById("filter");
elInView = document.getElementById("in-view")
elRadius = document.getElementById("radius");
elFilterTypeSelectBox = document.getElementById("filter-type-select-box");
elFilterTypeText = document.getElementById("filter-type-text");
elFilterTypeCheckboxes = document.getElementById("filter-type-checkboxes");
elFilterSearch = document.getElementById("filter-search");
elFilterInView = document.getElementById("filter-in-view")
elFilterRadius = document.getElementById("filter-radius");
elExport = document.getElementById("export");
elPal = document.getElementById("pal");
elInfoToggle = document.getElementById("info-toggle");
@ -115,9 +149,8 @@ function loaded() {
elSelectedEntitiesCount = document.getElementById("selected-entities-count");
elVisibleEntitiesCount = document.getElementById("visible-entities-count");
elNoEntitiesMessage = document.getElementById("no-entities");
elNoEntitiesInView = document.getElementById("no-entities-in-view");
elNoEntitiesRadius = document.getElementById("no-entities-radius");
document.body.onclick = onBodyClick;
document.getElementById("entity-name").onclick = function() {
setSortColumn('name');
};
@ -127,74 +160,185 @@ function loaded() {
document.getElementById("entity-url").onclick = function() {
setSortColumn('url');
};
document.getElementById("entity-locked").onclick = function () {
document.getElementById("entity-locked").onclick = function() {
setSortColumn('locked');
};
document.getElementById("entity-visible").onclick = function () {
document.getElementById("entity-visible").onclick = function() {
setSortColumn('visible');
};
document.getElementById("entity-verticesCount").onclick = function () {
document.getElementById("entity-verticesCount").onclick = function() {
setSortColumn('verticesCount');
};
document.getElementById("entity-texturesCount").onclick = function () {
document.getElementById("entity-texturesCount").onclick = function() {
setSortColumn('texturesCount');
};
document.getElementById("entity-texturesSize").onclick = function () {
document.getElementById("entity-texturesSize").onclick = function() {
setSortColumn('texturesSize');
};
document.getElementById("entity-hasTransparent").onclick = function () {
document.getElementById("entity-hasTransparent").onclick = function() {
setSortColumn('hasTransparent');
};
document.getElementById("entity-isBaked").onclick = function () {
document.getElementById("entity-isBaked").onclick = function() {
setSortColumn('isBaked');
};
document.getElementById("entity-drawCalls").onclick = function () {
document.getElementById("entity-drawCalls").onclick = function() {
setSortColumn('drawCalls');
};
document.getElementById("entity-hasScript").onclick = function () {
document.getElementById("entity-hasScript").onclick = function() {
setSortColumn('hasScript');
};
elRefresh.onclick = function() {
refreshEntities();
}
};
elToggleLocked.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' }));
}
};
elToggleVisible.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' }));
}
};
elExport.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'export'}));
}
};
elPal.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' }));
}
};
elDelete.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
}
elFilter.onkeyup = refreshEntityList;
elFilter.onpaste = refreshEntityList;
elFilter.onchange = onFilterChange;
elFilter.onblur = refreshFooter;
elInView.onclick = toggleFilterInView;
elRadius.onchange = onRadiusChange;
};
elFilterTypeSelectBox.onclick = onToggleTypeDropdown;
elFilterSearch.onkeyup = refreshEntityList;
elFilterSearch.onsearch = refreshEntityList;
elFilterInView.onclick = toggleFilterInView;
elFilterRadius.onkeyup = onRadiusChange;
elFilterRadius.onchange = onRadiusChange;
elFilterRadius.onclick = onRadiusChange;
elInfoToggle.onclick = toggleInfo;
elNoEntitiesInView.style.display = "none";
// create filter type dropdown checkboxes with label and icon for each type
for (let i = 0; i < FILTER_TYPES.length; ++i) {
let type = FILTER_TYPES[i];
let typeFilterID = "filter-type-" + type;
let elDiv = document.createElement('div');
let elLabel = document.createElement('label');
elLabel.setAttribute("for", typeFilterID);
elLabel.innerText = type;
let elSpan = document.createElement('span');
elSpan.setAttribute("class", "typeIcon");
elSpan.innerHTML = ICON_FOR_TYPE[type];
let elInput = document.createElement('input');
elInput.setAttribute("type", "checkbox");
elInput.setAttribute("id", typeFilterID);
elInput.setAttribute("filterType", type);
elInput.checked = true; // all types are checked initially
toggleTypeFilter(elInput, false); // add all types to the initial types filter
elDiv.appendChild(elInput);
elLabel.insertBefore(elSpan, elLabel.childNodes[0]);
elDiv.appendChild(elLabel);
elFilterTypeCheckboxes.appendChild(elDiv);
elDiv.onclick = onToggleTypeFilter;
}
entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow,
createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT);
entityListContextMenu = new EntityListContextMenu();
function startRenamingEntity(entityID) {
let entity = entitiesByID[entityID];
if (!entity || entity.locked || !entity.elRow) {
return;
}
let elCell = entity.elRow.childNodes[COLUMN_INDEX.NAME];
let elRenameInput = document.createElement("input");
elRenameInput.setAttribute('class', 'rename-entity');
elRenameInput.value = entity.name;
let ignoreClicks = function(event) {
event.stopPropagation();
};
elRenameInput.onclick = ignoreClicks;
elRenameInput.ondblclick = ignoreClicks;
elRenameInput.onkeyup = function(keyEvent) {
if (keyEvent.key === "Enter") {
elRenameInput.blur();
}
};
elRenameInput.onblur = function(event) {
let value = elRenameInput.value;
EventBridge.emitWebEvent(JSON.stringify({
type: 'rename',
entityID: entityID,
name: value
}));
entity.name = value;
elCell.innerText = value;
};
elCell.innerHTML = "";
elCell.appendChild(elRenameInput);
elRenameInput.select();
}
entityListContextMenu.setOnSelectedCallback(function(optionName, selectedEntityID) {
switch (optionName) {
case "Cut":
EventBridge.emitWebEvent(JSON.stringify({ type: 'cut' }));
break;
case "Copy":
EventBridge.emitWebEvent(JSON.stringify({ type: 'copy' }));
break;
case "Paste":
EventBridge.emitWebEvent(JSON.stringify({ type: 'paste' }));
break;
case "Rename":
startRenamingEntity(selectedEntityID);
break;
case "Duplicate":
EventBridge.emitWebEvent(JSON.stringify({ type: 'duplicate' }));
break;
case "Delete":
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
break;
}
});
function onRowContextMenu(clickEvent) {
let entityID = this.dataset.entityID;
if (!selectedEntities.includes(entityID)) {
let selection = [entityID];
updateSelectedEntities(selection);
EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate",
focus: false,
entityIds: selection,
}));
}
let enabledContextMenuItems = ['Copy', 'Paste', 'Duplicate'];
if (entitiesByID[entityID] && !entitiesByID[entityID].locked) {
enabledContextMenuItems.push('Cut');
enabledContextMenuItems.push('Rename');
enabledContextMenuItems.push('Delete');
}
entityListContextMenu.open(clickEvent, entityID, enabledContextMenuItems);
}
function onRowClicked(clickEvent) {
let entityID = this.dataset.entityID;
let selection = [entityID];
if (clickEvent.ctrlKey) {
let selectedIndex = selectedEntities.indexOf(entityID);
if (selectedIndex >= 0) {
selection = [];
selection = selection.concat(selectedEntities);
selection.splice(selectedIndex, 1)
selection.splice(selectedIndex, 1);
} else {
selection = selection.concat(selectedEntities);
}
@ -221,28 +365,29 @@ function loaded() {
}
}
} else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) {
// if reselecting the same entity then deselect it
// if reselecting the same entity then start renaming it
if (selectedEntities[0] === entityID) {
selection = [];
startRenamingEntity(entityID);
}
}
updateSelectedEntities(selection);
updateSelectedEntities(selection, false);
EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate",
focus: false,
entityIds: selection,
}));
refreshFooter();
}
function onRowDoubleClicked() {
let selection = [this.dataset.entityID];
updateSelectedEntities(selection, false);
EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate",
focus: true,
entityIds: [this.dataset.entityID],
entityIds: selection,
}));
}
@ -289,7 +434,7 @@ function loaded() {
hasScript: entity.hasScript,
elRow: null, // if this entity has a visible row element assigned to it
selected: false // if this entity is selected for edit regardless of having a visible row
}
};
entities.push(entityData);
entitiesByID[entityData.id] = entityData;
@ -302,17 +447,16 @@ function loaded() {
function refreshEntityList() {
PROFILE("refresh-entity-list", function() {
PROFILE("filter", function() {
let searchTerm = elFilter.value.toLowerCase();
if (searchTerm === '') {
visibleEntities = entities.slice(0);
} else {
visibleEntities = entities.filter(function(e) {
return e.name.toLowerCase().indexOf(searchTerm) > -1
|| e.type.toLowerCase().indexOf(searchTerm) > -1
|| e.fullUrl.toLowerCase().indexOf(searchTerm) > -1
|| e.id.toLowerCase().indexOf(searchTerm) > -1;
});
}
let searchTerm = elFilterSearch.value.toLowerCase();
visibleEntities = entities.filter(function(e) {
let type = e.type === "Box" || e.type === "Sphere" ? "Shape" : e.type;
let typeFilter = typeFilters.indexOf(type) > -1;
let searchFilter = searchTerm === '' || (e.name.toLowerCase().indexOf(searchTerm) > -1 ||
e.type.toLowerCase().indexOf(searchTerm) > -1 ||
e.fullUrl.toLowerCase().indexOf(searchTerm) > -1 ||
e.id.toLowerCase().indexOf(searchTerm) > -1);
return typeFilter && searchFilter;
});
});
PROFILE("sort", function() {
@ -418,7 +562,7 @@ function loaded() {
isBaked: document.querySelector('#entity-isBaked .sort-order'),
drawCalls: document.querySelector('#entity-drawCalls .sort-order'),
hasScript: document.querySelector('#entity-hasScript .sort-order'),
}
};
function setSortColumn(column) {
PROFILE("set-sort-column", function() {
if (currentSortColumn === column) {
@ -453,7 +597,7 @@ function loaded() {
}
}
function updateSelectedEntities(selectedIDs) {
function updateSelectedEntities(selectedIDs, autoScroll) {
let notFound = false;
// reset all currently selected entities and their rows first
@ -482,6 +626,26 @@ function loaded() {
}
});
if (autoScroll && selectedIDs.length > 0) {
let firstItem = Number.MAX_VALUE;
let lastItem = -1;
let itemFound = false;
visibleEntities.forEach(function(entity, index) {
if (selectedIDs.indexOf(entity.id) !== -1) {
if (firstItem > index) {
firstItem = index;
}
if (lastItem < index) {
lastItem = index;
}
itemFound = true;
}
});
if (itemFound) {
entityList.scrollToRow(firstItem, lastItem);
}
}
refreshFooter();
return notFound;
@ -502,6 +666,7 @@ function loaded() {
}
row.appendChild(column);
}
row.oncontextmenu = onRowContextMenu;
row.onclick = onRowClicked;
row.ondblclick = onRowDoubleClicked;
return row;
@ -582,29 +747,74 @@ function loaded() {
function toggleFilterInView() {
isFilterInView = !isFilterInView;
if (isFilterInView) {
elInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE);
elNoEntitiesInView.style.display = "inline";
elFilterInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE);
} else {
elInView.removeAttribute(FILTER_IN_VIEW_ATTRIBUTE);
elNoEntitiesInView.style.display = "none";
elFilterInView.removeAttribute(FILTER_IN_VIEW_ATTRIBUTE);
}
EventBridge.emitWebEvent(JSON.stringify({ type: "filterInView", filterInView: isFilterInView }));
refreshEntities();
}
function onFilterChange() {
refreshEntityList();
entityList.resize();
}
function onRadiusChange() {
elRadius.value = Math.max(elRadius.value, 0);
elNoEntitiesRadius.firstChild.nodeValue = elRadius.value;
elNoEntitiesMessage.style.display = "none";
EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value }));
elFilterRadius.value = elFilterRadius.value.replace(/[^0-9]/g, '');
elFilterRadius.value = Math.max(elFilterRadius.value, 0);
EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elFilterRadius.value }));
refreshEntities();
}
function isTypeDropdownVisible() {
return elFilterTypeCheckboxes.style.display === "block";
}
function toggleTypeDropdown() {
elFilterTypeCheckboxes.style.display = isTypeDropdownVisible() ? "none" : "block";
}
function onToggleTypeDropdown(event) {
toggleTypeDropdown();
event.stopPropagation();
}
function toggleTypeFilter(elInput, refresh) {
let type = elInput.getAttribute("filterType");
let typeChecked = elInput.checked;
let typeFilterIndex = typeFilters.indexOf(type);
if (!typeChecked && typeFilterIndex > -1) {
typeFilters.splice(typeFilterIndex, 1);
} else if (typeChecked && typeFilterIndex === -1) {
typeFilters.push(type);
}
if (typeFilters.length === 0) {
elFilterTypeText.innerText = "No Types";
} else if (typeFilters.length === FILTER_TYPES.length) {
elFilterTypeText.innerText = "All Types";
} else {
elFilterTypeText.innerText = "Types...";
}
if (refresh) {
refreshEntityList();
}
}
function onToggleTypeFilter(event) {
let elTarget = event.target;
if (elTarget instanceof HTMLInputElement) {
toggleTypeFilter(elTarget, true);
}
event.stopPropagation();
}
function onBodyClick(event) {
// if clicking anywhere outside of the type filter dropdown (since click event bubbled up to onBodyClick and
// propagation wasn't stopped by onToggleTypeFilter or onToggleTypeDropdown) and the dropdown is open then close it
if (isTypeDropdownVisible()) {
toggleTypeDropdown();
}
}
function toggleInfo(event) {
showExtraInfo = !showExtraInfo;
if (showExtraInfo) {
@ -617,7 +827,7 @@ function loaded() {
entityList.resize();
event.stopPropagation();
}
document.addEventListener("keydown", function (keyDownEvent) {
if (keyDownEvent.target.nodeName === "INPUT") {
return;
@ -641,7 +851,7 @@ function loaded() {
if (data.type === "clearEntityList") {
clearEntities();
} else if (data.type === "selectionUpdate") {
let notFound = updateSelectedEntities(data.selectedIDs);
let notFound = updateSelectedEntities(data.selectedIDs, true);
if (notFound) {
refreshEntities();
}
@ -653,13 +863,13 @@ function loaded() {
clearEntities();
} else {
updateEntityData(newEntities);
updateSelectedEntities(data.selectedIDs);
updateSelectedEntities(data.selectedIDs, true);
}
}
});
} else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) {
removeEntities(data.deletedIDs);
updateSelectedEntities(data.selectedIDs);
updateSelectedEntities(data.selectedIDs, true);
} else if (data.type === "deleted" && data.ids) {
removeEntities(data.ids);
}
@ -672,8 +882,15 @@ function loaded() {
augmentSpinButtons();
// Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked
document.addEventListener("contextmenu", function (event) {
entityListContextMenu.close();
// Disable default right-click context menu which is not visible in the HMD and makes it seem like the app has locked
event.preventDefault();
}, false);
// close context menu when switching focus to another window
$(window).blur(function() {
entityListContextMenu.close();
});
}

View file

@ -0,0 +1,163 @@
//
// entityListContextMenu.js
//
// exampleContextMenus.js was originally created by David Rowe on 22 Aug 2018.
// Modified to entityListContextMenu.js by Thijs Wenker on 10 Oct 2018
// Copyright 2018 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
//
/* eslint-env browser */
const CONTEXT_MENU_CLASS = "context-menu";
/**
* ContextMenu class for EntityList
* @constructor
*/
function EntityListContextMenu() {
this._elContextMenu = null;
this._onSelectedCallback = null;
this._listItems = [];
this._initialize();
}
EntityListContextMenu.prototype = {
/**
* @private
*/
_elContextMenu: null,
/**
* @private
*/
_onSelectedCallback: null,
/**
* @private
*/
_selectedEntityID: null,
/**
* @private
*/
_listItems: null,
/**
* Close the context menu
*/
close: function() {
if (this.isContextMenuOpen()) {
this._elContextMenu.style.display = "none";
}
},
isContextMenuOpen: function() {
return this._elContextMenu.style.display === "block";
},
/**
* Open the context menu
* @param clickEvent
* @param selectedEntityID
* @param enabledOptions
*/
open: function(clickEvent, selectedEntityID, enabledOptions) {
this._selectedEntityID = selectedEntityID;
this._listItems.forEach(function(listItem) {
let enabled = enabledOptions.includes(listItem.label);
listItem.enabled = enabled;
listItem.element.setAttribute('class', enabled ? '' : 'disabled');
});
this._elContextMenu.style.display = "block";
this._elContextMenu.style.left
= Math.min(clickEvent.pageX, document.body.offsetWidth - this._elContextMenu.offsetWidth).toString() + "px";
this._elContextMenu.style.top
= Math.min(clickEvent.pageY, document.body.clientHeight - this._elContextMenu.offsetHeight).toString() + "px";
clickEvent.stopPropagation();
},
/**
* Set the callback for when a menu item is selected
* @param onSelectedCallback
*/
setOnSelectedCallback: function(onSelectedCallback) {
this._onSelectedCallback = onSelectedCallback;
},
/**
* Add a labeled item to the context menu
* @param itemLabel
* @private
*/
_addListItem: function(itemLabel) {
let elListItem = document.createElement("li");
elListItem.innerText = itemLabel;
let listItem = {
label: itemLabel,
element: elListItem,
enabled: false
};
elListItem.addEventListener("click", function () {
if (listItem.enabled && this._onSelectedCallback) {
this._onSelectedCallback.call(this, itemLabel, this._selectedEntityID);
}
}.bind(this), false);
elListItem.setAttribute('class', 'disabled');
this._listItems.push(listItem);
this._elContextMenu.appendChild(elListItem);
},
/**
* Add a separator item to the context menu
* @private
*/
_addListSeparator: function() {
let elListItem = document.createElement("li");
elListItem.setAttribute('class', 'separator');
this._elContextMenu.appendChild(elListItem);
},
/**
* Initialize the context menu.
* @private
*/
_initialize: function() {
this._elContextMenu = document.createElement("ul");
this._elContextMenu.setAttribute("class", CONTEXT_MENU_CLASS);
document.body.appendChild(this._elContextMenu);
this._addListItem("Cut");
this._addListItem("Copy");
this._addListItem("Paste");
this._addListSeparator();
this._addListItem("Rename");
this._addListItem("Duplicate");
this._addListItem("Delete");
// Ignore clicks on context menu background or separator.
this._elContextMenu.addEventListener("click", function(event) {
// Sink clicks on context menu background or separator but let context menu item clicks through.
if (event.target.classList.contains(CONTEXT_MENU_CLASS)) {
event.stopPropagation();
}
});
// Provide means to close context menu without clicking menu item.
document.body.addEventListener("click", this.close.bind(this));
document.body.addEventListener("keydown", function(event) {
// Close context menu with Esc key.
if (this.isContextMenuOpen() && event.key === "Escape") {
this.close();
}
}.bind(this));
}
};

File diff suppressed because it is too large Load diff

View file

@ -38,7 +38,7 @@ function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunctio
this.lastRowShiftScrollTop = 0;
this.initialize();
};
}
ListView.prototype = {
getNumRows: function() {
@ -152,6 +152,30 @@ ListView.prototype = {
this.refresh();
}
},
/**
* Scrolls firstRowIndex with least effort, also tries to make the window include the other selections in case lastRowIndex is set.
* In the case that firstRowIndex and lastRowIndex are already within the visible bounds then nothing will happen.
* @param {number} firstRowIndex - The row that will be scrolled to.
* @param {number} lastRowIndex - The last index of the bound.
*/
scrollToRow: function (firstRowIndex, lastRowIndex) {
lastRowIndex = lastRowIndex ? lastRowIndex : firstRowIndex;
let boundingTop = firstRowIndex * this.rowHeight;
let boundingBottom = (lastRowIndex * this.rowHeight) + this.rowHeight;
if ((boundingBottom - boundingTop) > this.elTableScroll.clientHeight) {
boundingBottom = boundingTop + this.elTableScroll.clientHeight;
}
let currentVisibleAreaTop = this.elTableScroll.scrollTop;
let currentVisibleAreaBottom = currentVisibleAreaTop + this.elTableScroll.clientHeight;
if (boundingTop < currentVisibleAreaTop) {
this.elTableScroll.scrollTop = boundingTop;
} else if (boundingBottom > currentVisibleAreaBottom) {
this.elTableScroll.scrollTop = boundingBottom - (this.elTableScroll.clientHeight);
}
},
refresh: function() {
// block refreshing before rows are initialized

View file

@ -15,7 +15,7 @@ var PROFILING_ENABLED = false;
var profileIndent = '';
const PROFILE_NOOP = function(_name, fn, args) {
fn.apply(this, args);
} ;
};
PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) {
console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin");
var previousIndent = profileIndent;
@ -98,7 +98,11 @@ EntityListTool = function(shouldUseEditTabletApp) {
that.setVisible(!visible);
};
selectionManager.addEventListener(function() {
selectionManager.addEventListener(function(isSelectionUpdate, caller) {
if (caller === that) {
// ignore events that we emitted from the entity list itself
return;
}
var selectedIDs = [];
for (var i = 0; i < selectionManager.selections.length; i++) {
@ -224,7 +228,7 @@ EntityListTool = function(shouldUseEditTabletApp) {
for (var i = 0; i < ids.length; i++) {
entityIDs.push(ids[i]);
}
selectionManager.setSelections(entityIDs);
selectionManager.setSelections(entityIDs, that);
if (data.focus) {
cameraManager.enable();
cameraManager.focus(selectionManager.worldPosition,
@ -245,7 +249,7 @@ EntityListTool = function(shouldUseEditTabletApp) {
Window.saveAsync("Select Where to Save", "", "*.json");
}
} else if (data.type === "pal") {
var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates.
var sessionIds = {}; // Collect the sessionsIds of all selected entities, w/o duplicates.
selectionManager.selections.forEach(function (id) {
var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy;
if (lastEditedBy) {
@ -271,6 +275,19 @@ EntityListTool = function(shouldUseEditTabletApp) {
filterInView = data.filterInView === true;
} else if (data.type === "radius") {
searchRadius = data.radius;
} else if (data.type === "cut") {
SelectionManager.cutSelectedEntities();
} else if (data.type === "copy") {
SelectionManager.copySelectedEntities();
} else if (data.type === "paste") {
SelectionManager.pasteEntities();
} else if (data.type === "duplicate") {
SelectionManager.duplicateSelection();
that.sendUpdate();
} else if (data.type === "rename") {
Entities.editEntity(data.entityID, {name: data.name});
// make sure that the name also gets updated in the properties window
SelectionManager._update();
}
};

View file

@ -40,7 +40,7 @@ SelectionManager = (function() {
Messages.messageReceived.connect(handleEntitySelectionToolUpdates);
}
// FUNCTION: HANDLE ENTITY SELECTION TOOL UDPATES
// FUNCTION: HANDLE ENTITY SELECTION TOOL UPDATES
function handleEntitySelectionToolUpdates(channel, message, sender) {
if (channel !== 'entityToolUpdates') {
return;
@ -63,7 +63,7 @@ SelectionManager = (function() {
if (wantDebug) {
print("setting selection to " + messageParsed.entityID);
}
that.setSelections([messageParsed.entityID]);
that.setSelections([messageParsed.entityID], that);
}
} else if (messageParsed.method === "clearSelection") {
if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) {
@ -136,7 +136,7 @@ SelectionManager = (function() {
return that.selections.length > 0;
};
that.setSelections = function(entityIDs) {
that.setSelections = function(entityIDs, caller) {
that.selections = [];
for (var i = 0; i < entityIDs.length; i++) {
var entityID = entityIDs[i];
@ -144,10 +144,10 @@ SelectionManager = (function() {
Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID);
}
that._update(true);
that._update(true, caller);
};
that.addEntity = function(entityID, toggleSelection) {
that.addEntity = function(entityID, toggleSelection, caller) {
if (entityID) {
var idx = -1;
for (var i = 0; i < that.selections.length; i++) {
@ -165,7 +165,7 @@ SelectionManager = (function() {
}
}
that._update(true);
that._update(true, caller);
};
function removeEntityByID(entityID) {
@ -176,21 +176,21 @@ SelectionManager = (function() {
}
}
that.removeEntity = function (entityID) {
that.removeEntity = function (entityID, caller) {
removeEntityByID(entityID);
that._update(true);
that._update(true, caller);
};
that.removeEntities = function(entityIDs) {
that.removeEntities = function(entityIDs, caller) {
for (var i = 0, length = entityIDs.length; i < length; i++) {
removeEntityByID(entityIDs[i]);
}
that._update(true);
that._update(true, caller);
};
that.clearSelections = function() {
that.clearSelections = function(caller) {
that.selections = [];
that._update(true);
that._update(true, caller);
};
that.addChildrenEntities = function(parentEntityID, entityList) {
@ -353,12 +353,12 @@ SelectionManager = (function() {
}
return createdEntityIDs;
}
};
that.cutSelectedEntities = function() {
copySelectedEntities();
that.copySelectedEntities();
deleteSelectedEntities();
}
};
that.copySelectedEntities = function() {
var entityProperties = Entities.getMultipleEntityProperties(that.selections);
@ -434,7 +434,7 @@ SelectionManager = (function() {
z: brn.z + entityClipboard.dimensions.z / 2
};
}
}
};
that.pasteEntities = function() {
var dimensions = entityClipboard.dimensions;
@ -442,7 +442,7 @@ SelectionManager = (function() {
var pastePosition = getPositionToCreateEntity(maxDimension);
var deltaPosition = Vec3.subtract(pastePosition, entityClipboard.position);
var copiedProperties = []
var copiedProperties = [];
var ids = [];
entityClipboard.entities.forEach(function(originalProperties) {
var properties = deepCopy(originalProperties);
@ -475,9 +475,9 @@ SelectionManager = (function() {
redo(copiedProperties);
undoHistory.pushCommand(undo, copiedProperties, redo, copiedProperties);
}
};
that._update = function(selectionUpdated) {
that._update = function(selectionUpdated, caller) {
var properties = null;
if (that.selections.length === 0) {
that.localDimensions = null;
@ -542,7 +542,7 @@ SelectionManager = (function() {
for (var j = 0; j < listeners.length; j++) {
try {
listeners[j](selectionUpdated === true);
listeners[j](selectionUpdated === true, caller);
} catch (e) {
print("ERROR: entitySelectionTool.update got exception: " + JSON.stringify(e));
}
@ -985,7 +985,7 @@ SelectionDisplay = (function() {
that.pressedHand = NO_HAND;
that.triggered = function() {
return that.triggeredHand !== NO_HAND;
}
};
function pointingAtDesktopWindowOrTablet(hand) {
var pointingAtDesktopWindow = (hand === Controller.Standard.RightHand &&
SelectionManager.pointingAtDesktopWindowRight) ||
@ -1032,7 +1032,7 @@ SelectionDisplay = (function() {
that.disableTriggerMapping = function() {
that.triggerClickMapping.disable();
that.triggerPressMapping.disable();
}
};
Script.scriptEnding.connect(that.disableTriggerMapping);
// FUNCTION DEF(s): Intersection Check Helpers
@ -1234,7 +1234,7 @@ SelectionDisplay = (function() {
if (wantDebug) {
print(" Trigger SelectionManager::update");
}
SelectionManager._update();
SelectionManager._update(false, that);
if (wantDebug) {
print("=============== eST::MouseMoveEvent END =======================");
@ -1299,7 +1299,7 @@ SelectionDisplay = (function() {
lastMouseEvent.isControl = event.isControl;
lastMouseEvent.isAlt = event.isAlt;
activeTool.onMove(lastMouseEvent);
SelectionManager._update();
SelectionManager._update(false, this);
}
};
@ -1315,7 +1315,7 @@ SelectionDisplay = (function() {
lastMouseEvent.isControl = event.isControl;
lastMouseEvent.isAlt = event.isAlt;
activeTool.onMove(lastMouseEvent);
SelectionManager._update();
SelectionManager._update(false, this);
}
};
@ -2179,7 +2179,7 @@ SelectionDisplay = (function() {
}
}
SelectionManager._update();
SelectionManager._update(false, this);
}
});
}
@ -2301,7 +2301,7 @@ SelectionDisplay = (function() {
previousPickRay = pickRay;
SelectionManager._update();
SelectionManager._update(false, this);
}
});
}
@ -2488,7 +2488,7 @@ SelectionDisplay = (function() {
previousPickRay = pickRay;
SelectionManager._update();
SelectionManager._update(false, this);
}
});
}
@ -2599,7 +2599,7 @@ SelectionDisplay = (function() {
previousPickRay = pickRay;
SelectionManager._update();
SelectionManager._update(false, this);
}
});
}

View file

@ -1,709 +0,0 @@
/* global window, document, print, alert, console,setTimeout, clearTimeout, _ $ */
/* eslint no-console: 0 */
/**
UI Builder V1.0
Created by Matti 'Menithal' Lahtinen
24/5/2017
Copyright 2017 High Fidelity, Inc.
This can eventually be expanded to all of Edit, for now, starting
with Particles Only.
This is created for the sole purpose of streamliming the bridge, and to simplify
the logic between an inputfield in WebView and Entities in High Fidelity.
We also do not need anything as heavy as jquery or any other platform,
as we are mostly only building for QT (while, all the other JS frameworks usually do alot of polyfilling)
Available Types:
JSONInputField - Accepts JSON input, once one presses Save, it will be propegated.
Button- A Button that listens for a custom event as defined by callback
Boolean - Creates a checkbox that the user can either check or uncheck
SliderFloat - Creates a slider (with input) that has Float values from min to max.
Default is min 0, max 1
SliderInteger - Creates a slider (with input) that has a Integer value from min to max.
Default is min 1, max 10000
SliderRadian - Creates a slider (with input) that has Float values in degrees,
that are converted to radians. default is min 0, max Math.PI.
Texture - Creates a Image with an url input field that points to texture.
If image cannot form, show "cannot find image"
VecQuaternion - Creates a 3D Vector field that converts to quaternions.
Checkbox exists to show quaternions instead.
Color - Create field color button, that when pressed, opens the color picker.
Vector - Create a 3D Vector field that has one to one correspondence.
The script will use this structure to build a UI that is connected The
id fields within High Fidelity
This should make editing, and everything related much more simpler to maintain,
and If there is any changes to either the Entities or properties of
**/
var RADIANS_PER_DEGREE = Math.PI / 180;
var DEBOUNCE_TIMEOUT = 125;
var roundFloat = function (input, round) {
round = round ? round : 1000;
var sanitizedInput;
if (typeof input === "string") {
sanitizedInput = parseFloat(input);
} else {
sanitizedInput = input;
}
return Math.round(sanitizedInput * round) / round;
};
function HifiEntityUI(parent) {
this.parent = parent;
var self = this;
this.sendPackage = {};
this.settingsUpdateLock = false;
this.webBridgeSync = function(id, val) {
if (!this.settingsUpdateLock) {
this.sendPackage[id] = val;
this.webBridgeSyncDebounce();
}
};
this.webBridgeSyncDebounce = _.debounce(function () {
if (self.EventBridge) {
self.submitChanges(self.sendPackage);
self.sendPackage = {};
}
}, DEBOUNCE_TIMEOUT);
}
HifiEntityUI.prototype = {
setOnSelect: function (callback) {
this.onSelect = callback;
},
submitChanges: function (structure) {
var message = {
messageType: "settings_update",
updatedSettings: structure
};
this.EventBridge.emitWebEvent(JSON.stringify(message));
},
setUI: function (structure) {
this.structure = structure;
},
disableFields: function () {
var fields = document.getElementsByTagName("input");
for (var i = 0; i < fields.length; i++) {
if (fields[i].getAttribute("type") !== "button") {
fields[i].value = "";
}
fields[i].setAttribute("disabled", true);
}
var textures = document.getElementsByTagName("img");
for (i = 0; i < textures.length; i++) {
textures[i].src = "";
}
textures = document.getElementsByClassName("with-texture");
for (i = 0; i < textures.length; i++) {
textures[i].classList.remove("with-textures");
textures[i].classList.add("no-texture");
}
var textareas = document.getElementsByTagName("textarea");
for (var x = 0; x < textareas.length; x++) {
textareas[x].remove();
}
},
getSettings: function () {
var self = this;
var json = {};
var keys = Object.keys(self.builtRows);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var el = self.builtRows[key];
if (el.className.indexOf("checkbox") !== -1) {
json[key] = document.getElementById(key)
.checked ? true : false;
} else if (el.className.indexOf("vector-section") !== -1) {
var vector = {};
if (el.className.indexOf("rgb") !== -1) {
var red = document.getElementById(key + "-red");
var blue = document.getElementById(key + "-blue");
var green = document.getElementById(key + "-green");
vector.red = red.value;
vector.blue = blue.value;
vector.green = green.value;
} else if (el.className.indexOf("pyr") !== -1) {
var p = document.getElementById(key + "-Pitch");
var y = document.getElementById(key + "-Yaw");
var r = document.getElementById(key + "-Roll");
vector.x = p.value;
vector.y = y.value;
vector.z = r.value;
} else {
var x = document.getElementById(key + "-x");
var ey = document.getElementById(key + "-y");
var z = document.getElementById(key + "-z");
vector.x = x.value;
vector.y = ey.value;
vector.z = z.value;
}
json[key] = vector;
} else if (el.className.indexOf("radian") !== -1) {
json[key] = document.getElementById(key).value * RADIANS_PER_DEGREE;
} else if (el.className.length > 0) {
json[key] = document.getElementById(key).value;
}
}
return json;
},
fillFields: function (currentProperties) {
var self = this;
var fields = document.getElementsByTagName("input");
if (!currentProperties.locked) {
for (var i = 0; i < fields.length; i++) {
fields[i].removeAttribute("disabled");
if (fields[i].hasAttribute("data-max")) {
// Reset Max to original max
fields[i].setAttribute("max", fields[i].getAttribute("data-max"));
}
}
}
if (self.onSelect) {
self.onSelect();
}
var keys = Object.keys(currentProperties);
for (var e in keys) {
if (keys.hasOwnProperty(e)) {
var value = keys[e];
var property = currentProperties[value];
var field = self.builtRows[value];
if (field) {
var el = document.getElementById(value);
if (field.className.indexOf("radian") !== -1) {
el.value = property / RADIANS_PER_DEGREE;
el.onchange({
target: el
});
} else if (field.className.indexOf("range") !== -1 || field.className.indexOf("texture") !== -1) {
el.value = property;
el.onchange({
target: el
});
} else if (field.className.indexOf("checkbox") !== -1) {
if (property) {
el.setAttribute("checked", property);
} else {
el.removeAttribute("checked");
}
} else if (field.className.indexOf("vector-section") !== -1) {
if (field.className.indexOf("rgb") !== -1) {
var red = document.getElementById(value + "-red");
var blue = document.getElementById(value + "-blue");
var green = document.getElementById(value + "-green");
red.value = parseInt(property.red);
blue.value = parseInt(property.blue);
green.value = parseInt(property.green);
red.oninput({
target: red
});
} else if (field.className.indexOf("xyz") !== -1) {
var x = document.getElementById(value + "-x");
var y = document.getElementById(value + "-y");
var z = document.getElementById(value + "-z");
x.value = roundFloat(property.x, 100);
y.value = roundFloat(property.y, 100);
z.value = roundFloat(property.z, 100);
} else if (field.className.indexOf("pyr") !== -1) {
var pitch = document.getElementById(value + "-Pitch");
var yaw = document.getElementById(value + "-Yaw");
var roll = document.getElementById(value + "-Roll");
pitch.value = roundFloat(property.x, 100);
yaw.value = roundFloat(property.y, 100);
roll.value = roundFloat(property.z, 100);
}
}
}
}
}
},
connect: function (EventBridge) {
this.EventBridge = EventBridge;
var self = this;
EventBridge.emitWebEvent(JSON.stringify({
messageType: 'page_loaded'
}));
EventBridge.scriptEventReceived.connect(function (data) {
data = JSON.parse(data);
if (data.messageType === 'particle_settings') {
self.settingsUpdateLock = true;
self.fillFields(data.currentProperties);
self.settingsUpdateLock = false;
// Do expected property match with structure;
} else if (data.messageType === 'particle_close') {
self.disableFields();
}
});
},
build: function () {
var self = this;
var sections = Object.keys(this.structure);
this.builtRows = {};
sections.forEach(function (section, index) {
var properties = self.structure[section];
self.addSection(self.parent, section, properties, index);
});
},
addSection: function (parent, section, properties, index) {
var self = this;
var sectionDivHeader = document.createElement("fieldset");
var title = document.createElement("legend");
var dropDown = document.createElement("span");
dropDown.className = "arrow";
sectionDivHeader.className = "major";
title.className = "section-header";
title.id = section + "-section";
title.innerHTML = section;
title.appendChild(dropDown);
sectionDivHeader.appendChild(title);
var collapsed = index !== 0;
dropDown.innerHTML = collapsed ? "L" : "M";
sectionDivHeader.setAttribute("collapsed", collapsed);
parent.appendChild(sectionDivHeader);
var sectionDivBody = document.createElement("div");
sectionDivBody.className = "property-group";
var animationWrapper = document.createElement("div");
animationWrapper.className = "section-wrap";
for (var property in properties) {
if (properties.hasOwnProperty(property)) {
var builtRow = self.addElement(animationWrapper, properties[property]);
var id = properties[property].id;
if (id) {
self.builtRows[id] = builtRow;
}
}
}
sectionDivBody.appendChild(animationWrapper);
sectionDivHeader.appendChild(sectionDivBody);
_.defer(function () {
var height = (animationWrapper.clientHeight) + "px";
if (collapsed) {
sectionDivBody.classList.remove("visible");
sectionDivBody.style.maxHeight = "0px";
} else {
sectionDivBody.classList.add("visible");
sectionDivBody.style.maxHeight = height;
}
title.onclick = function () {
collapsed = !collapsed;
if (collapsed) {
sectionDivBody.classList.remove("visible");
sectionDivBody.style.maxHeight = "0px";
} else {
sectionDivBody.classList.add("visible");
sectionDivBody.style.maxHeight = (animationWrapper.clientHeight) + "px";
}
// sectionDivBody.style.display = collapsed ? "none": "block";
dropDown.innerHTML = collapsed ? "L" : "M";
title.setAttribute("collapsed", collapsed);
};
});
},
addLabel: function (parent, group) {
var label = document.createElement("label");
label.innerHTML = group.name;
parent.appendChild(label);
if (group.unit) {
var span = document.createElement("span");
span.innerHTML = group.unit;
span.className = "unit";
label.appendChild(span);
}
return label;
},
addVector: function (parent, group, labels, domArray) {
var self = this;
var inputs = labels ? labels : ["x", "y", "z"];
domArray = domArray ? domArray : [];
parent.id = group.id;
for (var index in inputs) {
var element = document.createElement("input");
element.setAttribute("type", "number");
element.className = inputs[index];
element.id = group.id + "-" + inputs[index];
if (group.defaultRange) {
if (group.defaultRange.min) {
element.setAttribute("min", group.defaultRange.min);
}
if (group.defaultRange.max) {
element.setAttribute("max", group.defaultRange.max);
}
if (group.defaultRange.step) {
element.setAttribute("step", group.defaultRange.step);
}
}
if (group.oninput) {
element.oninput = group.oninput;
} else {
element.oninput = function (event) {
self.webBridgeSync(group.id, {
x: domArray[0].value,
y: domArray[1].value,
z: domArray[2].value
});
};
}
element.onchange = element.oninput;
domArray.push(element);
}
this.addLabel(parent, group);
var className = "";
for (var i = 0; i < inputs.length; i++) {
className += inputs[i].charAt(0)
.toLowerCase();
}
parent.className += " property vector-section " + className;
// Add Tuple and the rest
var tupleContainer = document.createElement("div");
tupleContainer.className = "tuple";
for (var domIndex in domArray) {
var container = domArray[domIndex];
var div = document.createElement("div");
var label = document.createElement("label");
label.innerHTML = inputs[domIndex] + ":";
label.setAttribute("for", container.id);
div.appendChild(container);
div.appendChild(label);
tupleContainer.appendChild(div);
}
parent.appendChild(tupleContainer);
},
addVectorQuaternion: function (parent, group) {
this.addVector(parent, group, ["Pitch", "Yaw", "Roll"]);
},
addColorPicker: function (parent, group) {
var self = this;
var $colPickContainer = $('<div>', {
id: group.id,
class: "color-picker"
});
var updateColors = function (red, green, blue) {
$colPickContainer.css('background-color', "rgb(" +
red + "," +
green + "," +
blue + ")");
};
var inputs = ["red", "green", "blue"];
var domArray = [];
group.oninput = function (event) {
$colPickContainer.colpickSetColor(
{
r: domArray[0].value,
g: domArray[1].value,
b: domArray[2].value
},
true);
};
group.defaultRange = {
min: 0,
max: 255,
step: 1
};
parent.appendChild($colPickContainer[0]);
self.addVector(parent, group, inputs, domArray);
updateColors(domArray[0].value, domArray[1].value, domArray[2].value);
// Could probably write a custom one for this to completely write out jquery,
// but for now, using the same as earlier.
/* Color Picker Logic Here */
$colPickContainer.colpick({
colorScheme: (group.layoutColorScheme === undefined ? 'dark' : group.layoutColorScheme),
layout: (group.layoutType === undefined ? 'hex' : group.layoutType),
submit: (group.useSubmitButton === undefined ? true : group.useSubmitButton),
color: {
r: domArray[0].value,
g: domArray[1].value,
b: domArray[2].value
},
onChange: function (hsb, hex, rgb, el) {
updateColors(rgb.r, rgb.g, rgb.b);
domArray[0].value = rgb.r;
domArray[1].value = rgb.g;
domArray[2].value = rgb.b;
self.webBridgeSync(group.id, {
red: rgb.r,
green: rgb.g,
blue: rgb.b
});
},
onSubmit: function (hsb, hex, rgb, el) {
$(el)
.css('background-color', '#' + hex);
$(el)
.colpickHide();
domArray[0].value = rgb.r;
domArray[1].value = rgb.g;
domArray[2].value = rgb.b;
self.webBridgeSync(group.id, {
red: rgb.r,
green: rgb.g,
blue: rgb.b
});
}
});
},
addTextureField: function (parent, group) {
var self = this;
this.addLabel(parent, group);
parent.className += " property texture";
var textureImage = document.createElement("div");
var textureUrl = document.createElement("input");
textureUrl.setAttribute("type", "text");
textureUrl.id = group.id;
textureImage.className = "texture-image no-texture";
var image = document.createElement("img");
var imageLoad = _.debounce(function (url) {
if (url.slice(0, 5).toLowerCase() === "atp:/") {
image.src = "";
image.style.display = "none";
textureImage.classList.remove("with-texture");
textureImage.classList.remove("no-texture");
textureImage.classList.add("no-preview");
} else if (url.length > 0) {
textureImage.classList.remove("no-texture");
textureImage.classList.remove("no-preview");
textureImage.classList.add("with-texture");
image.src = url;
image.style.display = "block";
} else {
image.src = "";
image.style.display = "none";
textureImage.classList.remove("with-texture");
textureImage.classList.remove("no-preview");
textureImage.classList.add("no-texture");
}
}, DEBOUNCE_TIMEOUT * 2);
textureUrl.oninput = function (event) {
// Add throttle
var url = event.target.value;
imageLoad(url);
self.webBridgeSync(group.id, url);
};
textureUrl.onchange = textureUrl.oninput;
textureImage.appendChild(image);
parent.appendChild(textureImage);
parent.appendChild(textureUrl);
},
addSlider: function (parent, group) {
var self = this;
this.addLabel(parent, group);
parent.className += " property range";
var container = document.createElement("div");
container.className = "slider-wrapper";
var slider = document.createElement("input");
slider.setAttribute("type", "range");
var inputField = document.createElement("input");
inputField.setAttribute("type", "number");
container.appendChild(slider);
container.appendChild(inputField);
parent.appendChild(container);
if (group.type === "SliderInteger") {
inputField.setAttribute("min", group.min !== undefined ? group.min : 0);
inputField.setAttribute("step", 1);
slider.setAttribute("min", group.min !== undefined ? group.min : 0);
slider.setAttribute("max", group.max !== undefined ? group.max : 10000);
slider.setAttribute("data-max", group.max !== undefined ? group.max : 10000);
slider.setAttribute("step", 1);
inputField.oninput = function (event) {
// TODO: Remove this functionality? Alan finds it confusing
if (parseInt(event.target.value) > parseInt(slider.getAttribute("max")) && group.max !== 1) {
slider.setAttribute("max", event.target.value);
}
slider.value = event.target.value;
self.webBridgeSync(group.id, slider.value);
};
inputField.onchange = inputField.oninput;
slider.oninput = function (event) {
inputField.value = event.target.value;
self.webBridgeSync(group.id, inputField.value);
};
inputField.id = group.id;
} else if (group.type === "SliderRadian") {
slider.setAttribute("min", group.min !== undefined ? group.min : 0);
slider.setAttribute("max", group.max !== undefined ? group.max : 180);
slider.setAttribute("step", 1);
parent.className += " radian";
inputField.setAttribute("min", (group.min !== undefined ? group.min : 0));
inputField.setAttribute("max", (group.max !== undefined ? group.max : 180));
inputField.oninput = function (event) {
slider.value = event.target.value;
self.webBridgeSync(group.id, slider.value * RADIANS_PER_DEGREE);
};
inputField.onchange = inputField.oninput;
inputField.id = group.id;
slider.oninput = function (event) {
if (event.target.value > 0) {
inputField.value = Math.floor(event.target.value);
} else {
inputField.value = Math.ceil(event.target.value);
}
self.webBridgeSync(group.id, inputField.value * RADIANS_PER_DEGREE);
};
var degrees = document.createElement("label");
degrees.innerHTML = "&#176;";
degrees.style.fontSize = "1.4rem";
degrees.style.display = "inline";
degrees.style.verticalAlign = "top";
degrees.style.paddingLeft = "0.4rem";
container.appendChild(degrees);
} else {
// Must then be Float
inputField.setAttribute("min", group.min !== undefined ? group.min : 0);
slider.setAttribute("step", 0.01);
slider.setAttribute("min", group.min !== undefined ? group.min : 0);
slider.setAttribute("max", group.max !== undefined ? group.max : 1);
slider.setAttribute("data-max", group.max !== undefined ? group.max : 1);
slider.setAttribute("step", 0.01);
inputField.oninput = function (event) {
// TODO: Remove this functionality? Alan finds it confusing
if (parseFloat(event.target.value) > parseFloat(slider.getAttribute("max")) && group.max !== 1) {
slider.setAttribute("max", event.target.value);
}
slider.value = event.target.value;
self.webBridgeSync(group.id, slider.value);
// bind web sock update here.
};
inputField.onchange = inputField.oninput;
slider.oninput = function (event) {
inputField.value = event.target.value;
self.webBridgeSync(group.id, inputField.value);
};
inputField.id = group.id;
}
// UpdateBinding
},
addCheckBox: function (parent, group) {
var checkBox = document.createElement("input");
checkBox.setAttribute("type", "checkbox");
var self = this;
checkBox.onchange = function (event) {
self.webBridgeSync(group.id, event.target.checked);
};
checkBox.id = group.id;
parent.appendChild(checkBox);
var label = this.addLabel(parent, group);
label.setAttribute("for", checkBox.id);
parent.className += " property checkbox";
},
addElement: function (parent, group) {
var self = this;
var property = document.createElement("div");
property.id = group.id;
var row = document.createElement("div");
switch (group.type) {
case "Button":
var button = document.createElement("input");
button.setAttribute("type", "button");
button.id = group.id;
if (group.disabled) {
button.disabled = group.disabled;
}
button.className = group.class;
button.value = group.name;
button.onclick = group.callback;
parent.appendChild(button);
break;
case "Row":
var hr = document.createElement("hr");
hr.className = "splitter";
if (group.id) {
hr.id = group.id;
}
parent.appendChild(hr);
break;
case "Boolean":
self.addCheckBox(row, group);
parent.appendChild(row);
break;
case "SliderFloat":
case "SliderInteger":
case "SliderRadian":
self.addSlider(row, group);
parent.appendChild(row);
break;
case "Texture":
self.addTextureField(row, group);
parent.appendChild(row);
break;
case "Color":
self.addColorPicker(row, group);
parent.appendChild(row);
break;
case "Vector":
self.addVector(row, group);
parent.appendChild(row);
break;
case "VectorQuaternion":
self.addVectorQuaternion(row, group);
parent.appendChild(row);
break;
default:
console.log("not defined");
}
return row;
}
};

File diff suppressed because one or more lines are too long

View file

@ -1,43 +0,0 @@
<!--
// particleExplorer.hml
//
//
// Created by James B. Pollack @imgntn on 9/26/2015
// Copyright 2015 High Fidelity, Inc.
//
// Reworked by Menithal on 20/5/2017
// Using a custom built system for High Fidelity
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
-->
<html>
<head>
<link rel="stylesheet" type="text/css" href="../html/css/colpick.css">
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="../html/js/eventBridgeLoader.js"></script>
<!---->
<script type="text/javascript" src="../html/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="../html/js/colpick.js"></script>
<script type="text/javascript" src="underscore-min.js"></script>
<script type="text/javascript" src="hifi-entity-ui.js?v1"></script>
<link rel="stylesheet" type="text/css" href="../html/css/hifi-style.css">
<link rel="stylesheet" type="text/css" href="../html/css/edit-style.css">
<link rel="stylesheet" type="text/css" href="../html/css/colpick.css">
<link rel="stylesheet" type="text/css" href="particle-style.css">
</head>
<body>
<div id="properties-list">
<div class="section-header" id="main-header">
<label> Particle Explorer </label>
</div>
<!-- This will be filled by the script! -->
</div>
<div id="rem"></div>
<script type="text/javascript" src="particleExplorer.js"></script>
</body>
</html>

View file

@ -1,485 +0,0 @@
//
// particleExplorer.js
//
// Created by James B. Pollack @imgntn on 9/26/2015
// Copyright 2017 High Fidelity, Inc.
//
// Reworked by Menithal on 20/5/2017
// Reworked by Daniela Fontes and Artur Gomes (Mimicry) on 12/18/2017
//
// Web app side of the App - contains GUI.
// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* global HifiEntityUI, openEventBridge, console, EventBridge, document, window */
/* eslint no-console: 0, no-global-assign: 0 */
(function () {
var root = document.getElementById("properties-list");
window.onload = function () {
var ui = new HifiEntityUI(root);
var textarea = document.createElement("textarea");
var properties = "";
var menuStructure = {
General: [
{
type: "Row",
id: "export-import-field"
},
{
id: "show-properties-button",
name: "Show Properties",
type: "Button",
class: "blue",
disabled: true,
callback: function (event) {
var insertZone = document.getElementById("export-import-field");
var json = ui.getSettings();
properties = JSON.stringify(json);
textarea.value = properties;
if (!insertZone.contains(textarea)) {
insertZone.appendChild(textarea);
insertZone.parentNode.parentNode.style.maxHeight =
insertZone.parentNode.clientHeight + "px";
document.getElementById("export-properties-button").removeAttribute("disabled");
textarea.onchange = function (e) {
if (e.target.value !== properties) {
document.getElementById("import-properties-button").removeAttribute("disabled");
}
};
textarea.oninput = textarea.onchange;
document.getElementById("show-properties-button").value = "Hide Properties";
} else {
textarea.onchange = function () {};
textarea.oninput = textarea.onchange;
textarea.value = "";
textarea.remove();
insertZone.parentNode.parentNode.style.maxHeight =
insertZone.parentNode.clientHeight + "px";
document.getElementById("export-properties-button").setAttribute("disabled", true);
document.getElementById("import-properties-button").setAttribute("disabled", true);
document.getElementById("show-properties-button").value = "Show Properties";
}
}
},
{
id: "import-properties-button",
name: "Import",
type: "Button",
class: "blue",
disabled: true,
callback: function (event) {
ui.fillFields(JSON.parse(textarea.value));
ui.submitChanges(JSON.parse(textarea.value));
}
},
{
id: "export-properties-button",
name: "Export",
type: "Button",
class: "red",
disabled: true,
callback: function (event) {
textarea.select();
try {
var success = document.execCommand('copy');
if (!success) {
throw "Not success :(";
}
} catch (e) {
print("couldnt copy field");
}
}
},
{
type: "Row"
},
{
id: "isEmitting",
name: "Is Emitting",
type: "Boolean"
},
{
type: "Row"
},
{
id: "lifespan",
name: "Lifespan",
type: "SliderFloat",
min: 0.01,
max: 10
},
{
type: "Row"
},
{
id: "maxParticles",
name: "Max Particles",
type: "SliderInteger",
min: 1,
max: 10000
},
{
type: "Row"
},
{
id: "textures",
name: "Textures",
type: "Texture"
},
{
type: "Row"
}
],
Emit: [
{
id: "emitRate",
name: "Emit Rate",
type: "SliderInteger",
max: 1000,
min: 1
},
{
type: "Row"
},
{
id: "emitSpeed",
name: "Emit Speed",
type: "SliderFloat",
max: 5
},
{
id: "speedSpread",
name: "Speed Spread",
type: "SliderFloat",
max: 5
},
{
type: "Row"
},
{
id: "emitDimensions",
name: "Emit Dimension",
type: "Vector",
defaultRange: {
min: 0,
step: 0.01
}
},
{
type: "Row"
},
{
id: "emitOrientation",
unit: "deg",
name: "Emit Orientation",
type: "VectorQuaternion",
defaultRange: {
min: 0,
step: 0.01
}
},
{
type: "Row"
},
{
id: "emitterShouldTrail",
name: "Emitter Should Trail",
type: "Boolean"
},
{
type: "Row"
}
],
Radius: [
{
id: "particleRadius",
name: "Particle Radius",
type: "SliderFloat",
max: 4.0
},
{
type: "Row"
},
{
id: "radiusSpread",
name: "Radius Spread",
type: "SliderFloat",
max: 4.0
},
{
type: "Row"
},
{
id: "radiusStart",
name: "Radius Start",
type: "SliderFloat",
max: 4.0
},
{
type: "Row"
},
{
id: "radiusFinish",
name: "Radius Finish",
type: "SliderFloat",
max: 4.0
},
{
type: "Row"
}
],
Color: [
{
id: "color",
name: "Color",
type: "Color",
defaultColor: {
red: 255,
green: 255,
blue: 255
},
layoutType: "hex",
layoutColorScheme: "dark",
useSubmitButton: false
},
{
type: "Row"
},
{
id: "colorSpread",
name: "Color Spread",
type: "Color",
defaultColor: {
red: 0,
green: 0,
blue: 0
},
layoutType: "hex",
layoutColorScheme: "dark",
useSubmitButton: false
},
{
type: "Row"
},
{
id: "colorStart",
name: "Color Start",
type: "Color",
defaultColor: {
red: 255,
green: 255,
blue: 255
},
layoutType: "hex",
layoutColorScheme: "dark",
useSubmitButton: false
},
{
type: "Row"
},
{
id: "colorFinish",
name: "Color Finish",
type: "Color",
defaultColor: {
red: 255,
green: 255,
blue: 255
},
layoutType: "hex",
layoutColorScheme: "dark",
useSubmitButton: false
},
{
type: "Row"
}
],
Acceleration: [
{
id: "emitAcceleration",
name: "Emit Acceleration",
type: "Vector",
defaultRange: {
step: 0.01
}
},
{
type: "Row"
},
{
id: "accelerationSpread",
name: "Acceleration Spread",
type: "Vector",
defaultRange: {
step: 0.01
}
},
{
type: "Row"
}
],
Alpha: [
{
id: "alpha",
name: "Alpha",
type: "SliderFloat",
max: 1.0
},
{
type: "Row"
},
{
id: "alphaSpread",
name: "Alpha Spread",
type: "SliderFloat",
max: 1.0
},
{
type: "Row"
},
{
id: "alphaStart",
name: "Alpha Start",
type: "SliderFloat",
max: 1.0
},
{
type: "Row"
},
{
id: "alphaFinish",
name: "Alpha Finish",
type: "SliderFloat",
max: 1.0
},
{
type: "Row"
}
],
Spin: [
{
id: "particleSpin",
name: "Particle Spin",
type: "SliderRadian",
min: -360.0,
max: 360.0
},
{
type: "Row"
},
{
id: "spinSpread",
name: "Spin Spread",
type: "SliderRadian",
max: 360.0
},
{
type: "Row"
},
{
id: "spinStart",
name: "Spin Start",
type: "SliderRadian",
min: -360.0,
max: 360.0
},
{
type: "Row"
},
{
id: "spinFinish",
name: "Spin Finish",
type: "SliderRadian",
min: -360.0,
max: 360.0
},
{
type: "Row"
},
{
id: "rotateWithEntity",
name: "Rotate with Entity",
type: "Boolean"
},
{
type: "Row"
}
],
Polar: [
{
id: "polarStart",
name: "Polar Start",
unit: "deg",
type: "SliderRadian"
},
{
type: "Row"
},
{
id: "polarFinish",
name: "Polar Finish",
unit: "deg",
type: "SliderRadian"
},
{
type: "Row"
}
],
Azimuth: [
{
id: "azimuthStart",
name: "Azimuth Start",
unit: "deg",
type: "SliderRadian",
min: -180,
max: 0
},
{
type: "Row"
},
{
id: "azimuthFinish",
name: "Azimuth Finish",
unit: "deg",
type: "SliderRadian"
},
{
type: "Row"
}
]
};
ui.setUI(menuStructure);
ui.setOnSelect(function () {
document.getElementById("show-properties-button").removeAttribute("disabled");
document.getElementById("export-properties-button").setAttribute("disabled", true);
document.getElementById("import-properties-button").setAttribute("disabled", true);
});
ui.build();
var overrideLoad = false;
if (openEventBridge === undefined) {
overrideLoad = true,
openEventBridge = function (callback) {
callback({
emitWebEvent: function () {},
submitChanges: function () {},
scriptEventReceived: {
connect: function () {
}
}
});
};
}
openEventBridge(function (EventBridge) {
ui.connect(EventBridge);
});
if (overrideLoad) {
openEventBridge();
}
};
})();

View file

@ -1,144 +0,0 @@
//
// particleExplorerTool.js
//
// Created by Eric Levin on 2/15/16
// Copyright 2016 High Fidelity, Inc.
// Adds particleExplorer tool to the edit panel when a user selects a particle entity from the edit tool window
// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* global ParticleExplorerTool */
var PARTICLE_EXPLORER_HTML_URL = Script.resolvePath('particleExplorer.html');
ParticleExplorerTool = function(createToolsWindow) {
var that = {};
that.activeParticleEntity = 0;
that.updatedActiveParticleProperties = {};
that.createWebView = function() {
that.webView = Tablet.getTablet("com.highfidelity.interface.tablet.system");
that.webView.setVisible = function(value) {};
that.webView.webEventReceived.connect(that.webEventReceived);
createToolsWindow.webEventReceived.addListener(this, that.webEventReceived);
};
function emitScriptEvent(data) {
var messageData = JSON.stringify(data);
that.webView.emitScriptEvent(messageData);
createToolsWindow.emitScriptEvent(messageData);
}
that.destroyWebView = function() {
if (!that.webView) {
return;
}
that.activeParticleEntity = 0;
that.updatedActiveParticleProperties = {};
emitScriptEvent({
messageType: "particle_close"
});
};
function sendParticleProperties(properties) {
emitScriptEvent({
messageType: "particle_settings",
currentProperties: properties
});
}
function sendActiveParticleProperties() {
var properties = Entities.getEntityProperties(that.activeParticleEntity);
if (properties.emitOrientation) {
properties.emitOrientation = Quat.safeEulerAngles(properties.emitOrientation);
}
// Update uninitialized variables
if (isNaN(properties.alphaStart)) {
properties.alphaStart = properties.alpha;
}
if (isNaN(properties.alphaFinish)) {
properties.alphaFinish = properties.alpha;
}
if (isNaN(properties.radiusStart)) {
properties.radiusStart = properties.particleRadius;
}
if (isNaN(properties.radiusFinish)) {
properties.radiusFinish = properties.particleRadius;
}
if (isNaN(properties.colorStart.red)) {
properties.colorStart = properties.color;
}
if (isNaN(properties.colorFinish.red)) {
properties.colorFinish = properties.color;
}
if (isNaN(properties.spinStart)) {
properties.spinStart = properties.particleSpin;
}
if (isNaN(properties.spinFinish)) {
properties.spinFinish = properties.particleSpin;
}
sendParticleProperties(properties);
}
function sendUpdatedActiveParticleProperties() {
sendParticleProperties(that.updatedActiveParticleProperties);
that.updatedActiveParticleProperties = {};
}
that.webEventReceived = function(message) {
var data = JSON.parse(message);
if (data.messageType === "settings_update") {
var updatedSettings = data.updatedSettings;
var optionalProps = ["alphaStart", "alphaFinish", "radiusStart", "radiusFinish", "colorStart", "colorFinish", "spinStart", "spinFinish"];
var fallbackProps = ["alpha", "particleRadius", "color", "particleSpin"];
for (var i = 0; i < optionalProps.length; i++) {
var fallbackProp = fallbackProps[Math.floor(i / 2)];
var optionalValue = updatedSettings[optionalProps[i]];
var fallbackValue = updatedSettings[fallbackProp];
if (optionalValue && fallbackValue) {
delete updatedSettings[optionalProps[i]];
}
}
if (updatedSettings.emitOrientation) {
updatedSettings.emitOrientation = Quat.fromVec3Degrees(updatedSettings.emitOrientation);
}
Entities.editEntity(that.activeParticleEntity, updatedSettings);
var entityProps = Entities.getEntityProperties(that.activeParticleEntity, optionalProps);
var needsUpdate = false;
for (var i = 0; i < optionalProps.length; i++) {
var fallbackProp = fallbackProps[Math.floor(i / 2)];
var fallbackValue = updatedSettings[fallbackProp];
if (fallbackValue) {
var optionalProp = optionalProps[i];
if ((fallbackProp !== "color" && isNaN(entityProps[optionalProp])) || (fallbackProp === "color" && isNaN(entityProps[optionalProp].red))) {
that.updatedActiveParticleProperties[optionalProp] = fallbackValue;
needsUpdate = true;
}
}
}
if (needsUpdate) {
sendUpdatedActiveParticleProperties();
}
} else if (data.messageType === "page_loaded") {
sendActiveParticleProperties();
}
};
that.setActiveParticleEntity = function(id) {
that.activeParticleEntity = id;
sendActiveParticleProperties();
};
return that;
};

View file

@ -874,10 +874,6 @@ function onContentLoaded() {
hasShownUpdateNotification = true;
}
});
notifier.on('click', function(notifierObject, options) {
log.debug("Got click", options.url);
shell.openExternal(options.url);
});
}
deleteOldFiles(logPath, DELETE_LOG_FILES_OLDER_THAN_X_SECONDS, LOG_FILE_REGEX);

View file

@ -5,6 +5,8 @@ const process = require('process');
const hfApp = require('./hf-app');
const path = require('path');
const AccountInfo = require('./hf-acctinfo').AccountInfo;
const url = require('url');
const shell = require('electron').shell;
const GetBuildInfo = hfApp.getBuildInfo;
const buildInfo = GetBuildInfo();
const osType = os.type();
@ -154,8 +156,13 @@ function HifiNotifications(config, menuNotificationCallback) {
var _menuNotificationCallback = menuNotificationCallback;
notifier.on('click', function (notifierObject, options) {
StartInterface(options.url);
_menuNotificationCallback(options.notificationType, false);
const optUrl = url.parse(options.url);
if ((optUrl.protocol === "hifi:") || (optUrl.protocol === "hifiapp:")) {
StartInterface(options.url);
_menuNotificationCallback(options.notificationType, false);
} else {
shell.openExternal(options.url);
}
});
}

View file

@ -0,0 +1,77 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
import QtQuick 2.2
import QtQuick.Layouts 1.1
import QtQuick.Dialogs 1.1
import QtQuick.Controls 1.2
import QtWebEngine 1.5
Item {
width: 640
height: 480
Rectangle {
width: 5
height: 5
color: "red"
ColorAnimation on color { loops: Animation.Infinite; from: "red"; to: "yellow"; duration: 1000 }
}
WebEngineView {
id: root
url: "https://google.com/"
x: 6; y: 6;
width: parent.width * 0.8
height: parent.height * 0.8
}
}

View file

@ -0,0 +1,60 @@
#include "MacQml.h"
#include <cmath>
#include <QtQuick/QQuickItem>
#include <SharedUtil.h>
using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence;
void MacQml::update() {
auto rootItem =_surface->getRootItem();
float now = sinf(secTimestampNow());
rootItem->setProperty("level", fabs(now));
rootItem->setProperty("muted", now > 0.0f);
rootItem->setProperty("statsValue", rand());
// Fetch any new textures
TextureAndFence newTextureAndFence;
if (_surface->fetchTexture(newTextureAndFence)) {
if (_texture != 0) {
auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
glFlush();
_discardLamdba(_texture, readFence);
}
_texture = newTextureAndFence.first;
_glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED);
}
}
void MacQml::init() {
Parent::init();
_glf.glGenFramebuffers(1, &_fbo);
_surface.reset(new hifi::qml::OffscreenSurface());
//QUrl url =getTestResource("qml/main.qml");
QUrl url = getTestResource("qml/MacQml.qml");
hifi::qml::QmlContextObjectCallback callback =[](QQmlContext* context, QQuickItem* item) {
};
_surface->load(url, callback);
_surface->resize(_window->size());
_surface->resume();
}
void MacQml::draw() {
auto size = _window->geometry().size();
if (_texture) {
_glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo);
_glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
_glf.glBlitFramebuffer(
// src coordinates
0, 0, size.width(), size.height(),
// dst coordinates
0, 0, size.width(), size.height(),
// blit mask and filter
GL_COLOR_BUFFER_BIT, GL_NEAREST);
_glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
_glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
}
}

Some files were not shown because too many files have changed in this diff Show more