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:
commit
39dc25ea92
252 changed files with 11421 additions and 5162 deletions
.gitignore
android
assignment-client
interface
resources
fonts
icons
images
qml
+android
Stats.qmlcontrolsUit
hifi
AvatarApp.qmlAvatarPackagerWindow.qml
avatarPackager
AvatarPackagerApp.qmlAvatarPackagerFooter.qmlAvatarPackagerHeader.qmlAvatarPackagerState.qmlAvatarProject.qmlAvatarProjectCard.qmlAvatarProjectUpload.qmlAvatarUploadStatusItem.qmlClickableArea.qmlCreateAvatarProject.qmlInfoBox.qmlLoadingCircle.qmlProjectInputControl.qmlRalewayButton.qmlStyle.qmlqmldir
avatarapp
tablet
src
Application.cppApplication.hMenu.cppMenu.h
avatar
AvatarActionHold.cppAvatarActionHold.hAvatarManager.cppAvatarManager.hAvatarMotionState.cppAvatarMotionState.hAvatarPackager.cppAvatarPackager.hAvatarProject.cppAvatarProject.hGrabManager.cppGrabManager.hMarketplaceItemUploader.cppMarketplaceItemUploader.hMyAvatar.cppMyAvatar.hOtherAvatar.cppOtherAvatar.h
raypick
LaserPointer.cppLaserPointerScriptingInterface.hParabolaPick.cppPathPointer.cppPickScriptingInterface.cppPickScriptingInterface.hPointerScriptingInterface.hRayPick.cppRayPickScriptingInterface.hStylusPointer.cpp
scripting
ClipboardScriptingInterface.cppClipboardScriptingInterface.hPlatformInfoScriptingInterface.cppPlatformInfoScriptingInterface.h
ui
libraries
avatars-renderer/src/avatars-renderer
avatars/src
entities-renderer/src
EntityTreeRenderer.cppEntityTreeRenderer.hRenderableEntityItem.cppRenderableGridEntityItem.cppRenderableGridEntityItem.hRenderableImageEntityItem.hRenderableLineEntityItem.cppRenderableModelEntityItem.cppRenderableModelEntityItem.hRenderablePolyLineEntityItem.cppRenderablePolyLineEntityItem.hRenderableShapeEntityItem.cppRenderableShapeEntityItem.h
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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}
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
Binary file not shown.
4
interface/resources/icons/checkmark-stroke.svg
Normal file
4
interface/resources/icons/checkmark-stroke.svg
Normal 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 |
BIN
interface/resources/icons/loader-snake-256-wf.gif
Normal file
BIN
interface/resources/icons/loader-snake-256-wf.gif
Normal file
Binary file not shown.
After ![]() (image error) Size: 27 KiB |
BIN
interface/resources/icons/loader-snake-256.gif
Normal file
BIN
interface/resources/icons/loader-snake-256.gif
Normal file
Binary file not shown.
After ![]() (image error) Size: 26 KiB |
BIN
interface/resources/images/loader-snake-128.png
Normal file
BIN
interface/resources/images/loader-snake-128.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 463 B |
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
24
interface/resources/qml/hifi/AvatarPackagerWindow.qml
Normal file
24
interface/resources/qml/hifi/AvatarPackagerWindow.qml
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
336
interface/resources/qml/hifi/avatarPackager/AvatarProject.qml
Normal file
336
interface/resources/qml/hifi/avatarPackager/AvatarProject.qml
Normal 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 won’t 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
120
interface/resources/qml/hifi/avatarPackager/InfoBox.qml
Normal file
120
interface/resources/qml/hifi/avatarPackager/InfoBox.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
20
interface/resources/qml/hifi/avatarPackager/Style.qml
Normal file
20
interface/resources/qml/hifi/avatarPackager/Style.qml
Normal 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"
|
||||
}
|
||||
}
|
2
interface/resources/qml/hifi/avatarPackager/qmldir
Normal file
2
interface/resources/qml/hifi/avatarPackager/qmldir
Normal file
|
@ -0,0 +1,2 @@
|
|||
module AvatarPackager
|
||||
singleton AvatarPackagerState 1.0 AvatarPackagerState.qml
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
15
interface/resources/qml/hifi/tablet/AvatarPackager.qml
Normal file
15
interface/resources/qml/hifi/tablet/AvatarPackager.qml
Normal 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
|
||||
}
|
||||
}
|
|
@ -869,7 +869,7 @@ Flickable {
|
|||
id: outOfRangeDataStrategyComboBox
|
||||
|
||||
height: 25
|
||||
width: 100
|
||||
width: 150
|
||||
|
||||
editable: true
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
|
|
|
@ -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, ¢er);
|
||||
return exportEntities(filename, entities, ¢er);
|
||||
}
|
||||
|
||||
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>();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
149
interface/src/avatar/AvatarPackager.cpp
Normal file
149
interface/src/avatar/AvatarPackager.cpp
Normal 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();
|
||||
}
|
100
interface/src/avatar/AvatarPackager.h
Normal file
100
interface/src/avatar/AvatarPackager.h
Normal 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
|
260
interface/src/avatar/AvatarProject.cpp
Normal file
260
interface/src/avatar/AvatarProject.cpp
Normal 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 } }));
|
||||
});
|
||||
}
|
115
interface/src/avatar/AvatarProject.h
Normal file
115
interface/src/avatar/AvatarProject.h
Normal 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
|
39
interface/src/avatar/GrabManager.cpp
Normal file
39
interface/src/avatar/GrabManager.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
23
interface/src/avatar/GrabManager.h
Normal file
23
interface/src/avatar/GrabManager.h
Normal 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();
|
||||
|
||||
};
|
321
interface/src/avatar/MarketplaceItemUploader.cpp
Normal file
321
interface/src/avatar/MarketplaceItemUploader.cpp
Normal 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(); });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
105
interface/src/avatar/MarketplaceItemUploader.h
Normal file
105
interface/src/avatar/MarketplaceItemUploader.h
Normal 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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
135
interface/src/scripting/PlatformInfoScriptingInterface.cpp
Normal file
135
interface/src/scripting/PlatformInfoScriptingInterface.cpp
Normal 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();
|
||||
}
|
70
interface/src/scripting/PlatformInfoScriptingInterface.h
Normal file
70
interface/src/scripting/PlatformInfoScriptingInterface.h
Normal 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
|
|
@ -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() }
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -24,6 +24,7 @@ namespace AvatarTraits {
|
|||
SkeletonModelURL,
|
||||
FirstInstancedTrait,
|
||||
AvatarEntity = FirstInstancedTrait,
|
||||
Grab,
|
||||
TotalTraitTypes
|
||||
};
|
||||
|
||||
|
|
13
libraries/avatars/src/ProjectFile.h
Normal file
13
libraries/avatars/src/ProjectFile.h
Normal 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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
148
libraries/entities-renderer/src/RenderableGridEntityItem.cpp
Normal file
148
libraries/entities-renderer/src/RenderableGridEntityItem.cpp
Normal 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);
|
||||
}
|
51
libraries/entities-renderer/src/RenderableGridEntityItem.h
Normal file
51
libraries/entities-renderer/src/RenderableGridEntityItem.h
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue