3
0
Fork 0
mirror of https://github.com/lubosz/overte.git synced 2025-04-27 21:55:28 +02:00

Merge branch 'master' into multiSphereAvatar04

# Conflicts:
#	interface/src/avatar/OtherAvatar.h
#	libraries/avatars-renderer/src/avatars-renderer/Avatar.h
This commit is contained in:
luiscuenca 2019-01-08 15:41:33 -07:00
commit 39dc25ea92
252 changed files with 11421 additions and 5162 deletions
.gitignore
android
assignment-client
interface
libraries

1
.gitignore vendored
View file

@ -98,6 +98,7 @@ tools/jsdoc/package-lock.json
# ignore unneeded unity project files for avatar exporter
tools/unity-avatar-exporter/Library
tools/unity-avatar-exporter/Logs
tools/unity-avatar-exporter/Packages
tools/unity-avatar-exporter/ProjectSettings
tools/unity-avatar-exporter/Temp

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -xeuo pipefail
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}

View file

@ -9,14 +9,19 @@ docker run \
--rm \
--security-opt seccomp:unconfined \
-v "${WORKSPACE}":/home/jenkins/hifi \
-e "RELEASE_NUMBER=${RELEASE_NUMBER}" \
-e "RELEASE_TYPE=${RELEASE_TYPE}" \
-e "ANDROID_BUILD_TARGET=assembleDebug" \
-e "CMAKE_BACKTRACE_URL=${CMAKE_BACKTRACE_URL}" \
-e "CMAKE_BACKTRACE_TOKEN=${CMAKE_BACKTRACE_TOKEN}" \
-e "CMAKE_BACKTRACE_SYMBOLS_TOKEN=${CMAKE_BACKTRACE_SYMBOLS_TOKEN}" \
-e "GA_TRACKING_ID=${GA_TRACKING_ID}" \
-e "GIT_PR_COMMIT=${GIT_PR_COMMIT}" \
-e "VERSION_CODE=${VERSION_CODE}" \
-e RELEASE_NUMBER \
-e RELEASE_TYPE \
-e ANDROID_BUILD_TARGET \
-e ANDROID_BUILD_DIR \
-e CMAKE_BACKTRACE_URL \
-e CMAKE_BACKTRACE_TOKEN \
-e CMAKE_BACKTRACE_SYMBOLS_TOKEN \
-e GA_TRACKING_ID \
-e OAUTH_CLIENT_SECRET \
-e OAUTH_CLIENT_ID \
-e OAUTH_REDIRECT_URI \
-e VERSION_CODE \
"${DOCKER_IMAGE_NAME}" \
sh -c "./build_android.sh"

View file

@ -14,6 +14,7 @@ link_hifi_libraries(
audio avatars octree gpu graphics shaders fbx hfm entities
networking animation recording shared script-engine embedded-webserver
controllers physics plugins midi image
model-networking ktx shaders
)
add_dependencies(${TARGET_NAME} oven)

View file

@ -276,6 +276,7 @@ void AssignmentClientMonitor::checkSpares() {
// Spawn or kill children, as needed. If --min or --max weren't specified, allow the child count
// to drift up or down as far as needed.
if (spareCount < 1 || totalCount < _minAssignmentClientForks) {
if (!_maxAssignmentClientForks || totalCount < _maxAssignmentClientForks) {
spawnChildClient();
@ -307,7 +308,7 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer<ReceivedMes
AssignmentClientChildData* childData = nullptr;
if (!matchingNode) {
// The parent only expects to be talking with prorams running on this same machine.
// The parent only expects to be talking with programs running on this same machine.
if (senderSockAddr.getAddress() == QHostAddress::LocalHost ||
senderSockAddr.getAddress() == QHostAddress::LocalHostIPv6) {
@ -316,9 +317,9 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer<ReceivedMes
matchingNode = DependencyManager::get<LimitedNodeList>()->addOrUpdateNode(senderID, NodeType::Unassigned,
senderSockAddr, senderSockAddr);
auto childData = std::unique_ptr<AssignmentClientChildData>
auto newChildData = std::unique_ptr<AssignmentClientChildData>
{ new AssignmentClientChildData(Assignment::Type::AllTypes) };
matchingNode->setLinkedData(std::move(childData));
matchingNode->setLinkedData(std::move(newChildData));
} else {
// tell unknown assignment-client child to exit.
qDebug() << "Asking unknown child at" << senderSockAddr << "to exit.";
@ -329,9 +330,8 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer<ReceivedMes
return;
}
}
} else {
childData = dynamic_cast<AssignmentClientChildData*>(matchingNode->getLinkedData());
}
childData = dynamic_cast<AssignmentClientChildData*>(matchingNode->getLinkedData());
if (childData) {
// update our records about how to reach this child

View file

@ -915,59 +915,52 @@ void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, Sha
void AssetServer::sendStatsPacket() {
QJsonObject serverStats;
auto stats = DependencyManager::get<NodeList>()->sampleStatsForAllConnections();
auto nodeList = DependencyManager::get<NodeList>();
nodeList->eachNode([&](auto& node) {
auto& stats = node->getConnectionStats();
for (const auto& stat : stats) {
QJsonObject nodeStats;
auto endTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stat.second.endTime);
auto endTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stats.endTime);
QDateTime date = QDateTime::fromMSecsSinceEpoch(endTimeMs.count());
static const float USEC_PER_SEC = 1000000.0f;
static const float MEGABITS_PER_BYTE = 8.0f / 1000000.0f; // Bytes => Mbits
float elapsed = (float)(stat.second.endTime - stat.second.startTime).count() / USEC_PER_SEC; // sec
float elapsed = (float)(stats.endTime - stats.startTime).count() / USEC_PER_SEC; // sec
float megabitsPerSecPerByte = MEGABITS_PER_BYTE / elapsed; // Bytes => Mb/s
QJsonObject connectionStats;
connectionStats["1. Last Heard"] = date.toString();
connectionStats["2. Est. Max (P/s)"] = stat.second.estimatedBandwith;
connectionStats["3. RTT (ms)"] = stat.second.rtt;
connectionStats["4. CW (P)"] = stat.second.congestionWindowSize;
connectionStats["5. Period (us)"] = stat.second.packetSendPeriod;
connectionStats["6. Up (Mb/s)"] = stat.second.sentBytes * megabitsPerSecPerByte;
connectionStats["7. Down (Mb/s)"] = stat.second.receivedBytes * megabitsPerSecPerByte;
connectionStats["2. Est. Max (P/s)"] = stats.estimatedBandwith;
connectionStats["3. RTT (ms)"] = stats.rtt;
connectionStats["4. CW (P)"] = stats.congestionWindowSize;
connectionStats["5. Period (us)"] = stats.packetSendPeriod;
connectionStats["6. Up (Mb/s)"] = stats.sentBytes * megabitsPerSecPerByte;
connectionStats["7. Down (Mb/s)"] = stats.receivedBytes * megabitsPerSecPerByte;
nodeStats["Connection Stats"] = connectionStats;
using Events = udt::ConnectionStats::Stats::Event;
const auto& events = stat.second.events;
const auto& events = stats.events;
QJsonObject upstreamStats;
upstreamStats["1. Sent (P/s)"] = stat.second.sendRate;
upstreamStats["2. Sent Packets"] = stat.second.sentPackets;
upstreamStats["1. Sent (P/s)"] = stats.sendRate;
upstreamStats["2. Sent Packets"] = (int)stats.sentPackets;
upstreamStats["3. Recvd ACK"] = events[Events::ReceivedACK];
upstreamStats["4. Procd ACK"] = events[Events::ProcessedACK];
upstreamStats["5. Retransmitted"] = events[Events::Retransmission];
upstreamStats["5. Retransmitted"] = (int)stats.retransmittedPackets;
nodeStats["Upstream Stats"] = upstreamStats;
QJsonObject downstreamStats;
downstreamStats["1. Recvd (P/s)"] = stat.second.receiveRate;
downstreamStats["2. Recvd Packets"] = stat.second.receivedPackets;
downstreamStats["1. Recvd (P/s)"] = stats.receiveRate;
downstreamStats["2. Recvd Packets"] = (int)stats.receivedPackets;
downstreamStats["3. Sent ACK"] = events[Events::SentACK];
downstreamStats["4. Duplicates"] = events[Events::Duplicate];
downstreamStats["4. Duplicates"] = (int)stats.duplicatePackets;
nodeStats["Downstream Stats"] = downstreamStats;
QString uuid;
auto nodelist = DependencyManager::get<NodeList>();
if (stat.first == nodelist->getDomainHandler().getSockAddr()) {
uuid = uuidStringWithoutCurlyBraces(nodelist->getDomainHandler().getUUID());
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = "DomainServer";
} else {
auto node = nodelist->findNodeWithAddr(stat.first);
uuid = uuidStringWithoutCurlyBraces(node ? node->getUUID() : QUuid());
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuid;
}
QString uuid = uuidStringWithoutCurlyBraces(node->getUUID());
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuid;
serverStats[uuid] = nodeStats;
}
});
// send off the stats packets
ThreadedAssignment::addPacketStatsAndSendStatsPacket(serverStats);

View file

@ -338,7 +338,7 @@ void AudioMixer::sendStatsPacket() {
QJsonObject nodeStats;
QString uuidString = uuidStringWithoutCurlyBraces(node->getUUID());
nodeStats["outbound_kbps"] = node->getOutboundBandwidth();
nodeStats["outbound_kbps"] = node->getOutboundKbps();
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidString;
nodeStats["jitter"] = clientData->getAudioStreamStats();

View file

@ -746,65 +746,27 @@ void AvatarMixer::sendStatsPacket() {
AvatarMixerSlaveStats aggregateStats;
QJsonObject slavesObject;
float secondsSinceLastStats = (float)(start - _lastStatsTime) / (float)USECS_PER_SECOND;
// gather stats
int slaveNumber = 1;
_slavePool.each([&](AvatarMixerSlave& slave) {
QJsonObject slaveObject;
AvatarMixerSlaveStats stats;
slave.harvestStats(stats);
slaveObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(stats.nodesProcessed);
slaveObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(stats.packetsProcessed);
slaveObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(stats.nodesBroadcastedTo);
slaveObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(stats.numBytesSent);
slaveObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(stats.numPacketsSent);
slaveObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(stats.numIdentityPackets);
float averageNodes = ((float)stats.nodesBroadcastedTo / (float)tightLoopFrames);
float averageOutboundAvatarKbps = averageNodes ? ((stats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
slaveObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
float averageOthersIncluded = averageNodes ? stats.numOthersIncluded / averageNodes : 0.0f;
slaveObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
float averageOverBudgetAvatars = averageNodes ? stats.overBudgetAvatars / averageNodes : 0.0f;
slaveObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
slaveObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(stats.processIncomingPacketsElapsedTime);
slaveObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(stats.ignoreCalculationElapsedTime);
slaveObject["timing_3_toByteArray"] = TIGHT_LOOP_STAT_UINT64(stats.toByteArrayElapsedTime);
slaveObject["timing_4_avatarDataPacking"] = TIGHT_LOOP_STAT_UINT64(stats.avatarDataPackingElapsedTime);
slaveObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(stats.packetSendingElapsedTime);
slaveObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(stats.jobElapsedTime);
slavesObject[QString::number(slaveNumber)] = slaveObject;
slaveNumber++;
aggregateStats += stats;
});
QJsonObject slavesAggregatObject;
slavesAggregatObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
slavesAggregatObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(aggregateStats.packetsProcessed);
slavesAggregatObject["received_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
slavesAggregatObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(aggregateStats.nodesBroadcastedTo);
slavesAggregatObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(aggregateStats.numBytesSent);
slavesAggregatObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(aggregateStats.numPacketsSent);
slavesAggregatObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityPackets);
float averageNodes = ((float)aggregateStats.nodesBroadcastedTo / (float)tightLoopFrames);
float averageOutboundAvatarKbps = averageNodes ? ((aggregateStats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
slavesAggregatObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
float averageOthersIncluded = averageNodes ? aggregateStats.numOthersIncluded / averageNodes : 0.0f;
slavesAggregatObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
slavesAggregatObject["sent_2_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f;
slavesAggregatObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
slavesAggregatObject["sent_3_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
@ -814,7 +776,6 @@ void AvatarMixer::sendStatsPacket() {
slavesAggregatObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.jobElapsedTime);
statsObject["slaves_aggregate"] = slavesAggregatObject;
statsObject["slaves_individual"] = slavesObject;
_handleViewFrustumPacketElapsedTime = 0;
_handleAvatarIdentityPacketElapsedTime = 0;
@ -839,8 +800,8 @@ void AvatarMixer::sendStatsPacket() {
// add the key to ask the domain-server for a username replacement, if it has it
avatarStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID());
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY] = node->getOutboundBandwidth();
avatarStats[NODE_INBOUND_KBPS_STAT_KEY] = node->getInboundBandwidth();
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY] = node->getOutboundKbps();
avatarStats[NODE_INBOUND_KBPS_STAT_KEY] = node->getInboundKbps();
AvatarMixerClientData* clientData = static_cast<AvatarMixerClientData*>(node->getLinkedData());
if (clientData) {

View file

@ -146,7 +146,8 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
break;
}
if (traitType == AvatarTraits::AvatarEntity) {
if (traitType == AvatarTraits::AvatarEntity ||
traitType == AvatarTraits::Grab) {
auto& instanceVersionRef = _lastReceivedTraitVersions.getInstanceValueRef(traitType, instanceID);
if (packetTraitVersion > instanceVersionRef) {

View file

@ -23,6 +23,7 @@
#include <EntityEditFilters.h>
#include <NetworkingConstants.h>
#include <AddressManager.h>
#include <hfm/ModelFormatRegistry.h>
#include "../AssignmentDynamicFactory.h"
#include "AssignmentParentFinder.h"
@ -45,6 +46,8 @@ EntityServer::EntityServer(ReceivedMessage& message) :
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
DependencyManager::set<AssignmentDynamicFactory>();
DependencyManager::set<ModelFormatRegistry>(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache ctor
DependencyManager::set<ModelCache>();
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd,

View file

@ -75,8 +75,8 @@ void MessagesMixer::sendStatsPacket() {
DependencyManager::get<NodeList>()->eachNode([&](const SharedNodePointer& node) {
QJsonObject clientStats;
clientStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID());
clientStats["outbound_kbps"] = node->getOutboundBandwidth();
clientStats["inbound_kbps"] = node->getInboundBandwidth();
clientStats["outbound_kbps"] = node->getOutboundKbps();
clientStats["inbound_kbps"] = node->getInboundKbps();
messagesMixerObject[uuidStringWithoutCurlyBraces(node->getUUID())] = clientStats;
});

View file

@ -0,0 +1,4 @@
<svg width="149" height="150" viewBox="0 0 149 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M74.3055 0C115.543 0 149 33.5916 149 74.6047C149 116.008 115.543 149.6 74.3055 149.6C33.4569 149.6 0 116.008 0 74.6047C0 33.5916 33.4569 0 74.3055 0ZM74.3055 139.054C109.708 139.054 138.496 110.149 138.496 74.6047C138.496 39.4507 109.708 10.5462 74.3055 10.5462C39.2924 10.5462 10.5039 39.4507 10.5039 74.6047C10.5039 110.149 39.2924 139.054 74.3055 139.054Z" fill="#1FC6A6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M65.3575 89.8376L106.595 43.3562C110.874 38.2784 119.044 45.3092 114.376 50.387L70.0259 100.384C68.0807 102.727 64.9684 102.727 63.0233 101.165L35.0128 78.9008C29.9554 74.6042 36.569 66.4016 41.6264 70.6982L65.3575 89.8376Z" fill="#1FC6A6"/>
</svg>

After

(image error) Size: 824 B

Binary file not shown.

After

(image error) Size: 27 KiB

Binary file not shown.

After

(image error) Size: 26 KiB

Binary file not shown.

After

(image error) Size: 463 B

View file

@ -192,13 +192,13 @@ Item {
}
StatText {
visible: root.expanded;
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
"Silent: " + root.audioSilentInboundPPS + " pps";
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
root.audioMixerOutPps + "pps";
}
StatText {
visible: root.expanded;
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
root.audioMixerOutPps + "pps";
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
"Silent: " + root.audioSilentInboundPPS + " pps";
}
StatText {
visible: root.expanded;

View file

@ -210,13 +210,13 @@ Item {
}
StatText {
visible: root.expanded;
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
"Silent: " + root.audioSilentInboundPPS + " pps";
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
root.audioMixerOutPps + "pps";
}
StatText {
visible: root.expanded;
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
root.audioMixerOutPps + "pps";
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
"Silent: " + root.audioSilentInboundPPS + " pps";
}
StatText {
visible: root.expanded;

View file

@ -32,6 +32,10 @@ Original.Button {
width: hifi.dimensions.buttonWidth
height: hifi.dimensions.controlLineHeight
property size implicitPadding: Qt.size(20, 16)
property int implicitWidth: buttonContentItem.implicitWidth + implicitPadding.width
property int implicitHeight: buttonContentItem.implicitHeight + implicitPadding.height
HifiConstants { id: hifi }
onHoveredChanged: {
@ -94,6 +98,8 @@ Original.Button {
contentItem: Item {
id: buttonContentItem
implicitWidth: (buttonGlyph.visible ? buttonGlyph.implicitWidth : 0) + buttonText.implicitWidth
implicitHeight: buttonText.implicitHeight
TextMetrics {
id: buttonGlyphTextMetrics;
font: buttonGlyph.font;

View file

@ -254,7 +254,8 @@ Rectangle {
onSaveClicked: function() {
var avatarSettings = {
dominantHand : settings.dominantHandIsLeft ? 'left' : 'right',
collisionsEnabled : settings.avatarCollisionsOn,
collisionsEnabled : settings.environmentCollisionsOn,
otherAvatarsCollisionsEnabled : settings.otherAvatarsCollisionsOn,
animGraphOverrideUrl : settings.avatarAnimationOverrideJSON,
collisionSoundUrl : settings.avatarCollisionSoundUrl
};

View file

@ -0,0 +1,24 @@
import QtQuick 2.6
import "../stylesUit" 1.0
import "../windows" as Windows
import "avatarPackager" 1.0
Windows.ScrollingWindow {
id: root
objectName: "AvatarPackager"
width: 480
height: 706
title: "Avatar Packager"
resizable: false
opacity: parent.opacity
destroyOnHidden: true
implicitWidth: 384; implicitHeight: 640
minSize: Qt.vector2d(480, 706)
HifiConstants { id: hifi }
AvatarPackagerApp {
height: pane.height
width: pane.width
}
}

View file

@ -0,0 +1,396 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQml.Models 2.1
import QtGraphicalEffects 1.0
import Hifi.AvatarPackager.AvatarProjectStatus 1.0
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import "../../controls" 1.0
import "../../dialogs"
import "../avatarapp" 1.0 as AvatarApp
Item {
id: windowContent
HifiConstants { id: hifi }
property alias desktopObject: avatarPackager.desktopObject
MouseArea {
anchors.fill: parent
onClicked: {
unfocusser.forceActiveFocus();
}
Item {
id: unfocusser
visible: false
}
}
InfoBox {
id: fileListPopup
title: "List of Files"
content: Rectangle {
id: fileList
color: "#404040"
anchors.fill: parent
anchors.topMargin: 10
anchors.bottomMargin: 10
anchors.leftMargin: 29
anchors.rightMargin: 29
clip: true
ListView {
anchors.fill: parent
model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles
delegate: Rectangle {
width: parent.width
height: fileText.implicitHeight + 8
color: "#404040"
RalewaySemiBold {
id: fileText
size: 16
elide: Text.ElideLeft
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 4
width: parent.width - 10
color: "white"
text: modelData
}
}
}
}
}
InfoBox {
id: errorPopup
property string errorMessage
boxWidth: 380
boxHeight: 293
content: RalewayRegular {
id: bodyMessage
anchors.fill: parent
anchors.bottomMargin: 10
anchors.leftMargin: 29
anchors.rightMargin: 29
size: 20
color: "white"
text: errorPopup.errorMessage
width: parent.width
wrapMode: Text.WordWrap
}
function show(title, message) {
errorPopup.title = title;
errorMessage = message;
errorPopup.open();
}
}
Rectangle {
id: modalOverlay
anchors.fill: parent
z: 20
color: "#a15d5d5d"
visible: false
// This mouse area captures the cursor events while the modalOverlay is active
MouseArea {
anchors.fill: parent
propagateComposedEvents: false
hoverEnabled: true
}
}
AvatarApp.MessageBox {
id: popup
anchors.fill: parent
visible: false
closeOnClickOutside: true
}
Column {
id: avatarPackager
anchors.fill: parent
state: "main"
states: [
State {
name: AvatarPackagerState.main
PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; backButtonVisible: false }
PropertyChanges { target: avatarPackagerMain; visible: true }
PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer }
},
State {
name: AvatarPackagerState.createProject
PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") }
PropertyChanges { target: createAvatarProject; visible: true }
PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer }
},
State {
name: AvatarPackagerState.project
PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true }
PropertyChanges { target: avatarProject; visible: true }
PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer }
},
State {
name: AvatarPackagerState.projectUpload
PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false }
PropertyChanges { target: avatarUploader; visible: true }
PropertyChanges { target: avatarPackagerFooter; visible: false }
}
]
property alias showModalOverlay: modalOverlay.visible
property var desktopObject: desktop
function openProject(path) {
let status = AvatarPackagerCore.openAvatarProject(path);
if (status !== AvatarProjectStatus.SUCCESS) {
displayErrorMessage(status);
return status;
}
avatarProject.reset();
avatarPackager.state = AvatarPackagerState.project;
return status;
}
function displayErrorMessage(status) {
if (status === AvatarProjectStatus.SUCCESS) {
return;
}
switch (status) {
case AvatarProjectStatus.ERROR_CREATE_PROJECT_NAME:
errorPopup.show("Project Folder Already Exists", "A folder with that name already exists at that location. Please choose a different project name or location.");
break;
case AvatarProjectStatus.ERROR_CREATE_CREATING_DIRECTORIES:
errorPopup.show("Project Folders Creation Error", "There was a problem creating the Avatar Project directory. Please check the project location and try again.");
break;
case AvatarProjectStatus.ERROR_CREATE_FIND_MODEL:
errorPopup.show("Cannot Find Model File", "There was a problem while trying to find the specified model file. Please verify that it exists at the specified location.");
break;
case AvatarProjectStatus.ERROR_CREATE_OPEN_MODEL:
errorPopup.show("Cannot Open Model File", "There was a problem while trying to open the specified model file.");
break;
case AvatarProjectStatus.ERROR_CREATE_READ_MODEL:
errorPopup.show("Error Read Model File", "There was a problem while trying to read the specified model file. Please check that the file is a valid FBX file and try again.");
break;
case AvatarProjectStatus.ERROR_CREATE_WRITE_FST:
errorPopup.show("Error Writing Project File", "There was a problem while trying to write the FST file.");
break;
case AvatarProjectStatus.ERROR_OPEN_INVALID_FILE_TYPE:
errorPopup.show("Invalid Project Path", "The avatar packager can only open FST files.");
break;
case AvatarProjectStatus.ERROR_OPEN_PROJECT_FOLDER:
errorPopup.show("Project Missing", "Project folder cannot be found. Please locate the folder and copy/move it to its original location.");
break;
case AvatarProjectStatus.ERROR_OPEN_FIND_FST:
errorPopup.show("File Missing", "We cannot find the project file (.fst) in the project folder. Please locate it and move it to the project folder.");
break;
case AvatarProjectStatus.ERROR_OPEN_OPEN_FST:
errorPopup.show("File Read Error", "We cannot read the project file (.fst).");
break;
case AvatarProjectStatus.ERROR_OPEN_FIND_MODEL:
errorPopup.show("File Missing", "We cannot find the avatar model file (.fbx) in the project folder. Please locate it and move it to the project folder.");
break;
default:
errorPopup.show("Error Message Missing", "Error message missing for status " + status);
}
}
function openDocs() {
Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar");
}
AvatarPackagerHeader {
z: 100
id: avatarPackagerHeader
colorScheme: root.colorScheme
onBackButtonClicked: {
avatarPackager.state = AvatarPackagerState.main;
}
onDocsButtonClicked: {
avatarPackager.openDocs();
}
}
Item {
height: windowContent.height - avatarPackagerHeader.height - avatarPackagerFooter.height
width: windowContent.width
Rectangle {
anchors.fill: parent
color: "#404040"
}
AvatarProject {
id: avatarProject
colorScheme: root.colorScheme
anchors.fill: parent
}
AvatarProjectUpload {
id: avatarUploader
anchors.fill: parent
root: avatarProject
}
CreateAvatarProject {
id: createAvatarProject
colorScheme: root.colorScheme
anchors.fill: parent
}
Item {
id: avatarPackagerMain
visible: false
anchors.fill: parent
property var footer: Item {
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: createProjectButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: openProjectButton.left
anchors.rightMargin: 22
height: 40
width: 134
text: qsTr("New Project")
colorScheme: root.colorScheme
onClicked: {
createAvatarProject.clearInputs();
avatarPackager.state = AvatarPackagerState.createProject;
}
}
HifiControls.Button {
id: openProjectButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
height: 40
width: 133
text: qsTr("Open Project")
color: hifi.buttons.blue
colorScheme: root.colorScheme
onClicked: {
avatarPackager.showModalOverlay = true;
let browser = avatarPackager.desktopObject.fileDialog({
selectDirectory: false,
dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH),
filter: "Avatar Project FST Files (*.fst)",
title: "Open Project (.fst)",
});
browser.canceled.connect(function() {
avatarPackager.showModalOverlay = false;
});
browser.selectedFile.connect(function(fileUrl) {
let fstFilePath = fileDialogHelper.urlToPath(fileUrl);
avatarPackager.showModalOverlay = false;
avatarPackager.openProject(fstFilePath);
});
}
}
}
Flow {
visible: AvatarPackagerCore.recentProjects.length === 0
anchors {
fill: parent
topMargin: 18
leftMargin: 16
rightMargin: 16
}
RalewayRegular {
size: 20
color: "white"
text: "Use a custom avatar of your choice."
width: parent.width
wrapMode: Text.WordWrap
}
RalewayRegular {
size: 20
color: "white"
text: "<a href='javascript:void'>Visit our docs</a> to learn more about using the packager."
linkColor: "#00B4EF"
width: parent.width
wrapMode: Text.WordWrap
onLinkActivated: {
avatarPackager.openDocs();
}
}
}
Item {
anchors.fill: parent
visible: AvatarPackagerCore.recentProjects.length > 0
RalewayRegular {
id: recentProjectsText
color: 'white'
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: 16
anchors.leftMargin: 16
size: 20
text: "Recent Projects"
onLinkActivated: fileListPopup.open()
}
Column {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
top: recentProjectsText.bottom
topMargin: 16
leftMargin: 16
rightMargin: 16
}
spacing: 10
Repeater {
model: AvatarPackagerCore.recentProjects
AvatarProjectCard {
title: modelData.name
path: modelData.projectPath
onOpen: avatarPackager.openProject(modelData.path)
}
}
}
}
}
}
AvatarPackagerFooter {
id: avatarPackagerFooter
}
}
}

View file

@ -0,0 +1,41 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Rectangle {
id: avatarPackagerFooter
color: "#404040"
height: content === defaultContent ? 0 : 74
visible: content !== defaultContent
width: parent.width
property var content: Item { id: defaultContent }
children: [background, content]
property var background: Rectangle {
anchors.fill: parent
color: "#404040"
Rectangle {
id: topBorder1
anchors.top: parent.top
color: "#252525"
height: 1
width: parent.width
}
Rectangle {
id: topBorder2
anchors.top: topBorder1.bottom
color: "#575757"
height: 1
width: parent.width
}
}
}

View file

@ -0,0 +1,144 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import "../avatarapp" 1.0
ShadowRectangle {
id: root
width: parent.width
height: 74
color: "#252525"
property string title: qsTr("Avatar Packager")
property alias docsEnabled: docs.visible
property bool backButtonVisible: true // If false, is not visible and does not take up space
property bool backButtonEnabled: true // If false, is not visible but does not affect space
property bool canRename: false
property int colorScheme
property color textColor: "white"
property color hoverTextColor: "gray"
property color pressedTextColor: "#6A6A6A"
signal backButtonClicked
signal docsButtonClicked
RalewayButton {
id: back
visible: backButtonEnabled && backButtonVisible
size: 28
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: 16
text: "◀"
onClicked: root.backButtonClicked()
}
Item {
id: titleArea
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: root.backButtonVisible ? back.right : parent.left
anchors.leftMargin: root.backButtonVisible ? 11 : 21
anchors.right: docs.left
states: [
State {
name: "renaming"
PropertyChanges { target: title; visible: false }
PropertyChanges { target: titleInputArea; visible: true }
}
]
Item {
id: title
anchors.fill: parent
RalewaySemiBold {
id: titleNotRenameable
visible: !root.canRename
size: 28
anchors.fill: parent
text: root.title
color: "white"
}
RalewayButton {
id: titleRenameable
visible: root.canRename
enabled: root.canRename
size: 28
anchors.fill: parent
text: root.title
onClicked: {
if (!root.canRename || AvatarPackagerCore.currentAvatarProject === null) {
return;
}
titleArea.state = "renaming";
titleInput.text = AvatarPackagerCore.currentAvatarProject.name;
titleInput.selectAll();
titleInput.forceActiveFocus(Qt.MouseFocusReason);
}
}
}
Item {
id: titleInputArea
visible: false
anchors.fill: parent
HifiControls.TextField {
id: titleInput
anchors.fill: parent
text: ""
colorScheme: root.colorScheme
font.family: "Fira Sans"
font.pixelSize: 28
z: 200
onFocusChanged: {
if (titleArea.state === "renaming" && !focus) {
accepted();
}
}
Keys.onPressed: {
if (event.key === Qt.Key_Escape) {
titleArea.state = "";
}
}
onAccepted: {
if (acceptableInput) {
AvatarPackagerCore.currentAvatarProject.name = text;
}
titleArea.state = "";
}
}
}
}
RalewayButton {
id: docs
visible: false
size: 28
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: 16
text: qsTr("Docs")
onClicked: {
docsButtonClicked();
}
}
}

View file

@ -0,0 +1,10 @@
pragma Singleton
import QtQuick 2.6
Item {
id: singleton
readonly property string main: "main"
readonly property string project: "project"
readonly property string createProject: "createProject"
readonly property string projectUpload: "projectUpload"
}

View file

@ -0,0 +1,336 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.2 as Original
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: root
HifiConstants { id: hifi }
Style { id: style }
property int colorScheme
property var uploader: null
property bool hasSuccessfullyUploaded: true
visible: false
anchors.fill: parent
anchors.margins: 10
function reset() {
hasSuccessfullyUploaded = false;
uploader = null;
}
property var footer: Item {
anchors.fill: parent
Item {
id: uploadFooter
visible: !root.uploader || root.finished || root.uploader.state !== 4
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: uploadButton
visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
enabled: Account.loggedIn
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("Upload")
color: hifi.buttons.blue
colorScheme: root.colorScheme
width: 133
height: 40
onClicked: {
uploadNew();
}
}
HifiControls.Button {
id: updateButton
visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
enabled: Account.loggedIn
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("Update")
color: hifi.buttons.blue
colorScheme: root.colorScheme
width: 134
height: 40
onClicked: {
showConfirmUploadPopup(uploadNew, uploadUpdate);
}
}
Item {
anchors.fill: parent
visible: root.hasSuccessfullyUploaded
HifiControls.Button {
enabled: Account.loggedIn
anchors.verticalCenter: parent.verticalCenter
anchors.right: viewInInventoryButton.left
anchors.rightMargin: 16
text: qsTr("Update")
color: hifi.buttons.white
colorScheme: root.colorScheme
width: 134
height: 40
onClicked: {
showConfirmUploadPopup(uploadNew, uploadUpdate);
}
}
HifiControls.Button {
id: viewInInventoryButton
enabled: Account.loggedIn
width: 168
height: 40
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("View in Inventory")
color: hifi.buttons.blue
colorScheme: root.colorScheme
onClicked: AvatarPackagerCore.currentAvatarProject.openInInventory()
}
}
}
Rectangle {
id: uploadingItemFooter
anchors.fill: parent
anchors.topMargin: 1
visible: !!root.uploader && !root.finished && root.uploader.state === 4
color: "#00B4EF"
LoadingCircle {
id: runningImage
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 16
width: 28
height: 28
white: true
}
RalewayRegular {
id: stepText
size: 20
anchors.verticalCenter: parent.verticalCenter
anchors.left: runningImage.right
anchors.leftMargin: 16
text: "Adding item to Inventory"
color: "white"
}
}
}
function uploadNew() {
upload(false);
}
function uploadUpdate() {
upload(true);
}
Connections {
target: root.uploader
onStateChanged: {
root.hasSuccessfullyUploaded = newState >= 4;
}
}
function upload(updateExisting) {
root.uploader = AvatarPackagerCore.currentAvatarProject.upload(updateExisting);
console.log("uploader: "+ root.uploader);
root.uploader.send();
avatarPackager.state = AvatarPackagerState.projectUpload;
}
function showConfirmUploadPopup() {
popup.titleText = 'Overwrite Avatar';
popup.bodyText = 'You have previously uploaded the avatar file from this project.' +
' This will overwrite that avatar and you wont be able to access the older version.';
popup.button1text = 'CREATE NEW';
popup.button2text = 'OVERWRITE';
popup.onButton2Clicked = function() {
popup.close();
uploadUpdate();
};
popup.onButton1Clicked = function() {
popup.close();
showConfirmCreateNewPopup();
};
popup.open();
}
function showConfirmCreateNewPopup(confirmCallback) {
popup.titleText = 'Create New';
popup.bodyText = 'This will upload your current files with the same avatar name.' +
' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?';
popup.button1text = 'CANCEL';
popup.button2text = 'CONFIRM';
popup.onButton1Clicked = function() {
popup.close()
};
popup.onButton2Clicked = function() {
popup.close();
uploadNew();
};
popup.open();
}
RalewayRegular {
id: infoMessage
states: [
State {
when: root.hasSuccessfullyUploaded
name: "upload-success"
PropertyChanges {
target: infoMessage
text: "Your avatar has been successfully uploaded to our servers. Make changes to your avatar by editing and uploading the project files."
}
},
State {
name: "has-previous-success"
when: !!AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID
PropertyChanges {
target: infoMessage
text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes."
}
}
]
color: 'white'
size: 20
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottomMargin: 24
wrapMode: Text.Wrap
text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users."
}
HifiControls.Button {
id: openFolderButton
visible: false
width: parent.width
anchors.top: infoMessage.bottom
anchors.topMargin: 10
text: qsTr("Open Project Folder")
colorScheme: root.colorScheme
height: 30
onClicked: {
fileDialogHelper.openDirectory(fileDialogHelper.pathToUrl(AvatarPackagerCore.currentAvatarProject.projectFolderPath));
}
}
RalewayRegular {
id: showFilesText
color: 'white'
linkColor: style.colors.blueHighlight
visible: AvatarPackagerCore.currentAvatarProject !== null
anchors.bottom: loginRequiredMessage.top
anchors.bottomMargin: 10
size: 20
text: AvatarPackagerCore.currentAvatarProject ? AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" : ""
onLinkActivated: fileListPopup.open()
}
Rectangle {
id: loginRequiredMessage
visible: !Account.loggedIn
height: !Account.loggedIn ? loginRequiredTextRow.height + 20 : 0
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
color: "#FFD6AD"
border.color: "#F39622"
border.width: 2
radius: 2
Item {
id: loginRequiredTextRow
height: Math.max(loginWarningGlyph.implicitHeight, loginWarningText.implicitHeight)
anchors.fill: parent
anchors.margins: 10
HiFiGlyphs {
id: loginWarningGlyph
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: implicitWidth
size: 48
text: "+"
color: "black"
}
RalewayRegular {
id: loginWarningText
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 16
anchors.left: loginWarningGlyph.right
anchors.right: parent.right
text: "Please login to upload your avatar to High Fidelity hosting."
size: 18
wrapMode: Text.Wrap
}
}
}
}

View file

@ -0,0 +1,102 @@
import QtQuick 2.0
import QtGraphicalEffects 1.0
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: projectCard
height: 80
width: parent.width
property alias title: title.text
property alias path: path.text
property color textColor: "#E3E3E3"
property color hoverTextColor: "#121212"
property color pressedTextColor: "#121212"
property color backgroundColor: "#121212"
property color hoverBackgroundColor: "#E3E3E3"
property color pressedBackgroundColor: "#6A6A6A"
signal open
state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal")
states: [
State {
name: "normal"
PropertyChanges { target: background; color: backgroundColor }
PropertyChanges { target: title; color: textColor }
PropertyChanges { target: path; color: textColor }
},
State {
name: "hover"
PropertyChanges { target: background; color: hoverBackgroundColor }
PropertyChanges { target: title; color: hoverTextColor }
PropertyChanges { target: path; color: hoverTextColor }
},
State {
name: "pressed"
PropertyChanges { target: background; color: pressedBackgroundColor }
PropertyChanges { target: title; color: pressedTextColor }
PropertyChanges { target: path; color: pressedTextColor }
}
]
Rectangle {
id: background
width: parent.width
height: parent.height
color: "#121212"
radius: 4
RalewayBold {
id: title
elide: "ElideRight"
anchors {
top: parent.top
topMargin: 13
left: parent.left
leftMargin: 16
right: parent.right
rightMargin: 16
}
text: "<title missing>"
size: 24
}
RalewayRegular {
id: path
anchors {
top: title.bottom
left: parent.left
leftMargin: 32
right: background.right
rightMargin: 16
}
elide: "ElideLeft"
horizontalAlignment: Text.AlignRight
text: "<path missing>"
size: 20
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: open()
}
}
DropShadow {
id: shadow
anchors.fill: background
radius: 4
horizontalOffset: 0
verticalOffset: 4
color: Qt.rgba(0, 0, 0, 0.25)
source: background
}
}

View file

@ -0,0 +1,202 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.2 as Original
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: uploadingScreen
property var root: undefined
visible: false
anchors.fill: parent
Timer {
id: backToProjectTimer
interval: 2000
running: false
repeat: false
onTriggered: {
if (avatarPackager.state === AvatarPackagerState.projectUpload) {
avatarPackager.state = AvatarPackagerState.project;
}
}
}
function stateChangedCallback(newState) {
if (newState >= 4) {
root.uploader.stateChanged.disconnect(stateChangedCallback);
backToProjectTimer.start();
}
}
onVisibleChanged: {
if (visible) {
root.uploader.stateChanged.connect(stateChangedCallback);
root.uploader.finishedChanged.connect(function() {
if (root.uploader.error === 0) {
backToProjectTimer.start();
}
});
}
}
Item {
id: uploadStatus
anchors.fill: parent
Item {
id: statusItem
width: parent.width
height: 256
states: [
State {
name: "success"
when: root.uploader.state >= 4 && root.uploader.error === 0
PropertyChanges { target: uploadSpinner; visible: false }
PropertyChanges { target: errorIcon; visible: false }
PropertyChanges { target: successIcon; visible: true }
},
State {
name: "error"
when: root.uploader.finished && root.uploader.error !== 0
PropertyChanges { target: uploadSpinner; visible: false }
PropertyChanges { target: errorIcon; visible: true }
PropertyChanges { target: successIcon; visible: false }
PropertyChanges { target: errorFooter; visible: true }
PropertyChanges { target: errorMessage; visible: true }
}
]
AnimatedImage {
id: uploadSpinner
visible: true
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
width: 164
height: 164
source: "../../../icons/loader-snake-256.gif"
playing: true
}
HiFiGlyphs {
id: errorIcon
visible: false
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
size: 315
text: "+"
color: "#EA4C5F"
}
Image {
id: successIcon
visible: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 148
height: 148
source: "../../../icons/checkmark-stroke.svg"
}
}
Item {
id: statusRows
anchors.top: statusItem.bottom
anchors.left: parent.left
anchors.leftMargin: 12
AvatarUploadStatusItem {
id: statusCategories
uploader: root.uploader
text: "Retrieving categories"
uploaderState: 1
}
AvatarUploadStatusItem {
id: statusUploading
uploader: root.uploader
anchors.top: statusCategories.bottom
text: "Uploading data"
uploaderState: 2
}
AvatarUploadStatusItem {
id: statusResponse
uploader: root.uploader
anchors.top: statusUploading.bottom
text: "Waiting for response"
uploaderState: 3
}
}
RalewayRegular {
id: errorMessage
visible: false
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: errorFooter.top
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 32
size: 28
wrapMode: Text.Wrap
color: "white"
text: "We couldn't upload your avatar at this time. Please try again later."
}
AvatarPackagerFooter {
id: errorFooter
anchors.bottom: parent.bottom
visible: false
content: Item {
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: backButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("Back")
color: hifi.buttons.blue
colorScheme: root.colorScheme
width: 133
height: 40
onClicked: {
avatarPackager.state = AvatarPackagerState.project;
}
}
}
}
}
}

View file

@ -0,0 +1,96 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: root
height: 48
property string text: "NO STEP TEXT"
property int uploaderState;
property var uploader;
states: [
State {
name: ""
when: root.uploader === null
},
State {
name: "success"
when: root.uploader.state > uploaderState
PropertyChanges { target: stepText; color: "white" }
PropertyChanges { target: successGlyph; visible: true }
},
State {
name: "fail"
when: root.uploader.error !== 0
PropertyChanges { target: stepText; color: "#EA4C5F" }
PropertyChanges { target: failGlyph; visible: true }
},
State {
name: "running"
when: root.uploader.state === uploaderState
PropertyChanges { target: stepText; color: "white" }
PropertyChanges { target: runningImage; visible: true; playing: true }
}
]
Item {
id: statusItem
width: 48
height: parent.height
LoadingCircle {
id: runningImage
visible: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 32
height: 32
}
Image {
id: successGlyph
visible: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 30
height: 30
source: "../../../icons/checkmark-stroke.svg"
}
HiFiGlyphs {
id: failGlyph
visible: false
width: implicitWidth
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
size: 48
text: "+"
color: "#EA4C5F"
}
}
RalewayRegular {
id: stepText
anchors.left: statusItem.right
anchors.verticalCenter: parent.verticalCenter
text: root.text
size: 28
color: "#777777"
}
}

View file

@ -0,0 +1,63 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import TabletScriptingInterface 1.0
Item {
id: root
readonly property bool pressed: mouseArea.state == "pressed"
readonly property bool hovered: mouseArea.state == "hovering"
signal clicked()
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.focus = true
Tablet.playSound(TabletEnums.ButtonClick);
root.clicked();
}
property string lastState: ""
states: [
State {
name: ""
StateChangeScript {
script: {
mouseArea.lastState = mouseArea.state;
}
}
},
State {
name: "pressed"
when: mouseArea.containsMouse && mouseArea.pressed
StateChangeScript {
script: {
mouseArea.lastState = mouseArea.state;
}
}
},
State {
name: "hovering"
when: mouseArea.containsMouse
StateChangeScript {
script: {
if (mouseArea.lastState == "") {
Tablet.playSound(TabletEnums.ButtonHover);
}
mouseArea.lastState = mouseArea.state;
}
}
}
]
}
}

View file

@ -0,0 +1,135 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import Hifi.AvatarPackager.AvatarProjectStatus 1.0
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: root
HifiConstants { id: hifi }
property int colorScheme
property var footer: Item {
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: createButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
height: 30
width: 133
text: qsTr("Create")
enabled: false
onClicked: {
let status = AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text);
if (status !== AvatarProjectStatus.SUCCESS) {
avatarPackager.displayErrorMessage(status);
return;
}
avatarProject.reset();
avatarPackager.state = AvatarPackagerState.project;
}
}
}
visible: false
anchors.fill: parent
height: parent.height
width: parent.width
function clearInputs() {
name.text = projectLocation.text = avatarModel.text = textureFolder.text = "";
}
function checkErrors() {
let newErrorMessageText = "";
let projectName = name.text;
let projectFolder = projectLocation.text;
let hasProjectNameError = projectName !== "" && projectFolder !== "" && !AvatarPackagerCore.isValidNewProjectName(projectFolder, projectName);
if (hasProjectNameError) {
newErrorMessageText = "A folder with that name already exists at that location. Please choose a different project name or location.";
}
name.error = projectLocation.error = hasProjectNameError;
errorMessage.text = newErrorMessageText;
createButton.enabled = newErrorMessageText === "" && requiredFieldsFilledIn();
}
function requiredFieldsFilledIn() {
return name.text !== "" && projectLocation.text !== "" && avatarModel.text !== "";
}
RalewayRegular {
id: errorMessage
visible: text !== ""
text: ""
color: "#EA4C5F"
wrapMode: Text.WordWrap
size: 20
anchors {
left: parent.left
right: parent.right
}
}
Column {
id: createAvatarColumns
anchors.top: errorMessage.visible ? errorMessage.bottom : parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 10
spacing: 17
property string defaultFileBrowserPath: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH)
ProjectInputControl {
id: name
label: "Name"
colorScheme: root.colorScheme
onTextChanged: checkErrors()
}
ProjectInputControl {
id: projectLocation
label: "Specify Project Location"
colorScheme: root.colorScheme
browseEnabled: true
browseFolder: true
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
browseTitle: "Project Location"
onTextChanged: checkErrors()
}
ProjectInputControl {
id: avatarModel
label: "Specify Avatar Model (.fbx)"
colorScheme: root.colorScheme
browseEnabled: true
browseFolder: false
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
browseFilter: "Avatar Model File (*.fbx)"
browseTitle: "Open Avatar Model (.fbx)"
onTextChanged: checkErrors()
}
ProjectInputControl {
id: textureFolder
label: "Specify Texture Folder - <i>Optional</i>"
colorScheme: root.colorScheme
browseEnabled: true
browseFolder: true
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
browseTitle: "Texture Folder"
onTextChanged: checkErrors()
}
}
}

View file

@ -0,0 +1,120 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import stylesUit 1.0
import controlsUit 1.0 as HifiControlsUit
import "../../controls" as HifiControls
Rectangle {
id: root
visible: false
color: Qt.rgba(.34, .34, .34, 0.6)
z: 999;
anchors.fill: parent
property alias title: titleText.text
property alias content: loader.sourceComponent
property bool closeOnClickOutside: false;
property alias boxWidth: mainContainer.width
property alias boxHeight: mainContainer.height
onVisibleChanged: {
if (visible) {
focus = true;
}
}
function open() {
visible = true;
}
function close() {
visible = false;
}
HifiConstants {
id: hifi
}
// This object is always used in a popup.
// This MouseArea is used to prevent a user from being
// able to click on a button/mouseArea underneath the popup.
MouseArea {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
onClicked: {
if (closeOnClickOutside) {
root.close()
}
}
}
Rectangle {
id: mainContainer
width: Math.max(parent.width * 0.8, 400)
height: parent.height * 0.6
MouseArea {
anchors.fill: parent
propagateComposedEvents: false
hoverEnabled: true
onClicked: function(ev) {
ev.accepted = true;
}
}
anchors.centerIn: parent
color: "#252525"
// TextStyle1
RalewaySemiBold {
id: titleText
size: 24
color: "white"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 30
text: "Title not defined"
}
Item {
anchors.topMargin: 10
anchors.top: titleText.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: button.top
Loader {
id: loader
anchors.fill: parent
}
}
Item {
id: button
height: 40
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 12
HifiControlsUit.Button {
anchors.centerIn: parent
text: "CLOSE"
onClicked: close()
color: hifi.buttons.noneBorderlessWhite;
colorScheme: hifi.colorSchemes.dark;
}
}
}
}

View file

@ -0,0 +1,16 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
AnimatedImage {
id: root
width: 128
height: 128
property bool white: false
source: white ? "../../../icons/loader-snake-256-wf.gif" : "../../../icons/loader-snake-256.gif"
playing: true
}

View file

@ -0,0 +1,78 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Column {
id: control
anchors.left: parent.left
anchors.leftMargin: 21
anchors.right: parent.right
anchors.rightMargin: 16
height: 75
spacing: 4
property alias label: label.text
property alias browseEnabled: browseButton.visible
property bool browseFolder: false
property string browseFilter: "All Files (*.*)"
property string browseTitle: "Open file"
property string browseDir: ""
property alias text: input.text
property alias error: input.error
property int colorScheme
Row {
RalewaySemiBold {
id: label
size: 20
font.weight: Font.Medium
text: ""
color: "white"
}
}
Row {
width: control.width
spacing: 16
height: 40
HifiControls.TextField {
id: input
colorScheme: control.colorScheme
font.family: "Fira Sans"
font.pixelSize: 18
height: parent.height
width: browseButton.visible ? parent.width - browseButton.width - parent.spacing : parent.width
}
HifiControls.Button {
id: browseButton
visible: false
height: parent.height
width: 133
text: qsTr("Browse")
colorScheme: root.colorScheme
onClicked: {
avatarPackager.showModalOverlay = true;
let browser = avatarPackager.desktopObject.fileDialog({
selectDirectory: browseFolder,
dir: browseDir,
filter: browseFilter,
title: browseTitle,
});
browser.canceled.connect(function() {
avatarPackager.showModalOverlay = false;
});
browser.selectedFile.connect(function(fileUrl) {
input.text = fileDialogHelper.urlToPath(fileUrl);
avatarPackager.showModalOverlay = false;
});
}
}
}
}

View file

@ -0,0 +1,26 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import TabletScriptingInterface 1.0
RalewaySemiBold {
id: root
property color idleColor: "white"
property color hoverColor: "#AFAFAF"
property color pressedColor: "#575757"
color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor)
signal clicked()
ClickableArea {
id: clickable
anchors.fill: root
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,20 @@
import QtQuick 2.5
import QtQuick.Window 2.2
import "../../stylesUit" 1.0
QtObject {
readonly property QtObject colors: QtObject {
readonly property color lightGrayBackground: "#f2f2f2"
readonly property color black: "#000000"
readonly property color white: "#ffffff"
readonly property color blueHighlight: "#00b4ef"
readonly property color inputFieldBackground: "#d4d4d4"
readonly property color yellowishOrange: "#ffb017"
readonly property color blueAccent: "#0093c5"
readonly property color greenHighlight: "#1fc6a6"
readonly property color lightGray: "#afafaf"
readonly property color redHighlight: "#ea4c5f"
readonly property color orangeAccent: "#ff6309"
}
}

View file

@ -0,0 +1,2 @@
module AvatarPackager
singleton AvatarPackagerState 1.0 AvatarPackagerState.qml

View file

@ -23,6 +23,8 @@ Rectangle {
property string button2color: hifi.buttons.blue;
property string button2text: ''
property bool closeOnClickOutside: false;
property var onButton2Clicked;
property var onButton1Clicked;
property var onLinkClicked;
@ -56,6 +58,11 @@ Rectangle {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
onClicked: {
if (closeOnClickOutside) {
root.close()
}
}
}
Rectangle {
@ -68,6 +75,15 @@ Rectangle {
console.debug('mainContainer: height = ', height)
}
MouseArea {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
onClicked: function(ev) {
ev.accepted = true;
}
}
anchors.centerIn: parent
color: "white"

View file

@ -35,7 +35,8 @@ Rectangle {
property real scaleValue: scaleSlider.value / 10
property alias dominantHandIsLeft: leftHandRadioButton.checked
property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked
property alias otherAvatarsCollisionsOn: otherAvatarsCollisionsEnabledCheckBox.checked
property alias environmentCollisionsOn: environmentCollisionsEnabledCheckBox.checked
property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text
property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText
property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text
@ -54,11 +55,11 @@ Rectangle {
} else {
rightHandRadioButton.checked = true;
}
if (settings.otherAvatarsCollisionsEnabled) {
otherAvatarsCollisionsEnabledCheckBox.checked = true;
}
if (settings.collisionsEnabled) {
collisionsEnabledRadiobutton.checked = true;
} else {
collisionsDisabledRadioButton.checked = true;
environmentCollisionsEnabledCheckBox.checked = true;
}
avatarAnimationJSON = settings.animGraphUrl;
@ -255,55 +256,43 @@ Rectangle {
text: "Right"
boxSize: 20
}
HifiConstants {
id: hifi
}
// TextStyle9
RalewaySemiBold {
size: 17;
Layout.row: 1
Layout.column: 0
text: "Avatar Collisions"
text: "Avatar collides with other avatars"
}
ButtonGroup {
id: onOff
}
HifiControlsUit.RadioButton {
id: collisionsEnabledRadiobutton
Layout.row: 1
Layout.column: 1
Layout.leftMargin: -40
ButtonGroup.group: onOff
colorScheme: hifi.colorSchemes.light
fontSize: 17
letterSpacing: 1.4
checked: true
text: "ON"
boxSize: 20
}
HifiConstants {
id: hifi
}
HifiControlsUit.RadioButton {
id: collisionsDisabledRadioButton
HifiControlsUit.CheckBox {
id: otherAvatarsCollisionsEnabledCheckBox;
boxSize: 20;
Layout.row: 1
Layout.column: 2
Layout.rightMargin: 20
ButtonGroup.group: onOff
Layout.leftMargin: 60
colorScheme: hifi.colorSchemes.light
fontSize: 17
letterSpacing: 1.4
}
text: "OFF"
boxSize: 20
// TextStyle9
RalewaySemiBold {
size: 17;
Layout.row: 2
Layout.column: 0
text: "Avatar collides with environment"
}
HifiControlsUit.CheckBox {
id: environmentCollisionsEnabledCheckBox;
boxSize: 20;
Layout.row: 2
Layout.column: 2
Layout.leftMargin: 60
colorScheme: hifi.colorSchemes.light
}
}

View file

@ -0,0 +1,15 @@
import QtQuick 2.0
import "../avatarPackager" 1.0
Item {
id: root
width: 480
height: 706
AvatarPackagerApp {
width: parent.width
height: parent.height
desktopObject: tabletRoot
}
}

View file

@ -869,7 +869,7 @@ Flickable {
id: outOfRangeDataStrategyComboBox
height: 25
width: 100
width: 150
editable: true
colorScheme: hifi.colorSchemes.dark

View file

@ -158,6 +158,7 @@
#include "audio/AudioScope.h"
#include "avatar/AvatarManager.h"
#include "avatar/MyHead.h"
#include "avatar/AvatarPackager.h"
#include "CrashRecoveryHandler.h"
#include "CrashHandler.h"
#include "devices/DdeFaceTracker.h"
@ -170,6 +171,7 @@
#include "scripting/Audio.h"
#include "networking/CloseEventSender.h"
#include "scripting/TestScriptingInterface.h"
#include "scripting/PlatformInfoScriptingInterface.h"
#include "scripting/AssetMappingsScriptingInterface.h"
#include "scripting/ClipboardScriptingInterface.h"
#include "scripting/DesktopScriptingInterface.h"
@ -208,6 +210,8 @@
#include "InterfaceParentFinder.h"
#include "ui/OctreeStatsProvider.h"
#include "avatar/GrabManager.h"
#include <GPUIdent.h>
#include <gl/GLHelpers.h>
#include <src/scripting/GooglePolyScriptingInterface.h>
@ -859,7 +863,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<LODManager>();
DependencyManager::set<StandAloneJSConsole>();
DependencyManager::set<DialogsManager>();
DependencyManager::set<BandwidthRecorder>();
DependencyManager::set<ResourceCacheSharedItems>();
DependencyManager::set<DesktopScriptingInterface>();
DependencyManager::set<EntityScriptingInterface>(true);
@ -919,6 +922,8 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<ResourceRequestObserver>();
DependencyManager::set<Keyboard>();
DependencyManager::set<KeyboardScriptingInterface>();
DependencyManager::set<GrabManager>();
DependencyManager::set<AvatarPackager>();
return previousSessionCrashed;
}
@ -1078,6 +1083,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
auto nodeList = DependencyManager::get<NodeList>();
nodeList->startThread();
nodeList->setFlagTimeForConnectionStep(true);
// move the AddressManager to the NodeList thread so that domain resets due to domain changes always occur
// before we tell MyAvatar to go to a new location in the new domain
@ -1573,13 +1579,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
connect(this, SIGNAL(aboutToQuit()), this, SLOT(onAboutToQuit()));
// hook up bandwidth estimator
QSharedPointer<BandwidthRecorder> bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
connect(nodeList.data(), &LimitedNodeList::dataSent,
bandwidthRecorder.data(), &BandwidthRecorder::updateOutboundData);
connect(nodeList.data(), &LimitedNodeList::dataReceived,
bandwidthRecorder.data(), &BandwidthRecorder::updateInboundData);
// FIXME -- I'm a little concerned about this.
connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded,
this, &Application::checkSkeleton, Qt::QueuedConnection);
@ -2045,15 +2044,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed;
properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage;
auto bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond();
properties["packet_rate_out"] = bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond();
properties["kbps_in"] = bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond();
properties["kbps_out"] = bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond();
properties["atp_in_kbps"] = bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AssetServer);
auto nodeList = DependencyManager::get<NodeList>();
properties["packet_rate_in"] = nodeList->getInboundPPS();
properties["packet_rate_out"] = nodeList->getOutboundPPS();
properties["kbps_in"] = nodeList->getInboundKbps();
properties["kbps_out"] = nodeList->getOutboundKbps();
SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer);
SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer);
SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer);
@ -2064,6 +2060,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1;
properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1;
properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1;
properties["atp_in_kbps"] = messagesMixerNode ? assetServerNode->getInboundKbps() : 0.0f;
auto loadingRequests = ResourceCache::getLoadingRequests();
@ -2289,7 +2286,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// Setup the mouse ray pick and related operators
{
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_INCLUDE_NONCOLLIDABLE()), 0.0f, true);
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES()), 0.0f, true);
mouseRayPick->parentTransform = std::make_shared<MouseTransformNode>();
mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE);
auto mouseRayPickID = DependencyManager::get<PickManager>()->addPick(PickQuery::Ray, mouseRayPick);
@ -2622,6 +2619,7 @@ void Application::cleanupBeforeQuit() {
DependencyManager::destroy<PickManager>();
DependencyManager::destroy<KeyboardScriptingInterface>();
DependencyManager::destroy<Keyboard>();
DependencyManager::destroy<AvatarPackager>();
qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete";
}
@ -4911,7 +4909,7 @@ void Application::calibrateEyeTracker5Points() {
#endif
bool Application::exportEntities(const QString& filename,
const QVector<EntityItemID>& entityIDs,
const QVector<QUuid>& entityIDs,
const glm::vec3* givenOffset) {
QHash<EntityItemID, EntityItemPointer> entities;
@ -4986,16 +4984,12 @@ bool Application::exportEntities(const QString& filename, float x, float y, floa
glm::vec3 minCorner = center - vec3(scale);
float cubeSize = scale * 2;
AACube boundingCube(minCorner, cubeSize);
QVector<EntityItemPointer> entities;
QVector<EntityItemID> ids;
QVector<QUuid> entities;
auto entityTree = getEntities()->getTree();
entityTree->withReadLock([&] {
entityTree->findEntities(boundingCube, entities);
foreach(EntityItemPointer entity, entities) {
ids << entity->getEntityItemID();
}
entityTree->evalEntitiesInCube(boundingCube, PickFilter(), entities);
});
return exportEntities(filename, ids, &center);
return exportEntities(filename, entities, &center);
}
void Application::loadSettings() {
@ -6088,6 +6082,9 @@ void Application::update(float deltaTime) {
updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process...
updateDialogs(deltaTime); // update various stats dialogs if present
auto grabManager = DependencyManager::get<GrabManager>();
grabManager->simulateGrabs();
QSharedPointer<AvatarManager> avatarManager = DependencyManager::get<AvatarManager>();
{
@ -6995,6 +6992,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe
scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance());
}
scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance());
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
// hook our avatar and avatar hash map object into this script engine
@ -8712,6 +8710,14 @@ void Application::updateLoginDialogOverlayPosition() {
}
}
bool Application::hasRiftControllers() {
return PluginUtils::isOculusTouchControllerAvailable();
}
bool Application::hasViveControllers() {
return PluginUtils::isViveControllerAvailable();
}
void Application::onDismissedLoginDialog() {
_loginDialogPoppedUp = false;
loginDialogPoppedUp.set(false);
@ -8930,6 +8936,10 @@ void Application::copyToClipboard(const QString& text) {
QApplication::clipboard()->setText(text);
}
QString Application::getGraphicsCardType() {
return GPUIdent::getInstance()->getName();
}
#if defined(Q_OS_ANDROID)
void Application::beforeEnterBackground() {
auto nodeList = DependencyManager::get<NodeList>();

View file

@ -326,6 +326,10 @@ public:
void createLoginDialogOverlay();
void updateLoginDialogOverlayPosition();
// Check if a headset is connected
bool hasRiftControllers();
bool hasViveControllers();
#if defined(Q_OS_ANDROID)
void beforeEnterBackground();
void enterBackground();
@ -351,7 +355,7 @@ signals:
public slots:
QVector<EntityItemID> pasteEntities(float x, float y, float z);
bool exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs, const glm::vec3* givenOffset = nullptr);
bool exportEntities(const QString& filename, const QVector<QUuid>& entityIDs, const glm::vec3* givenOffset = nullptr);
bool exportEntities(const QString& filename, float x, float y, float z, float scale);
bool importEntities(const QString& url, const bool isObservable = true, const qint64 callerId = -1);
void updateThreadPoolCount() const;
@ -459,6 +463,8 @@ public slots:
void changeViewAsNeeded(float boomLength);
QString getGraphicsCardType();
private slots:
void onDesktopRootItemCreated(QQuickItem* qmlContext);
void onDesktopRootContextCreated(QQmlContext* qmlContext);
@ -787,6 +793,5 @@ private:
bool _showTrackedObjects { false };
bool _prevShowTrackedObjects { false };
};
#endif // hifi_Application_h

View file

@ -35,6 +35,7 @@
#include "assets/ATPAssetMigrator.h"
#include "audio/AudioScope.h"
#include "avatar/AvatarManager.h"
#include "avatar/AvatarPackager.h"
#include "AvatarBookmarks.h"
#include "devices/DdeFaceTracker.h"
#include "MainWindow.h"
@ -144,9 +145,13 @@ Menu::Menu() {
assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets());
}
// Edit > Package Avatar as .fst...
addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,
qApp, SLOT(packageModel()));
// Edit > Avatar Packager
#ifndef Q_OS_ANDROID
action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager);
connect(action, &QAction::triggered, [] {
DependencyManager::get<AvatarPackager>()->open();
});
#endif
// Edit > Reload All Content
addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
@ -645,6 +650,8 @@ Menu::Menu() {
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowTrackedObjects, 0, false, qApp, SLOT(setShowTrackedObjects(bool)));
addActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel()));
// Developer > Hands >>>
MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands");
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false,

View file

@ -46,6 +46,7 @@ namespace MenuOption {
const QString AutoMuteAudio = "Auto Mute Microphone";
const QString AvatarReceiveStats = "Show Receive Stats";
const QString AvatarBookmarks = "Avatar Bookmarks";
const QString AvatarPackager = "Avatar Packager";
const QString Back = "Back";
const QString BinaryEyelidControl = "Binary Eyelid Control";
const QString BookmarkAvatar = "Bookmark Avatar";

View file

@ -58,6 +58,16 @@ AvatarActionHold::~AvatarActionHold() {
#endif
}
void AvatarActionHold::removeFromOwner() {
auto avatarManager = DependencyManager::get<AvatarManager>();
if (avatarManager) {
auto myAvatar = avatarManager->getMyAvatar();
if (myAvatar) {
myAvatar->removeHoldAction(this);
}
}
}
bool AvatarActionHold::getAvatarRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) {
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
MyCharacterController* controller = myAvatar ? myAvatar->getCharacterController() : nullptr;
@ -143,7 +153,7 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm::
ownerEntity->setTransitingWithAvatar(_isTransitingWithAvatar);
}
}
if (holdingAvatar->isMyAvatar()) {
std::shared_ptr<MyAvatar> myAvatar = avatarManager->getMyAvatar();
@ -226,7 +236,7 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm::
}
rotation = palmRotation * _relativeRotation;
position = palmPosition + rotation * _relativePosition;
position = palmPosition + palmRotation * _relativePosition;
// update linearVelocity based on offset via _relativePosition;
linearVelocity = linearVelocity + glm::cross(angularVelocity, position - palmPosition);
@ -369,8 +379,12 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) {
hand = _hand;
}
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
holderID = myAvatar->getSessionUUID();
ok = true;
holderID = EntityDynamicInterface::extractStringArgument("hold", arguments, "holderID", ok, false);
if (!ok) {
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
holderID = myAvatar->getSessionUUID();
}
ok = true;
kinematic = EntityDynamicInterface::extractBooleanArgument("hold", arguments, "kinematic", ok, false);
@ -417,13 +431,13 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) {
_kinematicSetVelocity = kinematicSetVelocity;
_ignoreIK = ignoreIK;
_active = true;
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto ownerEntity = _ownerEntity.lock();
if (ownerEntity) {
ownerEntity->setDynamicDataDirty(true);
ownerEntity->setDynamicDataNeedsTransmit(true);
ownerEntity->setDynamicDataNeedsTransmit(true);
ownerEntity->setTransitingWithAvatar(myAvatar->getTransit()->isActive());
}
});

View file

@ -26,6 +26,8 @@ public:
AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntity);
virtual ~AvatarActionHold();
virtual void removeFromOwner() override;
virtual bool updateArguments(QVariantMap arguments) override;
virtual QVariantMap getArguments() override;

View file

@ -28,7 +28,6 @@
#pragma GCC diagnostic pop
#endif
#include <shared/QtHelpers.h>
#include <AvatarData.h>
#include <PerfStat.h>
@ -268,6 +267,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
if (avatar->getSkeletonModel()->isLoaded()) {
// remove the orb if it is there
avatar->removeOrb();
avatar->updateCollisionGroup(_myAvatar->getOtherAvatarsCollisionsEnabled());
if (avatar->needsPhysicsUpdate()) {
_avatarsToChangeInPhysics.insert(avatar);
}
@ -559,6 +559,7 @@ void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motion
}
void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents) {
bool playedCollisionSound { false };
for (Collision collision : collisionEvents) {
// TODO: The plan is to handle MOTIONSTATE_TYPE_AVATAR, and then MOTIONSTATE_TYPE_MYAVATAR. As it is, other
// people's avatars will have an id that doesn't match any entities, and one's own avatar will have
@ -566,43 +567,47 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents
// my avatar. (Other user machines will make a similar analysis and inject sound for their collisions.)
if (collision.idA.isNull() || collision.idB.isNull()) {
auto myAvatar = getMyAvatar();
auto collisionSound = myAvatar->getCollisionSound();
if (collisionSound) {
const auto characterController = myAvatar->getCharacterController();
const float avatarVelocityChange = (characterController ? glm::length(characterController->getVelocityChange()) : 0.0f);
const float velocityChange = glm::length(collision.velocityChange) + avatarVelocityChange;
const float MIN_AVATAR_COLLISION_ACCELERATION = 2.4f; // walking speed
const bool isSound = (collision.type == CONTACT_EVENT_TYPE_START) && (velocityChange > MIN_AVATAR_COLLISION_ACCELERATION);
myAvatar->collisionWithEntity(collision);
if (!isSound) {
return; // No sense iterating for others. We only have one avatar.
if (!playedCollisionSound) {
playedCollisionSound = true;
auto collisionSound = myAvatar->getCollisionSound();
if (collisionSound) {
const auto characterController = myAvatar->getCharacterController();
const float avatarVelocityChange =
(characterController ? glm::length(characterController->getVelocityChange()) : 0.0f);
const float velocityChange = glm::length(collision.velocityChange) + avatarVelocityChange;
const float MIN_AVATAR_COLLISION_ACCELERATION = 2.4f; // walking speed
const bool isSound =
(collision.type == CONTACT_EVENT_TYPE_START) && (velocityChange > MIN_AVATAR_COLLISION_ACCELERATION);
if (!isSound) {
return; // No sense iterating for others. We only have one avatar.
}
// Your avatar sound is personal to you, so let's say the "mass" part of the kinetic energy is already accounted for.
const float energy = velocityChange * velocityChange;
const float COLLISION_ENERGY_AT_FULL_VOLUME = 10.0f;
const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME);
// For general entity collisionSoundURL, playSound supports changing the pitch for the sound based on the size of the object,
// but most avatars are roughly the same size, so let's not be so fancy yet.
const float AVATAR_STRETCH_FACTOR = 1.0f;
_collisionInjectors.remove_if(
[](const AudioInjectorPointer& injector) { return !injector || injector->isFinished(); });
static const int MAX_INJECTOR_COUNT = 3;
if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) {
AudioInjectorOptions options;
options.stereo = collisionSound->isStereo();
options.position = myAvatar->getWorldPosition();
options.volume = energyFactorOfFull;
options.pitch = 1.0f / AVATAR_STRETCH_FACTOR;
auto injector = AudioInjector::playSoundAndDelete(collisionSound, options);
_collisionInjectors.emplace_back(injector);
}
}
// Your avatar sound is personal to you, so let's say the "mass" part of the kinetic energy is already accounted for.
const float energy = velocityChange * velocityChange;
const float COLLISION_ENERGY_AT_FULL_VOLUME = 10.0f;
const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME);
// For general entity collisionSoundURL, playSound supports changing the pitch for the sound based on the size of the object,
// but most avatars are roughly the same size, so let's not be so fancy yet.
const float AVATAR_STRETCH_FACTOR = 1.0f;
_collisionInjectors.remove_if([](const AudioInjectorPointer& injector) {
return !injector || injector->isFinished();
});
static const int MAX_INJECTOR_COUNT = 3;
if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) {
AudioInjectorOptions options;
options.stereo = collisionSound->isStereo();
options.position = myAvatar->getWorldPosition();
options.volume = energyFactorOfFull;
options.pitch = 1.0f / AVATAR_STRETCH_FACTOR;
auto injector = AudioInjector::playSoundAndDelete(collisionSound, options);
_collisionInjectors.emplace_back(injector);
}
myAvatar->collisionWithEntity(collision);
return;
}
}
}
@ -917,3 +922,13 @@ QVariantMap AvatarManager::getPalData(const QStringList& specificAvatarIdentifie
doc.insert("data", palData);
return doc.toVariantMap();
}
void AvatarManager::accumulateGrabPositions(std::map<QUuid, GrabLocationAccumulator>& grabAccumulators) {
auto avatarMap = getHashCopy();
AvatarHash::iterator itr = avatarMap.begin();
while (itr != avatarMap.end()) {
const auto& avatar = std::static_pointer_cast<Avatar>(*itr);
avatar->accumulateGrabPositions(grabAccumulators);
itr++;
}
}

View file

@ -199,6 +199,8 @@ public:
void handleProcessedPhysicsTransaction(PhysicsEngine::Transaction& transaction);
void removeDeadAvatarEntities(const SetOfEntities& deadEntities);
void accumulateGrabPositions(std::map<QUuid, GrabLocationAccumulator>& grabAccumulators);
public slots:
/**jsdoc
* @function AvatarManager.updateAvatarRenderStatus
@ -216,7 +218,7 @@ private:
void simulateAvatarFades(float deltaTime);
AvatarSharedPointer newSharedAvatar() override;
// called only from the AvatarHashMap thread - cannot be called while this thread holds the
// hash lock, since handleRemovedAvatar needs a write lock on the entity tree and the entity tree
// frequently grabs a read lock on the hash to get a given avatar by ID

View file

@ -19,6 +19,7 @@
AvatarMotionState::AvatarMotionState(OtherAvatarPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) {
assert(_avatar);
_type = MOTIONSTATE_TYPE_AVATAR;
_collisionGroup = BULLET_COLLISION_GROUP_OTHER_AVATAR;
cacheShapeDiameter();
}
@ -171,8 +172,8 @@ QUuid AvatarMotionState::getSimulatorID() const {
// virtual
void AvatarMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const {
group = BULLET_COLLISION_GROUP_OTHER_AVATAR;
mask = Physics::getDefaultCollisionMask(group);
group = _collisionGroup;
mask = _collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ? 0 : Physics::getDefaultCollisionMask(group);
}
// virtual

View file

@ -66,6 +66,9 @@ public:
void addDirtyFlags(uint32_t flags) { _dirtyFlags |= flags; }
void setCollisionGroup(int32_t group) { _collisionGroup = group; }
int32_t getCollisionGroup() { return _collisionGroup; }
virtual void computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const override;
virtual float getMass() const override;
@ -87,7 +90,7 @@ protected:
OtherAvatarPointer _avatar;
float _diameter { 0.0f };
int32_t _collisionGroup;
uint32_t _dirtyFlags;
};

View file

@ -0,0 +1,149 @@
//
// AvatarPackager.cpp
//
//
// Created by Thijs Wenker on 12/6/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
//
#include "AvatarPackager.h"
#include "Application.h"
#include <QQmlContext>
#include <QQmlEngine>
#include <QUrl>
#include <OffscreenUi.h>
#include "ModelSelector.h"
#include <avatar/MarketplaceItemUploader.h>
#include <mutex>
#include "ui/TabletScriptingInterface.h"
std::once_flag setupQMLTypesFlag;
AvatarPackager::AvatarPackager() {
std::call_once(setupQMLTypesFlag, []() {
qmlRegisterType<FST>();
qmlRegisterType<MarketplaceItemUploader>();
qRegisterMetaType<AvatarPackager*>();
qRegisterMetaType<AvatarProject*>();
qRegisterMetaType<AvatarProjectStatus::AvatarProjectStatus>();
qmlRegisterUncreatableMetaObject(
AvatarProjectStatus::staticMetaObject,
"Hifi.AvatarPackager.AvatarProjectStatus",
1, 0,
"AvatarProjectStatus",
"Error: only enums"
);
});
recentProjectsFromVariantList(_recentProjectsSetting.get());
QDir defaultProjectsDir(AvatarProject::getDefaultProjectsPath());
defaultProjectsDir.mkpath(".");
}
bool AvatarPackager::open() {
const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) {
context->setContextProperty("AvatarPackagerCore", this);
};
static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
auto tablet = dynamic_cast<TabletProxy*>(DependencyManager::get<TabletScriptingInterface>()->getTablet(SYSTEM_TABLET));
if (tablet->getToolbarMode()) {
static const QUrl url{ "hifi/AvatarPackagerWindow.qml" };
DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated);
return true;
}
static const QUrl url{ "hifi/tablet/AvatarPackager.qml" };
if (!tablet->isPathLoaded(url)) {
tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this);
tablet->pushOntoStack(url);
return true;
}
return false;
}
void AvatarPackager::addCurrentProjectToRecentProjects() {
const int MAX_RECENT_PROJECTS = 5;
const QString& fstPath = _currentAvatarProject->getFSTPath();
auto removeProjects = QVector<RecentAvatarProject>();
for (const auto& project : _recentProjects) {
if (project.getProjectFSTPath() == fstPath) {
removeProjects.append(project);
}
}
for (const auto& removeProject : removeProjects) {
_recentProjects.removeOne(removeProject);
}
const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath);
_recentProjects.prepend(newRecentProject);
while (_recentProjects.size() > MAX_RECENT_PROJECTS) {
_recentProjects.pop_back();
}
_recentProjectsSetting.set(recentProjectsToVariantList(false));
emit recentProjectsChanged();
}
QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) const {
QVariantList result;
for (const auto& project : _recentProjects) {
QVariantMap projectVariant;
projectVariant.insert("name", project.getProjectName());
projectVariant.insert("path", project.getProjectFSTPath());
if (includeProjectPaths) {
projectVariant.insert("projectPath", project.getProjectPath());
}
result.append(projectVariant);
}
return result;
}
void AvatarPackager::recentProjectsFromVariantList(QVariantList projectsVariant) {
_recentProjects.clear();
for (const auto& projectVariant : projectsVariant) {
auto map = projectVariant.toMap();
_recentProjects.append(RecentAvatarProject(map.value("name").toString(), map.value("path").toString()));
}
}
AvatarProjectStatus::AvatarProjectStatus AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) {
AvatarProjectStatus::AvatarProjectStatus status;
setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath, status));
return status;
}
AvatarProjectStatus::AvatarProjectStatus AvatarPackager::createAvatarProject(const QString& projectsFolder,
const QString& avatarProjectName,
const QString& avatarModelPath,
const QString& textureFolder) {
AvatarProjectStatus::AvatarProjectStatus status;
setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder, status));
return status;
}
void AvatarPackager::setAvatarProject(AvatarProject* avatarProject) {
if (avatarProject == _currentAvatarProject) {
return;
}
if (_currentAvatarProject) {
_currentAvatarProject->deleteLater();
}
_currentAvatarProject = avatarProject;
if (_currentAvatarProject) {
addCurrentProjectToRecentProjects();
connect(_currentAvatarProject, &AvatarProject::nameChanged, this, &AvatarPackager::addCurrentProjectToRecentProjects);
QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership);
}
emit avatarProjectChanged();
}

View file

@ -0,0 +1,100 @@
//
// AvatarPackager.h
//
//
// Created by Thijs Wenker on 12/6/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
//
#pragma once
#ifndef hifi_AvatarPackager_h
#define hifi_AvatarPackager_h
#include <QObject>
#include <DependencyManager.h>
#include "FileDialogHelper.h"
#include "avatar/AvatarProject.h"
#include "SettingHandle.h"
class RecentAvatarProject {
public:
RecentAvatarProject() = default;
RecentAvatarProject(QString projectName, QString projectFSTPath) {
_projectName = projectName;
_projectFSTPath = projectFSTPath;
}
RecentAvatarProject(const RecentAvatarProject& other) {
_projectName = other._projectName;
_projectFSTPath = other._projectFSTPath;
}
QString getProjectName() const { return _projectName; }
QString getProjectFSTPath() const { return _projectFSTPath; }
QString getProjectPath() const {
return QFileInfo(_projectFSTPath).absoluteDir().absolutePath();
}
bool operator==(const RecentAvatarProject& other) const {
return _projectName == other._projectName && _projectFSTPath == other._projectFSTPath;
}
private:
QString _projectName;
QString _projectFSTPath;
};
class AvatarPackager : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
Q_PROPERTY(AvatarProject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged)
Q_PROPERTY(QString AVATAR_PROJECTS_PATH READ getAvatarProjectsPath CONSTANT)
Q_PROPERTY(QVariantList recentProjects READ getRecentProjects NOTIFY recentProjectsChanged)
public:
AvatarPackager();
bool open();
Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus createAvatarProject(const QString& projectsFolder,
const QString& avatarProjectName,
const QString& avatarModelPath,
const QString& textureFolder);
Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath);
Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) const {
return AvatarProject::isValidNewProjectName(projectPath, projectName);
}
signals:
void avatarProjectChanged();
void recentProjectsChanged();
private:
Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; };
Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); }
Q_INVOKABLE QVariantList getRecentProjects() const { return recentProjectsToVariantList(true); }
void setAvatarProject(AvatarProject* avatarProject);
void addCurrentProjectToRecentProjects();
AvatarProject* _currentAvatarProject { nullptr };
QVector<RecentAvatarProject> _recentProjects;
QVariantList recentProjectsToVariantList(bool includeProjectPaths) const;
void recentProjectsFromVariantList(QVariantList projectsVariant);
Setting::Handle<QVariantList> _recentProjectsSetting { "io.highfidelity.avatarPackager.recentProjects", QVariantList() };
};
#endif // hifi_AvatarPackager_h

View file

@ -0,0 +1,260 @@
//
// AvatarProject.cpp
//
//
// Created by Thijs Wenker on 12/7/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
//
#include "AvatarProject.h"
#include <FSTReader.h>
#include <QFile>
#include <QFileInfo>
#include <QQmlEngine>
#include <QTimer>
#include "FBXSerializer.h"
#include <ui/TabletScriptingInterface.h>
#include "scripting/HMDScriptingInterface.h"
AvatarProject* AvatarProject::openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status) {
status = AvatarProjectStatus::NONE;
if (!path.toLower().endsWith(".fst")) {
status = AvatarProjectStatus::ERROR_OPEN_INVALID_FILE_TYPE;
return nullptr;
}
QFileInfo fstFileInfo{ path };
if (!fstFileInfo.absoluteDir().exists()) {
status = AvatarProjectStatus::ERROR_OPEN_PROJECT_FOLDER;
return nullptr;
}
if (!fstFileInfo.exists()) {
status = AvatarProjectStatus::ERROR_OPEN_FIND_FST;
return nullptr;
}
QFile file{ fstFileInfo.filePath() };
if (!file.open(QIODevice::ReadOnly)) {
status = AvatarProjectStatus::ERROR_OPEN_OPEN_FST;
return nullptr;
}
const auto project = new AvatarProject(path, file.readAll());
QFileInfo fbxFileInfo{ project->getFBXPath() };
if (!fbxFileInfo.exists()) {
project->deleteLater();
status = AvatarProjectStatus::ERROR_OPEN_FIND_MODEL;
return nullptr;
}
QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership);
status = AvatarProjectStatus::SUCCESS;
return project;
}
AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName,
const QString& avatarModelPath, const QString& textureFolder,
AvatarProjectStatus::AvatarProjectStatus& status) {
status = AvatarProjectStatus::NONE;
if (!isValidNewProjectName(projectsFolder, avatarProjectName)) {
status = AvatarProjectStatus::ERROR_CREATE_PROJECT_NAME;
return nullptr;
}
QDir projectDir(projectsFolder + "/" + avatarProjectName);
if (!projectDir.mkpath(".")) {
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
return nullptr;
}
QDir projectTexturesDir(projectDir.path() + "/textures");
if (!projectTexturesDir.mkpath(".")) {
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
return nullptr;
}
QDir projectScriptsDir(projectDir.path() + "/scripts");
if (!projectScriptsDir.mkpath(".")) {
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
return nullptr;
}
const auto fileName = QFileInfo(avatarModelPath).fileName();
const auto newModelPath = projectDir.absoluteFilePath(fileName);
const auto newFSTPath = projectDir.absoluteFilePath("avatar.fst");
QFile::copy(avatarModelPath, newModelPath);
QFileInfo fbxInfo{ newModelPath };
if (!fbxInfo.exists() || !fbxInfo.isFile()) {
status = AvatarProjectStatus::ERROR_CREATE_FIND_MODEL;
return nullptr;
}
QFile fbx{ fbxInfo.filePath() };
if (!fbx.open(QIODevice::ReadOnly)) {
status = AvatarProjectStatus::ERROR_CREATE_OPEN_MODEL;
return nullptr;
}
std::shared_ptr<hfm::Model> hfmModel;
try {
const QByteArray fbxContents = fbx.readAll();
hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath());
} catch (const QString& error) {
Q_UNUSED(error)
status = AvatarProjectStatus::ERROR_CREATE_READ_MODEL;
return nullptr;
}
QStringList textures{};
auto addTextureToList = [&textures](hfm::Texture texture) mutable {
if (!texture.filename.isEmpty() && texture.content.isEmpty() && !textures.contains(texture.filename)) {
textures << texture.filename;
}
};
foreach(const HFMMaterial material, hfmModel->materials) {
addTextureToList(material.normalTexture);
addTextureToList(material.albedoTexture);
addTextureToList(material.opacityTexture);
addTextureToList(material.glossTexture);
addTextureToList(material.roughnessTexture);
addTextureToList(material.specularTexture);
addTextureToList(material.metallicTexture);
addTextureToList(material.emissiveTexture);
addTextureToList(material.occlusionTexture);
addTextureToList(material.scatteringTexture);
addTextureToList(material.lightmapTexture);
}
QDir textureDir(textureFolder.isEmpty() ? fbxInfo.absoluteDir() : textureFolder);
for (const auto& texture : textures) {
QString sourcePath = textureDir.path() + "/" + texture;
QString targetPath = projectTexturesDir.path() + "/" + texture;
QFileInfo sourceTexturePath(sourcePath);
if (sourceTexturePath.exists()) {
QFile::copy(sourcePath, targetPath);
}
}
auto fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel);
fst->setName(avatarProjectName);
if (!fst->write()) {
status = AvatarProjectStatus::ERROR_CREATE_WRITE_FST;
return nullptr;
}
status = AvatarProjectStatus::SUCCESS;
return new AvatarProject(fst);
}
QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const {
QStringList result{};
constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
if (!scriptsDir.exists()) {
return result;
}
for (const auto& script : scriptsDir.entryInfoList({}, flags)) {
if (script.fileName().toLower().endsWith(".js")) {
result.push_back("scripts/" + script.fileName());
}
}
return result;
}
bool AvatarProject::isValidNewProjectName(const QString& projectPath, const QString& projectName) {
if (projectPath.trimmed().isEmpty() || projectName.trimmed().isEmpty()) {
return false;
}
QDir dir(projectPath + "/" + projectName);
return !dir.exists();
}
AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) :
AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) {
}
AvatarProject::AvatarProject(FST* fst) {
_fst = fst;
auto fileInfo = QFileInfo(getFSTPath());
_directory = fileInfo.absoluteDir();
_fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts")));
_fst->write();
refreshProjectFiles();
_projectPath = fileInfo.absoluteDir().absolutePath();
}
void AvatarProject::appendDirectory(const QString& prefix, const QDir& dir) {
constexpr auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
for (auto& entry : dir.entryInfoList({}, flags)) {
if (entry.isFile()) {
_projectFiles.append({ entry.absoluteFilePath(), prefix + entry.fileName() });
} else if (entry.isDir()) {
appendDirectory(prefix + entry.fileName() + "/", entry.absoluteFilePath());
}
}
}
void AvatarProject::refreshProjectFiles() {
_projectFiles.clear();
appendDirectory("", _directory);
}
QStringList AvatarProject::getProjectFiles() const {
QStringList paths;
for (auto& path : _projectFiles) {
paths.append(path.relativePath);
}
return paths;
}
MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) {
QUuid itemID;
if (updateExisting) {
itemID = _fst->getMarketplaceID();
}
auto uploader = new MarketplaceItemUploader(getProjectName(), "", QFileInfo(getFSTPath()).fileName(),
itemID, _projectFiles);
connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() {
if (uploader->getError() == MarketplaceItemUploader::Error::None) {
_fst->setMarketplaceID(uploader->getMarketplaceID());
_fst->write();
}
});
return uploader;
}
void AvatarProject::openInInventory() const {
constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 };
auto tablet = dynamic_cast<TabletProxy*>(
DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system"));
tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml");
DependencyManager::get<HMDScriptingInterface>()->openTablet();
tablet->getTabletRoot()->forceActiveFocus();
auto name = getProjectName();
// I'm not a fan of this, but it's the only current option.
QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() {
tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } }));
});
}

View file

@ -0,0 +1,115 @@
//
// AvatarProject.h
//
//
// Created by Thijs Wenker on 12/7/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
//
#pragma once
#ifndef hifi_AvatarProject_h
#define hifi_AvatarProject_h
#include "MarketplaceItemUploader.h"
#include "ProjectFile.h"
#include "FST.h"
#include <QObject>
#include <QDir>
#include <QVariantHash>
#include <QStandardPaths>
namespace AvatarProjectStatus {
Q_NAMESPACE
enum AvatarProjectStatus {
NONE,
SUCCESS,
ERROR_CREATE_PROJECT_NAME,
ERROR_CREATE_CREATING_DIRECTORIES,
ERROR_CREATE_FIND_MODEL,
ERROR_CREATE_OPEN_MODEL,
ERROR_CREATE_READ_MODEL,
ERROR_CREATE_WRITE_FST,
ERROR_OPEN_INVALID_FILE_TYPE,
ERROR_OPEN_PROJECT_FOLDER,
ERROR_OPEN_FIND_FST,
ERROR_OPEN_OPEN_FST,
ERROR_OPEN_FIND_MODEL
};
Q_ENUM_NS(AvatarProjectStatus)
}
class AvatarProject : public QObject {
Q_OBJECT
Q_PROPERTY(FST* fst READ getFST CONSTANT)
Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged)
Q_PROPERTY(QString projectFolderPath READ getProjectPath CONSTANT)
Q_PROPERTY(QString projectFSTPath READ getFSTPath CONSTANT)
Q_PROPERTY(QString projectFBXPath READ getFBXPath CONSTANT)
Q_PROPERTY(QString name READ getProjectName WRITE setProjectName NOTIFY nameChanged)
public:
Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting);
Q_INVOKABLE void openInInventory() const;
Q_INVOKABLE QStringList getProjectFiles() const;
Q_INVOKABLE QString getProjectName() const { return _fst->getName(); }
Q_INVOKABLE void setProjectName(const QString& newProjectName) {
if (newProjectName.trimmed().length() > 0) {
_fst->setName(newProjectName);
_fst->write();
emit nameChanged();
}
}
Q_INVOKABLE QString getProjectPath() const { return _projectPath; }
Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); }
Q_INVOKABLE QString getFBXPath() const {
return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath()));
}
/**
* returns the AvatarProject or a nullptr on failure.
*/
static AvatarProject* openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status);
static AvatarProject* createAvatarProject(const QString& projectsFolder,
const QString& avatarProjectName,
const QString& avatarModelPath,
const QString& textureFolder,
AvatarProjectStatus::AvatarProjectStatus& status);
static bool isValidNewProjectName(const QString& projectPath, const QString& projectName);
static QString getDefaultProjectsPath() {
return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects";
}
signals:
void nameChanged();
void projectFilesChanged();
private:
AvatarProject(const QString& fstPath, const QByteArray& data);
AvatarProject(FST* fst);
~AvatarProject() { _fst->deleteLater(); }
FST* getFST() { return _fst; }
void refreshProjectFiles();
void appendDirectory(const QString& prefix, const QDir& dir);
QStringList getScriptPaths(const QDir& scriptsDir) const;
FST* _fst;
QDir _directory;
QList<ProjectFilePath> _projectFiles{};
QString _projectPath;
};
#endif // hifi_AvatarProject_h

View file

@ -0,0 +1,39 @@
//
// GrabManager.cpp
// interface/src/avatar/
//
// Created by Seth Alves on 2018-12-4.
// 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 "GrabManager.h"
void GrabManager::simulateGrabs() {
QSharedPointer<AvatarManager> avatarManager = DependencyManager::get<AvatarManager>();
// Update grabbed objects
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
auto entityTree = entityTreeRenderer->getTree();
entityTree->withReadLock([&] {
PROFILE_RANGE(simulation, "Grabs");
std::map<QUuid, GrabLocationAccumulator> grabAccumulators;
avatarManager->accumulateGrabPositions(grabAccumulators);
for (auto& accumulatedLocation : grabAccumulators) {
QUuid grabbedThingID = accumulatedLocation.first;
GrabLocationAccumulator& acc = accumulatedLocation.second;
bool success;
SpatiallyNestablePointer grabbedThing = SpatiallyNestable::findByID(grabbedThingID, success);
if (success && grabbedThing) {
glm::vec3 finalPosition = acc.finalizePosition();
glm::quat finalOrientation = acc.finalizeOrientation();
grabbedThing->setTransform(createMatFromQuatAndPos(finalOrientation, finalPosition));
}
}
});
}

View file

@ -0,0 +1,23 @@
//
// GrabManager.h
// interface/src/avatar/
//
// Created by Seth Alves on 2018-12-4.
// 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 <AvatarData.h>
#include <EntityTreeRenderer.h>
#include "AvatarManager.h"
class GrabManager : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
public:
void simulateGrabs();
};

View file

@ -0,0 +1,321 @@
//
// MarketplaceItemUploader.cpp
//
//
// Created by Ryan Huffman on 12/10/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
//
#include "MarketplaceItemUploader.h"
#include <AccountManager.h>
#include <DependencyManager.h>
#ifndef Q_OS_ANDROID
#include <quazip5/quazip.h>
#include <quazip5/quazipfile.h>
#endif
#include <QTimer>
#include <QBuffer>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
MarketplaceItemUploader::MarketplaceItemUploader(QString title,
QString description,
QString rootFilename,
QUuid marketplaceID,
QList<ProjectFilePath> filePaths) :
_title(title),
_description(description),
_rootFilename(rootFilename),
_marketplaceID(marketplaceID),
_filePaths(filePaths) {
}
void MarketplaceItemUploader::setState(State newState) {
Q_ASSERT(_state != State::Complete);
Q_ASSERT(_error == Error::None);
Q_ASSERT(newState != _state);
_state = newState;
emit stateChanged(newState);
if (newState == State::Complete) {
emit completed();
emit finishedChanged();
}
}
void MarketplaceItemUploader::setError(Error error) {
Q_ASSERT(_state != State::Complete);
Q_ASSERT(_error == Error::None);
_error = error;
emit errorChanged(error);
emit finishedChanged();
}
void MarketplaceItemUploader::send() {
doGetCategories();
}
void MarketplaceItemUploader::doGetCategories() {
setState(State::GettingCategories);
static const QString path = "/api/v1/marketplace/categories";
auto accountManager = DependencyManager::get<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::None);
qWarning() << "Request url is: " << request.url();
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply = networkAccessManager.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
auto error = reply->error();
if (error == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(reply->readAll());
auto extractCategoryID = [&doc]() -> std::pair<bool, int> {
auto items = doc.object()["data"].toObject()["items"];
if (!items.isArray()) {
qWarning() << "Categories parse error: data.items is not an array";
return { false, 0 };
}
auto itemsArray = items.toArray();
for (const auto item : itemsArray) {
if (!item.isObject()) {
qWarning() << "Categories parse error: item is not an object";
return { false, 0 };
}
auto itemObject = item.toObject();
if (itemObject["name"].toString() == "Avatars") {
auto idValue = itemObject["id"];
if (!idValue.isDouble()) {
qWarning() << "Categories parse error: id is not a number";
return { false, 0 };
}
return { true, (int)idValue.toDouble() };
}
}
qWarning() << "Categories parse error: could not find a category for 'Avatar'";
return { false, 0 };
};
bool success;
std::tie(success, _categoryID) = extractCategoryID();
if (!success) {
qWarning() << "Failed to find marketplace category id";
setError(Error::Unknown);
} else {
qDebug() << "Marketplace Avatar category ID is" << _categoryID;
doUploadAvatar();
}
} else {
setError(Error::Unknown);
}
});
}
void MarketplaceItemUploader::doUploadAvatar() {
#ifdef Q_OS_ANDROID
qWarning() << "Marketplace uploading is not supported on Android";
setError(Error::Unknown);
return;
#else
QBuffer buffer{ &_fileData };
QuaZip zip{ &buffer };
if (!zip.open(QuaZip::Mode::mdAdd)) {
qWarning() << "Failed to open zip";
setError(Error::Unknown);
return;
}
for (auto& filePath : _filePaths) {
qWarning() << "Zipping: " << filePath.absolutePath << filePath.relativePath;
QFileInfo fileInfo{ filePath.absolutePath };
QuaZipFile zipFile{ &zip };
if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(filePath.relativePath))) {
qWarning() << "Could not open zip file:" << zipFile.getZipError();
setError(Error::Unknown);
return;
}
QFile file{ filePath.absolutePath };
if (file.open(QIODevice::ReadOnly)) {
zipFile.write(file.readAll());
} else {
qWarning() << "Failed to open: " << filePath.absolutePath;
}
file.close();
zipFile.close();
if (zipFile.getZipError() != UNZ_OK) {
qWarning() << "Could not close zip file: " << zipFile.getZipError();
setState(State::Complete);
return;
}
}
zip.close();
qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB";
QString path = "/api/v1/marketplace/items";
bool creating = true;
if (!_marketplaceID.isNull()) {
creating = false;
auto idWithBraces = _marketplaceID.toString();
auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2);
path += "/" + idWithoutBraces;
}
auto accountManager = DependencyManager::get<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
// TODO(huffman) add JSON escaping
auto escapeJson = [](QString str) -> QString { return str; };
QString jsonString = "{\"marketplace_item\":{";
jsonString += "\"title\":\"" + escapeJson(_title) + "\"";
// Items cannot have their description updated after they have been submitted.
if (creating) {
jsonString += ",\"description\":\"" + escapeJson(_description) + "\"";
}
jsonString += ",\"root_file_key\":\"" + escapeJson(_rootFilename) + "\"";
jsonString += ",\"category_ids\":[" + QStringLiteral("%1").arg(_categoryID) + "]";
jsonString += ",\"license\":0";
jsonString += ",\"files\":\"" + QString::fromLatin1(_fileData.toBase64()) + "\"}}";
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply{ nullptr };
if (creating) {
reply = networkAccessManager.post(request, jsonString.toUtf8());
} else {
reply = networkAccessManager.put(request, jsonString.toUtf8());
}
connect(reply, &QNetworkReply::uploadProgress, this, [this](float bytesSent, float bytesTotal) {
if (_state == State::UploadingAvatar) {
emit uploadProgress(bytesSent, bytesTotal);
if (bytesSent >= bytesTotal) {
setState(State::WaitingForUploadResponse);
}
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
_responseData = reply->readAll();
auto error = reply->error();
if (error == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(_responseData.toLatin1());
auto status = doc.object()["status"].toString();
if (status == "success") {
_marketplaceID = QUuid::fromString(doc["data"].toObject()["marketplace_id"].toString());
_itemVersion = doc["data"].toObject()["version"].toDouble();
setState(State::WaitingForInventory);
doWaitForInventory();
} else {
qWarning() << "Got error response while uploading avatar: " << _responseData;
setError(Error::Unknown);
}
} else {
qWarning() << "Got error while uploading avatar: " << reply->error() << reply->errorString() << _responseData;
setError(Error::Unknown);
}
});
setState(State::UploadingAvatar);
#endif
}
void MarketplaceItemUploader::doWaitForInventory() {
static const QString path = "/api/v1/commerce/inventory";
auto accountManager = DependencyManager::get<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply = networkAccessManager.post(request, "");
_numRequestsForInventory++;
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
auto data = reply->readAll();
bool success = false;
auto error = reply->error();
if (error == QNetworkReply::NoError) {
// Parse response data
auto doc = QJsonDocument::fromJson(data);
auto isAssetAvailable = [this, &doc]() -> bool {
if (!doc.isObject()) {
return false;
}
auto root = doc.object();
auto status = root["status"].toString();
if (status != "success") {
return false;
}
auto data = root["data"];
if (!data.isObject()) {
return false;
}
auto assets = data.toObject()["assets"];
if (!assets.isArray()) {
return false;
}
for (auto asset : assets.toArray()) {
auto assetObject = asset.toObject();
auto id = QUuid::fromString(assetObject["id"].toString());
if (id.isNull()) {
continue;
}
if (id == _marketplaceID) {
auto version = assetObject["version"];
auto valid = assetObject["valid"];
if (version.isDouble() && valid.isBool()) {
if ((int)version.toDouble() >= _itemVersion && valid.toBool()) {
return true;
}
}
}
}
return false;
};
success = isAssetAvailable();
}
if (success) {
qDebug() << "Found item in inventory";
setState(State::Complete);
} else {
constexpr int MAX_INVENTORY_REQUESTS { 8 };
constexpr int TIME_BETWEEN_INVENTORY_REQUESTS_MS { 5000 };
if (_numRequestsForInventory > MAX_INVENTORY_REQUESTS) {
qDebug() << "Failed to find item in inventory";
setError(Error::Unknown);
} else {
QTimer::singleShot(TIME_BETWEEN_INVENTORY_REQUESTS_MS, [this]() { doWaitForInventory(); });
}
}
});
}

View file

@ -0,0 +1,105 @@
//
// MarketplaceItemUploader.h
//
//
// Created by Ryan Huffman on 12/10/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
//
#pragma once
#ifndef hifi_MarketplaceItemUploader_h
#define hifi_MarketplaceItemUploader_h
#include "ProjectFile.h"
#include <QObject>
#include <QUuid>
class QNetworkReply;
class MarketplaceItemUploader : public QObject {
Q_OBJECT
Q_PROPERTY(bool finished READ getFinished NOTIFY finishedChanged)
Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged)
Q_PROPERTY(State state READ getState NOTIFY stateChanged)
Q_PROPERTY(Error error READ getError NOTIFY errorChanged)
Q_PROPERTY(QString responseData READ getResponseData)
public:
enum class Error {
None,
Unknown,
};
Q_ENUM(Error);
enum class State {
Idle,
GettingCategories,
UploadingAvatar,
WaitingForUploadResponse,
WaitingForInventory,
Complete,
};
Q_ENUM(State);
MarketplaceItemUploader(QString title,
QString description,
QString rootFilename,
QUuid marketplaceID,
QList<ProjectFilePath> filePaths);
Q_INVOKABLE void send();
void setError(Error error);
QString getResponseData() const { return _responseData; }
void setState(State newState);
State getState() const { return _state; }
bool getComplete() const { return _state == State::Complete; }
QUuid getMarketplaceID() const { return _marketplaceID; }
Error getError() const { return _error; }
bool getFinished() const { return _state == State::Complete || _error != Error::None; }
signals:
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
void completed();
void stateChanged(State newState);
void errorChanged(Error error);
// Triggered when the upload has finished, either succesfully completing, or stopping with an error
void finishedChanged();
private:
void doGetCategories();
void doUploadAvatar();
void doWaitForInventory();
QNetworkReply* _reply;
State _state { State::Idle };
Error _error { Error::None };
QString _title;
QString _description;
QString _rootFilename;
QUuid _marketplaceID;
int _categoryID;
int _itemVersion;
QString _responseData;
int _numRequestsForInventory { 0 };
QString _rootFilePath;
QList<ProjectFilePath> _filePaths;
QByteArray _fileData;
};
#endif // hifi_MarketplaceItemUploader_h

View file

@ -746,7 +746,7 @@ void MyAvatar::updateChildCauterization(SpatiallyNestablePointer object, bool ca
void MyAvatar::simulate(float deltaTime) {
PerformanceTimer perfTimer("simulate");
animateScaleChanges(deltaTime);
setFlyingEnabled(getFlyingEnabled());
if (_cauterizationNeedsUpdate) {
@ -820,6 +820,7 @@ void MyAvatar::simulate(float deltaTime) {
// and all of its joints, now update our attachements.
Avatar::simulateAttachments(deltaTime);
relayJointDataToChildren();
updateGrabs();
if (!_skeletonModel->hasSkeleton()) {
// All the simulation that can be done has been done
@ -874,47 +875,12 @@ void MyAvatar::simulate(float deltaTime) {
zoneAllowsFlying = zone->getFlyingAllowed();
collisionlessAllowed = zone->getGhostingAllowed();
}
auto now = usecTimestampNow();
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
MovingEntitiesOperator moveOperator;
bool force = false;
bool iShouldTellServer = true;
forEachDescendant([&](SpatiallyNestablePointer object) {
// if the queryBox has changed, tell the entity-server
if (object->getNestableType() == NestableType::Entity && object->updateQueryAACube()) {
EntityItemPointer entity = std::static_pointer_cast<EntityItem>(object);
bool success;
AACube newCube = entity->getQueryAACube(success);
if (success) {
moveOperator.addEntityToMoveList(entity, newCube);
}
// send an edit packet to update the entity-server about the queryAABox
if (packetSender && entity->isDomainEntity()) {
EntityItemProperties properties = entity->getProperties();
properties.setQueryAACubeDirty();
properties.setLastEdited(now);
packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree,
entity->getID(), properties);
entity->setLastBroadcast(usecTimestampNow());
entity->forEachDescendant([&](SpatiallyNestablePointer descendant) {
EntityItemPointer entityDescendant = std::dynamic_pointer_cast<EntityItem>(descendant);
if (entityDescendant && entityDescendant->isDomainEntity() && descendant->updateQueryAACube()) {
EntityItemProperties descendantProperties;
descendantProperties.setQueryAACube(descendant->getQueryAACube());
descendantProperties.setLastEdited(now);
packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree,
entityDescendant->getID(), descendantProperties);
entityDescendant->setLastBroadcast(now); // for debug/physics status icons
}
});
}
}
entityTree->updateEntityQueryAACube(object, packetSender, force, iShouldTellServer);
});
// also update the position of children in our local octree
if (moveOperator.hasMovingEntities()) {
PerformanceTimer perfTimer("recurseTreeWithOperator");
entityTree->recurseTreeWithOperator(&moveOperator);
}
});
bool isPhysicsEnabled = qApp->isPhysicsEnabled();
_characterController.setFlyingAllowed((zoneAllowsFlying && _enableFlying) || !isPhysicsEnabled);
@ -1565,35 +1531,74 @@ ScriptAvatarData* MyAvatar::getTargetAvatar() const {
}
}
void MyAvatar::updateLookAtTargetAvatar() {
//
// Look at the avatar whose eyes are closest to the ray in direction of my avatar's head
// And set the correctedLookAt for all (nearby) avatars that are looking at me.
_lookAtTargetAvatar.reset();
_targetAvatarPosition = glm::vec3(0.0f);
static float lookAtCostFunction(const glm::vec3& myForward, const glm::vec3& myPosition, const glm::vec3& otherForward, const glm::vec3& otherPosition,
bool otherIsTalking, bool lookingAtOtherAlready) {
const float DISTANCE_FACTOR = 3.14f;
const float MY_ANGLE_FACTOR = 1.0f;
const float OTHER_ANGLE_FACTOR = 1.0f;
const float OTHER_IS_TALKING_TERM = otherIsTalking ? 1.0f : 0.0f;
const float LOOKING_AT_OTHER_ALREADY_TERM = lookingAtOtherAlready ? -0.2f : 0.0f;
glm::vec3 lookForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD;
glm::vec3 cameraPosition = qApp->getCamera().getPosition();
const float GREATEST_LOOKING_AT_DISTANCE = 10.0f; // meters
const float MAX_MY_ANGLE = PI / 8.0f; // 22.5 degrees, Don't look too far away from the head facing.
const float MAX_OTHER_ANGLE = (3.0f * PI) / 4.0f; // 135 degrees, Don't stare at the back of another avatars head.
float smallestAngleTo = glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES) / 2.0f;
const float KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR = 1.3f;
const float GREATEST_LOOKING_AT_DISTANCE = 10.0f;
glm::vec3 d = otherPosition - myPosition;
float distance = glm::length(d);
glm::vec3 dUnit = d / distance;
float myAngle = acosf(glm::dot(myForward, dUnit));
float otherAngle = acosf(glm::dot(otherForward, -dUnit));
AvatarHash hash = DependencyManager::get<AvatarManager>()->getHashCopy();
if (distance > GREATEST_LOOKING_AT_DISTANCE || myAngle > MAX_MY_ANGLE || otherAngle > MAX_OTHER_ANGLE) {
return FLT_MAX;
} else {
return (DISTANCE_FACTOR * distance +
MY_ANGLE_FACTOR * myAngle +
OTHER_ANGLE_FACTOR * otherAngle +
OTHER_IS_TALKING_TERM +
LOOKING_AT_OTHER_ALREADY_TERM);
}
}
foreach (const AvatarSharedPointer& avatarPointer, hash) {
auto avatar = static_pointer_cast<Avatar>(avatarPointer);
bool isCurrentTarget = avatar->getIsLookAtTarget();
float distanceTo = glm::length(avatar->getHead()->getEyePosition() - cameraPosition);
avatar->setIsLookAtTarget(false);
if (!avatar->isMyAvatar() && avatar->isInitialized() &&
(distanceTo < GREATEST_LOOKING_AT_DISTANCE * getModelScale())) {
float radius = glm::length(avatar->getHead()->getEyePosition() - avatar->getHead()->getRightEyePosition());
float angleTo = coneSphereAngle(getHead()->getEyePosition(), lookForward, avatar->getHead()->getEyePosition(), radius);
if (angleTo < (smallestAngleTo * (isCurrentTarget ? KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR : 1.0f))) {
_lookAtTargetAvatar = avatarPointer;
_targetAvatarPosition = avatarPointer->getWorldPosition();
void MyAvatar::computeMyLookAtTarget(const AvatarHash& hash) {
glm::vec3 myForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD;
glm::vec3 myPosition = getHead()->getEyePosition();
CameraMode mode = qApp->getCamera().getMode();
if (mode == CAMERA_MODE_FIRST_PERSON) {
myPosition = qApp->getCamera().getPosition();
}
float bestCost = FLT_MAX;
std::shared_ptr<Avatar> bestAvatar;
foreach (const AvatarSharedPointer& avatarData, hash) {
std::shared_ptr<Avatar> avatar = std::static_pointer_cast<Avatar>(avatarData);
if (!avatar->isMyAvatar() && avatar->isInitialized()) {
glm::vec3 otherForward = avatar->getHead()->getForwardDirection();
glm::vec3 otherPosition = avatar->getHead()->getEyePosition();
const float TIME_WITHOUT_TALKING_THRESHOLD = 1.0f;
bool otherIsTalking = avatar->getHead()->getTimeWithoutTalking() <= TIME_WITHOUT_TALKING_THRESHOLD;
bool lookingAtOtherAlready = _lookAtTargetAvatar.lock().get() == avatar.get();
float cost = lookAtCostFunction(myForward, myPosition, otherForward, otherPosition, otherIsTalking, lookingAtOtherAlready);
if (cost < bestCost) {
bestCost = cost;
bestAvatar = avatar;
}
}
}
if (bestAvatar) {
_lookAtTargetAvatar = bestAvatar;
_targetAvatarPosition = bestAvatar->getWorldPosition();
} else {
_lookAtTargetAvatar.reset();
}
}
void MyAvatar::snapOtherAvatarLookAtTargetsToMe(const AvatarHash& hash) {
foreach (const AvatarSharedPointer& avatarData, hash) {
std::shared_ptr<Avatar> avatar = std::static_pointer_cast<Avatar>(avatarData);
if (!avatar->isMyAvatar() && avatar->isInitialized()) {
if (_lookAtSnappingEnabled && avatar->getLookAtSnappingEnabled() && isLookingAtMe(avatar)) {
// Alter their gaze to look directly at my camera; this looks more natural than looking at my avatar's face.
@ -1648,10 +1653,19 @@ void MyAvatar::updateLookAtTargetAvatar() {
avatar->getHead()->clearCorrectedLookAtPosition();
}
}
auto avatarPointer = _lookAtTargetAvatar.lock();
if (avatarPointer) {
static_pointer_cast<Avatar>(avatarPointer)->setIsLookAtTarget(true);
}
}
void MyAvatar::updateLookAtTargetAvatar() {
// The AvatarManager is a mutable class shared by many threads. We make a thread-safe deep copy of it,
// to avoid having to hold a lock while we iterate over all the avatars within.
AvatarHash hash = DependencyManager::get<AvatarManager>()->getHashCopy();
// determine what the best look at target for my avatar should be.
computeMyLookAtTarget(hash);
// snap look at position for avatars that are looking at me.
snapOtherAvatarLookAtTargetsToMe(hash);
}
void MyAvatar::clearLookAtTargetAvatar() {
@ -3187,17 +3201,15 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette
OctreeElementPointer element;
float distance;
BoxFace face;
const bool visibleOnly = false;
// This isn't quite what we really want here. findRayIntersection always works on mesh, skipping entirely based on collidable.
// What we really want is to use the collision hull!
// See https://highfidelity.fogbugz.com/f/cases/5003/findRayIntersection-has-option-to-use-collidableOnly-but-doesn-t-actually-use-colliders
const bool collidableOnly = true;
const bool precisionPicking = true;
const auto lockType = Octree::Lock; // Should we refactor to take a lock just once?
bool* accurateResult = NULL;
// This isn't quite what we really want here. findRayIntersection always works on mesh, skipping entirely based on collidable.
// What we really want is to use the collision hull!
// See https://highfidelity.fogbugz.com/f/cases/5003/findRayIntersection-has-option-to-use-collidableOnly-but-doesn-t-actually-use-colliders
QVariantMap extraInfo;
EntityItemID entityID = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking,
EntityItemID entityID = entityTree->evalRayIntersection(startPointIn, directionIn, include, ignore,
PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::COLLIDABLE) | PickFilter::getBitMask(PickFilter::FlagBit::PRECISE)),
element, distance, face, normalOut, extraInfo, lockType, accurateResult);
if (entityID.isNull()) {
return false;
@ -3362,7 +3374,6 @@ void MyAvatar::setCollisionsEnabled(bool enabled) {
QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled));
return;
}
_characterController.setCollisionless(!enabled);
emit collisionsEnabledChanged(enabled);
}
@ -3373,6 +3384,20 @@ bool MyAvatar::getCollisionsEnabled() {
return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS;
}
void MyAvatar::setOtherAvatarsCollisionsEnabled(bool enabled) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setOtherAvatarsCollisionsEnabled", Q_ARG(bool, enabled));
return;
}
_collideWithOtherAvatars = enabled;
emit otherAvatarsCollisionsEnabledChanged(enabled);
}
bool MyAvatar::getOtherAvatarsCollisionsEnabled() {
return _collideWithOtherAvatars;
}
void MyAvatar::updateCollisionCapsuleCache() {
glm::vec3 start, end;
float radius;
@ -4748,3 +4773,50 @@ SpatialParentTree* MyAvatar::getParentTree() const {
EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr;
return entityTree.get();
}
const QUuid MyAvatar::grab(const QUuid& targetID, int parentJointIndex,
glm::vec3 positionalOffset, glm::quat rotationalOffset) {
auto grabID = QUuid::createUuid();
// create a temporary grab object to get grabData
QString hand = "none";
if (parentJointIndex == CONTROLLER_RIGHTHAND_INDEX ||
parentJointIndex == CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX ||
parentJointIndex == FARGRAB_RIGHTHAND_INDEX ||
parentJointIndex == getJointIndex("RightHand")) {
hand = "right";
} else if (parentJointIndex == CONTROLLER_LEFTHAND_INDEX ||
parentJointIndex == CAMERA_RELATIVE_CONTROLLER_LEFTHAND_INDEX ||
parentJointIndex == FARGRAB_LEFTHAND_INDEX ||
parentJointIndex == getJointIndex("LeftHand")) {
hand = "left";
}
Grab tmpGrab(DependencyManager::get<NodeList>()->getSessionUUID(),
targetID, parentJointIndex, hand, positionalOffset, rotationalOffset);
QByteArray grabData = tmpGrab.toByteArray();
bool dataChanged = updateAvatarGrabData(grabID, grabData);
if (dataChanged && _clientTraitsHandler) {
// indicate that the changed data should be sent to the mixer
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID);
}
return grabID;
}
void MyAvatar::releaseGrab(const QUuid& grabID) {
bool tellHandler { false };
_avatarGrabsLock.withWriteLock([&] {
if (_avatarGrabData.remove(grabID)) {
_deletedAvatarGrabs.insert(grabID);
tellHandler = true;
}
});
if (tellHandler && _clientTraitsHandler) {
// indicate the deletion of the data to the mixer
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::Grab, grabID);
}
}

View file

@ -225,6 +225,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(bool centerOfGravityModelEnabled READ getCenterOfGravityModelEnabled WRITE setCenterOfGravityModelEnabled)
Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled)
Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled)
Q_PROPERTY(bool otherAvatarsCollisionsEnabled READ getOtherAvatarsCollisionsEnabled WRITE setOtherAvatarsCollisionsEnabled)
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
Q_PROPERTY(bool showPlayArea READ getShowPlayArea WRITE setShowPlayArea)
@ -832,6 +833,8 @@ public:
AvatarWeakPointer getLookAtTargetAvatar() const { return _lookAtTargetAvatar; }
void updateLookAtTargetAvatar();
void computeMyLookAtTarget(const AvatarHash& hash);
void snapOtherAvatarLookAtTargetsToMe(const AvatarHash& hash);
void clearLookAtTargetAvatar();
virtual void setJointRotations(const QVector<glm::quat>& jointRotations) override;
@ -1062,6 +1065,18 @@ public:
*/
Q_INVOKABLE bool getCollisionsEnabled();
/**jsdoc
* @function MyAvatar.setOtherAvatarsCollisionsEnabled
* @param {boolean} enabled
*/
Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled);
/**jsdoc
* @function MyAvatar.getOtherAvatarsCollisionsEnabled
* @returns {boolean}
*/
Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled();
/**jsdoc
* @function MyAvatar.getCollisionCapsule
* @returns {object}
@ -1170,6 +1185,25 @@ public:
glm::vec3 getNextPosition() { return _goToPending ? _goToPosition : getWorldPosition(); }
/**jsdoc
* Create a new grab.
* @function MyAvatar.grab
* @param {Uuid} targetID - id of grabbed thing
* @param {number} parentJointIndex - avatar joint being used to grab
* @param {Vec3} offset - target's positional offset from joint
* @param {Quat} rotationalOffset - target's rotational offset from joint
* @returns {Uuid} id of the new grab
*/
Q_INVOKABLE const QUuid grab(const QUuid& targetID, int parentJointIndex,
glm::vec3 positionalOffset, glm::quat rotationalOffset);
/**jsdoc
* Release (delete) a grab.
* @function MyAvatar.releaseGrab
* @param {Uuid} grabID - id of grabbed thing
*/
Q_INVOKABLE void releaseGrab(const QUuid& grabID);
public slots:
/**jsdoc
@ -1470,6 +1504,14 @@ signals:
*/
void collisionsEnabledChanged(bool enabled);
/**jsdoc
* Triggered when collisions with other avatars enabled or disabled
* @function MyAvatar.otherAvatarsCollisionsEnabledChanged
* @param {boolean} enabled
* @returns {Signal}
*/
void otherAvatarsCollisionsEnabledChanged(bool enabled);
/**jsdoc
* Triggered when avatar's animation url changes
* @function MyAvatar.animGraphUrlChanged

View file

@ -158,7 +158,7 @@ bool OtherAvatar::shouldBeInPhysicsSimulation() const {
}
bool OtherAvatar::needsPhysicsUpdate() const {
constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION;
constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION | Simulation::DIRTY_COLLISION_GROUP;
return (_motionState && (bool)(_motionState->getIncomingDirtyFlags() & FLAGS_OF_INTEREST));
}
@ -172,3 +172,17 @@ void OtherAvatar::rebuildCollisionShape() {
}
}
}
void OtherAvatar::updateCollisionGroup(bool myAvatarCollide) {
if (_motionState) {
bool collides = _motionState->getCollisionGroup() == BULLET_COLLISION_GROUP_OTHER_AVATAR && myAvatarCollide;
if (_collideWithOtherAvatars != collides) {
if (!myAvatarCollide) {
_collideWithOtherAvatars = false;
}
auto newCollisionGroup = _collideWithOtherAvatars ? BULLET_COLLISION_GROUP_OTHER_AVATAR : BULLET_COLLISION_GROUP_COLLISIONLESS;
_motionState->setCollisionGroup(newCollisionGroup);
_motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP);
}
}
}

View file

@ -50,6 +50,7 @@ public:
DetailedMotionState* createDetailedMotionStateForJoint(std::shared_ptr<OtherAvatar> avatar, int jointIndex);
std::vector<DetailedMotionState*>& getDetailedMotionStates() { return _detailedMotionStates; }
void resetDetailedMotionStates();
void updateCollisionGroup(bool myAvatarCollide);
friend AvatarManager;

View file

@ -125,7 +125,7 @@ LaserPointer::RenderState::RenderState(const OverlayID& startID, const OverlayID
StartEndRenderState(startID, endID), _pathID(pathID)
{
if (!_pathID.isNull()) {
_pathIgnoreRays = qApp->getOverlays().getProperty(_pathID, "ignoreRayIntersection").value.toBool();
_pathIgnoreRays = qApp->getOverlays().getProperty(_pathID, "ignorePickIntersection").value.toBool();
_lineWidth = qApp->getOverlays().getProperty(_pathID, "lineWidth").value.toFloat();
}
}
@ -142,7 +142,7 @@ void LaserPointer::RenderState::disable() {
if (!getPathID().isNull()) {
QVariantMap pathProps;
pathProps.insert("visible", false);
pathProps.insert("ignoreRayIntersection", true);
pathProps.insert("ignorePickIntersection", true);
qApp->getOverlays().editOverlay(getPathID(), pathProps);
}
}
@ -156,7 +156,7 @@ void LaserPointer::RenderState::update(const glm::vec3& origin, const glm::vec3&
pathProps.insert("start", vec3toVariant(origin));
pathProps.insert("end", endVariant);
pathProps.insert("visible", true);
pathProps.insert("ignoreRayIntersection", doesPathIgnoreRays());
pathProps.insert("ignorePickIntersection", doesPathIgnoreRays());
pathProps.insert("lineWidth", getLineWidth() * parentScale);
qApp->getOverlays().editOverlay(getPathID(), pathProps);
}

View file

@ -21,7 +21,7 @@ class LaserPointerScriptingInterface : public QObject, public Dependency {
SINGLETON_DEPENDENCY
/**jsdoc
* Synonym for {@link Pointers} as used for laser pointers.
* Synonym for {@link Pointers} as used for laser pointers. Deprecated.
*
* @namespace LaserPointers
*

View file

@ -9,6 +9,7 @@
#include "Application.h"
#include "EntityScriptingInterface.h"
#include "PickScriptingInterface.h"
#include "ui/overlays/Overlays.h"
#include "avatar/AvatarManager.h"
#include "scripting/HMDScriptingInterface.h"
@ -57,10 +58,15 @@ PickParabola ParabolaPick::getMathematicalPick() const {
PickResultPointer ParabolaPick::getEntityIntersection(const PickParabola& pick) {
if (glm::length2(pick.acceleration) > EPSILON && glm::length2(pick.velocity) > EPSILON) {
bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
PickFilter searchFilter = getFilter();
if (DependencyManager::get<PickManager>()->getForceCoarsePicking()) {
searchFilter.setFlag(PickFilter::COARSE, true);
searchFilter.setFlag(PickFilter::PRECISE, false);
}
ParabolaToEntityIntersectionResult entityRes =
DependencyManager::get<EntityScriptingInterface>()->findParabolaIntersectionVector(pick, precisionPicking,
getIncludeItemsAs<EntityItemID>(), getIgnoreItemsAs<EntityItemID>(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable());
DependencyManager::get<EntityScriptingInterface>()->evalParabolaIntersectionVector(pick, searchFilter,
getIncludeItemsAs<EntityItemID>(), getIgnoreItemsAs<EntityItemID>());
if (entityRes.intersects) {
return std::make_shared<ParabolaPickResult>(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.parabolicDistance, entityRes.intersection, pick, entityRes.surfaceNormal, entityRes.extraInfo);
}
@ -70,7 +76,7 @@ PickResultPointer ParabolaPick::getEntityIntersection(const PickParabola& pick)
PickResultPointer ParabolaPick::getOverlayIntersection(const PickParabola& pick) {
if (glm::length2(pick.acceleration) > EPSILON && glm::length2(pick.velocity) > EPSILON) {
bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
bool precisionPicking = !(getFilter().isCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
ParabolaToOverlayIntersectionResult overlayRes =
qApp->getOverlays().findParabolaIntersectionVector(pick, precisionPicking,
getIncludeItemsAs<OverlayID>(), getIgnoreItemsAs<OverlayID>(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable());

View file

@ -253,12 +253,12 @@ StartEndRenderState::StartEndRenderState(const OverlayID& startID, const Overlay
_startID(startID), _endID(endID) {
if (!_startID.isNull()) {
_startDim = vec3FromVariant(qApp->getOverlays().getProperty(_startID, "dimensions").value);
_startIgnoreRays = qApp->getOverlays().getProperty(_startID, "ignoreRayIntersection").value.toBool();
_startIgnoreRays = qApp->getOverlays().getProperty(_startID, "ignorePickIntersection").value.toBool();
}
if (!_endID.isNull()) {
_endDim = vec3FromVariant(qApp->getOverlays().getProperty(_endID, "dimensions").value);
_endRot = quatFromVariant(qApp->getOverlays().getProperty(_endID, "rotation").value);
_endIgnoreRays = qApp->getOverlays().getProperty(_endID, "ignoreRayIntersection").value.toBool();
_endIgnoreRays = qApp->getOverlays().getProperty(_endID, "ignorePickIntersection").value.toBool();
}
}
@ -275,13 +275,13 @@ void StartEndRenderState::disable() {
if (!getStartID().isNull()) {
QVariantMap startProps;
startProps.insert("visible", false);
startProps.insert("ignoreRayIntersection", true);
startProps.insert("ignorePickIntersection", true);
qApp->getOverlays().editOverlay(getStartID(), startProps);
}
if (!getEndID().isNull()) {
QVariantMap endProps;
endProps.insert("visible", false);
endProps.insert("ignoreRayIntersection", true);
endProps.insert("ignorePickIntersection", true);
qApp->getOverlays().editOverlay(getEndID(), endProps);
}
_enabled = false;
@ -294,7 +294,7 @@ void StartEndRenderState::update(const glm::vec3& origin, const glm::vec3& end,
startProps.insert("position", vec3toVariant(origin));
startProps.insert("visible", true);
startProps.insert("dimensions", vec3toVariant(getStartDim() * parentScale));
startProps.insert("ignoreRayIntersection", doesStartIgnoreRays());
startProps.insert("ignorePickIntersection", doesStartIgnoreRays());
qApp->getOverlays().editOverlay(getStartID(), startProps);
}
@ -346,7 +346,7 @@ void StartEndRenderState::update(const glm::vec3& origin, const glm::vec3& end,
endProps.insert("position", vec3toVariant(position));
endProps.insert("rotation", quatToVariant(rotation));
endProps.insert("visible", true);
endProps.insert("ignoreRayIntersection", doesEndIgnoreRays());
endProps.insert("ignorePickIntersection", doesEndIgnoreRays());
qApp->getOverlays().editOverlay(getEndID(), endProps);
}
_enabled = true;

View file

@ -49,11 +49,17 @@ unsigned int PickScriptingInterface::createPick(const PickQuery::PickType type,
}
}
PickFilter getPickFilter(unsigned int filter) {
// FIXME: Picks always intersect visible and collidable things right now
filter = filter | (PickScriptingInterface::PICK_INCLUDE_VISIBLE() | PickScriptingInterface::PICK_INCLUDE_COLLIDABLE());
return PickFilter(filter);
}
/**jsdoc
* A set of properties that can be passed to {@link Picks.createPick} to create a new Ray Pick.
* @typedef {object} Picks.RayPickProperties
* @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results.
* @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
* @property {number} [filter=0] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
* @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid.
* @property {Uuid} parentID - The ID of the parent, either an avatar, an entity, an overlay, or a pick.
* @property {number} [parentJointIndex=0] - The joint of the parent to parent to, for example, the joints on the model of an avatar. (default = 0, no joint)
@ -73,7 +79,7 @@ unsigned int PickScriptingInterface::createRayPick(const QVariant& properties) {
PickFilter filter = PickFilter();
if (propMap["filter"].isValid()) {
filter = PickFilter(propMap["filter"].toUInt());
filter = getPickFilter(propMap["filter"].toUInt());
}
float maxDistance = 0.0f;
@ -111,7 +117,7 @@ unsigned int PickScriptingInterface::createRayPick(const QVariant& properties) {
* @typedef {object} Picks.StylusPickProperties
* @property {number} [hand=-1] An integer. 0 == left, 1 == right. Invalid otherwise.
* @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results.
* @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
* @property {number} [filter=0] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
* @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid.
*/
unsigned int PickScriptingInterface::createStylusPick(const QVariant& properties) {
@ -132,7 +138,7 @@ unsigned int PickScriptingInterface::createStylusPick(const QVariant& properties
PickFilter filter = PickFilter();
if (propMap["filter"].isValid()) {
filter = PickFilter(propMap["filter"].toUInt());
filter = getPickFilter(propMap["filter"].toUInt());
}
float maxDistance = 0.0f;
@ -153,7 +159,7 @@ unsigned int PickScriptingInterface::createStylusPick(const QVariant& properties
* A set of properties that can be passed to {@link Picks.createPick} to create a new Parabola Pick.
* @typedef {object} Picks.ParabolaPickProperties
* @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results.
* @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
* @property {number} [filter=0] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
* @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid.
* @property {Uuid} parentID - The ID of the parent, either an avatar, an entity, an overlay, or a pick.
* @property {number} [parentJointIndex=0] - The joint of the parent to parent to, for example, the joints on the model of an avatar. (default = 0, no joint)
@ -178,7 +184,7 @@ unsigned int PickScriptingInterface::createParabolaPick(const QVariant& properti
PickFilter filter = PickFilter();
if (propMap["filter"].isValid()) {
filter = PickFilter(propMap["filter"].toUInt());
filter = getPickFilter(propMap["filter"].toUInt());
}
float maxDistance = 0.0f;
@ -250,7 +256,7 @@ unsigned int PickScriptingInterface::createParabolaPick(const QVariant& properti
* @typedef {object} Picks.CollisionPickProperties
* @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results.
* @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
* @property {number} [filter=0] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
* @property {Shape} shape - The information about the collision region's size and shape. Dimensions are in world space, but will scale with the parent if defined.
* @property {Vec3} position - The position of the collision region, relative to a parent if defined.
* @property {Quat} orientation - The orientation of the collision region, relative to a parent if defined.
@ -273,7 +279,7 @@ unsigned int PickScriptingInterface::createCollisionPick(const QVariant& propert
PickFilter filter = PickFilter();
if (propMap["filter"].isValid()) {
filter = PickFilter(propMap["filter"].toUInt());
filter = getPickFilter(propMap["filter"].toUInt());
}
float maxDistance = 0.0f;

View file

@ -14,6 +14,7 @@
#include <DependencyManager.h>
#include <PhysicsEngine.h>
#include <Pick.h>
#include <PickFilter.h>
/**jsdoc
* The Picks API lets you create and manage objects for repeatedly calculating intersections in different ways.
@ -23,41 +24,62 @@
* @hifi-interface
* @hifi-client-entity
*
* @property {number} PICK_NOTHING A filter flag. Don't intersect with anything. <em>Read-only.</em>
* @property {number} PICK_ENTITIES A filter flag. Include entities when intersecting. <em>Read-only.</em>
* @property {number} PICK_OVERLAYS A filter flag. Include overlays when intersecting. <em>Read-only.</em>
* @property {number} PICK_AVATARS A filter flag. Include avatars when intersecting. <em>Read-only.</em>
* @property {number} PICK_HUD A filter flag. Include the HUD sphere when intersecting in HMD mode. <em>Read-only.</em>
* @property {number} PICK_COARSE A filter flag. Pick against coarse meshes, instead of exact meshes. <em>Read-only.</em>
* @property {number} PICK_INCLUDE_INVISIBLE A filter flag. Include invisible objects when intersecting. <em>Read-only.</em>
* @property {number} PICK_INCLUDE_NONCOLLIDABLE A filter flag. Include non-collidable objects when intersecting.
* <em>Read-only.</em>
* @property {number} PICK_ALL_INTERSECTIONS <em>Read-only.</em>
* @property {number} INTERSECTED_NONE An intersection type. Intersected nothing with the given filter flags.
* <em>Read-only.</em>
* @property {number} PICK_ENTITIES A filter flag. Include domain and avatar entities when intersecting. <em>Read-only.</em>. Deprecated.
* @property {number} PICK_OVERLAYS A filter flag. Include local entities when intersecting. <em>Read-only.</em>. Deprecated.
*
* @property {number} PICK_DOMAIN_ENTITIES A filter flag. Include domain entities when intersecting. <em>Read-only.</em>.
* @property {number} PICK_AVATAR_ENTITIES A filter flag. Include avatar entities when intersecting. <em>Read-only.</em>.
* @property {number} PICK_LOCAL_ENTITIES A filter flag. Include local entities when intersecting. <em>Read-only.</em>.
* @property {number} PICK_AVATARS A filter flag. Include avatars when intersecting. <em>Read-only.</em>.
* @property {number} PICK_HUD A filter flag. Include the HUD sphere when intersecting in HMD mode. <em>Read-only.</em>.
*
* @property {number} PICK_INCLUDE_VISIBLE A filter flag. Include visible objects when intersecting. <em>Read-only.</em>.
* @property {number} PICK_INCLUDE_INVISIBLE A filter flag. Include invisible objects when intersecting. <em>Read-only.</em>.
*
* @property {number} PICK_INCLUDE_COLLIDABLE A filter flag. Include collidable objects when intersecting. <em>Read-only.</em>.
* @property {number} PICK_INCLUDE_NONCOLLIDABLE A filter flag. Include non-collidable objects when intersecting. <em>Read-only.</em>.
*
* @property {number} PICK_PRECISE A filter flag. Pick against exact meshes. <em>Read-only.</em>.
* @property {number} PICK_COARSE A filter flag. Pick against coarse meshes. <em>Read-only.</em>.
*
* @property {number} PICK_ALL_INTERSECTIONS <em>Read-only.</em>.
*
* @property {number} INTERSECTED_NONE An intersection type. Intersected nothing with the given filter flags. <em>Read-only.</em>
* @property {number} INTERSECTED_ENTITY An intersection type. Intersected an entity. <em>Read-only.</em>
* @property {number} INTERSECTED_OVERLAY An intersection type. Intersected an overlay. <em>Read-only.</em>
* @property {number} INTERSECTED_AVATAR An intersection type. Intersected an avatar. <em>Read-only.</em>
* @property {number} INTERSECTED_HUD An intersection type. Intersected the HUD sphere. <em>Read-only.</em>
* @property {number} perFrameTimeBudget - The max number of usec to spend per frame updating Pick results. <em>Read-only.</em>
* @property {number} perFrameTimeBudget - The max number of usec to spend per frame updating Pick results.
*/
class PickScriptingInterface : public QObject, public Dependency {
Q_OBJECT
Q_PROPERTY(unsigned int PICK_NOTHING READ PICK_NOTHING CONSTANT)
Q_PROPERTY(unsigned int PICK_ENTITIES READ PICK_ENTITIES CONSTANT)
Q_PROPERTY(unsigned int PICK_OVERLAYS READ PICK_OVERLAYS CONSTANT)
Q_PROPERTY(unsigned int PICK_DOMAIN_ENTITIES READ PICK_DOMAIN_ENTITIES CONSTANT)
Q_PROPERTY(unsigned int PICK_AVATAR_ENTITIES READ PICK_AVATAR_ENTITIES CONSTANT)
Q_PROPERTY(unsigned int PICK_LOCAL_ENTITIES READ PICK_LOCAL_ENTITIES CONSTANT)
Q_PROPERTY(unsigned int PICK_AVATARS READ PICK_AVATARS CONSTANT)
Q_PROPERTY(unsigned int PICK_HUD READ PICK_HUD CONSTANT)
Q_PROPERTY(unsigned int PICK_COARSE READ PICK_COARSE CONSTANT)
Q_PROPERTY(unsigned int PICK_INCLUDE_VISIBLE READ PICK_INCLUDE_VISIBLE CONSTANT)
Q_PROPERTY(unsigned int PICK_INCLUDE_INVISIBLE READ PICK_INCLUDE_INVISIBLE CONSTANT)
Q_PROPERTY(unsigned int PICK_INCLUDE_COLLIDABLE READ PICK_INCLUDE_COLLIDABLE CONSTANT)
Q_PROPERTY(unsigned int PICK_INCLUDE_NONCOLLIDABLE READ PICK_INCLUDE_NONCOLLIDABLE CONSTANT)
Q_PROPERTY(unsigned int PICK_PRECISE READ PICK_PRECISE CONSTANT)
Q_PROPERTY(unsigned int PICK_COARSE READ PICK_COARSE CONSTANT)
Q_PROPERTY(unsigned int PICK_ALL_INTERSECTIONS READ PICK_ALL_INTERSECTIONS CONSTANT)
Q_PROPERTY(unsigned int INTERSECTED_NONE READ INTERSECTED_NONE CONSTANT)
Q_PROPERTY(unsigned int INTERSECTED_ENTITY READ INTERSECTED_ENTITY CONSTANT)
Q_PROPERTY(unsigned int INTERSECTED_OVERLAY READ INTERSECTED_OVERLAY CONSTANT)
Q_PROPERTY(unsigned int INTERSECTED_AVATAR READ INTERSECTED_AVATAR CONSTANT)
Q_PROPERTY(unsigned int INTERSECTED_HUD READ INTERSECTED_HUD CONSTANT)
Q_PROPERTY(unsigned int perFrameTimeBudget READ getPerFrameTimeBudget WRITE setPerFrameTimeBudget)
SINGLETON_DEPENDENCY
public:
@ -72,11 +94,13 @@ public:
* Adds a new Pick.
* Different {@link PickType}s use different properties, and within one PickType, the properties you choose can lead to a wide range of behaviors. For example,
* with PickType.Ray, depending on which optional parameters you pass, you could create a Static Ray Pick, a Mouse Ray Pick, or a Joint Ray Pick.
* Picks created with this method always intersect at least visible and collidable things
* @function Picks.createPick
* @param {PickType} type A PickType that specifies the method of picking to use
* @param {Picks.RayPickProperties|Picks.StylusPickProperties|Picks.ParabolaPickProperties|Picks.CollisionPickProperties} properties A PickProperties object, containing all the properties for initializing this Pick
* @returns {number} The ID of the created Pick. Used for managing the Pick. 0 if invalid.
*/
// TODO: expand Pointers to be able to be fully configurable with PickFilters
Q_INVOKABLE unsigned int createPick(const PickQuery::PickType type, const QVariant& properties);
/**jsdoc
@ -227,61 +251,80 @@ public:
*/
Q_INVOKABLE bool isMouse(unsigned int uid);
// FIXME: Move to other property definitions.
Q_PROPERTY(unsigned int perFrameTimeBudget READ getPerFrameTimeBudget WRITE setPerFrameTimeBudget)
unsigned int getPerFrameTimeBudget() const;
void setPerFrameTimeBudget(unsigned int numUsecs);
public slots:
/**jsdoc
* @function Picks.PICK_NOTHING
* @returns {number}
*/
static constexpr unsigned int PICK_NOTHING() { return 0; }
/**jsdoc
* @function Picks.PICK_ENTITIES
* @returns {number}
*/
static constexpr unsigned int PICK_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_ENTITIES); }
static constexpr unsigned int PICK_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES) | PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES); }
/**jsdoc
* @function Picks.PICK_OVERLAYS
* @returns {number}
*/
static constexpr unsigned int PICK_OVERLAYS() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_OVERLAYS); }
static constexpr unsigned int PICK_OVERLAYS() { return PickFilter::getBitMask(PickFilter::FlagBit::LOCAL_ENTITIES); }
/**jsdoc
* @function Picks.PICK_DOMAIN_ENTITIES
* @returns {number}
*/
static constexpr unsigned int PICK_DOMAIN_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES); }
/**jsdoc
* @function Picks.PICK_AVATAR_ENTITIES
* @returns {number}
*/
static constexpr unsigned int PICK_AVATAR_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES); }
/**jsdoc
* @function Picks.PICK_LOCAL_ENTITIES
* @returns {number}
*/
static constexpr unsigned int PICK_LOCAL_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::LOCAL_ENTITIES); }
/**jsdoc
* @function Picks.PICK_AVATARS
* @returns {number}
*/
static constexpr unsigned int PICK_AVATARS() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_AVATARS); }
static constexpr unsigned int PICK_AVATARS() { return PickFilter::getBitMask(PickFilter::FlagBit::AVATARS); }
/**jsdoc
* @function Picks.PICK_HUD
* @returns {number}
*/
static constexpr unsigned int PICK_HUD() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_HUD); }
static constexpr unsigned int PICK_HUD() { return PickFilter::getBitMask(PickFilter::FlagBit::HUD); }
/**jsdoc
* @function Picks.PICK_COARSE
* @function Picks.PICK_INCLUDE_VISIBLE
* @returns {number}
*/
static constexpr unsigned int PICK_COARSE() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_COARSE); }
static constexpr unsigned int PICK_INCLUDE_VISIBLE() { return PickFilter::getBitMask(PickFilter::FlagBit::VISIBLE); }
/**jsdoc
* @function Picks.PICK_INCLUDE_INVISIBLE
* @returns {number}
*/
static constexpr unsigned int PICK_INCLUDE_INVISIBLE() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_INCLUDE_INVISIBLE); }
static constexpr unsigned int PICK_INCLUDE_INVISIBLE() { return PickFilter::getBitMask(PickFilter::FlagBit::INVISIBLE); }
/**jsdoc
* @function Picks.PICK_INCLUDE_COLLIDABLE
* @returns {number}
*/
static constexpr unsigned int PICK_INCLUDE_COLLIDABLE() { return PickFilter::getBitMask(PickFilter::FlagBit::COLLIDABLE); }
/**jsdoc
* @function Picks.PICK_INCLUDE_NONCOLLIDABLE
* @returns {number}
*/
static constexpr unsigned int PICK_INCLUDE_NONCOLLIDABLE() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_INCLUDE_NONCOLLIDABLE); }
static constexpr unsigned int PICK_INCLUDE_NONCOLLIDABLE() { return PickFilter::getBitMask(PickFilter::FlagBit::NONCOLLIDABLE); }
/**jsdoc
* @function Picks.PICK_PRECISE
* @returns {number}
*/
static constexpr unsigned int PICK_PRECISE() { return PickFilter::getBitMask(PickFilter::FlagBit::PRECISE); }
/**jsdoc
* @function Picks.PICK_COARSE
* @returns {number}
*/
static constexpr unsigned int PICK_COARSE() { return PickFilter::getBitMask(PickFilter::FlagBit::COARSE); }
/**jsdoc
* @function Picks.PICK_ALL_INTERSECTIONS

View file

@ -41,10 +41,12 @@ public:
* @property {string} button Which button to trigger. "Primary", "Secondary", "Tertiary", and "Focus" are currently supported. Only "Primary" will trigger clicks on web surfaces. If "Focus" is triggered,
* it will try to set the entity or overlay focus to the object at which the Pointer is aimed. Buttons besides the first three will still trigger events, but event.button will be "None".
*/
/**jsdoc
* Adds a new Pointer
* Different {@link PickType}s use different properties, and within one PickType, the properties you choose can lead to a wide range of behaviors. For example,
* with PickType.Ray, depending on which optional parameters you pass, you could create a Static Ray Pointer, a Mouse Ray Pointer, or a Joint Ray Pointer.
* Pointers created with this method always intersect at least visible and collidable things
* @function Pointers.createPointer
* @param {PickType} type A PickType that specifies the method of picking to use
* @param {Pointers.LaserPointerProperties|Pointers.StylusPointerProperties|Pointers.ParabolaPointerProperties} properties A PointerProperties object, containing all the properties for initializing this Pointer <b>and</b> the {@link Picks.PickProperties} for the Pick that
@ -58,21 +60,21 @@ public:
* dimensions: {x:0.5, y:0.5, z:0.5},
* solid: true,
* color: {red:0, green:255, blue:0},
* ignoreRayIntersection: true
* ignorePickIntersection: true
* };
* var end2 = {
* type: "sphere",
* dimensions: {x:0.5, y:0.5, z:0.5},
* solid: true,
* color: {red:255, green:0, blue:0},
* ignoreRayIntersection: true
* ignorePickIntersection: true
* };
*
* var renderStates = [ {name: "test", end: end} ];
* var defaultRenderStates = [ {name: "test", distance: 10.0, end: end2} ];
* var pointer = Pointers.createPointer(PickType.Ray, {
* joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
* filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
* filter: Picks.PICK_LOCAL_ENTITIES | Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
* renderStates: renderStates,
* defaultRenderStates: defaultRenderStates,
* distanceScaleEnd: true,
@ -82,6 +84,7 @@ public:
* });
* Pointers.setRenderState(pointer, "test");
*/
// TODO: expand Pointers to be able to be fully configurable with PickFilters
Q_INVOKABLE unsigned int createPointer(const PickQuery::PickType& type, const QVariant& properties);
/**jsdoc

View file

@ -27,10 +27,15 @@ PickRay RayPick::getMathematicalPick() const {
}
PickResultPointer RayPick::getEntityIntersection(const PickRay& pick) {
bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
PickFilter searchFilter = getFilter();
if (DependencyManager::get<PickManager>()->getForceCoarsePicking()) {
searchFilter.setFlag(PickFilter::COARSE, true);
searchFilter.setFlag(PickFilter::PRECISE, false);
}
RayToEntityIntersectionResult entityRes =
DependencyManager::get<EntityScriptingInterface>()->findRayIntersectionVector(pick, precisionPicking,
getIncludeItemsAs<EntityItemID>(), getIgnoreItemsAs<EntityItemID>(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable());
DependencyManager::get<EntityScriptingInterface>()->evalRayIntersectionVector(pick, searchFilter,
getIncludeItemsAs<EntityItemID>(), getIgnoreItemsAs<EntityItemID>());
if (entityRes.intersects) {
return std::make_shared<RayPickResult>(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, pick, entityRes.surfaceNormal, entityRes.extraInfo);
} else {
@ -39,7 +44,7 @@ PickResultPointer RayPick::getEntityIntersection(const PickRay& pick) {
}
PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) {
bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
bool precisionPicking = !(getFilter().isCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
RayToOverlayIntersectionResult overlayRes =
qApp->getOverlays().findRayIntersectionVector(pick, precisionPicking,
getIncludeItemsAs<OverlayID>(), getIgnoreItemsAs<OverlayID>(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable());

View file

@ -19,14 +19,13 @@
#include "PickScriptingInterface.h"
/**jsdoc
* Synonym for {@link Picks} as used for ray picks.
* Synonym for {@link Picks} as used for ray picks. Deprecated.
*
* @namespace RayPick
*
* @hifi-interface
* @hifi-client-entity
*
* @property {number} PICK_NOTHING <em>Read-only.</em>
* @property {number} PICK_ENTITIES <em>Read-only.</em>
* @property {number} PICK_OVERLAYS <em>Read-only.</em>
* @property {number} PICK_AVATARS <em>Read-only.</em>
@ -44,7 +43,6 @@
class RayPickScriptingInterface : public QObject, public Dependency {
Q_OBJECT
Q_PROPERTY(unsigned int PICK_NOTHING READ PICK_NOTHING CONSTANT)
Q_PROPERTY(unsigned int PICK_ENTITIES READ PICK_ENTITIES CONSTANT)
Q_PROPERTY(unsigned int PICK_OVERLAYS READ PICK_OVERLAYS CONSTANT)
Q_PROPERTY(unsigned int PICK_AVATARS READ PICK_AVATARS CONSTANT)
@ -140,12 +138,6 @@ public:
public slots:
/**jsdoc
* @function RayPick.PICK_NOTHING
* @returns {number}
*/
static unsigned int PICK_NOTHING() { return PickScriptingInterface::PICK_NOTHING(); }
/**jsdoc
* @function RayPick.PICK_ENTITIES
* @returns {number}

View file

@ -61,7 +61,7 @@ OverlayID StylusPointer::buildStylusOverlay(const QVariantMap& properties) {
overlayProperties["loadPriority"] = 10.0f;
overlayProperties["solid"] = true;
overlayProperties["visible"] = false;
overlayProperties["ignoreRayIntersection"] = true;
overlayProperties["ignorePickIntersection"] = true;
overlayProperties["drawInFront"] = false;
return qApp->getOverlays().addOverlay("model", overlayProperties);

View file

@ -25,12 +25,12 @@ float ClipboardScriptingInterface::getClipboardContentsLargestDimension() {
return qApp->getEntityClipboard()->getContentsLargestDimension();
}
bool ClipboardScriptingInterface::exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs) {
bool ClipboardScriptingInterface::exportEntities(const QString& filename, const QVector<QUuid>& entityIDs) {
bool retVal;
BLOCKING_INVOKE_METHOD(qApp, "exportEntities",
Q_RETURN_ARG(bool, retVal),
Q_ARG(const QString&, filename),
Q_ARG(const QVector<EntityItemID>&, entityIDs));
Q_ARG(const QVector<QUuid>&, entityIDs));
return retVal;
}

View file

@ -63,7 +63,7 @@ public:
* @param {Uuid[]} entityIDs Array of IDs of the entities to export.
* @returns {boolean} <code>true</code> if the export was successful, otherwise <code>false</code>.
*/
Q_INVOKABLE bool exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs);
Q_INVOKABLE bool exportEntities(const QString& filename, const QVector<QUuid>& entityIDs);
/**jsdoc
* Export the entities with centers within a cube to a JSON file.

View file

@ -0,0 +1,135 @@
//
// Created by Nissim Hadar on 2018/12/28
// Copyright 2013-2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "PlatformInfoScriptingInterface.h"
#include "Application.h"
#include <thread>
#ifdef Q_OS_WIN
#include <Windows.h>
#elif defined Q_OS_MAC
#include <sstream>
#endif
PlatformInfoScriptingInterface* PlatformInfoScriptingInterface::getInstance() {
static PlatformInfoScriptingInterface sharedInstance;
return &sharedInstance;
}
QString PlatformInfoScriptingInterface::getOperatingSystemType() {
#ifdef Q_OS_WIN
return "WINDOWS";
#elif defined Q_OS_MAC
return "MACOS";
#else
return "UNKNOWN";
#endif
}
QString PlatformInfoScriptingInterface::getCPUBrand() {
#ifdef Q_OS_WIN
int CPUInfo[4] = { -1 };
unsigned nExIds, i = 0;
char CPUBrandString[0x40];
// Get the information associated with each extended ID.
__cpuid(CPUInfo, 0x80000000);
nExIds = CPUInfo[0];
for (i = 0x80000000; i <= nExIds; ++i) {
__cpuid(CPUInfo, i);
// Interpret CPU brand string
if (i == 0x80000002) {
memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo));
} else if (i == 0x80000003) {
memcpy(CPUBrandString + 16, CPUInfo, sizeof(CPUInfo));
} else if (i == 0x80000004) {
memcpy(CPUBrandString + 32, CPUInfo, sizeof(CPUInfo));
}
}
return CPUBrandString;
#elif defined Q_OS_MAC
FILE* stream = popen("sysctl -n machdep.cpu.brand_string", "r");
std::ostringstream hostStream;
while (!feof(stream) && !ferror(stream)) {
char buf[128];
int bytesRead = fread(buf, 1, 128, stream);
hostStream.write(buf, bytesRead);
}
return QString::fromStdString(hostStream.str());
#else
return QString("NO IMPLEMENTED");
#endif
}
unsigned int PlatformInfoScriptingInterface::getNumLogicalCores() {
return std::thread::hardware_concurrency();
}
int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() {
#ifdef Q_OS_WIN
MEMORYSTATUSEX statex;
statex.dwLength = sizeof (statex);
GlobalMemoryStatusEx(&statex);
return statex.ullTotalPhys / 1024 / 1024;
#elif defined Q_OS_MAC
FILE* stream = popen("sysctl -a | grep hw.memsize", "r");
std::ostringstream hostStream;
while (!feof(stream) && !ferror(stream)) {
char buf[128];
int bytesRead = fread(buf, 1, 128, stream);
hostStream.write(buf, bytesRead);
}
QString result = QString::fromStdString(hostStream.str());
QStringList parts = result.split(' ');
return (int)(parts[1].toDouble() / 1024 / 1024);
#else
return -1;
#endif
}
QString PlatformInfoScriptingInterface::getGraphicsCardType() {
#ifdef Q_OS_WIN
return qApp->getGraphicsCardType();
#elif defined Q_OS_MAC
FILE* stream = popen("system_profiler SPDisplaysDataType | grep Chipset", "r");
std::ostringstream hostStream;
while (!feof(stream) && !ferror(stream)) {
char buf[128];
int bytesRead = fread(buf, 1, 128, stream);
hostStream.write(buf, bytesRead);
}
QString result = QString::fromStdString(hostStream.str());
QStringList parts = result.split('\n');
for (int i = 0; i < parts.size(); ++i) {
if (parts[i].toLower().contains("radeon") || parts[i].toLower().contains("nvidia")) {
return parts[i];
}
}
// unkown graphics card
return "UNKNOWN";
#else
return QString("NO IMPLEMENTED");
#endif
}
bool PlatformInfoScriptingInterface::hasRiftControllers() {
return qApp->hasRiftControllers();
}
bool PlatformInfoScriptingInterface::hasViveControllers() {
return qApp->hasViveControllers();
}

View file

@ -0,0 +1,70 @@
//
// Created by Nissim Hadar on 2018/12/28
// Copyright 2013-2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_PlatformInfoScriptingInterface_h
#define hifi_PlatformInfoScriptingInterface_h
#include <QtCore/QObject>
class QScriptValue;
class PlatformInfoScriptingInterface : public QObject {
Q_OBJECT
public slots:
static PlatformInfoScriptingInterface* getInstance();
/**jsdoc
* Returns the Operating Sytem type
* @function Test.getOperatingSystemType
* @returns {string} "WINDOWS", "MACOS" or "UNKNOWN"
*/
QString getOperatingSystemType();
/**jsdoc
* Returns the CPU brand
*function PlatformInfo.getCPUBrand()
* @returns {string} brand of CPU
*/
QString getCPUBrand();
/**jsdoc
* Returns the number of logical CPU cores
*function PlatformInfo.getNumLogicalCores()
* @returns {int} number of logical CPU cores
*/
unsigned int getNumLogicalCores();
/**jsdoc
* Returns the total system memory in megabyte
*function PlatformInfo.getTotalSystemMemory()
* @returns {int} size of memory in megabytes
*/
int getTotalSystemMemoryMB();
/**jsdoc
* Returns the graphics card type
* @function Test.getGraphicsCardType
* @returns {string} graphics card type
*/
QString getGraphicsCardType();
/**jsdoc
* Returns true if Oculus Rift is connected (looks for hand controllers)
* @function Window.hasRift
* @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/
bool hasRiftControllers();
/**jsdoc
* Returns true if HTC Vive is connected (looks for hand controllers)
* @function Window.hasRift
* @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/
bool hasViveControllers();
};
#endif // hifi_PlatformInfoScriptingInterface_h

View file

@ -781,7 +781,7 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) {
{ "isSolid", true },
{ "visible", false },
{ "grabbable", true },
{ "ignoreRayIntersection", false },
{ "ignorePickIntersection", false },
{ "dimensions", anchorObject["dimensions"].toVariant() },
{ "position", anchorObject["position"].toVariant() },
{ "orientation", anchorObject["rotation"].toVariant() }

View file

@ -30,7 +30,6 @@
#include <gl/Context.h>
#include "BandwidthRecorder.h"
#include "Menu.h"
#include "Util.h"
#include "SequenceNumberStats.h"
@ -166,20 +165,25 @@ void Stats::updateStats(bool force) {
STAT_UPDATE(collisionPicksUpdated, updatedPicks[PickQuery::Collision]);
}
auto bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
STAT_UPDATE(packetInCount, (int)bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond());
STAT_UPDATE(packetOutCount, (int)bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond());
STAT_UPDATE_FLOAT(mbpsIn, (float)bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond() / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(mbpsOut, (float)bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond() / 1000.0f, 0.01f);
STAT_UPDATE(packetInCount, nodeList->getInboundPPS());
STAT_UPDATE(packetOutCount, nodeList->getOutboundPPS());
STAT_UPDATE_FLOAT(mbpsIn, nodeList->getInboundKbps() / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(mbpsOut, nodeList->getOutboundKbps() / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(assetMbpsIn, (float)bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AssetServer) / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(assetMbpsOut, (float)bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AssetServer) / 1000.0f, 0.01f);
// Second column: ping
SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer);
SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer);
SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer);
SharedNodePointer messageMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer);
if (assetServerNode) {
STAT_UPDATE_FLOAT(assetMbpsIn, assetServerNode->getInboundKbps() / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(assetMbpsOut, assetServerNode->getOutboundKbps() / 1000.0f, 0.01f);
} else {
STAT_UPDATE_FLOAT(assetMbpsIn, 0.0f, 0.01f);
STAT_UPDATE_FLOAT(assetMbpsOut, 0.0f, 0.01f);
}
// Second column: ping
STAT_UPDATE(audioPing, audioMixerNode ? audioMixerNode->getPingMs() : -1);
const int mixerLossRate = (int)roundf(_audioStats->data()->getMixerStream()->lossRateWindow() * 100.0f);
const int clientLossRate = (int)roundf(_audioStats->data()->getClientStream()->lossRateWindow() * 100.0f);
@ -198,7 +202,7 @@ void Stats::updateStats(bool force) {
// TODO: this should also support entities
if (node->getType() == NodeType::EntityServer) {
totalPingOctree += node->getPingMs();
totalEntityKbps += node->getInboundBandwidth();
totalEntityKbps += node->getInboundKbps();
octreeServerCount++;
if (pingOctreeMax < node->getPingMs()) {
pingOctreeMax = node->getPingMs();
@ -218,10 +222,10 @@ void Stats::updateStats(bool force) {
if (_expanded || force) {
SharedNodePointer avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer);
if (avatarMixer) {
STAT_UPDATE(avatarMixerInKbps, (int)roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AvatarMixer)));
STAT_UPDATE(avatarMixerInPps, (int)roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AvatarMixer)));
STAT_UPDATE(avatarMixerOutKbps, (int)roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AvatarMixer)));
STAT_UPDATE(avatarMixerOutPps, (int)roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AvatarMixer)));
STAT_UPDATE(avatarMixerInKbps, (int)roundf(avatarMixer->getInboundKbps()));
STAT_UPDATE(avatarMixerInPps, avatarMixer->getInboundPPS());
STAT_UPDATE(avatarMixerOutKbps, (int)roundf(avatarMixer->getOutboundKbps()));
STAT_UPDATE(avatarMixerOutPps, avatarMixer->getOutboundPPS());
} else {
STAT_UPDATE(avatarMixerInKbps, -1);
STAT_UPDATE(avatarMixerInPps, -1);
@ -233,17 +237,15 @@ void Stats::updateStats(bool force) {
SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer);
auto audioClient = DependencyManager::get<AudioClient>().data();
if (audioMixerNode || force) {
STAT_UPDATE(audioMixerKbps, (int)roundf(
bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AudioMixer) +
bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerPps, (int)roundf(
bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer) +
bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerKbps, (int)roundf(audioMixerNode->getInboundKbps() +
audioMixerNode->getOutboundKbps()));
STAT_UPDATE(audioMixerPps, audioMixerNode->getInboundPPS() +
audioMixerNode->getOutboundPPS());
STAT_UPDATE(audioMixerInKbps, (int)roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerInPps, (int)roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerOutKbps, (int)roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerOutPps, (int)roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerInKbps, (int)roundf(audioMixerNode->getInboundKbps()));
STAT_UPDATE(audioMixerInPps, audioMixerNode->getInboundPPS());
STAT_UPDATE(audioMixerOutKbps, (int)roundf(audioMixerNode->getOutboundKbps()));
STAT_UPDATE(audioMixerOutPps, audioMixerNode->getOutboundPPS());
STAT_UPDATE(audioAudioInboundPPS, (int)audioClient->getAudioInboundPPS());
STAT_UPDATE(audioSilentInboundPPS, (int)audioClient->getSilentInboundPPS());
STAT_UPDATE(audioOutboundPPS, (int)audioClient->getAudioOutboundPPS());

View file

@ -85,7 +85,7 @@ void Grid3DOverlay::render(RenderArgs* args) {
DependencyManager::get<GeometryCache>()->renderGrid(*batch, minCorner, maxCorner,
_minorGridRowDivisions, _minorGridColDivisions, MINOR_GRID_EDGE,
_majorGridRowDivisions, _majorGridColDivisions, MAJOR_GRID_EDGE,
gridColor, _drawInFront, _geometryId);
gridColor, _geometryId);
}
}

View file

@ -123,7 +123,7 @@ void Text3DOverlay::render(RenderArgs* args) {
glm::vec4 textColor = { toGlm(_color), getTextAlpha() };
// FIXME: Factor out textRenderer so that Text3DOverlay overlay parts can be grouped by pipeline for a gpu performance increase.
_textRenderer->draw(batch, 0, 0, getText(), textColor, glm::vec2(-1.0f), true);
_textRenderer->draw(batch, 0, 0, getText(), textColor, glm::vec2(-1.0f));
}
const render::ShapeKey Text3DOverlay::getShapeKey() {

View file

@ -32,6 +32,7 @@
#include <shared/Camera.h>
#include <SoftAttachmentModel.h>
#include <render/TransitionStage.h>
#include <GLMHelpers.h>
#include "ModelEntityItem.h"
#include "RenderableModelEntityItem.h"
@ -295,6 +296,7 @@ void Avatar::setTargetScale(float targetScale) {
if (_targetScale != newValue) {
_targetScale = newValue;
_scaleChanged = usecTimestampNow();
_avatarScaleChanged = _scaleChanged;
_isAnimatingScale = true;
for (auto& sphere : _multiSphereShapes) {
sphere.setScale(_targetScale);
@ -390,7 +392,7 @@ void Avatar::updateAvatarEntities() {
QVariantMap asMap = variantProperties.toMap();
QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine);
EntityItemProperties properties;
EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, properties);
EntityItemPropertiesFromScriptValueIgnoreReadOnly(scriptProperties, properties);
properties.setEntityHostType(entity::HostType::AVATAR);
properties.setOwningAvatarID(getID());
@ -483,6 +485,111 @@ void Avatar::removeAvatarEntitiesFromTree() {
}
}
void Avatar::updateGrabs() {
// update the Grabs according to any changes in _avatarGrabData
_avatarGrabsLock.withWriteLock([&] {
if (_avatarGrabDataChanged) {
foreach (auto grabID, _avatarGrabData.keys()) {
AvatarGrabMap::iterator grabItr = _avatarGrabs.find(grabID);
if (grabItr == _avatarGrabs.end()) {
GrabPointer grab = std::make_shared<Grab>();
grab->fromByteArray(_avatarGrabData.value(grabID));
_avatarGrabs[grabID] = grab;
_changedAvatarGrabs.insert(grabID);
} else {
GrabPointer grab = grabItr.value();
bool changed = grab->fromByteArray(_avatarGrabData.value(grabID));
if (changed) {
_changedAvatarGrabs.insert(grabID);
}
}
}
_avatarGrabDataChanged = false;
}
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
auto entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
EntityEditPacketSender* packetSender = treeRenderer ? treeRenderer->getPacketSender() : nullptr;
auto sessionID = DependencyManager::get<NodeList>()->getSessionUUID();
QMutableSetIterator<QUuid> delItr(_deletedAvatarGrabs);
while (delItr.hasNext()) {
QUuid grabID = delItr.next();
GrabPointer grab = _avatarGrabs[grabID];
if (!grab) {
delItr.remove();
continue;
}
bool success;
SpatiallyNestablePointer target = SpatiallyNestable::findByID(grab->getTargetID(), success);
// only clear this entry from the _deletedAvatarGrabs if we found the entity.
if (success && target) {
bool iShouldTellServer = target->getEditSenderID() == sessionID;
EntityItemPointer entity = std::dynamic_pointer_cast<EntityItem>(target);
if (entity && entity->isAvatarEntity() && (entity->getOwningAvatarID() == sessionID ||
entity->getOwningAvatarID() == AVATAR_SELF_ID)) {
// this is our own avatar-entity, so we always tell the server about the release
iShouldTellServer = true;
}
target->removeGrab(grab);
delItr.remove();
// in case this is the last grab on an entity, we need to shrink the queryAACube and tell the server
// about the final position.
if (entityTree) {
bool force = true;
entityTree->withWriteLock([&] {
entityTree->updateEntityQueryAACube(target, packetSender, force, iShouldTellServer);
});
}
}
_avatarGrabs.remove(grabID);
_changedAvatarGrabs.remove(grabID);
}
QMutableSetIterator<QUuid> changeItr(_changedAvatarGrabs);
while (changeItr.hasNext()) {
QUuid grabID = changeItr.next();
GrabPointer& grab = _avatarGrabs[grabID];
bool success;
SpatiallyNestablePointer target = SpatiallyNestable::findByID(grab->getTargetID(), success);
if (success && target) {
target->addGrab(grab);
// only clear this entry from the _changedAvatarGrabs if we found the entity.
changeItr.remove();
}
}
});
}
void Avatar::accumulateGrabPositions(std::map<QUuid, GrabLocationAccumulator>& grabAccumulators) {
// relay avatar's joint position to grabbed target in a way that allows for averaging
_avatarGrabsLock.withReadLock([&] {
foreach (auto grabID, _avatarGrabs.keys()) {
const GrabPointer& grab = _avatarGrabs.value(grabID);
if (!grab || !grab->getActionID().isNull()) {
continue; // the accumulated value isn't used, in this case.
}
glm::vec3 jointTranslation = getAbsoluteJointTranslationInObjectFrame(grab->getParentJointIndex());
glm::quat jointRotation = getAbsoluteJointRotationInObjectFrame(grab->getParentJointIndex());
glm::mat4 jointMat = createMatFromQuatAndPos(jointRotation, jointTranslation);
glm::mat4 offsetMat = createMatFromQuatAndPos(grab->getRotationalOffset(), grab->getPositionalOffset());
glm::mat4 avatarMat = getTransform().getMatrix();
glm::mat4 worldTransform = avatarMat * jointMat * offsetMat;
GrabLocationAccumulator& grabLocationAccumulator = grabAccumulators[grab->getTargetID()];
grabLocationAccumulator.accumulate(extractTranslation(worldTransform), extractRotation(worldTransform));
}
});
}
void Avatar::relayJointDataToChildren() {
forEachChild([&](SpatiallyNestablePointer child) {
if (child->getNestableType() == NestableType::Entity) {
@ -553,7 +660,7 @@ void Avatar::simulate(float deltaTime, bool inView) {
if (!hasParent()) {
setLocalPosition(_globalPosition);
}
_simulationRate.increment();
if (inView) {
_simulationInViewRate.increment();
@ -619,6 +726,11 @@ void Avatar::simulate(float deltaTime, bool inView) {
updateAvatarEntities();
}
{
PROFILE_RANGE(simulation, "grabs");
updateGrabs();
}
updateFadingStatus();
}
@ -1297,6 +1409,9 @@ glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const {
}
switch (index) {
case NO_JOINT_INDEX: {
return glm::quat();
}
case SENSOR_TO_WORLD_MATRIX_INDEX: {
glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix();
glm::mat4 avatarMatrix = getLocalTransform().getMatrix();
@ -1346,6 +1461,9 @@ glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const {
}
switch (index) {
case NO_JOINT_INDEX: {
return glm::vec3(0.0f);
}
case SENSOR_TO_WORLD_MATRIX_INDEX: {
glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix();
glm::mat4 avatarMatrix = getLocalTransform().getMatrix();

View file

@ -157,9 +157,6 @@ public:
virtual void postUpdate(float deltaTime, const render::ScenePointer& scene);
//setters
void setIsLookAtTarget(const bool isLookAtTarget) { _isLookAtTarget = isLookAtTarget; }
bool getIsLookAtTarget() const { return _isLookAtTarget; }
//getters
bool isInitialized() const { return _initialized; }
SkeletonModelPointer getSkeletonModel() { return _skeletonModel; }
@ -441,6 +438,8 @@ public:
std::shared_ptr<AvatarTransit> getTransit() { return std::make_shared<AvatarTransit>(_transit); };
AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, float avatarScale, const AvatarTransit::TransitConfig& config);
void accumulateGrabPositions(std::map<QUuid, GrabLocationAccumulator>& grabAccumulators);
signals:
void targetScaleChanged(float targetScale);
@ -545,6 +544,7 @@ protected:
// protected methods...
bool isLookingAtMe(AvatarSharedPointer avatar) const;
void updateGrabs();
void relayJointDataToChildren();
void fade(render::Transaction& transaction, render::Transition::Type type);
@ -552,6 +552,7 @@ protected:
glm::vec3 getBodyRightDirection() const { return getWorldOrientation() * IDENTITY_RIGHT; }
glm::vec3 getBodyUpDirection() const { return getWorldOrientation() * IDENTITY_UP; }
void measureMotionDerivatives(float deltaTime);
bool getCollideWithOtherAvatars() const { return _collideWithOtherAvatars; }
float getSkeletonHeight() const;
float getHeadHeight() const;
@ -595,7 +596,6 @@ protected:
int _rightPointerGeometryID { 0 };
int _nameRectGeometryID { 0 };
bool _initialized { false };
bool _isLookAtTarget { false };
bool _isAnimatingScale { false };
bool _mustFadeIn { false };
bool _isFading { false };
@ -631,7 +631,10 @@ protected:
static void metaBlendshapeOperator(render::ItemID renderItemID, int blendshapeNumber, const QVector<BlendshapeOffset>& blendshapeOffsets,
const QVector<int>& blendedMeshSizes, const render::ItemIDs& subItemIDs);
std::vector<MultiSphereShape> _multiSphereShapes;
AvatarGrabMap _avatarGrabs;
};
#endif // hifi_Avatar_h

View file

@ -540,6 +540,10 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
if (_headData->getHasProceduralBlinkFaceMovement()) {
setAtBit16(flags, PROCEDURAL_BLINK_FACE_MOVEMENT);
}
// avatar collisions enabled
if (_collideWithOtherAvatars) {
setAtBit16(flags, COLLIDE_WITH_OTHER_AVATARS);
}
data->flags = flags;
destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
@ -1116,7 +1120,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
auto newHasAudioEnabledFaceMovement = oneAtBit16(bitItems, AUDIO_ENABLED_FACE_MOVEMENT);
auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT);
auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT);
auto newCollideWithOtherAvatars = oneAtBit16(bitItems, COLLIDE_WITH_OTHER_AVATARS);
bool keyStateChanged = (_keyState != newKeyState);
bool handStateChanged = (_handState != newHandState);
@ -1125,7 +1129,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
bool audioEnableFaceMovementChanged = (_headData->getHasAudioEnabledFaceMovement() != newHasAudioEnabledFaceMovement);
bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement);
bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement);
bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged || proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged;
bool collideWithOtherAvatarsChanged = (_collideWithOtherAvatars != newCollideWithOtherAvatars);
bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged ||
proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged;
_keyState = newKeyState;
_handState = newHandState;
@ -1134,6 +1140,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
_headData->setHasAudioEnabledFaceMovement(newHasAudioEnabledFaceMovement);
_headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement);
_headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement);
_collideWithOtherAvatars = newCollideWithOtherAvatars;
sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
@ -1893,42 +1900,96 @@ qint64 AvatarData::packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice
return bytesWritten;
}
qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType,
AvatarTraits::TraitInstanceID traitInstanceID,
ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) {
qint64 bytesWritten = 0;
// grab a read lock on the avatar entities and check for entity data for the given ID
QByteArray entityBinaryData;
_avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] {
if (_avatarEntityData.contains(traitInstanceID)) {
entityBinaryData = _avatarEntityData[traitInstanceID];
}
});
if (entityBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) {
qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << entityBinaryData.size()
<< "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes";
return 0;
}
bytesWritten += destination.writePrimitive(traitType);
if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
bytesWritten += destination.writePrimitive(traitVersion);
}
bytesWritten += destination.write(traitInstanceID.toRfc4122());
if (!entityBinaryData.isNull()) {
AvatarTraits::TraitWireSize entityBinarySize = entityBinaryData.size();
bytesWritten += destination.writePrimitive(entityBinarySize);
bytesWritten += destination.write(entityBinaryData);
} else {
bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE);
}
return bytesWritten;
}
qint64 AvatarData::packGrabTraitInstance(AvatarTraits::TraitType traitType,
AvatarTraits::TraitInstanceID traitInstanceID,
ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) {
qint64 bytesWritten = 0;
// grab a read lock on the avatar grabs and check for grab data for the given ID
QByteArray grabBinaryData;
_avatarGrabsLock.withReadLock([this, &grabBinaryData, &traitInstanceID] {
if (_avatarGrabData.contains(traitInstanceID)) {
grabBinaryData = _avatarGrabData[traitInstanceID];
}
});
if (grabBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) {
qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << grabBinaryData.size()
<< "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes";
return 0;
}
bytesWritten += destination.writePrimitive(traitType);
if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
bytesWritten += destination.writePrimitive(traitVersion);
}
bytesWritten += destination.write(traitInstanceID.toRfc4122());
if (!grabBinaryData.isNull()) {
AvatarTraits::TraitWireSize grabBinarySize = grabBinaryData.size();
bytesWritten += destination.writePrimitive(grabBinarySize);
bytesWritten += destination.write(grabBinaryData);
} else {
bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE);
}
return bytesWritten;
}
qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID,
ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) {
qint64 bytesWritten = 0;
if (traitType == AvatarTraits::AvatarEntity) {
// grab a read lock on the avatar entities and check for entity data for the given ID
QByteArray entityBinaryData;
_avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] {
if (_avatarEntityData.contains(traitInstanceID)) {
entityBinaryData = _avatarEntityData[traitInstanceID];
}
});
if (entityBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) {
qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << entityBinaryData.size()
<< "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes";
return 0;
}
bytesWritten += destination.writePrimitive(traitType);
if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
bytesWritten += destination.writePrimitive(traitVersion);
}
bytesWritten += destination.write(traitInstanceID.toRfc4122());
if (!entityBinaryData.isNull()) {
AvatarTraits::TraitWireSize entityBinarySize = entityBinaryData.size();
bytesWritten += destination.writePrimitive(entityBinarySize);
bytesWritten += destination.write(entityBinaryData);
} else {
bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE);
}
packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion);
} else if (traitType == AvatarTraits::Grab) {
packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion);
}
return bytesWritten;
@ -1940,6 +2001,9 @@ void AvatarData::prepareResetTraitInstances() {
foreach (auto entityID, _avatarEntityData.keys()) {
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
}
foreach (auto grabID, _avatarGrabData.keys()) {
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID);
}
});
}
}
@ -1956,12 +2020,16 @@ void AvatarData::processTraitInstance(AvatarTraits::TraitType traitType,
AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData) {
if (traitType == AvatarTraits::AvatarEntity) {
updateAvatarEntity(instanceID, traitBinaryData);
} else if (traitType == AvatarTraits::Grab) {
updateAvatarGrabData(instanceID, traitBinaryData);
}
}
void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID) {
if (traitType == AvatarTraits::AvatarEntity) {
clearAvatarEntity(instanceID);
} else if (traitType == AvatarTraits::Grab) {
clearAvatarGrabData(instanceID);
}
}
@ -2909,3 +2977,38 @@ AABox AvatarData::getDefaultBubbleBox() const {
bubbleBox.translate(_globalPosition);
return bubbleBox;
}
bool AvatarData::updateAvatarGrabData(const QUuid& grabID, const QByteArray& grabData) {
bool changed { false };
_avatarGrabsLock.withWriteLock([&] {
AvatarGrabDataMap::iterator itr = _avatarGrabData.find(grabID);
if (itr == _avatarGrabData.end()) {
// create a new one
if (_avatarGrabData.size() < MAX_NUM_AVATAR_GRABS) {
_avatarGrabData.insert(grabID, grabData);
_avatarGrabDataChanged = true;
changed = true;
} else {
qCWarning(avatars) << "Can't create more grabs on avatar, limit reached.";
}
} else {
// update an existing one
if (itr.value() != grabData) {
itr.value() = grabData;
_avatarGrabDataChanged = true;
changed = true;
}
}
});
return changed;
}
void AvatarData::clearAvatarGrabData(const QUuid& grabID) {
_avatarGrabsLock.withWriteLock([&] {
if (_avatarGrabData.remove(grabID)) {
_avatarGrabDataChanged = true;
_deletedAvatarGrabs.insert(grabID);
}
});
}

View file

@ -54,15 +54,21 @@
#include "AvatarTraits.h"
#include "HeadData.h"
#include "PathUtils.h"
#include "Grab.h"
#include <graphics/Material.h>
using AvatarSharedPointer = std::shared_ptr<AvatarData>;
using AvatarWeakPointer = std::weak_ptr<AvatarData>;
using AvatarHash = QHash<QUuid, AvatarSharedPointer>;
using AvatarEntityMap = QMap<QUuid, QByteArray>;
using AvatarEntityIDs = QSet<QUuid>;
using AvatarGrabDataMap = QMap<QUuid, QByteArray>;
using AvatarGrabIDs = QSet<QUuid>;
using AvatarGrabMap = QMap<QUuid, GrabPointer>;
using AvatarDataSequenceNumber = uint16_t;
// avatar motion behaviors
@ -104,6 +110,7 @@ const int HAND_STATE_FINGER_POINTING_BIT = 7; // 8th bit
const int AUDIO_ENABLED_FACE_MOVEMENT = 8; // 9th bit
const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit
const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit
const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit
const char HAND_STATE_NULL = 0;
@ -1342,6 +1349,13 @@ protected:
bool hasParent() const { return !getParentID().isNull(); }
bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; }
qint64 packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType,
AvatarTraits::TraitInstanceID traitInstanceID,
ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion);
qint64 packGrabTraitInstance(AvatarTraits::TraitType traitType,
AvatarTraits::TraitInstanceID traitInstanceID,
ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion);
// isReplicated will be true on downstream Avatar Mixers and their clients, but false on the upstream "master"
// Audio Mixer that the replicated avatar is connected to.
bool _isReplicated{ false };
@ -1452,6 +1466,12 @@ protected:
AvatarEntityMap _avatarEntityData;
bool _avatarEntityDataChanged { false };
mutable ReadWriteLockable _avatarGrabsLock;
AvatarGrabDataMap _avatarGrabData;
bool _avatarGrabDataChanged { false }; // by network
AvatarGrabIDs _changedAvatarGrabs; // updated grab IDs -- changes needed to entities or physics
AvatarGrabIDs _deletedAvatarGrabs; // deleted grab IDs -- changes needed to entities or physics
// used to transform any sensor into world space, including the _hmdSensorMat, or hand controllers.
ThreadSafeValueCache<glm::mat4> _sensorToWorldMatrixCache { glm::mat4() };
ThreadSafeValueCache<glm::mat4> _controllerLeftHandMatrixCache { glm::mat4() };
@ -1476,6 +1496,7 @@ protected:
int _replicaIndex { 0 };
bool _isNewAvatar { true };
bool _isClientAvatar { false };
bool _collideWithOtherAvatars { true };
// null unless MyAvatar or ScriptableAvatar sending traits data to mixer
std::unique_ptr<ClientTraitsHandler, LaterDeleter> _clientTraitsHandler;
@ -1511,6 +1532,9 @@ protected:
f(index);
}
bool updateAvatarGrabData(const QUuid& grabID, const QByteArray& grabData);
void clearAvatarGrabData(const QUuid& grabID);
private:
friend void avatarStateFromFrame(const QByteArray& frameData, AvatarData* _avatar);
static QUrl _defaultFullAvatarModelUrl;
@ -1614,6 +1638,7 @@ QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEnt
void AvatarEntityMapFromScriptValue(const QScriptValue& object, AvatarEntityMap& value);
// faux joint indexes (-1 means invalid)
const int NO_JOINT_INDEX = 65535; // -1
const int SENSOR_TO_WORLD_MATRIX_INDEX = 65534; // -2
const int CONTROLLER_RIGHTHAND_INDEX = 65533; // -3
const int CONTROLLER_LEFTHAND_INDEX = 65532; // -4
@ -1626,5 +1651,6 @@ const int FARGRAB_MOUSE_INDEX = 65526; // -10
const int LOWEST_PSEUDO_JOINT_INDEX = 65526;
const int MAX_NUM_AVATAR_GRABS = 6;
#endif // hifi_AvatarData_h

View file

@ -24,6 +24,7 @@ namespace AvatarTraits {
SkeletonModelURL,
FirstInstancedTrait,
AvatarEntity = FirstInstancedTrait,
Grab,
TotalTraitTypes
};

View file

@ -0,0 +1,13 @@
#ifndef hifi_AvatarProjectFile_h
#define hifi_AvatarProjectFile_h
#include <QObject>
class ProjectFilePath {
Q_GADGET;
public:
QString absolutePath;
QString relativePath;
};
#endif // hifi_AvatarProjectFile_h

View file

@ -32,6 +32,7 @@
#include <ScriptEngine.h>
#include <EntitySimulation.h>
#include <ZoneRenderer.h>
#include <PhysicalEntitySimulation.h>
#include "EntitiesRendererLogging.h"
#include "RenderableEntityItem.h"
@ -498,20 +499,27 @@ void EntityTreeRenderer::handleSpaceUpdate(std::pair<int32_t, glm::vec4> proxyUp
bool EntityTreeRenderer::findBestZoneAndMaybeContainingEntities(QVector<EntityItemID>* entitiesContainingAvatar) {
bool didUpdate = false;
float radius = 0.01f; // for now, assume 0.01 meter radius, because we actually check the point inside later
QVector<EntityItemPointer> foundEntities;
QVector<QUuid> entityIDs;
// find the entities near us
// don't let someone else change our tree while we search
_tree->withReadLock([&] {
auto entityTree = std::static_pointer_cast<EntityTree>(_tree);
// FIXME - if EntityTree had a findEntitiesContainingPoint() this could theoretically be a little faster
std::static_pointer_cast<EntityTree>(_tree)->findEntities(_avatarPosition, radius, foundEntities);
entityTree->evalEntitiesInSphere(_avatarPosition, radius,
PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES) | PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES)), entityIDs);
LayeredZones oldLayeredZones(std::move(_layeredZones));
_layeredZones.clear();
// create a list of entities that actually contain the avatar's position
for (auto& entity : foundEntities) {
for (auto& entityID : entityIDs) {
auto entity = entityTree->findEntityByID(entityID);
if (!entity) {
continue;
}
auto isZone = entity->getType() == EntityTypes::Zone;
auto hasScript = !entity->getScript().isEmpty();
@ -1243,3 +1251,11 @@ void EntityTreeRenderer::onEntityChanged(const EntityItemID& id) {
_changedEntities.insert(id);
});
}
EntityEditPacketSender* EntityTreeRenderer::getPacketSender() {
EntityTreePointer tree = getTree();
EntitySimulationPointer simulation = tree ? tree->getSimulation() : nullptr;
PhysicalEntitySimulationPointer peSimulation = std::static_pointer_cast<PhysicalEntitySimulation>(simulation);
EntityEditPacketSender* packetSender = peSimulation ? peSimulation->getPacketSender() : nullptr;
return packetSender;
}

View file

@ -120,6 +120,8 @@ public:
static void setGetAvatarUpOperator(std::function<glm::vec3()> getAvatarUpOperator) { _getAvatarUpOperator = getAvatarUpOperator; }
static glm::vec3 getAvatarUp() { return _getAvatarUpOperator(); }
EntityEditPacketSender* getPacketSender();
signals:
void enterEntity(const EntityItemID& entityItemID);
void leaveEntity(const EntityItemID& entityItemID);

View file

@ -26,6 +26,7 @@
#include "RenderableZoneEntityItem.h"
#include "RenderableMaterialEntityItem.h"
#include "RenderableImageEntityItem.h"
#include "RenderableGridEntityItem.h"
using namespace render;
@ -259,6 +260,10 @@ EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer,
result = make_renderer<PolyVoxEntityRenderer>(entity);
break;
case Type::Grid:
result = make_renderer<GridEntityRenderer>(entity);
break;
case Type::Light:
result = make_renderer<LightEntityRenderer>(entity);
break;

View file

@ -0,0 +1,148 @@
//
// Created by Sam Gondelman on 11/29/18
// 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 "RenderableGridEntityItem.h"
#include <DependencyManager.h>
#include <GeometryCache.h>
using namespace render;
using namespace render::entities;
GridEntityRenderer::GridEntityRenderer(const EntityItemPointer& entity) : Parent(entity) {
_geometryId = DependencyManager::get<GeometryCache>()->allocateID();
}
GridEntityRenderer::~GridEntityRenderer() {
auto geometryCache = DependencyManager::get<GeometryCache>();
if (geometryCache) {
geometryCache->releaseID(_geometryId);
}
}
bool GridEntityRenderer::isTransparent() const {
return Parent::isTransparent() || _alpha < 1.0f;
}
bool GridEntityRenderer::needsRenderUpdate() const {
return Parent::needsRenderUpdate();
}
bool GridEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const {
bool needsUpdate = resultWithReadLock<bool>([&] {
if (_color != entity->getColor()) {
return true;
}
if (_alpha != entity->getAlpha()) {
return true;
}
if (_followCamera != entity->getFollowCamera()) {
return true;
}
if (_majorGridEvery != entity->getMajorGridEvery()) {
return true;
}
if (_minorGridEvery != entity->getMinorGridEvery()) {
return true;
}
return false;
});
return needsUpdate;
}
void GridEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
withWriteLock([&] {
_color = entity->getColor();
_alpha = entity->getAlpha();
_followCamera = entity->getFollowCamera();
_majorGridEvery = entity->getMajorGridEvery();
_minorGridEvery = entity->getMinorGridEvery();
});
void* key = (void*)this;
AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this, entity]() {
withWriteLock([&] {
_dimensions = entity->getScaledDimensions();
updateModelTransformAndBound();
_renderTransform = getModelTransform();
});
});
}
Item::Bound GridEntityRenderer::getBound() {
if (_followCamera) {
// This is a UI element that should always be in view, lie to the octree to avoid culling
const AABox DOMAIN_BOX = AABox(glm::vec3(-TREE_SCALE / 2), TREE_SCALE);
return DOMAIN_BOX;
}
return Parent::getBound();
}
ShapeKey GridEntityRenderer::getShapeKey() {
auto builder = render::ShapeKey::Builder().withOwnPipeline().withUnlit().withDepthBias();
if (isTransparent()) {
builder.withTranslucent();
}
return builder.build();
}
void GridEntityRenderer::doRender(RenderArgs* args) {
glm::u8vec3 color;
glm::vec3 dimensions;
Transform renderTransform;
withReadLock([&] {
color = _color;
dimensions = _dimensions;
renderTransform = _renderTransform;
});
if (!_visible) {
return;
}
auto batch = args->_batch;
Transform transform;
transform.setScale(dimensions);
transform.setRotation(renderTransform.getRotation());
if (_followCamera) {
// Get the camera position rounded to the nearest major grid line
// This grid is for UI and should lie on worldlines
glm::vec3 localCameraPosition = glm::inverse(transform.getRotation()) * (args->getViewFrustum().getPosition() - renderTransform.getTranslation());
localCameraPosition.z = 0;
localCameraPosition = (float)_majorGridEvery * glm::round(localCameraPosition / (float)_majorGridEvery);
transform.setTranslation(renderTransform.getTranslation() + transform.getRotation() * localCameraPosition);
} else {
transform.setTranslation(renderTransform.getTranslation());
}
batch->setModelTransform(transform);
auto minCorner = glm::vec2(-0.5f, -0.5f);
auto maxCorner = glm::vec2(0.5f, 0.5f);
float majorGridRowDivisions = dimensions.x / _majorGridEvery;
float majorGridColDivisions = dimensions.y / _majorGridEvery;
float minorGridRowDivisions = dimensions.x / _minorGridEvery;
float minorGridColDivisions = dimensions.y / _minorGridEvery;
glm::vec4 gridColor(toGlm(color), _alpha);
const float MINOR_GRID_EDGE = 0.0025f;
const float MAJOR_GRID_EDGE = 0.005f;
DependencyManager::get<GeometryCache>()->renderGrid(*batch, minCorner, maxCorner,
minorGridRowDivisions, minorGridColDivisions, MINOR_GRID_EDGE,
majorGridRowDivisions, majorGridColDivisions, MAJOR_GRID_EDGE,
gridColor, _geometryId);
}

View file

@ -0,0 +1,51 @@
//
// Created by Sam Gondelman on 11/29/18
// 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_RenderableGridEntityItem_h
#define hifi_RenderableGridEntityItem_h
#include "RenderableEntityItem.h"
#include <GridEntityItem.h>
namespace render { namespace entities {
class GridEntityRenderer : public TypedEntityRenderer<GridEntityItem> {
using Parent = TypedEntityRenderer<GridEntityItem>;
using Pointer = std::shared_ptr<GridEntityRenderer>;
public:
GridEntityRenderer(const EntityItemPointer& entity);
~GridEntityRenderer();
protected:
Item::Bound getBound() override;
ShapeKey getShapeKey() override;
bool isTransparent() const override;
private:
virtual bool needsRenderUpdate() const override;
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
virtual void doRender(RenderArgs* args) override;
glm::u8vec3 _color;
float _alpha;
bool _followCamera;
uint32_t _majorGridEvery;
float _minorGridEvery;
glm::vec3 _dimensions;
int _geometryId { 0 };
};
} }
#endif // hifi_RenderableGridEntityItem_h

View file

@ -13,44 +13,42 @@
#include <ImageEntityItem.h>
namespace render {
namespace entities {
namespace render { namespace entities {
class ImageEntityRenderer : public TypedEntityRenderer<ImageEntityItem> {
using Parent = TypedEntityRenderer<ImageEntityItem>;
using Pointer = std::shared_ptr<ImageEntityRenderer>;
public:
ImageEntityRenderer(const EntityItemPointer& entity);
~ImageEntityRenderer();
class ImageEntityRenderer : public TypedEntityRenderer<ImageEntityItem> {
using Parent = TypedEntityRenderer<ImageEntityItem>;
using Pointer = std::shared_ptr<ImageEntityRenderer>;
public:
ImageEntityRenderer(const EntityItemPointer& entity);
~ImageEntityRenderer();
protected:
ShapeKey getShapeKey() override;
protected:
ShapeKey getShapeKey() override;
bool isTransparent() const override;
bool isTransparent() const override;
private:
virtual bool needsRenderUpdate() const override;
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
virtual void doRender(RenderArgs* args) override;
private:
virtual bool needsRenderUpdate() const override;
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
virtual void doRender(RenderArgs* args) override;
QString _imageURL;
NetworkTexturePointer _texture;
bool _textureIsLoaded { false };
QString _imageURL;
NetworkTexturePointer _texture;
bool _textureIsLoaded { false };
bool _emissive;
bool _keepAspectRatio;
BillboardMode _billboardMode;
QRect _subImage;
bool _emissive;
bool _keepAspectRatio;
BillboardMode _billboardMode;
QRect _subImage;
glm::u8vec3 _color;
float _alpha;
glm::u8vec3 _color;
float _alpha;
glm::vec3 _dimensions;
glm::vec3 _dimensions;
int _geometryId { 0 };
};
int _geometryId { 0 };
};
}
}
} }
#endif // hifi_RenderableImageEntityItem_h

View file

@ -37,7 +37,7 @@ void LineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointe
if (_lineVerticesID == GeometryCache::UNKNOWN_ID) {
_lineVerticesID = geometryCache->allocateID();
}
glm::vec4 lineColor(toGlm(entity->getColor()), entity->getLocalRenderAlpha());
glm::vec4 lineColor(toGlm(entity->getColor()), 1.0f);
geometryCache->updateVertices(_lineVerticesID, _linePoints, lineColor);
}

View file

@ -279,7 +279,7 @@ bool RenderableModelEntityItem::findDetailedParabolaIntersection(const glm::vec3
face, surfaceNormal, extraInfo, precisionPicking, false);
}
void RenderableModelEntityItem::getCollisionGeometryResource() {
void RenderableModelEntityItem::fetchCollisionGeometryResource() {
QUrl hullURL(getCollisionShapeURL());
QUrlQuery queryArgs(hullURL);
queryArgs.addQueryItem("collision-hull", "");
@ -289,7 +289,7 @@ void RenderableModelEntityItem::getCollisionGeometryResource() {
bool RenderableModelEntityItem::computeShapeFailedToLoad() {
if (!_compoundShapeResource) {
getCollisionGeometryResource();
fetchCollisionGeometryResource();
}
return (_compoundShapeResource && _compoundShapeResource->isFailed());
@ -300,7 +300,7 @@ void RenderableModelEntityItem::setShapeType(ShapeType type) {
auto shapeType = getShapeType();
if (shapeType == SHAPE_TYPE_COMPOUND || shapeType == SHAPE_TYPE_SIMPLE_COMPOUND) {
if (!_compoundShapeResource && !getCollisionShapeURL().isEmpty()) {
getCollisionGeometryResource();
fetchCollisionGeometryResource();
}
} else if (_compoundShapeResource && !getCompoundShapeURL().isEmpty()) {
// the compoundURL has been set but the shapeType does not agree
@ -317,7 +317,7 @@ void RenderableModelEntityItem::setCompoundShapeURL(const QString& url) {
ModelEntityItem::setCompoundShapeURL(url);
if (getCompoundShapeURL() != currentCompoundShapeURL || !getModel()) {
if (getShapeType() == SHAPE_TYPE_COMPOUND) {
getCollisionGeometryResource();
fetchCollisionGeometryResource();
}
}
}
@ -340,7 +340,7 @@ bool RenderableModelEntityItem::isReadyToComputeShape() const {
if (model->isLoaded()) {
if (!shapeURL.isEmpty() && !_compoundShapeResource) {
const_cast<RenderableModelEntityItem*>(this)->getCollisionGeometryResource();
const_cast<RenderableModelEntityItem*>(this)->fetchCollisionGeometryResource();
}
if (_compoundShapeResource && _compoundShapeResource->isLoaded()) {
@ -367,8 +367,6 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) {
const uint32_t QUAD_STRIDE = 4;
ShapeType type = getShapeType();
glm::vec3 dimensions = getScaledDimensions();
auto model = getModel();
if (type == SHAPE_TYPE_COMPOUND) {
updateModelBounds();
@ -450,6 +448,11 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) {
// to the visual model and apply them to the collision model (without regard for the
// collision model's extents).
auto model = getModel();
// assert we never fall in here when model not fully loaded
assert(model && model->isLoaded());
glm::vec3 dimensions = getScaledDimensions();
glm::vec3 scaleToFit = dimensions / model->getHFMModel().getUnscaledMeshExtents().size();
// multiply each point by scale before handing the point-set off to the physics engine.
// also determine the extents of the collision model.
@ -461,11 +464,12 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) {
}
}
shapeInfo.setParams(type, dimensions, getCompoundShapeURL());
adjustShapeInfoByRegistration(shapeInfo);
} else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) {
// TODO: assert we never fall in here when model not fully loaded
// assert(_model && _model->isLoaded());
updateModelBounds();
auto model = getModel();
// assert we never fall in here when model not fully loaded
assert(model && model->isLoaded());
model->updateGeometry();
// compute meshPart local transforms
@ -473,6 +477,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) {
const HFMModel& hfmModel = model->getHFMModel();
int numHFMMeshes = hfmModel.meshes.size();
int totalNumVertices = 0;
glm::vec3 dimensions = getScaledDimensions();
glm::mat4 invRegistraionOffset = glm::translate(dimensions * (getRegistrationPoint() - ENTITY_ITEM_DEFAULT_REGISTRATION_POINT));
for (int i = 0; i < numHFMMeshes; i++) {
const HFMMesh& mesh = hfmModel.meshes.at(i);
@ -695,12 +700,10 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) {
}
shapeInfo.setParams(type, 0.5f * dimensions, getModelURL());
adjustShapeInfoByRegistration(shapeInfo);
} else {
ModelEntityItem::computeShapeInfo(shapeInfo);
shapeInfo.setParams(type, 0.5f * dimensions);
EntityItem::computeShapeInfo(shapeInfo);
}
// finally apply the registration offset to the shapeInfo
adjustShapeInfoByRegistration(shapeInfo);
}
void RenderableModelEntityItem::setJointMap(std::vector<int> jointMap) {
@ -726,7 +729,9 @@ int RenderableModelEntityItem::avatarJointIndex(int modelJointIndex) {
bool RenderableModelEntityItem::contains(const glm::vec3& point) const {
auto model = getModel();
if (EntityItem::contains(point) && model && _compoundShapeResource && _compoundShapeResource->isLoaded()) {
return _compoundShapeResource->getHFMModel().convexHullContains(worldToEntity(point));
glm::mat4 worldToHFMMatrix = model->getWorldToHFMMatrix();
glm::vec3 hfmPoint = worldToHFMMatrix * glm::vec4(point, 1.0f);
return _compoundShapeResource->getHFMModel().convexHullContains(hfmPoint);
}
return false;

View file

@ -122,7 +122,7 @@ private:
void autoResizeJointArrays();
void copyAnimationJointDataToModel();
bool readyToAnimate() const;
void getCollisionGeometryResource();
void fetchCollisionGeometryResource();
GeometryResource::Pointer _compoundShapeResource;
std::vector<int> _jointMap;
@ -179,7 +179,6 @@ private:
bool _hasModel { false };
ModelPointer _model;
GeometryResource::Pointer _compoundShapeResource;
QString _lastTextures;
bool _texturesLoaded { false };
int _lastKnownCurrentFrame { -1 };

View file

@ -19,66 +19,39 @@
#include <PerfStat.h>
#include <shaders/Shaders.h>
//#define POLYLINE_ENTITY_USE_FADE_EFFECT
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
# include <FadeEffect.h>
#endif
#include "paintStroke_Shared.slh"
using namespace render;
using namespace render::entities;
static uint8_t CUSTOM_PIPELINE_NUMBER { 0 };
static const int32_t PAINTSTROKE_TEXTURE_SLOT { 0 };
static gpu::Stream::FormatPointer polylineFormat;
static gpu::PipelinePointer polylinePipeline;
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
static gpu::PipelinePointer polylineFadePipeline;
#endif
gpu::PipelinePointer PolyLineEntityRenderer::_pipeline = nullptr;
static render::ShapePipelinePointer shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key, gpu::Batch& batch) {
if (!polylinePipeline) {
gpu::ShaderPointer program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke);
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
auto fadeVS = gpu::Shader::createVertex(std::string(paintStroke_fade_vert));
auto fadePS = gpu::Shader::createPixel(std::string(paintStroke_fade_frag));
gpu::ShaderPointer fadeProgram = gpu::Shader::createProgram(fadeVS, fadePS);
#endif
gpu::StatePointer state = gpu::StatePointer(new gpu::State());
state->setDepthTest(true, true, gpu::LESS_EQUAL);
PrepareStencil::testMask(*state);
state->setBlendFunction(true,
gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
polylinePipeline = gpu::Pipeline::create(program, state);
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
_fadePipeline = gpu::Pipeline::create(fadeProgram, state);
#endif
}
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
if (key.isFaded()) {
auto fadeEffect = DependencyManager::get<FadeEffect>();
return std::make_shared<render::ShapePipeline>(_fadePipeline, nullptr, fadeEffect->getBatchSetter(), fadeEffect->getItemUniformSetter());
} else {
#endif
return std::make_shared<render::ShapePipeline>(polylinePipeline, nullptr, nullptr, nullptr);
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
}
#endif
}
static const QUrl DEFAULT_POLYLINE_TEXTURE = PathUtils::resourcesUrl("images/paintStroke.png");
PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) : Parent(entity) {
static std::once_flag once;
std::call_once(once, [&] {
CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory);
polylineFormat.reset(new gpu::Stream::Format());
polylineFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), offsetof(Vertex, position));
polylineFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), offsetof(Vertex, normal));
polylineFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), offsetof(Vertex, uv));
polylineFormat->setAttribute(gpu::Stream::COLOR, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB), offsetof(Vertex, color));
});
_texture = DependencyManager::get<TextureCache>()->getTexture(DEFAULT_POLYLINE_TEXTURE);
_verticesBuffer = std::make_shared<gpu::Buffer>();
{ // Initialize our buffers
_polylineDataBuffer = std::make_shared<gpu::Buffer>();
_polylineDataBuffer->resize(sizeof(PolylineData));
PolylineData data { glm::vec2(_faceCamera, _glow), glm::vec2(0.0f) };
_polylineDataBuffer->setSubData(0, data);
_polylineGeometryBuffer = std::make_shared<gpu::Buffer>();
}
}
void PolyLineEntityRenderer::buildPipeline() {
// FIXME: opaque pipeline
gpu::ShaderPointer program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke);
gpu::StatePointer state = gpu::StatePointer(new gpu::State());
state->setCullMode(gpu::State::CullMode::CULL_NONE);
state->setDepthTest(true, true, gpu::LESS_EQUAL);
PrepareStencil::testMask(*state);
state->setBlendFunction(true,
gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
_pipeline = gpu::Pipeline::create(program, state);
}
ItemKey PolyLineEntityRenderer::getKey() {
@ -86,152 +59,164 @@ ItemKey PolyLineEntityRenderer::getKey() {
}
ShapeKey PolyLineEntityRenderer::getShapeKey() {
return ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).build();
return ShapeKey::Builder().withOwnPipeline().withTranslucent().withoutCullFace();
}
bool PolyLineEntityRenderer::needsRenderUpdate() const {
bool textureLoadedChanged = resultWithReadLock<bool>([&] {
return (!_textureLoaded && _texture && _texture->isLoaded());
});
if (textureLoadedChanged) {
return true;
}
return Parent::needsRenderUpdate();
}
bool PolyLineEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const {
return (
entity->pointsChanged() ||
entity->strokeWidthsChanged() ||
entity->widthsChanged() ||
entity->normalsChanged() ||
entity->texturesChanged() ||
entity->strokeColorsChanged()
entity->colorsChanged() ||
_isUVModeStretch != entity->getIsUVModeStretch() ||
_glow != entity->getGlow() ||
_faceCamera != entity->getFaceCamera()
);
}
void PolyLineEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
static const QUrl DEFAULT_POLYLINE_TEXTURE = QUrl(PathUtils::resourcesPath() + "images/paintStroke.png");
QUrl entityTextures = DEFAULT_POLYLINE_TEXTURE;
void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) {
auto pointsChanged = entity->pointsChanged();
auto widthsChanged = entity->widthsChanged();
auto normalsChanged = entity->normalsChanged();
auto colorsChanged = entity->colorsChanged();
bool isUVModeStretch = entity->getIsUVModeStretch();
bool glow = entity->getGlow();
bool faceCamera = entity->getFaceCamera();
entity->resetPolyLineChanged();
// Transform
updateModelTransformAndBound();
_renderTransform = getModelTransform();
// Textures
if (entity->texturesChanged()) {
entity->resetTexturesChanged();
QUrl entityTextures = DEFAULT_POLYLINE_TEXTURE;
auto textures = entity->getTextures();
if (!textures.isEmpty()) {
entityTextures = QUrl(textures);
}
_texture = DependencyManager::get<TextureCache>()->getTexture(entityTextures);
_textureAspectRatio = 1.0f;
_textureLoaded = false;
}
if (!_texture) {
_texture = DependencyManager::get<TextureCache>()->getTexture(entityTextures);
bool textureChanged = false;
if (!_textureLoaded && _texture && _texture->isLoaded()) {
textureChanged = true;
_textureAspectRatio = (float)_texture->getOriginalHeight() / (float)_texture->getOriginalWidth();
_textureLoaded = true;
}
}
void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) {
auto pointsChanged = entity->pointsChanged();
auto strokeWidthsChanged = entity->strokeWidthsChanged();
auto normalsChanged = entity->normalsChanged();
auto strokeColorsChanged = entity->strokeColorsChanged();
bool isUVModeStretch = entity->getIsUVModeStretch();
entity->resetPolyLineChanged();
_polylineTransform = Transform();
_polylineTransform.setTranslation(entity->getWorldPosition());
_polylineTransform.setRotation(entity->getWorldOrientation());
// Data
if (faceCamera != _faceCamera || glow != _glow) {
_faceCamera = faceCamera;
_glow = glow;
updateData();
}
// Geometry
if (pointsChanged) {
_lastPoints = entity->getLinePoints();
_points = entity->getLinePoints();
}
if (strokeWidthsChanged) {
_lastStrokeWidths = entity->getStrokeWidths();
if (widthsChanged) {
_widths = entity->getStrokeWidths();
}
if (normalsChanged) {
_lastNormals = entity->getNormals();
_normals = entity->getNormals();
}
if (strokeColorsChanged) {
_lastStrokeColors = entity->getStrokeColors();
_lastStrokeColors = _lastNormals.size() == _lastStrokeColors.size() ? _lastStrokeColors : QVector<glm::vec3>({ toGlm(entity->getColor()) });
if (colorsChanged) {
_colors = entity->getStrokeColors();
_color = toGlm(entity->getColor());
}
if (pointsChanged || strokeWidthsChanged || normalsChanged || strokeColorsChanged) {
_empty = std::min(_lastPoints.size(), std::min(_lastNormals.size(), _lastStrokeWidths.size())) < 2;
if (!_empty) {
updateGeometry(updateVertices(_lastPoints, _lastNormals, _lastStrokeWidths, _lastStrokeColors, isUVModeStretch, _textureAspectRatio));
}
if (_isUVModeStretch != isUVModeStretch || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged) {
_isUVModeStretch = isUVModeStretch;
updateGeometry();
}
}
void PolyLineEntityRenderer::updateGeometry(const std::vector<Vertex>& vertices) {
_numVertices = (uint32_t)vertices.size();
auto bufferSize = _numVertices * sizeof(Vertex);
if (bufferSize > _verticesBuffer->getSize()) {
_verticesBuffer->resize(bufferSize);
}
_verticesBuffer->setSubData(0, vertices);
}
void PolyLineEntityRenderer::updateGeometry() {
int maxNumVertices = std::min(_points.length(), _normals.length());
std::vector<PolyLineEntityRenderer::Vertex> PolyLineEntityRenderer::updateVertices(const QVector<glm::vec3>& points,
const QVector<glm::vec3>& normals,
const QVector<float>& strokeWidths,
const QVector<glm::vec3>& strokeColors,
const bool isUVModeStretch,
const float textureAspectRatio) {
// Calculate the minimum vector size out of normals, points, and stroke widths
int size = std::min(points.size(), std::min(normals.size(), strokeWidths.size()));
std::vector<Vertex> vertices;
// Guard against an empty polyline
if (size <= 0) {
return vertices;
}
float uCoordInc = 1.0f / size;
float uCoord = 0.0f;
int finalIndex = size - 1;
glm::vec3 binormal;
float accumulatedDistance = 0.0f;
float distanceToLastPoint = 0.0f;
float accumulatedStrokeWidth = 0.0f;
float strokeWidth = 0.0f;
bool doesStrokeWidthVary = false;
for (int i = 1; i < strokeWidths.size(); i++) {
if (strokeWidths[i] != strokeWidths[i - 1]) {
doesStrokeWidthVary = true;
break;
if (_widths.size() >= 0) {
for (int i = 1; i < maxNumVertices; i++) {
float width = PolyLineEntityItem::DEFAULT_LINE_WIDTH;
if (i < _widths.length()) {
width = _widths[i];
}
if (width != _widths[i - 1]) {
doesStrokeWidthVary = true;
break;
}
}
}
for (int i = 0; i <= finalIndex; i++) {
const float& width = strokeWidths.at(i);
const auto& point = points.at(i);
const auto& normal = normals.at(i);
const auto& color = strokeColors.size() == normals.size() ? strokeColors.at(i) : strokeColors.at(0);
int vertexIndex = i * 2;
float uCoordInc = 1.0f / maxNumVertices;
float uCoord = 0.0f;
float accumulatedDistance = 0.0f;
float accumulatedStrokeWidth = 0.0f;
glm::vec3 binormal;
if (!isUVModeStretch && i >= 1) {
distanceToLastPoint = glm::distance(points.at(i), points.at(i - 1));
accumulatedDistance += distanceToLastPoint;
strokeWidth = 2 * strokeWidths[i];
std::vector<PolylineVertex> vertices;
vertices.reserve(maxNumVertices);
for (int i = 0; i < maxNumVertices; i++) {
// Position
glm::vec3 point = _points[i];
if (doesStrokeWidthVary) {
//If the stroke varies along the line the texture will stretch more or less depending on the speed
//because it looks better than using the same method as below
accumulatedStrokeWidth += strokeWidth;
float increaseValue = 1;
if (accumulatedStrokeWidth != 0) {
float newUcoord = glm::ceil(((1.0f / textureAspectRatio) * accumulatedDistance) / (accumulatedStrokeWidth / i));
increaseValue = newUcoord - uCoord;
// uCoord
float width = i < _widths.size() ? _widths[i] : PolyLineEntityItem::DEFAULT_LINE_WIDTH;
if (i > 0) { // First uCoord is 0.0f
if (!_isUVModeStretch) {
accumulatedDistance += glm::distance(point, _points[i - 1]);
if (doesStrokeWidthVary) {
//If the stroke varies along the line the texture will stretch more or less depending on the speed
//because it looks better than using the same method as below
accumulatedStrokeWidth += width;
float increaseValue = 1;
if (accumulatedStrokeWidth != 0) {
float newUcoord = glm::ceil((_textureAspectRatio * accumulatedDistance) / (accumulatedStrokeWidth / i));
increaseValue = newUcoord - uCoord;
}
increaseValue = increaseValue > 0 ? increaseValue : 1;
uCoord += increaseValue;
} else {
// If the stroke width is constant then the textures should keep the aspect ratio along the line
uCoord = (_textureAspectRatio * accumulatedDistance) / width;
}
increaseValue = increaseValue > 0 ? increaseValue : 1;
uCoord += increaseValue;
} else {
//If the stroke width is constant then the textures should keep the aspect ratio along the line
uCoord = ((1.0f / textureAspectRatio) * accumulatedDistance) / strokeWidth;
uCoord += uCoordInc;
}
} else if (vertexIndex >= 2) {
uCoord += uCoordInc;
}
// Color
glm::vec3 color = i < _colors.length() ? _colors[i] : _color;
// Normal
glm::vec3 normal = _normals[i];
// Binormal
// For last point we can assume binormals are the same since it represents the last two vertices of quad
if (i < finalIndex) {
const auto tangent = points.at(i + 1) - point;
binormal = glm::normalize(glm::cross(tangent, normal)) * width;
if (i < maxNumVertices - 1) {
glm::vec3 tangent = _points[i + 1] - point;
binormal = glm::normalize(glm::cross(tangent, normal));
// Check to make sure binormal is not a NAN. If it is, don't add to vertices vector
if (binormal.x != binormal.x) {
@ -239,54 +224,36 @@ std::vector<PolyLineEntityRenderer::Vertex> PolyLineEntityRenderer::updateVertic
}
}
const auto v1 = points.at(i) + binormal;
const auto v2 = points.at(i) - binormal;
vertices.emplace_back(v1, normal, vec2(uCoord, 0.0f), color);
vertices.emplace_back(v2, normal, vec2(uCoord, 1.0f), color);
PolylineVertex vertex = { glm::vec4(point, uCoord), glm::vec4(color, 1.0f), glm::vec4(normal, 0.0f), glm::vec4(binormal, 0.5f * width) };
vertices.push_back(vertex);
}
return vertices;
_numVertices = vertices.size();
_polylineGeometryBuffer->setData(vertices.size() * sizeof(PolylineVertex), (const gpu::Byte*) vertices.data());
}
scriptable::ScriptableModelBase PolyLineEntityRenderer::getScriptableModel() {
// TODO: adapt polyline into a triangles mesh...
return EntityRenderer::getScriptableModel();
void PolyLineEntityRenderer::updateData() {
PolylineData data { glm::vec2(_faceCamera, _glow), glm::vec2(0.0f) };
_polylineDataBuffer->setSubData(0, data);
}
void PolyLineEntityRenderer::doRender(RenderArgs* args) {
if (_empty) {
if (_numVertices < 2) {
return;
}
PerformanceTimer perfTimer("RenderablePolyLineEntityItem::render");
Q_ASSERT(args->_batch);
gpu::Batch& batch = *args->_batch;
batch.setModelTransform(_polylineTransform);
if (_texture && _texture->isLoaded()) {
batch.setResourceTexture(PAINTSTROKE_TEXTURE_SLOT, _texture->getGPUTexture());
} else {
batch.setResourceTexture(PAINTSTROKE_TEXTURE_SLOT, DependencyManager::get<TextureCache>()->getWhiteTexture());
if (!_pipeline) {
buildPipeline();
}
float textureWidth = (float)_texture->getOriginalWidth();
float textureHeight = (float)_texture->getOriginalHeight();
if (textureWidth != 0 && textureHeight != 0) {
_textureAspectRatio = textureWidth / textureHeight;
}
batch.setInputFormat(polylineFormat);
batch.setInputBuffer(0, _verticesBuffer, 0, sizeof(Vertex));
#ifndef POLYLINE_ENTITY_USE_FADE_EFFECT
// glColor4f must be called after setInputFormat if it must be taken into account
if (_isFading) {
batch._glColor4f(1.0f, 1.0f, 1.0f, Interpolate::calculateFadeRatio(_fadeStartTime));
} else {
batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
}
#endif
batch.draw(gpu::TRIANGLE_STRIP, _numVertices, 0);
batch.setPipeline(_pipeline);
batch.setModelTransform(_renderTransform);
batch.setResourceTexture(0, _textureLoaded ? _texture->getGPUTexture() : DependencyManager::get<TextureCache>()->getWhiteTexture());
batch.setResourceBuffer(0, _polylineGeometryBuffer);
batch.setUniformBuffer(0, _polylineDataBuffer);
batch.draw(gpu::TRIANGLE_STRIP, (gpu::uint32)(2 * _numVertices), 0);
}

View file

@ -25,52 +25,40 @@ class PolyLineEntityRenderer : public TypedEntityRenderer<PolyLineEntityItem> {
public:
PolyLineEntityRenderer(const EntityItemPointer& entity);
virtual scriptable::ScriptableModelBase getScriptableModel() override;
// FIXME: shouldn't always be transparent: take into account texture and glow
virtual bool isTransparent() const override { return true; }
protected:
virtual bool needsRenderUpdate() const override;
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene,
Transaction& transaction,
const TypedEntityPointer& entity) override;
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
virtual ItemKey getKey() override;
virtual ShapeKey getShapeKey() override;
virtual void doRender(RenderArgs* args) override;
virtual bool isTransparent() const override { return true; }
void buildPipeline();
void updateGeometry();
void updateData();
struct Vertex {
Vertex() {}
Vertex(const vec3& position, const vec3& normal, const vec2& uv, const vec3& color) : position(position),
normal(normal),
uv(uv),
color(color) {}
vec3 position;
vec3 normal;
vec2 uv;
vec3 color;
};
QVector<glm::vec3> _points;
QVector<glm::vec3> _normals;
QVector<glm::vec3> _colors;
glm::vec3 _color;
QVector<float> _widths;
void updateGeometry(const std::vector<Vertex>& vertices);
static std::vector<Vertex> updateVertices(const QVector<glm::vec3>& points,
const QVector<glm::vec3>& normals,
const QVector<float>& strokeWidths,
const QVector<glm::vec3>& strokeColors,
const bool isUVModeStretch,
const float textureAspectRatio);
Transform _polylineTransform;
QVector<glm::vec3> _lastPoints;
QVector<glm::vec3> _lastNormals;
QVector<glm::vec3> _lastStrokeColors;
QVector<float> _lastStrokeWidths;
gpu::BufferPointer _verticesBuffer;
uint32_t _numVertices { 0 };
bool _empty{ true };
NetworkTexturePointer _texture;
float _textureAspectRatio { 1.0f };
bool _textureLoaded { false };
bool _isUVModeStretch;
bool _faceCamera;
bool _glow;
size_t _numVertices;
gpu::BufferPointer _polylineDataBuffer;
gpu::BufferPointer _polylineGeometryBuffer;
static gpu::PipelinePointer _pipeline;
};
} } // namespace

View file

@ -131,21 +131,6 @@ bool ShapeEntityRenderer::isTransparent() const {
return Parent::isTransparent();
}
ItemKey ShapeEntityRenderer::getKey() {
ItemKey::Builder builder;
builder.withTypeShape().withTypeMeta().withTagBits(getTagMask());
withReadLock([&] {
if (isTransparent()) {
builder.withTransparent();
} else if (_canCastShadow) {
builder.withShadowCaster();
}
});
return builder.build();
}
bool ShapeEntityRenderer::useMaterialPipeline() const {
bool proceduralReady = resultWithReadLock<bool>([&] {
return _procedural.isReady();

View file

@ -25,7 +25,6 @@ public:
virtual scriptable::ScriptableModelBase getScriptableModel() override;
protected:
ItemKey getKey() override;
ShapeKey getShapeKey() override;
private:

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